Skip to content

The story begins by introducing our EROFS parser to cargo fuzz#256

Open
cgwalters wants to merge 15 commits intocomposefs:mainfrom
cgwalters:erofs-hardening
Open

The story begins by introducing our EROFS parser to cargo fuzz#256
cgwalters wants to merge 15 commits intocomposefs:mainfrom
cgwalters:erofs-hardening

Conversation

@cgwalters
Copy link
Collaborator

💣 🩹 ✔️

@cgwalters cgwalters marked this pull request as draft March 12, 2026 13:18
@cgwalters cgwalters marked this pull request as ready for review March 12, 2026 13:43
@cgwalters
Copy link
Collaborator Author

Ah well, I clearly should have let the fuzzer run longer locally

@cgwalters
Copy link
Collaborator Author

CI / Fuzz smoke test (pull_request)Successful in 5m

🎉

@cgwalters cgwalters enabled auto-merge (rebase) March 12, 2026 15:40
@hsiangkao
Copy link

hsiangkao commented Mar 12, 2026

@cgwalters , For this part (erofs: Validate block ranges against image size via Image::inode_blocks()), generally at runtime I don't think it's an issue since users can always interupt this (just because the chunk list is long for example) and the runtime (like the kernel) should add proper signal detection and the system won't be stuck; dsb.blocks is just a indicator for statfs use which is like FUSE.
But I think the dump the whole extent list may be unnecessary for many cases, we should map on demand. Even if it can be dumped, the chunk list can still be valid but the time could be long, so I think we just need to detect such timeout case instead and skip this.
Anyway, but for composefs, of course you could limit the image size or even the total numbers of chunks (because I guess it's just used for the sparse usage) instead.

@cgwalters
Copy link
Collaborator Author

Hi @hsiangkao ! 👋

generally at runtime I don't think it's an issue since users can always interupt this (just because the chunk list is long for example) and the runtime (like the kernel) should add proper signal detection and the system won't be stuck;

Yes, but in order to be very defensive I want to ensure that we (can) parse the EROFS with our memory-safe Rust parser here before giving it to the Linux kernel in general. We have the opportunity to impose a lot of structural constraints that should make it even harder to reach potentially buggy kernel codepaths.

Because this is just metadata, analysis is pretty tractable.

Anyway, but for composefs, of course you could limit the image size instead.

Yes that's now also done in this PR 207eb25#diff-06c4aa3c3f7346814be3b4d6eb9ee179c3ace7d29ba3b54383ab0cc9cab1098cR403

@hsiangkao
Copy link

hsiangkao commented Mar 12, 2026

Yes, but in order to be very defensive

I respect your choice but the core format is simple enough from either analysis each on-disk field, and since we're in the AI era, I'm confident that all implemention bugs should be detected by AIs (plus the core format and implemention are not complex).
Also the potential implemention bugs happen all over the kernel, like tcp/ip stacks which parse untrusted formats too, and it's not specific to filesystems; I will finish the security model of EROFS on-disk format, analysing each on-disk field risk, basically I think on-disk sb and on-disk inode metadata is just simply read and directly fed into vfs; directory dirent parsing logic is not complex, I don't get any impression where the on-disk field can cause any risk.

@cgwalters
Copy link
Collaborator Author

since we're in the AI era, I'm confident that all implemention bugs should be detected by AIs (plus the core format and implemention are not complex).

Yeah...AI. 😅 Obviously a lot of this PR itself is heavily generated by OpenCode+Opus but I have to say it's just a combination of exhilarating and exhausting to get 90% good code and have to hunt down carefully the 10% where it just decided to disable the tests or revert a previous commit because conflicts were too hard or re-implement base64 encoding for no reason or even more bizarre things...and this is the stuff that still makes it by enforced subagent reviews.

On the topic of AI and security, well there's a variety of things about there but this "aisle" company is putting out things like https://aisle.com/blog/openssl-stack-overflow-cve-2025-15467-deep-dive

Clearly one can get a lot here out of careful tuning: prompts, provided tools etc.


Anyways though obviously there's stuff happening in the EROFS space like the inode sharing where we're kind of iterating towards a similar space in terms of feature set from composefs.

That said, IMO composefs is still a great architecture in terms of splitting the metadata vs data planes (and a story of many storage systems is separating those two things). A cool trick we can do is quickly generate new "images" that are just metadata transformations (think e.g. squashing two container image layers). We need a userspace EROFS parser (and writer) to do things like that.

So I think we'll always have it. Of course, maybe at some point the Linux kernel EROFS impl will get ported to Rust, and these things can share code. That'd be cool. But OTOH it's not that much code.

@hsiangkao
Copy link

hsiangkao commented Mar 12, 2026

That said, IMO composefs is still a great architecture in terms of splitting the metadata vs data planes (and a story of many storage systems is separating those two things).

I don't think they are conflict, the core format can be simple enough to contain both cases, just the traditional archive format and/or metadata-only manifests: Of course composefs is useful, no one is really arguing that.

Of course, maybe at some point the Linux kernel EROFS impl will get ported to Rust, and these things can share code.

Just some random thought about this (unrelated to anything): I think in the future of AI, the language is the least important stuff; since it's just a formal expression of the computer program logic, but all bugs can be fixed by AI no matter of stupid memory issues or something.
At that time, I don't think Rust is more useful compared to C (because all code can be reviewed by ai; and AI can generate bugless code from formal on-disk spec for example too) since C is more close to the real architecture. Please don't misinterpret that, I just say a future possibility.

Move Debug impls for format types and EROFS structures to the top
of the file (before ImageVisitor), extract hexdump helper, and
add missing_debug_implementations allows. Pure reorganization,
no functional changes.

Assisted-by: OpenCode (Claude Opus 4)
Signed-off-by: Colin Walters <walters@verbum.org>
Convert the assert_eq! in ImageVisitor::note() to return an error
instead of panicking when a corrupt image has the same offset visited
as two different segment types. Found by the debug_image fuzz target.

Assisted-by: OpenCode (Claude Opus 4)
Signed-off-by: Colin Walters <walters@verbum.org>
Fix arithmetic operations that could overflow, underflow, or cause
resource exhaustion when processing malformed EROFS images:

- Use checked_mul instead of unchecked << for block address
  calculations in debug.rs
- Use checked_add for block range end computation in reader.rs to
  prevent u64 overflow
- Use usize::BITS instead of hardcoded 64 for blkszbits validation
  (correct on 32-bit platforms)
- Use usize::try_from instead of 'as usize' casts for inode size,
  inode ID, and block ID to avoid silent truncation on 32-bit
- Cap Vec allocation against image length to prevent OOM from crafted
  size fields
- Use saturating_sub for debug display calculations

Assisted-by: OpenCode (Claude Opus 4)
Signed-off-by: Colin Walters <walters@verbum.org>
Replace direct slice indexing with .get() where the bounds come from
image content: XAttr::suffix/value/padding, Inode::inline, and
debug_img's unassigned-region slicing. This prevents panics on
malformed images where field values are inconsistent with actual data
lengths.

Assisted-by: OpenCode (Claude Opus 4)
Signed-off-by: Colin Walters <walters@verbum.org>
…pers

Change XAttr::suffix(), value(), and padding() to return
Result<&[u8], ErofsReaderError> instead of silently returning empty
slices on out-of-bounds access. This ensures corrupt xattr data
is properly reported rather than silently swallowed.

Also deduplicate is_whiteout() (moved to InodeHeader trait method)
and find_child_nid() (moved to Image method), and remove the
redundant entry_nid() test helper in favor of DirectoryEntry::nid().

Assisted-by: OpenCode (Claude Opus 4)
Signed-off-by: Colin Walters <walters@verbum.org>
Add fuzz testing infrastructure under crates/composefs/fuzz/ with two
targets: read_image (exercises the full reader API surface including
inode traversal, xattr parsing, and object collection) and debug_image
(runs the debug_img dump on arbitrary input). Includes a seed corpus
generator that creates valid EROFS images exercising various code paths.

Assisted-by: OpenCode (Claude Opus 4)
Signed-off-by: Colin Walters <walters@verbum.org>
…verflow

A crafted EROFS image with directory cycles can cause unbounded recursion
in populate_directory(), leading to a stack overflow. Add a depth parameter
and enforce a maximum of PATH_MAX / 2 (2048) levels, matching the
theoretical limit for valid filesystem paths.

Found by cargo-fuzz.

Assisted-by: OpenCode (Claude Opus 4)
Signed-off-by: Colin Walters <walters@verbum.org>
The cargo-fuzz targets found multiple panics within seconds of fuzzing.
Convert all remaining .unwrap() calls and assert!() macros in non-test
reader code to return Result, and propagate errors at all call sites.

Key changes:
- data_layout() returns Result instead of unwrapping TryInto
- XAttr::from_prefix(), xattrs(), shared(), local() return Result
- DirectoryBlock::n_entries/entries/get_entry_header return Result
- DirectoryEntries iterator yields Result<DirectoryEntry>
- XAttrIter yields Result<&XAttr>
- All callers in reader.rs, debug.rs, and fuzz targets updated

Assisted-by: OpenCode (Claude Opus 4)
Signed-off-by: Colin Walters <walters@verbum.org>
This got introduced in a CI refactoring and wasn't
intentional. Our fuzzing had way too short of a timeout.
If CI job is actually stuck we'll figure that out when
it happens.

Signed-off-by: Colin Walters <walters@verbum.org>
…ks()

The fuzzer found a crafted EROFS image where an ExtendedInodeHeader has
an enormous size field (~63 petabytes), causing blocks() to return a
range of ~15.5 trillion block IDs. Iterating this range caused a timeout.

Change our flow so that we pass the image (including its size)
when iterating blocks, so we can validate those.

Also add a default 1 GiB maximum image size in Image::open(), since
composefs images are metadata-only and should never approach that.

Assisted-by: OpenCode (Claude Opus)
Signed-off-by: Colin Walters <walters@verbum.org>
Composefs images are metadata-only EROFS images with well-known
structural constraints. Add an opt-in restriction mode that enforces:

- blkszbits must be 12 (4096-byte blocks)
- For non-ChunkBased inodes (directories, inline files, symlinks,
  devices), size must not exceed the image size, since their data
  is stored within the image itself.  ChunkBased (external) files
  are exempt because their size reflects the real file on the
  underlying filesystem.

The high-level collect_objects() and erofs_to_filesystem() APIs now
enable this by default.  Lower-level callers using Image::open()
directly can opt in via .restrict_to_composefs().

Assisted-by: OpenCode (Claude Opus)
Signed-off-by: Colin Walters <walters@verbum.org>
…nd metacopy checks

Validate composefs header magic and EROFS format version, superblock
magic, enforce the INLINE_CONTENT_MAX (64 byte) limit on inline regular
files, and reject malformed trusted.overlay.metacopy xattrs instead of
silently ignoring them.

The composefs header version field is validated but composefs_version is
not, since the C mkcomposefs writes version 0 while the Rust writer
uses version 2.

Previously, a malformed metacopy xattr would be silently ignored,
causing the file to be treated as inline rather than external. In
composefs-restricted mode this is now an error with a detailed
diagnostic message.

Cap the proptest inline file data strategy at INLINE_CONTENT_MAX to
match the composefs invariant that files > 64 bytes are external.

Assisted-by: OpenCode (Claude Opus)
Signed-off-by: Colin Walters <walters@verbum.org>
The dumpfile parser accepted inline content up to 5000 bytes, which is
far beyond any reasonable composefs inline file size. Reduce to 512
bytes as a safety bound while still allowing room for future increases
to the inline-vs-external threshold (see composefs#107 for discussion of
adjusting INLINE_CONTENT_MAX per hash algorithm).

Update the special.dump test data to use 63/64/256-byte inline files
instead of the previous 4095/4096/4097-byte entries that exceeded the
new limit.

Assisted-by: OpenCode (Claude Opus)
Signed-off-by: Colin Walters <walters@verbum.org>
…NTENT

Rename the writer's inline threshold to INLINE_CONTENT_MAX_V0 to make
it clear that changing this value is effectively a format break: it
determines which files get fs-verity checksums vs. stored inline, so
images from different thresholds aren't interchangeable. A future
composefs format version will need to encode this in the header.

Add MAX_INLINE_CONTENT (512 bytes) in lib.rs as the shared parsing
safety bound for untrusted input. Both the dumpfile parser and the
EROFS reader (in composefs-restricted mode) use this limit. It is
intentionally higher than V0 to allow for future threshold increases
per issue composefs#107.

Assisted-by: OpenCode (Claude Opus)
Signed-off-by: Colin Walters <walters@verbum.org>
EROFS is a complex format supporting compression, metabox inodes,
and more. Whereas for composefs we only use it as a metadata
format, and we have a custom writer which is conservative in
what features it uses.

Add currently known EROFS feature_compat and feature_incompat flag
constants in format.rs. When we're in `restrict_to_composefs()` mode,
we filter these up front.

This should drastically cut down on the attack surface exposed
by malicious EROFS images when mounted directly by the Linux kernel.

Assisted-by: OpenCode (Claude Opus)
Signed-off-by: Colin Walters <walters@verbum.org>
@cgwalters
Copy link
Collaborator Author

OK while not strictly related to this PR originally, but since I am juggling too many and it's getting confusing I pushed some more commits here which start to filter the EROFS parsing even more and rationalize our inlining in dumpfiles etc.

@cgwalters cgwalters requested a review from jeckersb March 12, 2026 19:10
@cgwalters cgwalters mentioned this pull request Mar 13, 2026
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