Skip to content

NTFS Journal support and improved SparseStream flush mechanism - #97

Merged
Olof-Lagerkvist merged 18 commits into
LTRData.DiscUtils-initialfrom
review/pr-87-base
Nov 8, 2026
Merged

Olof-Lagerkvist merged 18 commits into
LTRData.DiscUtils-initialfrom
review/pr-87-base

Conversation

@Olof-Lagerkvist

@Olof-Lagerkvist Olof-Lagerkvist commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Summary

Consolidates the original NTFS $LogFile recovery work from #87 with the subsequent safety and durability fixes in #88–#92 and #94–#96. The result adds supported NTFS journal analysis and recovery, a storage-independent SparseStream.Flush(bool) contract, and iSCSI cache synchronization through that contract.

This PR targets LTRData.DiscUtils-initial from review/pr-87-base. It supersedes the original #87 implementation as reviewed; #93 is not included because its hibernation checks were largely superseded by #91.

NTFS journal behavior

  • NtfsJournal.Analyze(Stream) inspects $LogFile and reports Clean, NothingToReplay, NeedsReplay or Unsupported without writing.
  • Default writable open is conservative: new NtfsFileSystem(stream) refuses an unclean journal that needs replay or reset, and refuses unsupported journals. It does not silently replay on a writable open.
  • Default read-only open applies supported redo/undo to an in-memory overlay without changing the backing volume. If replay is unsupported, the volume is exposed without recovery and callers can inspect JournalInfo.Status.
  • NtfsJournalMode.Replay or NtfsJournal.Replay(stream) explicitly opts into persistent recovery on a writable stream when supported. The Refuse and Ignore modes and the per-open FileSystemParameters.Parameters["NtfsJournalMode"] entry are also available.
  • The journal is analyzed before the filesystem is mounted. Validated recovery handles log/restart pages, redo, supported in-memory undo and metadata changes, including updates to $MFTMirr.

Recovery safety

The follow-up PRs strengthen validation of LFS client identity, transaction/undo chains, page mappings, damaged or torn pages, overlapping changes and restart-page agreement. Unresolved metadata undo is not persisted: without restartable logged compensation records, the persistent path refuses it before writing. In-memory bitmap undo supports disjoint bit runs from different transactions.

Recovery refuses active or indeterminate local hibernation/Fast Startup evidence, inspecting both the original volume and the staged replay result. This is not proof of exclusive ownership: a suspended Windows installation may still own a data volume whose resume image is elsewhere.

Persistent replay writes and flushes in this order:

  1. Relevant $MFTMirr records.
  2. Primary metadata.
  3. Alternate restart page.
  4. Former source restart page.

The pre-reset metadata barrier is requested even when the current attempt has no metadata changes, covering buffered bytes from an earlier interrupted attempt. Partial reset recognition and retry checks refuse inconsistent recovery states instead of continuing to write.

Extensible storage flushes

SparseStream now has virtual Flush(bool flushToDisk), defaulting to Flush(). Built-in wrappers forward the request, SparseWrapperStream passes it to FileStream.Flush(bool) when appropriate, and VHD/VHDX content streams forward it to their backing streams. NTFS invokes Flush(true) through a non-owning sparse adapter; it does not search for a nested FileStream.

iSCSI DiskStream.Flush(), Flush(true) and FlushAsync() now send full-LUN SCSI SYNCHRONIZE CACHE (10) with IMMED=0, unconditionally, and propagate explicit synchronization failures. The iSCSI package README documents target-dependent guarantees.

Validation

  • Five retained Windows crash-image fixtures compare the supported replay result with Windows-recovered metadata. The original NTFS: read and replay the $LogFile journal (analysis, redo, undo) #87 reports additional manual crash comparisons.
  • Synthetic tests cover supported/unsupported operations, hibernation decisions, LFS client checks, disjoint bitmap undo, torn writes, flush failures, exact persisted bytes at interruption points and subsequent retries.
  • Tests verify Flush(true) dispatch through file/partition/sparse wrappers and the iSCSI synchronization CDB.
  • Latest code-change CI: Windows Server 2025, Ubuntu 24.04 and macOS 15 all passed. README-only updates on this branch should receive their own CI checks.

