@@ -572,6 +572,35 @@ struct SandboxRecord {
572572 deleting : bool ,
573573}
574574
575+ /// Resolve a lifecycle request to a registry key.
576+ ///
577+ /// A non-empty `sandbox_id` is authoritative: resolution uses that id alone and
578+ /// never falls back to the name, so a request for an already-removed sandbox
579+ /// reports absence instead of matching a same-named sandbox in another
580+ /// workspace. Only a caller that supplies no id resolves by name, and because
581+ /// sandbox names are unique per workspace rather than globally, a name matching
582+ /// more than one record is rejected instead of decided by iteration order.
583+ fn resolve_record_id (
584+ registry : & HashMap < String , SandboxRecord > ,
585+ sandbox_id : & str ,
586+ sandbox_name : & str ,
587+ ) -> Result < Option < String > , Status > {
588+ if !sandbox_id. is_empty ( ) {
589+ return Ok ( registry. get_key_value ( sandbox_id) . map ( |( id, _) | id. clone ( ) ) ) ;
590+ }
591+
592+ let mut matches = registry
593+ . iter ( )
594+ . filter ( |( _, record) | record. snapshot . name == sandbox_name) ;
595+ let first = matches. next ( ) . map ( |( id, _) | id. clone ( ) ) ;
596+ if matches. next ( ) . is_some ( ) {
597+ return Err ( Status :: failed_precondition ( format ! (
598+ "sandbox_name {sandbox_name} matched more than one sandbox; supply sandbox_id"
599+ ) ) ) ;
600+ }
601+ Ok ( first)
602+ }
603+
575604#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
576605enum OverlayPreparation {
577606 Fresh ,
@@ -1697,14 +1726,7 @@ impl VmDriver {
16971726 }
16981727 let record_id = {
16991728 let registry = self . registry . lock ( ) . await ;
1700- if registry. contains_key ( sandbox_id) {
1701- Some ( sandbox_id. to_string ( ) )
1702- } else {
1703- registry
1704- . iter ( )
1705- . find ( |( _, record) | record. snapshot . name == sandbox_name)
1706- . map ( |( id, _) | id. clone ( ) )
1707- }
1729+ resolve_record_id ( & registry, sandbox_id, sandbox_name) ?
17081730 }
17091731 . ok_or_else ( || Status :: not_found ( "sandbox not found" ) ) ?;
17101732
@@ -1789,16 +1811,13 @@ impl VmDriver {
17891811 . map_err ( |error| Status :: invalid_argument ( error. to_string ( ) ) ) ?;
17901812 let ( record_id, state_dir, already_running) = {
17911813 let registry = self . registry . lock ( ) . await ;
1792- let ( id, record) = if let Some ( entry) = registry. get_key_value ( sandbox_id) {
1793- entry
1794- } else {
1795- registry
1796- . iter ( )
1797- . find ( |( _, record) | record. snapshot . name == sandbox_name)
1798- . ok_or_else ( || Status :: not_found ( "sandbox not found" ) ) ?
1799- } ;
1814+ let id = resolve_record_id ( & registry, sandbox_id, sandbox_name) ?
1815+ . ok_or_else ( || Status :: not_found ( "sandbox not found" ) ) ?;
1816+ let record = registry
1817+ . get ( & id)
1818+ . ok_or_else ( || Status :: not_found ( "sandbox not found" ) ) ?;
18001819 (
1801- id. clone ( ) ,
1820+ id,
18021821 record. state_dir . clone ( ) ,
18031822 record. process . is_some ( ) || record. provisioning_task . is_some ( ) ,
18041823 )
@@ -1911,14 +1930,7 @@ impl VmDriver {
19111930
19121931 let record_id = {
19131932 let registry = self . registry . lock ( ) . await ;
1914- if let Some ( ( id, _record) ) = registry. get_key_value ( sandbox_id) {
1915- Some ( id. clone ( ) )
1916- } else {
1917- registry
1918- . iter ( )
1919- . find ( |( _, record) | record. snapshot . name == sandbox_name)
1920- . map ( |( id, _) | id. clone ( ) )
1921- }
1933+ resolve_record_id ( & registry, sandbox_id, sandbox_name) ?
19221934 } ;
19231935
19241936 let Some ( record_id) = record_id else {
@@ -10662,4 +10674,198 @@ mod tests {
1066210674 assert ! ( gpu. default_selection_supported) ;
1066310675 assert ! ( gpu. count_selection_supported) ;
1066410676 }
10677+
10678+ /// Register a stopped, process-free record whose id, name, and workspace are
10679+ /// set independently.
10680+ ///
10681+ /// The other test helpers reuse one string for both id and name, so a test
10682+ /// built on them cannot express the cross-workspace name collision that
10683+ /// lifecycle resolution has to tolerate.
10684+ async fn insert_named_record (
10685+ driver : & VmDriver ,
10686+ id : & str ,
10687+ name : & str ,
10688+ workspace : & str ,
10689+ ) -> PathBuf {
10690+ let state_dir = sandbox_state_dir ( & driver. config . state_dir , id) . unwrap ( ) ;
10691+ create_private_dir_all ( & state_dir) . await . unwrap ( ) ;
10692+ driver. registry . lock ( ) . await . insert (
10693+ id. to_string ( ) ,
10694+ SandboxRecord {
10695+ snapshot : Sandbox {
10696+ id : id. to_string ( ) ,
10697+ name : name. to_string ( ) ,
10698+ workspace : workspace. to_string ( ) ,
10699+ ..Default :: default ( )
10700+ } ,
10701+ state_dir : state_dir. clone ( ) ,
10702+ process : None ,
10703+ provisioning_task : None ,
10704+ gpu_bdf : None ,
10705+ deleting : false ,
10706+ } ,
10707+ ) ;
10708+ state_dir
10709+ }
10710+
10711+ fn resolution_test_driver ( state_dir : & Path ) -> VmDriver {
10712+ let mut driver = test_driver_with_extensions ( LifecycleExtensionRegistry :: new ( ) ) ;
10713+ driver. config . state_dir = state_dir. to_path_buf ( ) ;
10714+ driver
10715+ }
10716+
10717+ #[ tokio:: test]
10718+ async fn stop_targets_the_requested_id_when_two_workspaces_share_a_name ( ) {
10719+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
10720+ let driver = resolution_test_driver ( temp. path ( ) ) ;
10721+ let alpha = insert_named_record ( & driver, "vm-alpha" , "demo" , "alpha" ) . await ;
10722+ let beta = insert_named_record ( & driver, "vm-beta" , "demo" , "beta" ) . await ;
10723+
10724+ driver
10725+ . stop_sandbox ( "vm-beta" , "demo" )
10726+ . await
10727+ . expect ( "stop by id should be accepted" ) ;
10728+
10729+ assert ! (
10730+ beta. join( SANDBOX_STOPPED_FILE ) . exists( ) ,
10731+ "the requested sandbox should have been stopped"
10732+ ) ;
10733+ assert ! (
10734+ !alpha. join( SANDBOX_STOPPED_FILE ) . exists( ) ,
10735+ "the same-named sandbox in another workspace must be untouched"
10736+ ) ;
10737+ }
10738+
10739+ #[ tokio:: test]
10740+ async fn delete_targets_the_requested_id_when_two_workspaces_share_a_name ( ) {
10741+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
10742+ let driver = resolution_test_driver ( temp. path ( ) ) ;
10743+ let alpha = insert_named_record ( & driver, "vm-alpha" , "demo" , "alpha" ) . await ;
10744+ insert_named_record ( & driver, "vm-beta" , "demo" , "beta" ) . await ;
10745+
10746+ let response = driver
10747+ . delete_sandbox ( "vm-beta" , "demo" )
10748+ . await
10749+ . expect ( "delete by id should be accepted" ) ;
10750+
10751+ assert ! ( response. deleted) ;
10752+ let registry = driver. registry . lock ( ) . await ;
10753+ assert ! ( !registry. contains_key( "vm-beta" ) ) ;
10754+ assert ! (
10755+ registry. contains_key( "vm-alpha" ) ,
10756+ "the same-named sandbox in another workspace must survive"
10757+ ) ;
10758+ assert ! ( alpha. exists( ) , "the surviving sandbox must keep its state" ) ;
10759+ }
10760+
10761+ #[ tokio:: test]
10762+ async fn a_repeated_delete_does_not_fall_back_to_a_same_named_sandbox ( ) {
10763+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
10764+ let driver = resolution_test_driver ( temp. path ( ) ) ;
10765+ let alpha = insert_named_record ( & driver, "vm-alpha" , "demo" , "alpha" ) . await ;
10766+ insert_named_record ( & driver, "vm-beta" , "demo" , "beta" ) . await ;
10767+
10768+ let first = driver
10769+ . delete_sandbox ( "vm-beta" , "demo" )
10770+ . await
10771+ . expect ( "the first delete should be accepted" ) ;
10772+ assert ! ( first. deleted) ;
10773+
10774+ // Delete is idempotent, so a retry carrying the same id and name is
10775+ // ordinary caller behavior. The id is gone from the registry by now, and
10776+ // resolving it by name instead would destroy the sandbox in `alpha`.
10777+ let second = driver
10778+ . delete_sandbox ( "vm-beta" , "demo" )
10779+ . await
10780+ . expect ( "a repeated delete should be accepted" ) ;
10781+
10782+ assert ! (
10783+ !second. deleted,
10784+ "a repeated delete must report that nothing was removed"
10785+ ) ;
10786+ assert ! (
10787+ driver. registry. lock( ) . await . contains_key( "vm-alpha" ) ,
10788+ "a repeated delete must not remove a same-named sandbox in another workspace"
10789+ ) ;
10790+ assert ! ( alpha. exists( ) , "the surviving sandbox must keep its state" ) ;
10791+ }
10792+
10793+ #[ tokio:: test]
10794+ async fn stop_and_start_reject_an_absent_id_that_shares_a_name ( ) {
10795+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
10796+ let driver = resolution_test_driver ( temp. path ( ) ) ;
10797+ let alpha = insert_named_record ( & driver, "vm-alpha" , "demo" , "alpha" ) . await ;
10798+
10799+ let stop_error = driver
10800+ . stop_sandbox ( "vm-absent" , "demo" )
10801+ . await
10802+ . expect_err ( "a supplied id that is not registered must not resolve by name" ) ;
10803+ assert_eq ! ( stop_error. code( ) , Code :: NotFound ) ;
10804+
10805+ let ( start_authentication, _) = test_launch_authentication ( "absent" ) ;
10806+ let start_error = driver
10807+ . start_sandbox (
10808+ "vm-absent" ,
10809+ "demo" ,
10810+ "g0000000000000001" ,
10811+ start_authentication,
10812+ )
10813+ . await
10814+ . expect_err ( "a supplied id that is not registered must not resolve by name" ) ;
10815+ assert_eq ! ( start_error. code( ) , Code :: NotFound ) ;
10816+
10817+ assert ! (
10818+ !alpha. join( SANDBOX_STOPPED_FILE ) . exists( ) ,
10819+ "the same-named sandbox must not have been touched"
10820+ ) ;
10821+ }
10822+
10823+ #[ tokio:: test]
10824+ async fn a_name_only_request_matching_two_sandboxes_is_rejected ( ) {
10825+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
10826+ let driver = resolution_test_driver ( temp. path ( ) ) ;
10827+ let alpha = insert_named_record ( & driver, "vm-alpha" , "demo" , "alpha" ) . await ;
10828+ let beta = insert_named_record ( & driver, "vm-beta" , "demo" , "beta" ) . await ;
10829+
10830+ // Without an id the driver has nothing to disambiguate with: the request
10831+ // carries no workspace, and picking by iteration order would stop or
10832+ // delete an arbitrary one of the two.
10833+ for error in [
10834+ driver. stop_sandbox ( "" , "demo" ) . await . unwrap_err ( ) ,
10835+ driver
10836+ . start_sandbox (
10837+ "" ,
10838+ "demo" ,
10839+ "g0000000000000001" ,
10840+ test_launch_authentication ( "ambiguous" ) . 0 ,
10841+ )
10842+ . await
10843+ . unwrap_err ( ) ,
10844+ driver. delete_sandbox ( "" , "demo" ) . await . unwrap_err ( ) ,
10845+ ] {
10846+ assert_eq ! ( error. code( ) , Code :: FailedPrecondition ) ;
10847+ assert ! ( error. message( ) . contains( "matched more than one sandbox" ) ) ;
10848+ }
10849+
10850+ let registry = driver. registry . lock ( ) . await ;
10851+ assert ! ( registry. contains_key( "vm-alpha" ) ) ;
10852+ assert ! ( registry. contains_key( "vm-beta" ) ) ;
10853+ assert ! ( !alpha. join( SANDBOX_STOPPED_FILE ) . exists( ) ) ;
10854+ assert ! ( !beta. join( SANDBOX_STOPPED_FILE ) . exists( ) ) ;
10855+ }
10856+
10857+ #[ tokio:: test]
10858+ async fn a_name_only_request_still_resolves_a_unique_name ( ) {
10859+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
10860+ let driver = resolution_test_driver ( temp. path ( ) ) ;
10861+ let alpha = insert_named_record ( & driver, "vm-alpha" , "demo" , "alpha" ) . await ;
10862+ insert_named_record ( & driver, "vm-beta" , "other" , "beta" ) . await ;
10863+
10864+ driver
10865+ . stop_sandbox ( "" , "demo" )
10866+ . await
10867+ . expect ( "an unambiguous name-only stop should still resolve" ) ;
10868+
10869+ assert ! ( alpha. join( SANDBOX_STOPPED_FILE ) . exists( ) ) ;
10870+ }
1066510871}
0 commit comments