0.31.0: hardening and correctness pass across mux, codec, AACS/CSS, UDF/MPLS/CLPI, recovery, drive/SCSI, labels, and I/O
Library-wide review-and-fix pass: tightened AACS keydb/handshake/variant handling and trailing-partial-unit policy, corrected MPLS mark offset and added UDF allocation bounds, hardened the mux/codec framing and M2TS paths, guarded SCSI READ CAPACITY short transfers and unified error mapping, added overflow guards on untrusted disc input, and made prefetch shutdown deterministic. Release profile now builds with thin LTO + single codegen unit.
This commit is contained in:
@@ -19,14 +19,10 @@ use std::time::Duration;
|
||||
pub(super) fn preallocate(file: &File, size_bytes: u64) {
|
||||
// FALLOC_FL_KEEP_SIZE = 0x01 — keep the reported file size at 0
|
||||
// (writes grow it normally) while still pre-reserving the extents.
|
||||
let rc = unsafe {
|
||||
libc::fallocate(
|
||||
file.as_raw_fd(),
|
||||
libc::FALLOC_FL_KEEP_SIZE,
|
||||
0,
|
||||
size_bytes as i64,
|
||||
)
|
||||
};
|
||||
// Clamp to the signed `off_t` range; an unchecked `as i64` cast
|
||||
// would wrap a >= 2^63 size to a negative length (EINVAL no-op).
|
||||
let len = i64::try_from(size_bytes).unwrap_or(i64::MAX);
|
||||
let rc = unsafe { libc::fallocate(file.as_raw_fd(), libc::FALLOC_FL_KEEP_SIZE, 0, len) };
|
||||
tracing::debug!(
|
||||
target: "mux",
|
||||
"WritebackFile fallocate size_hint={size_bytes} rc={rc} ok={}",
|
||||
@@ -34,10 +30,14 @@ pub(super) fn preallocate(file: &File, size_bytes: u64) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Run `fsync` on `file` with a 60 s deadline. On timeout we log loudly
|
||||
/// and return `Ok(())` — the kernel will still flush on close, so the
|
||||
/// data is best-effort durable; the alternative (trap the thread for
|
||||
/// the rest of the rip) defeats `/api/stop`.
|
||||
/// Run `fsync` on `file` with a 60 s deadline. On timeout — and
|
||||
/// likewise on halt or a lost worker — we log and return `Ok(())`: the
|
||||
/// kernel will still flush on close, so the data is best-effort durable.
|
||||
/// The alternative (trap the thread for the rest of the rip, or return
|
||||
/// an error that aborts an otherwise-complete mux) is worse, so all
|
||||
/// three fallbacks return `Ok(())`. `Ok(())` from these paths is NOT a
|
||||
/// durability barrier — the durable flush did not complete; only the
|
||||
/// hang is bounded.
|
||||
pub(super) fn durable_sync(file: &File) -> io::Result<()> {
|
||||
let fd = file.as_raw_fd();
|
||||
match crate::io::bounded::bounded_syscall(
|
||||
@@ -60,7 +60,19 @@ pub(super) fn durable_sync(file: &File) -> io::Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(crate::io::bounded::BoundedError::Halted) => Ok(()),
|
||||
Err(crate::io::bounded::BoundedError::WorkerLost) => Ok(()),
|
||||
Err(crate::io::bounded::BoundedError::Halted) => {
|
||||
tracing::warn!(
|
||||
target: "mux",
|
||||
"WritebackFile::sync_all fsync skipped (halt requested); data not durably flushed, kernel will flush on close"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(crate::io::bounded::BoundedError::WorkerLost) => {
|
||||
tracing::error!(
|
||||
target: "mux",
|
||||
"WritebackFile::sync_all fsync worker lost before completion; data not durably flushed, kernel will flush on close"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
//! macOS platform impl for [`super::WritebackFile`].
|
||||
//!
|
||||
//! - `preallocate`: `fcntl(F_PREALLOCATE)` — macOS's fallocate-equiv.
|
||||
//! Reserves a contiguous extent when possible, falling back to a
|
||||
//! non-contiguous reservation if the FS can't satisfy it. Reported
|
||||
//! file size is unchanged (`F_ALLOCATEALL` is not set, so allocation
|
||||
//! is "best effort up to length"; growth happens via writes).
|
||||
//! First attempt requests `F_ALLOCATECONTIG | F_ALLOCATEALL` (prefer a
|
||||
//! contiguous run but accept scattered extents to satisfy the full
|
||||
//! length), falling back to `F_ALLOCATEALL` alone on failure.
|
||||
//! `F_PREALLOCATE` never advances EOF regardless of the flags — only
|
||||
//! `ftruncate`/writes grow the file — so the reported file size is
|
||||
//! unchanged; `F_ALLOCATEALL` governs the contiguity fallback, not size.
|
||||
//! - `durable_sync`: `fcntl(F_FULLFSYNC)` wrapped in
|
||||
//! [`crate::io::bounded::bounded_syscall`] with a 60 s deadline.
|
||||
//! F_FULLFSYNC is HFS+/APFS's true-fsync (flushes the disk's own
|
||||
@@ -25,11 +27,14 @@ use crate::io::platform_macos::{
|
||||
const F_FULLFSYNC: libc::c_int = 51;
|
||||
|
||||
pub(super) fn preallocate(file: &File, size_bytes: u64) {
|
||||
// Clamp to the signed `off_t` range; an unchecked `as off_t` cast
|
||||
// would wrap a >= 2^63 size to a negative length.
|
||||
let len = i64::try_from(size_bytes).unwrap_or(i64::MAX) as libc::off_t;
|
||||
let mut fst = Fstore {
|
||||
fst_flags: F_ALLOCATECONTIG | F_ALLOCATEALL,
|
||||
fst_posmode: F_PEOFPOSMODE,
|
||||
fst_offset: 0,
|
||||
fst_length: size_bytes as libc::off_t,
|
||||
fst_length: len,
|
||||
fst_bytesalloc: 0,
|
||||
};
|
||||
// First attempt: contiguous.
|
||||
|
||||
@@ -24,29 +24,32 @@
|
||||
//! ## Platform split
|
||||
//!
|
||||
//! The platform-specific pieces of this wrapper — extent preallocation
|
||||
//! (Linux `fallocate(KEEP_SIZE)`, macOS `F_PREALLOCATE`, Windows
|
||||
//! `SetFileValidData`) and the durable-flush primitive (Linux/macOS
|
||||
//! `fsync`/`F_FULLFSYNC` wrapped in a bounded syscall, Windows
|
||||
//! `FlushFileBuffers`) — live in per-OS sibling modules. The dispatch
|
||||
//! happens once at the bottom of this file via cfg-gated `mod` decls.
|
||||
//! No inline `#[cfg(target_os = "...")]` in the business-logic above.
|
||||
//! (Linux `fallocate(KEEP_SIZE)`, macOS `F_PREALLOCATE`, Windows no-op
|
||||
//! today) and the durable-flush primitive (Linux/macOS
|
||||
//! `fsync`/`F_FULLFSYNC` wrapped in a bounded syscall; Windows plain
|
||||
//! `FlushFileBuffers`, unbounded) — live in per-OS sibling modules. The
|
||||
//! dispatch happens once at the bottom of this file via cfg-gated `mod`
|
||||
//! decls. No inline `#[cfg(target_os = "...")]` in the business-logic
|
||||
//! above.
|
||||
//!
|
||||
//! ## Write path
|
||||
//!
|
||||
//! Writes are direct passthrough to the underlying `File` (no writer
|
||||
//! thread, no ring, no batching). Empirically the Phase-2.5
|
||||
//! writer-thread architecture introduced a ~60% mux throughput
|
||||
//! regression on NFS bidirectional workloads; reverting the write path
|
||||
//! to direct passthrough restores the 0.20.7 baseline. The writeback
|
||||
//! pipeline still runs (it's called inline from `write` / `write_all` /
|
||||
//! `seek`) so the bounded-cache invariant on Linux is preserved.
|
||||
//! thread, no ring, no batching). Empirically a writer-thread
|
||||
//! architecture introduced a ~60% mux throughput regression on NFS
|
||||
//! bidirectional workloads; the direct-passthrough write path is faster.
|
||||
//! The writeback pipeline still runs (it's called inline from `write` /
|
||||
//! `write_all` / `seek`) so the bounded-cache invariant on Linux is
|
||||
//! preserved.
|
||||
//!
|
||||
//! ## Halt-safety
|
||||
//!
|
||||
//! `sync_all` runs the per-OS durable-flush primitive, which on
|
||||
//! Linux/macOS is wrapped in [`crate::io::bounded::bounded_syscall`]
|
||||
//! with a 60 s deadline. A wedged NFS server cannot trap the muxer
|
||||
//! indefinitely on the final fsync.
|
||||
//! `sync_all` runs the per-OS durable-flush primitive. On Linux/macOS
|
||||
//! it is wrapped in [`crate::io::bounded::bounded_syscall`] with a 60 s
|
||||
//! deadline, so a wedged NFS server cannot trap the muxer indefinitely
|
||||
//! on the final fsync. Windows is a known deviation: its `durable_sync`
|
||||
//! calls `File::sync_all` (`FlushFileBuffers`) directly and is NOT
|
||||
//! bounded — a wedged UNC/SMB share can block the final flush there.
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
@@ -73,18 +76,23 @@ use std::path::Path;
|
||||
use super::writeback::WritebackPipeline;
|
||||
|
||||
/// Granularity at which the Linux writeback pipeline issues
|
||||
/// `sync_file_range` pairs. 32 MiB is the empirically best value on
|
||||
/// the rip1 test bed (NFS to unraid-1 over 1 GbE, single-disk SAS):
|
||||
/// 8 MiB / 64 MiB / 128 MiB all measured worse in the 0.21.x mux
|
||||
/// iteration runs. Override via `FREEMKV_WRITEBACK_CHUNK_MIB` —
|
||||
/// faster backends (NVMe, RAID) may tolerate larger windows.
|
||||
/// `sync_file_range` pairs. 32 MiB is the empirically best value on a
|
||||
/// 1 GbE NFS mount backed by a single spinning disk: 8 MiB / 64 MiB /
|
||||
/// 128 MiB all measured worse. Override via `FREEMKV_WRITEBACK_CHUNK_MIB`
|
||||
/// — faster backends (NVMe, RAID) may tolerate larger windows.
|
||||
const WRITEBACK_CHUNK_BYTES_DEFAULT: u64 = 32 * 1024 * 1024;
|
||||
|
||||
/// Upper bound (in MiB) accepted from `FREEMKV_WRITEBACK_CHUNK_MIB`.
|
||||
/// 64 GiB — far above `CHUNK_BYTES_MAX` (256 MiB), generous for any
|
||||
/// real backend, and small enough that `n * 1024 * 1024` cannot wrap
|
||||
/// `u64`. Out-of-range values fall back to the default.
|
||||
const WRITEBACK_CHUNK_MIB_MAX: u64 = 64 * 1024;
|
||||
|
||||
fn writeback_chunk_bytes() -> u64 {
|
||||
std::env::var("FREEMKV_WRITEBACK_CHUNK_MIB")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.filter(|&n| n > 0)
|
||||
.filter(|&n| n > 0 && n <= WRITEBACK_CHUNK_MIB_MAX)
|
||||
.map(|n| n * 1024 * 1024)
|
||||
.unwrap_or(WRITEBACK_CHUNK_BYTES_DEFAULT)
|
||||
}
|
||||
@@ -160,6 +168,13 @@ impl WritebackFile {
|
||||
/// trap the calling thread indefinitely. On timeout the page cache
|
||||
/// is left to the kernel's normal flush-on-close path — best
|
||||
/// effort, but bounded.
|
||||
///
|
||||
/// IMPORTANT: on Linux/macOS a successful `Ok(())` does NOT
|
||||
/// guarantee the data is durable if the bounded fsync timed out or
|
||||
/// was halted — only the hang is bounded, the fsync may not have
|
||||
/// completed. Callers needing crash-consistency (e.g. mux-finish
|
||||
/// then external commit/DB update) must not treat `Ok(())` as a
|
||||
/// durability barrier.
|
||||
pub(crate) fn sync_all(&mut self) -> io::Result<()> {
|
||||
self.pipeline.finalize();
|
||||
platform::durable_sync(&self.file)
|
||||
@@ -215,6 +230,21 @@ impl Seek for WritebackFile {
|
||||
}
|
||||
}
|
||||
|
||||
impl super::sink::SequentialSink for WritebackFile {
|
||||
/// Drain the writeback pipeline and run the bounded durable flush —
|
||||
/// the same work [`Self::sync_all`] does. Implemented explicitly (no
|
||||
/// blanket impl) so a `dyn SequentialSink` / `dyn RandomAccessSink`
|
||||
/// `finish()` actually finalises + fsyncs instead of hitting a no-op
|
||||
/// default. Note the bounded-fsync caveat from [`Self::sync_all`]
|
||||
/// applies: `Ok(())` is not a durability barrier if the fsync timed
|
||||
/// out or was halted.
|
||||
fn finish(&mut self) -> io::Result<()> {
|
||||
self.sync_all()
|
||||
}
|
||||
}
|
||||
|
||||
impl super::sink::RandomAccessSink for WritebackFile {}
|
||||
|
||||
impl Drop for WritebackFile {
|
||||
fn drop(&mut self) {
|
||||
// Run the pipeline's tail finalize so the last in-flight chunk
|
||||
@@ -312,4 +342,20 @@ mod tests {
|
||||
drop(w);
|
||||
assert_eq!(read_back(&p), b"onetwothree");
|
||||
}
|
||||
|
||||
/// finish() through a `dyn RandomAccessSink` trait object must
|
||||
/// dispatch to WritebackFile's override (finalize + durable_sync),
|
||||
/// not a no-op default. Bytes must be visible to a separate reader
|
||||
/// before drop.
|
||||
#[test]
|
||||
fn finish_through_trait_object_persists() {
|
||||
use crate::io::sink::RandomAccessSink;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p = dir.path().join("finish-dyn.bin");
|
||||
let w = WritebackFile::create(&p).unwrap();
|
||||
let mut boxed: Box<dyn RandomAccessSink> = Box::new(w);
|
||||
boxed.write_all(b"durable-tail").unwrap();
|
||||
boxed.finish().unwrap();
|
||||
assert_eq!(read_back(&p), b"durable-tail");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
//! Windows platform impl for [`super::WritebackFile`].
|
||||
//!
|
||||
//! TODO: this stub matches the design's "validate without a Windows
|
||||
//! build env, leave a stub" carve-out. The real impl should use:
|
||||
//! Current behaviour:
|
||||
//!
|
||||
//! - `SetEndOfFile` + `SetFileValidData` for extent preallocation
|
||||
//! (caller needs `SE_MANAGE_VOLUME_NAME` privilege; if unavailable
|
||||
//! fall back to a write-zero path or just skip).
|
||||
//! - `FlushFileBuffers` for fsync-equivalent durable flush.
|
||||
//!
|
||||
//! Until then: preallocate is a debug-logged no-op; durable_sync calls
|
||||
//! the std `File::sync_all` (which on Windows maps to
|
||||
//! `FlushFileBuffers` internally).
|
||||
//! - `preallocate` is a debug-logged no-op. Windows has no
|
||||
//! `fallocate`-equivalent that keeps the reported size, so extent
|
||||
//! reservation is not wired up.
|
||||
//! - `durable_sync` delegates to the std `File::sync_all`, which on
|
||||
//! Windows maps to `FlushFileBuffers`. Unlike the Linux/macOS impls
|
||||
//! this is NOT wrapped in the bounded-syscall primitive (that would
|
||||
//! need an `unsafe impl Send` for `RawHandle`, which cannot be
|
||||
//! validated without a Windows test env), so a wedged UNC/SMB share
|
||||
//! can block the final flush. This deviation is documented on
|
||||
//! [`super::WritebackFile::sync_all`] and the parent module's
|
||||
//! Halt-safety section.
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
@@ -18,15 +20,12 @@ use std::io;
|
||||
pub(super) fn preallocate(_file: &File, size_bytes: u64) {
|
||||
tracing::debug!(
|
||||
target: "mux",
|
||||
"WritebackFile preallocate size_hint={size_bytes} skipped (windows stub; TODO: SetFileValidData)"
|
||||
"WritebackFile preallocate size_hint={size_bytes} skipped (no-op on windows)"
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn durable_sync(file: &File) -> io::Result<()> {
|
||||
// `File::sync_all` on Windows is `FlushFileBuffers`. Acceptable
|
||||
// for now; the bounded-syscall wrapper is not used here because
|
||||
// the stub also skips the worker-thread + leak machinery (the
|
||||
// wrapper would need an `unsafe impl Send` for `RawHandle`, and
|
||||
// designing that without a Windows test env is asking for it).
|
||||
// `File::sync_all` on Windows is `FlushFileBuffers`. Not wrapped in
|
||||
// the bounded-syscall primitive (see the module doc) — unbounded.
|
||||
file.sync_all()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user