Skip to content

Commit 14b208c

Browse files
authored
fix(shim): hold registry lock across load-modify-save to prevent pane ID races (#640)
* fix(shim): hold registry lock across load-modify-save to prevent pane ID races state::load() and state::save() each acquired independent flocks. Between the two calls, a concurrent split-window could read the same next_pane_id, causing duplicate pane IDs and orphaned daemon PTYs. Add LockedRegistry guard type that holds both the Flock and PaneRegistry. save(self) writes while the lock is still held. Update all 8 callers in commands.rs to use load_and_lock() + locked.save(). Add thread-level concurrency test verifying unique pane ID allocation. * fix(shim): gate standalone load() behind #[cfg(test)], keep save() for init_registry * fix(shim): address review findings from PR #640 Update module doc to reflect that load() is now test-only and save() is init-only. Add read/write phase comments to handle_new_session. Restructure handle_new_window to do all reads before writes, eliminating interleaved registry()/registry_mut() calls.
1 parent 35a68b9 commit 14b208c

2 files changed

Lines changed: 203 additions & 51 deletions

File tree

crates/kild-tmux-shim/src/commands.rs

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -300,28 +300,41 @@ fn handle_split_window(args: SplitWindowArgs<'_>) -> Result<i32, ShimError> {
300300
);
301301

302302
let sid = session_id()?;
303-
let mut registry = state::load(&sid)?;
303+
let mut locked = state::load_and_lock(&sid)?;
304304

305305
// Determine which window the target pane belongs to
306306
let parent_pane_id = resolve_pane_id(args.target);
307-
let window_id = registry
307+
let window_id = locked
308+
.registry()
308309
.panes
309310
.get(&parent_pane_id)
310311
.map(|p| p.window_id.clone())
311312
.unwrap_or_else(|| "0".to_string());
312313

313-
let pane_id = create_pty_pane(&mut registry, &window_id, &args.command)?;
314-
state::save(&sid, &registry)?;
314+
let pane_id = create_pty_pane(locked.registry_mut(), &window_id, &args.command)?;
315315

316-
if args.print_info {
317-
let fmt = args.format.unwrap_or("#{pane_id}");
318-
let session_name = &registry.session_name;
319-
let window_name = registry
316+
// Capture format values before save consumes the guard (only when needed)
317+
let print_values = if args.print_info {
318+
let session_name = locked.registry().session_name.clone();
319+
let window_name = locked
320+
.registry()
320321
.windows
321322
.get(&window_id)
322-
.map(|w| w.name.as_str())
323-
.unwrap_or("main");
324-
let output = expand_format(fmt, &pane_id, session_name, &window_id, window_name, "");
323+
.map(|w| w.name.clone())
324+
.unwrap_or_else(|| "main".to_string());
325+
Some((
326+
args.format.unwrap_or("#{pane_id}"),
327+
session_name,
328+
window_name,
329+
))
330+
} else {
331+
None
332+
};
333+
334+
locked.save()?;
335+
336+
if let Some((fmt, session_name, window_name)) = print_values {
337+
let output = expand_format(fmt, &pane_id, &session_name, &window_id, &window_name, "");
325338
println!("{}", output);
326339
}
327340

@@ -451,10 +464,11 @@ fn handle_kill_pane(args: KillPaneArgs<'_>) -> Result<i32, ShimError> {
451464
debug!(event = "shim.kill_pane_started", target = ?args.target);
452465

453466
let sid = session_id()?;
454-
let mut registry = state::load(&sid)?;
467+
let mut locked = state::load_and_lock(&sid)?;
455468

456469
let pane_id = resolve_pane_id(args.target);
457-
let pane = registry
470+
let pane = locked
471+
.registry()
458472
.panes
459473
.get(&pane_id)
460474
.ok_or_else(|| ShimError::state(format!("pane {} not found in registry", pane_id)))?;
@@ -492,8 +506,8 @@ fn handle_kill_pane(args: KillPaneArgs<'_>) -> Result<i32, ShimError> {
492506
}
493507
}
494508

495-
registry.remove_pane(&pane_id);
496-
state::save(&sid, &registry)?;
509+
locked.registry_mut().remove_pane(&pane_id);
510+
locked.save()?;
497511

498512
debug!(event = "shim.kill_pane_completed", pane_id = pane_id);
499513
Ok(0)
@@ -574,17 +588,17 @@ fn handle_select_pane(args: SelectPaneArgs<'_>) -> Result<i32, ShimError> {
574588
// Only need state if we have style or title to store
575589
if args.style.is_some() || args.title.is_some() {
576590
let sid = session_id()?;
577-
let mut registry = state::load(&sid)?;
591+
let mut locked = state::load_and_lock(&sid)?;
578592
let pane_id = resolve_pane_id(args.target);
579593

580-
if let Some(pane) = registry.panes.get_mut(&pane_id) {
594+
if let Some(pane) = locked.registry_mut().panes.get_mut(&pane_id) {
581595
if let Some(style) = args.style {
582596
pane.border_style = style.to_string();
583597
}
584598
if let Some(title) = args.title {
585599
pane.title = title.to_string();
586600
}
587-
state::save(&sid, &registry)?;
601+
locked.save()?;
588602
}
589603
}
590604

@@ -604,15 +618,15 @@ fn handle_set_option(args: SetOptionArgs<'_>) -> Result<i32, ShimError> {
604618
// Store pane-scoped options in the pane entry
605619
if matches!(args.scope, OptionScope::Pane) {
606620
let sid = session_id()?;
607-
let mut registry = state::load(&sid)?;
621+
let mut locked = state::load_and_lock(&sid)?;
608622
let pane_id = resolve_pane_id(args.target);
609623

610-
if let Some(pane) = registry.panes.get_mut(&pane_id) {
624+
if let Some(pane) = locked.registry_mut().panes.get_mut(&pane_id) {
611625
// Store known pane options
612626
if args.key == "pane-border-style" || args.key.ends_with("-style") {
613627
pane.border_style = args.value;
614628
}
615-
state::save(&sid, &registry)?;
629+
locked.save()?;
616630
}
617631
}
618632

@@ -655,21 +669,21 @@ fn handle_new_session(args: NewSessionArgs<'_>) -> Result<i32, ShimError> {
655669
debug!(event = "shim.new_session_started", name = ?args.session_name);
656670

657671
let sid = session_id()?;
658-
let mut registry = state::load(&sid)?;
672+
let mut locked = state::load_and_lock(&sid)?;
659673

674+
// Read phase: derive names and IDs from current state
660675
let session_name = args
661676
.session_name
662677
.map(|s| s.to_string())
663-
.unwrap_or_else(|| format!("kild_{}", registry.sessions.len()));
664-
678+
.unwrap_or_else(|| format!("kild_{}", locked.registry().sessions.len()));
665679
let window_name = args
666680
.window_name
667681
.map(|s| s.to_string())
668682
.unwrap_or_else(|| "main".to_string());
683+
let window_id = format!("{}", locked.registry().windows.len());
669684

670-
// Allocate a new window
671-
let window_id = format!("{}", registry.windows.len());
672-
registry.windows.insert(
685+
// Write phase: mutate registry
686+
locked.registry_mut().windows.insert(
673687
window_id.clone(),
674688
WindowEntry {
675689
name: window_name,
@@ -678,18 +692,18 @@ fn handle_new_session(args: NewSessionArgs<'_>) -> Result<i32, ShimError> {
678692
);
679693

680694
// Create initial pane in the new window
681-
let pane_id = create_pty_pane(&mut registry, &window_id, &[])?;
695+
let pane_id = create_pty_pane(locked.registry_mut(), &window_id, &[])?;
682696

683697
// Register session
684-
registry.sessions.insert(
698+
locked.registry_mut().sessions.insert(
685699
session_name.clone(),
686700
SessionEntry {
687701
name: session_name.clone(),
688702
windows: vec![window_id],
689703
},
690704
);
691705

692-
state::save(&sid, &registry)?;
706+
locked.save()?;
693707

694708
if args.print_info {
695709
let fmt = args.format.unwrap_or("#{pane_id}");
@@ -708,36 +722,37 @@ fn handle_new_window(args: NewWindowArgs<'_>) -> Result<i32, ShimError> {
708722
debug!(event = "shim.new_window_started", name = ?args.name);
709723

710724
let sid = session_id()?;
711-
let mut registry = state::load(&sid)?;
725+
let mut locked = state::load_and_lock(&sid)?;
712726

713727
let window_name = args
714728
.name
715729
.map(|s| s.to_string())
716730
.unwrap_or_else(|| "window".to_string());
717-
let window_id = format!("{}", registry.windows.len());
718731

719-
registry.windows.insert(
732+
// Read phase: derive IDs from current state
733+
let window_id = format!("{}", locked.registry().windows.len());
734+
let session_key = args
735+
.target
736+
.and_then(|t| t.split(':').next())
737+
.unwrap_or(&locked.registry().session_name)
738+
.to_string();
739+
740+
// Write phase: mutate registry
741+
locked.registry_mut().windows.insert(
720742
window_id.clone(),
721743
WindowEntry {
722744
name: window_name.clone(),
723745
pane_ids: vec![],
724746
},
725747
);
726748

727-
let pane_id = create_pty_pane(&mut registry, &window_id, &[])?;
728-
729-
// Add window to the target session (or default session)
730-
let session_key = args
731-
.target
732-
.and_then(|t| t.split(':').next())
733-
.unwrap_or(&registry.session_name)
734-
.to_string();
749+
let pane_id = create_pty_pane(locked.registry_mut(), &window_id, &[])?;
735750

736-
if let Some(session) = registry.sessions.get_mut(&session_key) {
751+
if let Some(session) = locked.registry_mut().sessions.get_mut(&session_key) {
737752
session.windows.push(window_id.clone());
738753
}
739754

740-
state::save(&sid, &registry)?;
755+
locked.save()?;
741756

742757
if args.print_info {
743758
let fmt = args.format.unwrap_or("#{pane_id}");
@@ -789,13 +804,13 @@ fn handle_break_pane(args: BreakPaneArgs<'_>) -> Result<i32, ShimError> {
789804
debug!(event = "shim.break_pane_started", source = ?args.source);
790805

791806
let sid = session_id()?;
792-
let mut registry = state::load(&sid)?;
807+
let mut locked = state::load_and_lock(&sid)?;
793808

794809
let pane_id = resolve_pane_id(args.source);
795-
if let Some(pane) = registry.panes.get_mut(&pane_id) {
810+
if let Some(pane) = locked.registry_mut().panes.get_mut(&pane_id) {
796811
pane.hidden = true;
797812
}
798-
state::save(&sid, &registry)?;
813+
locked.save()?;
799814

800815
debug!(event = "shim.break_pane_completed", pane_id = pane_id);
801816
Ok(0)
@@ -805,13 +820,13 @@ fn handle_join_pane(args: JoinPaneArgs<'_>) -> Result<i32, ShimError> {
805820
debug!(event = "shim.join_pane_started", source = ?args.source);
806821

807822
let sid = session_id()?;
808-
let mut registry = state::load(&sid)?;
823+
let mut locked = state::load_and_lock(&sid)?;
809824

810825
let pane_id = resolve_pane_id(args.source);
811-
if let Some(pane) = registry.panes.get_mut(&pane_id) {
826+
if let Some(pane) = locked.registry_mut().panes.get_mut(&pane_id) {
812827
pane.hidden = false;
813828
}
814-
state::save(&sid, &registry)?;
829+
locked.save()?;
815830

816831
debug!(event = "shim.join_pane_completed", pane_id = pane_id);
817832
Ok(0)

0 commit comments

Comments
 (0)