rc2: macOS cross-compile fix + security/recovery hardening
- build.rs: pass target -arch to cc so macos_shim cross-compiles (x86_64-apple-darwin) - AACS/CSS: unit-aligned decrypting sweep; per-VTS CSS title keys (hard-fail on wrong VTS); reject truncated Unit_Key_RO; AACS 2.0 sig-verify skip; CSS bus-auth random nonce - recovery: gap-filling mapfile load; sweep/copy resume reconciliation; stale-mapfile abort; patch wedge/damage-window range reset - mux: TS continuity + PSI CC desync guards; HEVC numTemporalLayers clamp; MPEG-2 pending byte-cap; PS parse_pts marker-bit validation; HdrFormat strict parse; Unknown-variant metadata - net/keydb: network:// SSRF parity (IPv4-mapped, CGNAT, 0.0.0.0/8, Class-E); bounded keydb header read + size cap + error context - io: durable mapfile fsync; NFS writeback degrade; sync_file_range error capture; Windows SCSI u32 transfer guard
This commit is contained in:
+56
-9
@@ -112,15 +112,23 @@ pub(super) struct SharedPatchState {
|
||||
}
|
||||
|
||||
impl SharedPatchState {
|
||||
/// Cap on the republished `bad_ranges` Vec. Consumers (progress display,
|
||||
/// scheduler) only sample the head of the list; the full set is bounded by
|
||||
/// the mapfile entry cap so a pathologically fragmented disc can't make
|
||||
/// every per-record republish allocate unboundedly.
|
||||
const MAX_BAD_RANGES: usize = 8192;
|
||||
|
||||
fn from_map(map: &Mapfile) -> Self {
|
||||
let mut bad_ranges = map.ranges_with(&[
|
||||
SectorStatus::NonTrimmed,
|
||||
SectorStatus::Unreadable,
|
||||
SectorStatus::NonScraped,
|
||||
SectorStatus::NonTried,
|
||||
]);
|
||||
bad_ranges.truncate(Self::MAX_BAD_RANGES);
|
||||
Self {
|
||||
stats: map.stats(),
|
||||
bad_ranges: map.ranges_with(&[
|
||||
SectorStatus::NonTrimmed,
|
||||
SectorStatus::Unreadable,
|
||||
SectorStatus::NonScraped,
|
||||
SectorStatus::NonTried,
|
||||
]),
|
||||
bad_ranges,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,8 +157,15 @@ pub(super) struct PatchSink {
|
||||
/// `record()` call. `Mutex` rather than separate atomics because
|
||||
/// the producer wants stats + bad_ranges as a coherent pair.
|
||||
shared: Arc<Mutex<SharedPatchState>>,
|
||||
/// Last time the shared snapshot was republished. `from_map` allocates
|
||||
/// O(bad_ranges) every call, so the per-record path throttles to a time
|
||||
/// cadence (`REPUBLISH_CADENCE`); the final close always forces a publish.
|
||||
last_republish: Option<std::time::Instant>,
|
||||
}
|
||||
|
||||
/// Minimum interval between per-record snapshot republishes.
|
||||
const REPUBLISH_CADENCE: std::time::Duration = std::time::Duration::from_millis(250);
|
||||
|
||||
impl PatchSink {
|
||||
/// Open `path` as a [`crate::io::WritebackFile`] and pair it with
|
||||
/// `map` for the consumer. The producer holds onto the returned
|
||||
@@ -171,12 +186,29 @@ impl PatchSink {
|
||||
map,
|
||||
is_regular,
|
||||
shared,
|
||||
last_republish: None,
|
||||
},
|
||||
shared_clone,
|
||||
))
|
||||
}
|
||||
|
||||
fn republish(&self) {
|
||||
/// Republish the shared snapshot. When `force` is false the update is
|
||||
/// throttled to `REPUBLISH_CADENCE`; `force` (used at close) always
|
||||
/// publishes the final state.
|
||||
fn republish(&mut self, force: bool) {
|
||||
let now = std::time::Instant::now();
|
||||
if !force {
|
||||
if let Some(prev) = self.last_republish {
|
||||
if now.duration_since(prev) < REPUBLISH_CADENCE {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.last_republish = Some(now);
|
||||
self.publish_now();
|
||||
}
|
||||
|
||||
fn publish_now(&self) {
|
||||
// Best-effort lock — only the producer reads, only the consumer
|
||||
// writes; contention is single-acquire so the lock is never
|
||||
// poisoned in practice. If it ever did get poisoned we'd want
|
||||
@@ -220,7 +252,7 @@ impl Sink<PatchItem> for PatchSink {
|
||||
.map_err(|e| Error::IoError { source: e })?;
|
||||
}
|
||||
}
|
||||
self.republish();
|
||||
self.republish(false);
|
||||
Ok(Flow::Continue)
|
||||
}
|
||||
|
||||
@@ -254,7 +286,7 @@ impl Sink<PatchItem> for PatchSink {
|
||||
// returned `PatchSummary`, but the snapshot is part of the
|
||||
// public-ish contract of the consumer: it stays current
|
||||
// through close.)
|
||||
self.republish();
|
||||
self.republish(true);
|
||||
Ok(PatchSummary {
|
||||
stats: self.map.stats(),
|
||||
})
|
||||
@@ -824,6 +856,15 @@ pub(super) fn handle_read_success<R: SectorSource + ?Sized>(
|
||||
}
|
||||
Err(_err) => {
|
||||
state.blocks_read_failed += 1;
|
||||
// Feed the damage window / wedge counter exactly as the
|
||||
// main loop does, so a string of backtrack failures
|
||||
// escalates skip distance and trips wedge detection
|
||||
// rather than being silently under-counted.
|
||||
state.damage_window.push(false);
|
||||
if state.damage_window.len() > PASSN_DAMAGE_WINDOW {
|
||||
state.damage_window.remove(0);
|
||||
}
|
||||
state.consecutive_failures += 1;
|
||||
// Leave NonTrimmed (not Unreadable) so a
|
||||
// later pass gets another shot. Per the
|
||||
// project goal — "recover 100% of readable
|
||||
@@ -1763,6 +1804,12 @@ impl Disc {
|
||||
g.stats.bytes_good
|
||||
};
|
||||
state.skip_count = 0;
|
||||
// Reset the wedge counter at each range boundary too. Like
|
||||
// consecutive_failures below, wedge_count is a "stuck on THIS
|
||||
// range" signal; carrying it across boundaries lets wedges
|
||||
// accumulated on earlier ranges trip WEDGE_ABORT_THRESHOLD
|
||||
// prematurely on a later, healthy range.
|
||||
state.wedge_count = 0;
|
||||
// Reset consecutive_failures at each range boundary. The
|
||||
// wedge-exit detector is for "stuck on the same range" — many
|
||||
// tiny ranges that each fail their one sampled sector should
|
||||
|
||||
Reference in New Issue
Block a user