fix(store): truncating an append-only log goes through a write handle on Windows (P11c)
std's append(true) opens without FILE_WRITE_DATA, so SetEndOfFile on that handle is refused (PermissionDenied) on Windows: the torn-line cut on open (chain::tests::an_unfinished_last_line_is_cut_away_on_open) and the roll-back of a failed append (chain and journal) could not truncate. Not a rename/share-delete problem: std already opens with all three sharing flags. chain::truncate cuts through a short-lived write handle on Windows and the handle itself elsewhere; the journal's roll-back uses it too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
1 parent
73804ac8ba
commit
b96735da01
3 files changed
+33
-6
No files matched your search
@@ -211,8 +211,7 @@ impl ChainLog {
|
|||||||
crate::winsec::set_owner_only(path)?;
|
crate::winsec::set_owner_only(path)?;
|
||||||
}
|
}
|
||||||
if read.torn {
|
if read.torn {
|
||||||
file.set_len(read.good_len)?;
|
truncate(&file, path, read.good_len)?;
|
||||||
file.sync_all()?;
|
|
||||||
}
|
}
|
||||||
// A log just made is in its folder for good (its entry synced).
|
// A log just made is in its folder for good (its entry synced).
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -280,8 +279,7 @@ impl ChainLog {
|
|||||||
.and_then(|()| self.file.sync_data());
|
.and_then(|()| self.file.sync_data());
|
||||||
if let Err(e) = written {
|
if let Err(e) = written {
|
||||||
self.broken = true;
|
self.broken = true;
|
||||||
let _ = self.file.set_len(before);
|
let _ = truncate(&self.file, &self.path, before);
|
||||||
let _ = self.file.sync_all();
|
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
self.head = Head { seq, hash };
|
self.head = Head { seq, hash };
|
||||||
@@ -289,6 +287,28 @@ impl ChainLog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cut the log `path` (open as `file`) to `len` bytes and sync it. On
|
||||||
|
/// Windows an append-only handle (which is what `append(true)` opens) has
|
||||||
|
/// no right to truncate, so a short-lived handle with write access does it;
|
||||||
|
/// the log is opened with the sharing std gives every handle (read, write
|
||||||
|
/// and delete), so both can be open at once.
|
||||||
|
pub(crate) fn truncate(file: &File, path: &Path, len: u64) -> std::io::Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let cutter = OpenOptions::new().write(true).open(path)?;
|
||||||
|
cutter.set_len(len)?;
|
||||||
|
cutter.sync_all()?;
|
||||||
|
let _ = file;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let _ = path;
|
||||||
|
file.set_len(len)?;
|
||||||
|
file.sync_all()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -228,8 +228,7 @@ impl JournalWriter {
|
|||||||
.and_then(|()| self.file.write_all(b"\n"))
|
.and_then(|()| self.file.write_all(b"\n"))
|
||||||
.and_then(|()| self.file.sync_all());
|
.and_then(|()| self.file.sync_all());
|
||||||
if let Err(e) = written {
|
if let Err(e) = written {
|
||||||
let _ = self.file.set_len(before);
|
let _ = crate::chain::truncate(&self.file, &self.path, before);
|
||||||
let _ = self.file.sync_all();
|
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
self.prev_hash = sha256_hex(bytes.as_bytes());
|
self.prev_hash = sha256_hex(bytes.as_bytes());
|
||||||
|
|||||||
@@ -213,4 +213,12 @@ not a `cfg` gap. Today Unix-only, found by grep (2026-10-29):
|
|||||||
host's `close_range` stay Linux-only on purpose (D-32: nothing here
|
host's `close_range` stay Linux-only on purpose (D-32: nothing here
|
||||||
confines a process, so nothing is promised where they are absent).
|
confines a process, so nothing is promised where they are absent).
|
||||||
macOS, which has no `renameat2`, still looks before it moves.
|
macOS, which has no `renameat2`, still looks before it moves.
|
||||||
|
- Windows file semantics the port met (P11c): an `append(true)` handle has
|
||||||
|
no right to truncate, so the chain log and the journal cut a torn tail
|
||||||
|
or roll back a failed write through a short-lived write handle
|
||||||
|
(`chain::truncate`); a held `LockFileEx` lock is mandatory, so locks sit
|
||||||
|
on dedicated `*.lock` files and tests never read those; git refuses the
|
||||||
|
`\\?\` path `canonicalize` returns (`root::plain_path`). std opens every
|
||||||
|
file with read, write and delete sharing, so a rename never meets an
|
||||||
|
open handle of ours.
|
||||||
- The `portable` job runs Windows as a gate (`experimental: false`), since the port landed (P11c).
|
- The `portable` job runs Windows as a gate (`experimental: false`), since the port landed (P11c).
|
||||||
Reference in new issue
Block a user