Boundaries and remaining work

  • Recovery is not atomic. Success requires the supplied stream, filesystem, runtime and physical/remote storage to honor write ordering and flush semantics. The durability notes document a Linux .NET 10 fsync error-reporting limitation and cases no replay protocol can detect if a backend acknowledges lost writes.
  • Persistent recovery requiring metadata undo is intentionally unsupported pending independent Windows and compensation-record qualification. See undo and interruption qualification.
  • Local hibernation checks cannot detect a suspended operating system whose resume file is on another volume. The caller must establish offline, exclusive ownership. See hibernation notes.
  • Backend-specific power-loss behavior, container allocation updates and live iSCSI target failure scenarios need separate qualification. An unsupported journal on a read-only open can still expose stale metadata; check JournalInfo.

The NTFS package README, Streams package README and repository overview now summarize the behavior and limitations.

Review focus: conservative writable-open defaults, limits of persistent undo, interruption/retry ordering, and whether the documented storage guarantees are appropriate for downstream consumers. Please review and merge manually; no automatic merge requested.

devedse and others added 18 commits November 7, 2026 05:00
Adds a reader for the log file service (LFS 1.x and 2.0, tail copies,
multi-page records, wrap-around), the analysis pass (open attribute,
dirty page and transaction tables), and redo/undo of the NTFS operations
on file records, index buffers and non-resident pages. NtfsFileSystem
replays the journal when it opens a volume (NtfsJournalMode), in memory
for read-only streams.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Test journal writer and scenarios: DiscUtils-made changes logged like NTFS
  logs page images, replayed as redo (committed) and undo (unfinished),
  with wrap-around, LFS 1.1, torn commit page, interrupted replay,
  read-only and writable opens, refuse/ignore modes, unknown operations.
- Operation tests for every redo/undo action, including refusals.
- Two volumes Windows crashed on (Hyper-V, Windows 11 LTSC) with a digest
  of Windows' own replay; DiscUtils' replay must match it.
- Replay fixes: data Windows leaves out of a record counts as zeros, the
  used-part moves of Create/DeleteAttribute, padding left as Windows does,
  only real byte changes count, pages turned back into "never written".
- NtfsFileSystem.Dispose no longer throws after a refused open; the
  formatter's internal open ignores the journal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes found by comparing with Windows' own replay of crashed volumes
(512 B to 64K clusters, 4K sectors, a wrapped 200 MB log, compressed,
sparse, object id, reparse and quota activity):
- LFS flag 0x2/0x4: redo/undo data is all zeros and not stored (the bytes
  at the redo offset may be the undo data).
- An equal-length UpdateResidentValue overwrites any bytes of the attribute
  record, headers of non-resident attributes too (compression/sparse flags).
- With clusters bigger than a page, several dirty page entries share a VCN;
  the oldest LSN counts.
- A record that was never written can be freed again (compensation of a
  creation that never reached the disk): header fields and LSN only.

