Skip to content

feat(vfs): surface NTFS reparse-point and ntfs-3g symlinks - #10

Open
frankmanzhu wants to merge 3 commits into
SecurityRonin:mainfrom
frankmanzhu:main
Open

feat(vfs): surface NTFS reparse-point and ntfs-3g symlinks#10
frankmanzhu wants to merge 3 commits into
SecurityRonin:mainfrom
frankmanzhu:main

Conversation

@frankmanzhu

Copy link
Copy Markdown

feat(vfs): surface NTFS reparse-point and ntfs-3g symlinks

What this does

The vfs adapter now classifies NTFS symlinks as NodeKind::Symlink and implements FileSystem::read_link for both on-disk forms:

  1. Windows form — the $REPARSE_POINT attribute (0xC0): tags 0xA000000C (symbolic link) and 0xA0000003 (mount point/junction), substitute name decoded from the SymbolicLinkReparseBuffer/MountPointReparseBuffer layout.
  2. Linux form (ntfs-3g) — a resident unnamed $DATA starting with the IntxLNK magic (7 bytes + version byte) carries the UTF-16LE target. This is what ntfs-3g actually writes when creating symlinks on a mounted volume: no $REPARSE_POINT attribute exists at all (verified empirically — the record contains only $STANDARD_INFORMATION/$FILE_NAME/$SECURITY_DESCRIPTOR/$DATA).

Why it currently fails

read_link was unimplemented and reparse/IntxLNK nodes were surfaced as NodeKind::File with the raw target record as file content. A consumer extracting an NTFS volume with symlinks silently writes the reparse data (or the IntxLNK record) as a regular file — wrong data with no error. Verified against a real ntfs-3g-authored symlink: the image's own author (ntfs-3g mount) resolves it to ../README.txt, 7-Zip lists it as a symlink, while the unpatched adapter read the 34-byte record as a file.

Why it needs fixing

Same contract gap as the other adapters: forensic-vfs::FileSystem promises NodeKind::Symlink + read_link. NTFS volumes routinely contain symlinks/junctions (Windows junctions, %LOCALAPPDATA% links, ntfs-3g-created links), and the silent-junk failure mode is the worst kind for a forensic/extraction consumer.

Implementation notes

  • Both decoders are bounds-checked; hostile buffers cannot panic (None on malformed input — never a fabricated target).
  • A read/parse miss degrades to a regular-file classification rather than failing a directory listing (is_symlink_record is conservative by design).
  • The IntxLNK version byte is intentionally not validated (matching ntfs-3g and 7-Zip, which accept any version); documented.
  • build_meta classifies directories first, so reparse checks never misfire on directories.

Test coverage

  • Committed fixture tests/data/tiny.zip (provenance in tests/data/README.md): an 8 MiB NTFS volume with an ntfs-3g-authored IntxLNK symlink; the vfs_ntfs.rs integration test asserts NodeKind::Symlink + read_link == b"../README.txt" — ntfs-3g's own resolution as ground truth — and that regular files read an empty target.
  • Crafted-record unit tests cover the Windows $REPARSE_POINT form end-to-end at the classifier level (symlink + mount-point tags, substitute-name decode, unrelated-tag and truncation negatives) — Windows symlinks cannot be produced on macOS, so this form is unit-tested rather than integration-tested; the layout follows the documented REPARSE_DATA_BUFFER structure.
  • Bounds-safety tests for both decoders (truncated buffers, oversized length fields).
  • cargo llvm-cov --workspace --all-features: 98.68% workspace-wide; vfs.rs 96.16% (up from a 94.18% pre-patch baseline).

Code quality

  • cargo clippy --workspace --all-targets --all-features: clean. cargo test --workspace --all-features: all green.
  • No unsafe; no unwrap/expect in production code; failures map to typed VfsError with context.

Known limitations (documented, not addressed here)

  • NTFS ADS/resource-fork streams and other reparse tags (OneDrive placeholders, dedup) are intentionally not surfaced as links; only symlink/junction tags decode targets.

The vfs adapter now classifies symlinks as NodeKind::Symlink and
implements FileSystem::read_link for both on-disk forms:
- Windows: $REPARSE_POINT attribute (0xC0), tags 0xA000000C (symlink)
  and 0xA0000003 (mount point/junction) — the substitute name is decoded
  from the SymbolicLinkReparseBuffer/MountPointReparseBuffer layout.
- Linux (ntfs-3g): a resident unnamed $DATA starting with the 'IntxLNK'
  magic (7 bytes + version) carries the UTF-16LE target — the format
  ntfs-3g 2022.10.3 writes when creating symlinks on a mounted volume.

A read/parse miss degrades to a regular-file classification rather than
failing a directory listing. Verified against an ntfs-3g-authored image:
the symlink resolves to '../README.txt', matching ntfs-3g's own mount.
- Committed tiny.zip (8 MiB NTFS volume with an ntfs-3g-authored IntxLNK
  symlink; provenance in tests/data/README.md).
- vfs_ntfs.rs integration test walks the volume: Symlink classification
  and read_link == '../README.txt' (ntfs-3g's own resolution), regular
  files stay File with an empty target.
- vfs.rs unit tests build crafted MFT records + attributes to cover the
  Windows $REPARSE_POINT branch (symlink + mount-point tags, substitute
  name decode), the IntxLNK branch, plain-file/unknown-tag negatives, and
  bounds-safety of both decoders.
- llvm-cov --workspace --all-features: 98.68% lines; vfs.rs 96.16% (was
  94.18% pre-patch).
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.

1 participant