@@ -156,14 +156,27 @@ impl SandboxState {
156156 }
157157
158158 /// Persist state to disk. Creates the directory if needed.
159+ ///
160+ /// The write is atomic: serialize to a per-writer temp file, then `rename`
161+ /// it over `state.json`. During `create`/`restore` the CLI and the detached
162+ /// supervisor write `state.json` concurrently; a plain `write` (truncate then
163+ /// write) would let the other process `load` a torn or empty file and fail to
164+ /// parse it. `rename(2)` is atomic on the same filesystem, so a concurrent
165+ /// reader always sees a complete file (the previous one or the new one).
159166 pub fn save ( & self ) -> Result < ( ) > {
160167 let dir = self . state_dir ( ) ;
161168 std:: fs:: create_dir_all ( & dir)
162169 . with_context ( || format ! ( "create state dir {:?}" , dir) ) ?;
163170 let data = serde_json:: to_string_pretty ( self )
164171 . context ( "serialize sandbox state" ) ?;
165- std:: fs:: write ( self . state_file ( ) , data)
166- . with_context ( || format ! ( "write state to {:?}" , self . state_file( ) ) )
172+ // Temp and target share `dir` (same filesystem, so rename is atomic) and
173+ // the temp name carries the writer's PID so concurrent writers in
174+ // different processes never collide on it.
175+ let tmp = dir. join ( format ! ( "state.json.{}.tmp" , std:: process:: id( ) ) ) ;
176+ std:: fs:: write ( & tmp, data)
177+ . with_context ( || format ! ( "write state to {:?}" , tmp) ) ?;
178+ std:: fs:: rename ( & tmp, self . state_file ( ) )
179+ . with_context ( || format ! ( "rename {:?} -> {:?}" , tmp, self . state_file( ) ) )
167180 }
168181
169182 /// Load state from disk.
0 commit comments