Tests: five checked-in Windows crashes with a digest of Windows' replay
(replay, read-only open and the stale state are checked against it), and
detail tests for checkpoint tables, compensation records, top-level actions,
freed and moved clusters, both table layouts, tail copies, $MFTMirr and
every refusal path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every line and branch of the journal code (Journal/*.cs, NtfsJournal.cs) is
now covered by tests that run on net46, net48 and net10.0:

- NtfsLogActionsRefusalTest: each check an action makes on a file record or
  index buffer, broken one at a time; the action must refuse, and (apart from
  a run list found damaged after it is written) leave the page unchanged.
- NtfsJournalEdgeCaseTest / NtfsJournalDamagedLogTest: hand-built logs that
  are damaged in one place - restart pages (no client in use, chkdsk mark on
  one copy, damaged second copy), negative record and cluster counts, table
  dumps that are missing or no NTFS records, records before the checkpoint
  that the redo or undo pass reads, a checkpoint that is the last record, hot
  fixes, clusters off the volume, index buffer images without fixups, and
  index buffers of attributes that are not open.

Code that tests showed cannot be reached is removed rather than tested: the
analysis now gets the client in use from its caller (which already checks
it), FindExtent drops a check its ordered extents make always true, and
leftover debugging members are gone. The restart area must now have a
page-aligned file size, which the log page reader relies on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "NtfsJournalMode" FileSystemParameters entry (per volume) and
NtfsFileSystem.DefaultJournalMode (process-wide) had no tests. The
default-mode test runs in a non-parallel collection because it changes
a static. Also marks the test helpers' default-null parameters nullable
(no more CS8625 warnings from the journal tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows resumes a hibernated (or Fast Startup) volume from memory instead
of replaying its journal, so replaying it changes the volume behind
Windows' back. The class documentation said the opposite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DiscUtils returns enumerated paths with the platform's directory separator,
so on Linux and macOS the index and listing keys ("/w1/d000") did not match
the digests made on Windows ("\w1\d000"). The replay itself was identical;
only the test helper's keys differed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Match both client slot and sequence during forward analysis and redo. Require active-client ownership at NTFS checkpoint and undo references, and refuse persistent reset of a log with multiple client slots.
* Guard NTFS journal recovery against local hibernation evidence

Inspect the local hibernation header in the raw and staged volume views before admitting replay or reset. Refuse saved resume markers and indeterminate metadata, and document the limits of single-volume evidence.

* docs(ntfs): clarify retained hiberfil TRIM behavior and qualification status
… cluster (#92)

* NTFS journal: let transactions roll back different bits of one bitmap cluster

#89 refuses pending rollback in two transactions that touch the same
cluster. For bitmap bit runs that is too coarse: one 4 KB $Bitmap cluster
covers 128 MB of a volume with 4 KB clusters, so two unfinished allocations
in that region share it. The one real crash with metadata undo from the lab
(3 active transactions, two of them clearing bits in $Bitmap cluster 42315)
was refused entirely, also in memory.

Undo now claims bitmap bits per run. Different bits of one cluster may be
rolled back by different transactions, since setting or clearing disjoint
bits gives the same result in any order. Overlapping bits, and a bit run
in a cluster that another transaction's undo rewrites as a whole, are still
refused. On the lab crash the in-memory replay now equals two independent
Windows recoveries of the same capture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* NTFS journal: also test bitmap undo of bits that never reached the disk

In the lab crash the allocations' bits were 0 on disk and after both
Windows recoveries: redo sets them and undo clears them again, so the net
change is zero (NothingToReplay) but the two transactions still overlap
in one $Bitmap cluster.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
)

* Strengthen NTFS replay flush propagation and retry coverage

* Preserve custom FileStream flush dispatch behind partition ranges

* Forward durable flush requests through SparseStream wrappers

* Avoid implicit flush when releasing replay adapter
DiskStream.Flush() was empty, so nothing DiscUtils wrote over iSCSI was ever
flushed out of a target's volatile write cache. A target with WCE=1 (the LIO
default for block backstores) acknowledges a write before it is on stable
storage; a power loss on the target can then lose it, or keep a later write
without an earlier one, even after the caller flushed.

- Flush/FlushAsync send SYNCHRONIZE CACHE (10) for the whole medium (LBA 0,
  0 blocks, IMMED clear) when something was written since the last flush;
  without writes they cost nothing.
- A target that rejects the command as unknown (ILLEGAL REQUEST, ASC 0x20)
  has no cache to flush; the stream stops sending it.
- Disposing a Disk flushes first, like an OS unmounting a disk. A failure of
  that last flush is not reported; call Content.Flush() to see it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@Olof-Lagerkvist
Olof-Lagerkvist merged commit d602a08 into LTRData.DiscUtils-initial Nov 8, 2026
9 checks passed
Jones-Adam added a commit to smile-forge/DiscUtils that referenced this pull request Nov 9, 2026
Takes the NTFS $SDS mirror fix (LTRData#86) alone; the journal work after it (LTRData#97) waits for the next cycle.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants