Skip to content

EXT4: guard extent-tree parsing against out-of-bounds reads on malformed images - #849

Open
iabdullah215 wants to merge 1 commit into
apple:mainfrom
iabdullah215:harden-ext4-extent-bounds
Open

iabdullah215 wants to merge 1 commit into
apple:mainfrom
iabdullah215:harden-ext4-extent-bounds

Conversation

@iabdullah215

Copy link
Copy Markdown

What

Bounds-check the extent-tree decode in EXT4.EXT4Reader so a malformed inode can no longer drive Data.subdata past the inode's block region. The decode is extracted into an internal decodeExtents(inodeBlock:) helper so it can be unit-tested directly.

Why

An ext4 image is untrusted input. getExtents iterated 0..<entries reading fixed-size records (12 bytes each) with subdata, trusting the entry counts from the on-disk extent headers without checking them against the bytes available:

  • The inode block region is only 60 bytes (header + up to four 12-byte records). A depth-0 header claiming >4 leaves, or a depth-1 header claiming >4 indices, makes a read run past the block.
  • In the depth-1 path, each leaf block trusts its own header's entries the same way, past the end of the block buffer.

Because Data.subdata(in:) has a precondition on its range, either case traps the process (SIGTRAP) instead of returning an error, a denial of service for any caller that reads a file backed by a crafted inode.

Fix

  • Guard every fixed-size read with offset + size <= buffer.count (throwing EXT4.Error.invalidExtents when a header claims more records than fit, i.e. surfacing the corruption rather than silently truncating).
  • Fix a latent reversed range when reading a leaf block's header: blockOffset..<extentHeaderSizeblockOffset..<blockOffset + extentHeaderSize (previously benign only because blockOffset == 0 there).

Well-formed images are unaffected: their headers never claim more records than the block holds.

Testing

New Tests/ContainerizationEXT4Tests/TestEXT4Reader+ExtentBounds.swift:

  • wellFormedDepth0ExtentsParse — regression: valid depth-0 extents still parse.
  • tooManyDepth0EntriesDoesNotTrap — a header claiming 5 leaves in a 60-byte block is rejected (crashes the reader without this change).
  • shortInodeBlockReturnsEmpty / nonExtentInodeReturnsEmpty, no-extent cases.

All existing EXT4PathIOTests (which read real files through getExtents) still pass.

Related

@iabdullah215

Copy link
Copy Markdown
Author

@dkovba, mind giving these a look when you have a moment? #848 and #849 have had no CI since Aug 24; fork PRs need a maintainer to approve the workflow run. Both are bounds checks in EXT4+Reader.swift on the untrusted-image parse path, same shape as the xattr loop bounds you fixed in #658: a malformed inode's entries count drives subdata past the end of the block and traps the reader. Tests included in each.

@dkovba
dkovba requested a review from JaewonHur September 18, 2026 17:56
…med images

EXT4.EXT4Reader.getExtents decodes the extent tree from an inode's block
region, which is untrusted input. Both the depth-0 and depth-1 loops iterated
0..<entries reading fixed-size records via subdata without checking the
attacker-controlled entry count against the bytes available:

- the inode block is only 60 bytes, so a header claiming more than four
  depth-0 leaves (or depth-1 indices) reads past it; and
- a depth-1 leaf block trusts its own header's entries the same way.

A malformed inode therefore traps the reader (Data.subdata precondition
failure, SIGTRAP), a denial of service. Move the decode into an internal
decodeExtents(inodeBlock:) helper, bounds-check every fixed-size read, and fix
a latent reversed range when reading a leaf block's header
(blockOffset..<extentHeaderSize -> blockOffset..<blockOffset + extentHeaderSize).
Add regression tests for the depth-0 overflow and the valid/short/non-extent
cases (the overflow crashes the reader without the fix; existing EXT4 reader
tests still pass).
@iabdullah215
iabdullah215 force-pushed the harden-ext4-extent-bounds branch from 2e2a771 to 705faa4 Compare September 19, 2026 06:01
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