Skip to content

Commit 4585205

Browse files
committed
sandlock-oci: write state.json atomically to avoid torn concurrent reads
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent db3c3d0 commit 4585205

1 file changed

Lines changed: 15 additions & 2 deletions

File tree

crates/sandlock-oci/src/state.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)