Skip to content

fix(ext2): read sparse holes as zeros and return 0 at EOF - #267

Merged
Galfurian merged 1 commit into
developfrom
fix/192-242-ext2-read-path
Aug 26, 2026
Merged

fix(ext2): read sparse holes as zeros and return 0 at EOF#267
Galfurian merged 1 commit into
developfrom
fix/192-242-ext2-read-path

Conversation

@Galfurian

Copy link
Copy Markdown
Member

Summary

Fix two related boundary defects in the EXT2 read path:

  • sparse holes inside a file now read as zero-filled data instead of failing;
  • reads at or beyond EOF now return zero correctly and no longer compute invalid block/copy ranges.

The existing sparse-script regression in t_shebang is also updated because it previously depended on the broken sparse-hole behavior.

Fixes #192.
Fixes #242.

Problem / motivation

#192 — sparse holes were treated as invalid blocks

EXT2 sparse files may contain a zero block pointer inside the logical file size. Such a block is a hole and must read as zeros.

Previously, ext2_read_inode_block() validated the requested logical block against the number of physically allocated blocks.

That is incorrect for sparse files: a file can logically cover more blocks than it physically allocates.

After resolving the logical block, a sparse hole produced a physical block index of 0, which eventually reached ext2_read_block() and failed as an invalid block.

This made any read touching a hole fail entirely.

#242 — EOF reads could enter invalid block arithmetic

ext2_read_inode_data() did not reject reads whose offset was already at or beyond inode->size.

Depending on the offset and file layout, this could cause:

  • reads past EOF to return bogus negative values;
  • unsigned copy-length calculations to wrap;
  • reads exactly at block-aligned EOF to attempt the next block and return -1 instead of 0.

There was also a related end-block calculation problem for reads ending exactly on a block boundary:

end_block = end_offset / block_size;

When end_offset was block-aligned, this selected the block after the final byte actually requested.

Changes

Sparse-file reads

ext2_read_inode_block() now:

  • derives the valid logical block count from inode->size, not inode->blocks_count;
  • treats every block covered by the logical file size as a valid file block;
  • interprets a resolved block pointer of 0 as a sparse hole;
  • fills the destination block with zeros instead of attempting to read physical block zero.

This preserves the distinction between:

logical block inside i_size + no physical allocation
    -> sparse hole
    -> zero-filled data

logical block beyond i_size
    -> invalid block request
    -> error

EOF handling

ext2_read_inode_data() now returns 0 immediately when:

offset >= inode->size

or when:

nbyte == 0

The end block is now calculated from the last byte actually included in the read:

end_block = (end_offset - 1) / fs->block_size;

so an exact block-boundary end no longer advances into the following block.

t_shebang

The sparse-script case previously expected EIO because its shebang read crossed a sparse hole and therefore exercised #192.

With sparse reads fixed, that I/O failure is no longer valid behavior.

The test now accepts the outcomes that can legitimately follow a successful sparse-file read:

It specifically no longer accepts a sparse-read failure.

Validation

  • git diff --check
  • Relevant build completed
  • Relevant automated tests completed
  • Sparse-hole behavior reproduced before the fix
  • EOF boundary behavior reproduced before the fix
  • Temporary regression scaffolding fully reverted

Validation details:

Pre-fix targeted reproduction:
- staged a file containing four 0xAB data blocks followed by a sparse hole
- confirmed the hole in the generated EXT2 image with debugfs

Observed before the fix:
- reading the sparse hole failed:
    read -> -1
    errno -> 1
    Invalid block index: 4 >= 4
- read exactly at aligned EOF returned -1
- read past EOF returned -1

All three targeted failure modes reproduced in-guest.
Post-fix targeted verification:
- sparse hole read returned zero-filled data
- read at aligned EOF returned 0
- read past EOF returned 0
- temporary regression test passed

Temporary full suite:
- 53/53 passed
Final regression after removing temporary scaffolding:
- canonical make qemu-test completed successfully
- QEMU guest exit: 33
- userspace suite: 52/52 passed
- updated t_shebang passed
- git diff --check clean
- working tree clean

Scope

This fixes the EXT2 root cause originally tracked by #192. The historical secondary execve() crash propagation described there has already been addressed separately by the transactional executable-loading work.

The unrelated test-address randomization and low-user-address mapping problems discovered during verification are intentionally left out of this change and will be tracked separately.

Related issues

Fixes #192.
Fixes #242.

Related: #56, #222.

Two boundary defects of the ext2 read path:

- Sparse holes inside i_size failed to read: a resolved block pointer
  of zero is a legal hole, but ext2_read_inode_block treated it as an
  error (and rejected any block beyond the allocated count), so any
  read touching a hole returned -1. Holes now read as a block of
  zeros, and the block range check is based on the file size instead
  of the allocated block count (issue #192).

- Reads at or beyond EOF returned -1 (or worse): without a guard,
  offsets past i_size hit unallocated tail blocks or computed
  wrapping copy lengths in unsigned arithmetic. Additionally, when
  the read end was block-aligned, end_block overshot by one and
  end_size wrapped the last copy length. Reads at EOF now return 0,
  zero-length reads return 0, and the end block is derived from the
  last byte actually read (issue #242).

Update the t_shebang sparse-script case, which pinned the old #192
failure (EIO): the shebang read of the sparse script now succeeds and
the exec outcome is governed by the open #222 interpreter-path bug
(ENOENT today, success once #222 is fixed).
@Galfurian
Galfurian merged commit e37fe1d into develop Aug 26, 2026
4 checks passed
@Galfurian
Galfurian deleted the fix/192-242-ext2-read-path branch August 26, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant