Skip to content

nilfs2: extract 3.x volumes (closes #64) - #67

Merged
nmatt0 merged 2 commits into
masterfrom
feat/nilfs2-extract
Oct 25, 2026
Merged

nmatt0 merged 2 commits into
masterfrom
feat/nilfs2-extract

Conversation

@nmatt0

@nmatt0 nmatt0 commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Summary

Adds read-only extraction for NILFS2. Identification landed with the legacy-block-filesystem batch (#24); until now -e on a NILFS2 image printed the summary table and wrote nothing, because extractor_for() had no nilfs2 case. Closes #64.

What it does

NILFS2 is log-structured, so the live filesystem is reached through the latest checkpoint rather than a fixed inode table. src/extract/nilfs2.{cpp,hpp}, registered in extractor_for():

  • Superblock selection: reads both superblocks (primary at offset 1024, secondary near end of device) and uses the valid one with the greater s_wtime. NILFS2 writes the superblocks lazily, so the primary can lag the newest committed log; selecting the newer one is what makes a cleanly-unmounted image read back its latest state.
  • Super root: follows the chosen superblock's last partial segment to the super root (the last block of that segment), which embeds the DAT, cpfile and sufile inodes.
  • DAT translation: the DAT (disk address translation) is a palloc file whose own block map uses physical pointers; it translates the virtual block numbers that every non-DAT inode's pointers carry into physical blocks.
  • Checkpoint → ifile → tree: reads the cpfile checkpoint for the last checkpoint number to get the ifile inode, reads the root inode from the ifile (also a palloc file), and walks the ext2-like directory tree, rebuilding regular files, directories and symlinks.

Block maps are NILFS2 bmaps: an inline "direct" map (header + up to 6 pointers for file blocks 0..5) or a B-tree. A detail that matters on real images: B-tree non-root nodes carry an 8-byte pad between the node header and the keys (root nodes do not), and only the DAT inode's pointers are physical; all others are virtual and resolved through the DAT.

Safety

  • Every read goes through the bounds-checked Reader; a truncated or corrupt image yields a partial/error: status, never an out-of-bounds access.
  • Guardrails on file count, recursion depth, B-tree nodes and blocks per file, plus cycle guards on directory recursion.
  • A declared file size larger than the image is clamped, so a crafted size field cannot force a giant allocation.
  • No garbage collection, no roll-forward recovery, no write support, no mount (unlike the kernel driver).

Tests

tests/test_nilfs2_extract.py (wired into run.sh and ctest):

  1. Real empty image (always-on, no privilege): mkfs.nilfs2 builds an empty filesystem and moria must identify and extract it cleanly. This drives the entire metadata chain (superblock selection, super root, DAT translation, cpfile lookup, ifile read, root-directory walk) against genuine on-disk bytes. Self-skips if nilfs-utils is absent.
  2. Populated round-trip (self-skips without privilege): builds a known tree with a small multi-block file, a B-tree-mapped file, a tail file, a symlink and a nested directory; mounts the image and copies it in; extracts and compares byte-for-byte. This is the only layer that exercises file content and symlinks, so it is the primary correctness check where it can run. mkfs.nilfs2 has no populate-from-directory mode and the kernel driver is required to write, so this layer needs the nilfs2 module and mount privilege.
  3. Corruption robustness: mangled superblock/metadata inputs must not crash and must report a sane status (the ASan build is the OOB oracle).

fuzz/fuzz_nilfs2.cpp fuzzes the parse-only read chain (superblock selection through directory walk and block maps), plus the scan pipeline on the raw bytes.

Docs

NILFS2 added to the extractable-filesystem lists in README.md and moria.1.

moria identified NILFS2 (issue #24) but had no extractor, so -e wrote nothing.
Add src/extract/nilfs2.{cpp,hpp} and register it in extractor_for().

NILFS2 is log-structured: the live tree is reached through the latest
checkpoint, not a fixed inode table. The extractor:

  - picks the valid superblock (primary at 1024, secondary near EOF) with the
    greater write time, since NILFS2 updates them lazily and the primary can lag
    the newest log;
  - follows its last partial segment to the super root (the last block of that
    segment), which embeds the DAT, cpfile and sufile inodes;
  - reads the DAT (disk address translation), a palloc file whose own block map
    uses physical pointers, to translate the virtual block numbers that every
    other inode's pointers carry;
  - reads the cpfile checkpoint for the last checkpoint number to get the ifile
    inode, reads the root inode from the ifile (also a palloc file), and walks
    the directory tree, rebuilding regular files, directories and symlinks.

Block maps are NILFS2 bmaps: an inline "direct" map (up to 6 pointers) or a
B-tree whose non-root nodes carry an 8-byte pad between header and keys. All
reads go through the bounds-checked Reader, so a truncated or corrupt image
yields a partial result rather than a crash, and a declared file size larger
than the image is clamped to bound the allocation.

Tests (tests/test_nilfs2_extract.py): a real empty mkfs.nilfs2 image is
identified and extracted cleanly (always-on, no privilege), exercising the whole
metadata chain against genuine bytes; a populated round-trip builds a known tree,
mounts and copies it in, extracts and compares byte-for-byte (self-skips without
the kernel module and mount privilege, since mkfs.nilfs2 has no populate mode);
and mangled superblock/metadata inputs must not crash. Adds fuzz/fuzz_nilfs2.cpp
over the parse-only read chain.
@nmatt0
nmatt0 force-pushed the feat/nilfs2-extract branch from e88342d to c16a86c Compare October 17, 2026 21:57
@nmatt0
nmatt0 merged commit df8e4df into master Oct 25, 2026
4 checks passed
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.

NILFS2: extract the identified filesystem (identify-only since #24)

1 participant