-
Notifications
You must be signed in to change notification settings - Fork 6
fix(daemon): close socket-group and pointer-rename races #1819
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
740052a
5e03dfa
cc10394
8b75523
e2384db
f2ab808
f77e763
53d7f9d
b130bce
63c0784
6d8ef97
b05cab2
6099dda
de5f920
aab865a
15f25b3
f0150c7
bf444b5
ed6d8f9
d59b7eb
8889736
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1230,38 +1230,94 @@ impl DaemonCodeIndexPublicationStoreV1 { | |
| "durable code-generation index exceeds its retention bounds", | ||
| )); | ||
| } | ||
| *self | ||
| let mut memo = self | ||
| .pointer_memo | ||
| .lock() | ||
| .unwrap_or_else(PoisonError::into_inner) = Some(PublicationPointerMemoV1 { | ||
| mtime, | ||
| size, | ||
| digest, | ||
| pointer: pointer.clone(), | ||
| }); | ||
| .unwrap_or_else(PoisonError::into_inner); | ||
| // Install only when the file is still the bytes just parsed. A rename | ||
| // that landed during validation owns the memo. | ||
| if std::fs::read(&self.active_path).ok().as_deref() == Some(bytes.as_slice()) { | ||
| *memo = Some(PublicationPointerMemoV1 { | ||
| mtime, | ||
| size, | ||
| digest, | ||
| pointer: pointer.clone(), | ||
| }); | ||
| } | ||
| Ok(Some(pointer)) | ||
| } | ||
|
|
||
| fn remember_publication_pointer(&self, pointer: &DurablePublicationPointerV1, bytes: &[u8]) { | ||
| let metadata = match std::fs::metadata(&self.active_path) { | ||
| Ok(metadata) => metadata, | ||
| Err(_) => { | ||
| *self | ||
| .pointer_memo | ||
| .lock() | ||
| .unwrap_or_else(PoisonError::into_inner) = None; | ||
| return; | ||
| } | ||
| }; | ||
| *self | ||
| let mut memo = self | ||
| .pointer_memo | ||
| .lock() | ||
| .unwrap_or_else(PoisonError::into_inner) = Some(PublicationPointerMemoV1 { | ||
| mtime: metadata.modified().ok(), | ||
| size: metadata.len(), | ||
| digest: Self::state_digest(bytes), | ||
| pointer: pointer.clone(), | ||
| }); | ||
| .unwrap_or_else(PoisonError::into_inner); | ||
| // The memo and the file it names are one critical section. A publisher | ||
| // that observed older bytes must not install them over a newer file. | ||
| match std::fs::read(&self.active_path) { | ||
| Ok(current) if current == bytes => { | ||
| let metadata = std::fs::metadata(&self.active_path).ok(); | ||
| *memo = Some(PublicationPointerMemoV1 { | ||
| mtime: metadata | ||
| .as_ref() | ||
| .and_then(|metadata| metadata.modified().ok()), | ||
| size: metadata.map_or(0, |metadata| metadata.len()), | ||
| digest: Self::state_digest(bytes), | ||
| pointer: pointer.clone(), | ||
| }); | ||
| } | ||
| Ok(_) => {} | ||
| Err(_) => *memo = None, | ||
| } | ||
| } | ||
|
|
||
| /// Replace the active pointer only when it is still the exact bytes this | ||
| /// publication observed under the store lock. | ||
| /// | ||
| /// `rename(2)` replaces whatever occupies the path, including a truncated | ||
| /// or rewritten pointer. The observation is the compare-and-swap token: | ||
| /// a mismatch is a refusal, not a rewrite. `lock` is the witness that | ||
| /// this critical section is the exclusive owner of the store. | ||
| pub(super) fn commit_observed_pointer( | ||
| &self, | ||
| _lock: &CodeGenerationStoreLockV1, | ||
| observed: Option<&[u8]>, | ||
| pointer: &DurablePublicationPointerV1, | ||
| bytes: &[u8], | ||
| ) -> Result<(), CodeIndexPublicationStoreErrorV1> { | ||
| let current = match std::fs::read(&self.active_path) { | ||
| Ok(current) => Some(current), | ||
| Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, | ||
| Err(error) => return Err(Self::unavailable(error)), | ||
| }; | ||
| if current.as_deref() != observed { | ||
| return Err(match current { | ||
| Some(current) | ||
| if serde_json::from_slice::<DurablePublicationPointerV1>(¤t).is_err() => | ||
| { | ||
| Self::corruption("active code-generation pointer is corrupt") | ||
| } | ||
| _ => CodeIndexPublicationStoreErrorV1::CompareAndSwap, | ||
| }); | ||
| } | ||
| let temporary = self | ||
| .active_path | ||
| .with_extension(format!("json.{}.tmp", std::process::id())); | ||
| if temporary.exists() { | ||
| std::fs::remove_file(&temporary).map_err(Self::unavailable)?; | ||
| } | ||
| Self::write_durable(&temporary, bytes)?; | ||
| if let Err(error) = std::fs::rename(&temporary, &self.active_path) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a fault injector or writer bypassing the store lock changes the pointer after the read at line 1288 but before this rename, the equality decision is already stale and this rename still overwrites the changed file. Consequently AGENTS.md reference: AGENTS.md:L124-L131 Useful? React with 👍 / 👎. |
||
| let _ = std::fs::remove_file(&temporary); | ||
| return Err(Self::unavailable(error)); | ||
| } | ||
| Self::sync_directory( | ||
| self.active_path | ||
| .parent() | ||
| .ok_or_else(|| Self::unavailable("active pointer has no parent directory"))?, | ||
| )?; | ||
| self.remember_publication_pointer(pointer, bytes); | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub(super) fn read_retained_partitioned_segment( | ||
|
|
@@ -2174,6 +2230,15 @@ impl CodeIndexAtomicPublicationPort for DaemonCodeIndexPublicationStoreV1 { | |
| } else { | ||
| self.read_publication_pointer()? | ||
| }; | ||
| // The bytes behind `prior_pointer`, captured under the store lock. | ||
| // The commit below refuses to rename unless the file is still these | ||
| // exact bytes, so a pointer that changed after this observation is | ||
| // not overwritten. | ||
| let prior_bytes = if prior_pointer.is_some() { | ||
| Some(std::fs::read(&self.active_path).map_err(Self::unavailable)?) | ||
| } else { | ||
|
Comment on lines
+2237
to
+2239
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a fault injector or non-cooperating writer changes the active file between AGENTS.md reference: AGENTS.md:L177-L178 Useful? React with 👍 / 👎. |
||
| None | ||
| }; | ||
| if undecoded_expectation.is_none() | ||
| && prior_pointer | ||
| .as_ref() | ||
|
|
@@ -2551,22 +2616,8 @@ impl CodeIndexAtomicPublicationPort for DaemonCodeIndexPublicationStoreV1 { | |
| } else { | ||
| None | ||
| }; | ||
| let temporary = self | ||
| .active_path | ||
| .with_extension(format!("json.{}.tmp", std::process::id())); | ||
| if temporary.exists() { | ||
| std::fs::remove_file(&temporary).map_err(Self::unavailable)?; | ||
| } | ||
| hotpath::measure_block!("code_index.generation.publish.pointer_commit", { | ||
| Self::write_durable(&temporary, &bytes)?; | ||
| std::fs::rename(&temporary, &self.active_path).map_err(Self::unavailable)?; | ||
| Self::sync_directory( | ||
| self.active_path | ||
| .parent() | ||
| .ok_or_else(|| Self::unavailable("active pointer has no parent directory"))?, | ||
| )?; | ||
| self.remember_publication_pointer(&pointer, &bytes); | ||
| Ok::<(), CodeIndexPublicationStoreErrorV1>(()) | ||
| self.commit_observed_pointer(&_store_lock, prior_bytes.as_deref(), &pointer, &bytes) | ||
| })?; | ||
| drop(source_fence); | ||
| let mut state = self.cache.lock_state()?; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the active pointer is replaced or truncated after the re-read at line 204 but before
atomic_writeperforms its rename, this write still replaces that intervening state. Re-reading immediately before publication only narrows the race and does not provide the claimed refusal semantics; route this through the repository's conditional atomic-write authority and verify the displaced bytes against the original observation.AGENTS.md reference: AGENTS.md:L124-L131
Useful? React with 👍 / 👎.