fix(extract): anchor AACS unit base per-extent + crash-safety hardening
The dir:// extractor set the AACS unit-alignment base ONCE to the first extent's start, then read every extent against that single base. For a multi-extent (fragmented / Long-AD / continuation-ICB) file the second and later extents start at arbitrary LBAs whose offset from the first extent is generally not a multiple of 3 sectors, so the first read of each later extent failed the decrypt-on-read gate (is_unit_aligned(lba, unit_base)), returned DecryptFailed, and recorded the whole extent as a zero-filled hole even though the data was readable. Re-anchor the unit base PER extent (matching mux/disc.rs and sector/prefetched.rs), so each clip's encrypted region gates on its own unit grid. Same bug class as the rc.5.2 clip-anchor fix. Also harden the extract + keydb write paths: - finalize_file: fsync the .partial after set_len (the truncation runs on a second handle the content fsync never touched) and fsync the parent dir after rename so the new dirent is crash-durable. - keydb write_atomic: fsync the parent dir after rename (POSIX dirent durability), matching the finalize_file pattern. - AACS tail batch: document that decrypt_sectors' trailing-partial contract already handles the short final unit; no math change. - decrypt-loss delta loads use Acquire (defensive happens-before if file extraction is ever parallelised). - is_windows_reserved: add CONIN$/CONOUT$/CLOCK$; reserved names are now substituted (prefix _) instead of aborting the whole tree walk, so a legal Linux-authored NUL.cfg extracts. - http_get header cap: >= MAX_HEADER_BYTES (was > , one byte over). Regression tests: multi-extent AACS file (Δ4-sector extents) extracts both extents with zero loss; focused per-extent alignment-arithmetic test; reserved-name substitution assertions.
This commit is contained in:
+10
-2
@@ -176,6 +176,12 @@ fn write_atomic(path: &std::path::Path, text: &str) -> Result<()> {
|
||||
tracing::warn!(error = %e, path = %path.display(), "keydb rename failed; keydb unchanged");
|
||||
return Err(werr());
|
||||
}
|
||||
// Durably commit the new dirent: on POSIX filesystems (ext2, some NFS) a
|
||||
// crash right after the rename can lose the directory entry even though the
|
||||
// rename returned. Best-effort (swallowed on failure); no-op on Windows.
|
||||
if let Some(dir) = path.parent() {
|
||||
crate::io::fsync::dir(dir);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -241,9 +247,11 @@ fn http_get(url: &str) -> Result<Vec<u8>> {
|
||||
if header_buf.ends_with(b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
if header_buf.len() > MAX_HEADER_BYTES {
|
||||
if header_buf.len() >= MAX_HEADER_BYTES {
|
||||
// Oversized header block from the server: a protocol-level
|
||||
// fault, not a keydb content parse failure.
|
||||
// fault, not a keydb content parse failure. `>=` caps the
|
||||
// buffer at exactly MAX_HEADER_BYTES (the `>` form admitted one
|
||||
// extra byte before tripping).
|
||||
return Err(Error::KeydbConnect { host: host.clone() });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user