Skip to content

SquashFS: root as an extended directory inode, symlink targets, block starts computed once - #79

Merged
Olof-Lagerkvist merged 3 commits into
LTRData:LTRData.DiscUtils-initialfrom
0xorial:squashfs-extended-root-directory
Sep 10, 2026
Merged

Olof-Lagerkvist merged 3 commits into
LTRData:LTRData.DiscUtils-initialfrom
0xorial:squashfs-extended-root-directory

Conversation

@0xorial

@0xorial 0xorial commented Sep 10, 2026

Copy link
Copy Markdown

Things found while switching an application from its own SquashFS reader to DiscUtils after #78:

  • Root inode cast. VfsSquashFileSystemReader cast the root inode to DirectoryInode. mksquashfs stores a directory whose table exceeds one metadata block as an extended directory inode (with an index), the root included, so an image with a large root directory failed to open with InvalidCastException. Directory already accepts either kind through IDirectoryInode; the cast is gone.
  • Symlink targets. Symlink.TargetPath threw NotImplementedException, and since the VFS layer resolves a symlink at every path segment, any lookup that landed on a symbolic link threw. The target is the UTF-8 path stored right after the inode (SymlinkInode.SymlinkSize bytes), relative to the link's directory unless absolute, which is what ResolveSymlink expects. The length is validated against the format's SQUASHFS_SYMLINK_MAX (65535) before anything is allocated, and the target must lie within the inode table.
  • Extended symlink inodes (type 10) are read: the basic inode's fixed part and target path, then the 32-bit xattr index that follows the target. mksquashfs writes this form only for a symlink with extended attributes, so the reader no longer refuses an image with an xattr table; nothing in the reader reads the table, and the extended inodes skip their index.
  • Block starts. FileContentBuffer walked the block size list from the first block on every read to find where the target block sits on disk: O(blocks) per read, so a random read near the end of a large file summed every size before it (a 96 GB file with 1 MiB blocks: ~98k additions per read). The starts are now computed once when the file is opened (a long per block); the mask is the format's low 24 bits.

Tests: big-root.sqsh (1,000 files in the root and a symlink) and xattr-symlink.sqsh (a symlink with an attribute, made with -xattrs-add, so an extended symlink inode and an xattr table) are added to the SquashFixtures recipe, and extended-inodes.sqsh gains mixed.bin (a compressed, a stored, a sparse and a compressed block, then a fragment) for the block-start calculation. Like the existing fixtures, the images are built fresh by mksquashfs where it is present (the ubuntu runner) and come from embedded copies, made by squashfs-tools 4.7.5 from the same recipe, elsewhere, so every test executes on every runner. Verified with 4.7.5, with 4.6.1 built from source, and with no mksquashfs on PATH.

🤖 Generated with Claude Code

… starts computed once

- The reader cast the root inode to the basic directory type; mksquashfs stores a
  directory whose table exceeds one metadata block (the root included) as an
  extended directory inode with an index, so such images failed to open.
- Symlink.TargetPath threw NotImplementedException, so any path lookup that
  landed on a symbolic link threw. The target is the UTF-8 path stored after the
  inode, relative to the link's directory unless absolute.
- FileContentBuffer walked the block sizes from the first block on every read
  (O(blocks) per read; a random read near the end of a large file summed every
  size before it). The block starts are computed once when the file is opened.

Tests: big-root.sqsh, built by mksquashfs at test time (1,000 files in the root
and a symlink; no embedded fallback, so the tests are no-ops without the tool).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Olof-Lagerkvist

Olof-Lagerkvist commented Sep 10, 2026

Copy link
Copy Markdown
Member

The overall direction looks good to me, particularly the extended-root handling and the block-start precomputation.

Since this implementation was AI-assisted, here is a focused follow-up that you can feed back to Claude before I do a final review:

Please review this PR again specifically for completeness and robustness of the new SquashFS symlink support.

  1. Extended symlink inodes

    The repository defines InodeType.ExtendedSymlink, and the generic VFS layer already treats both normal and extended symlink inode types as symbolic links, but Inode.InstantiateType() currently only constructs SymlinkInode for InodeType.Symlink.

    Please verify the SquashFS v4 format and the behavior of the Linux/squashfs-tools implementations, then add support for extended symlink inodes if appropriate.

    Do not simply alias the extended type to the existing inode class unless the on-disk structure really permits that. Account correctly for any fields following the target path, such as extended/xattr metadata.

    Add a regression test that actually creates an extended symlink inode if mksquashfs can be made to produce one.

  2. Validate SymlinkSize before allocating

    Symlink.TargetPath currently trusts the on-disk SymlinkSize and allocates:

    new byte[inode.SymlinkSize]

    Treat the filesystem image as untrusted input. Verify the format-defined maximum symlink length and reject clearly invalid sizes before allocating or reading.

    Also consider whether the requested target bytes can be validated against the remaining metadata stream rather than relying on malformed metadata eventually failing elsewhere.

  3. Improve regression-test coverage

    The two new tests currently return successfully when mksquashfs is unavailable. That means they appear as passing tests on platforms where they did not actually exercise anything.

    Please consider one of these approaches, in preferred order:

    • add an embedded fallback SquashFS image, as used by the existing fixtures;
    • use the test framework's explicit skip mechanism if a runtime-generated fixture is genuinely required;
    • otherwise make it very obvious in test output that the test was not executed.

    The important point is that an unavailable external tool should not look indistinguishable from a successfully exercised regression test.

  4. Re-check the block-size mask change

    0x00FFFFFF appears correct for the SquashFS data-block stored-size field, with bit 24 carrying the compression/uncompressed flag. Please verify this against the authoritative format definitions and keep the corrected mask.

    Also confirm that sparse blocks contribute zero bytes to _blockStarts and that files involving a mixture of compressed, uncompressed, sparse, and final partial blocks still compute subsequent physical block starts correctly.

  5. Keep the change narrow

    Please do not redesign the VFS symlink resolver or otherwise broaden this PR unnecessarily. The existing root-directory change and block-start optimization look appropriately scoped.

After making any changes, summarize:

  • what was changed;
  • how normal and extended symlink inode layouts differ;
  • what maximum symlink target length is enforced and where that limit comes from;
  • which new tests actually execute on CI;
  • whether you found any additional correctness problem in the block-start calculation.

Please leave the PR as draft for maintainer review.

…d test images

- ExtendedSymlinkInode (type 10): the basic inode's fixed part and target path,
  then a 32-bit xattr index after the target, which Symlink.TargetPath reads so
  the whole inode is accounted for. mksquashfs writes this form only for a
  symlink with extended attributes, so the reader no longer refuses an image
  with an xattr table (nothing in it reads the table; the extended inodes skip
  their index).
- SymlinkSize is checked against 65536 (mksquashfs reads a link into a
  65536-byte buffer and refuses a longer one; the Linux driver rejects a target
  longer than a page) before allocating, and the target must lie within the
  inode table (MetablockReader.CurrentBlockStart stays before the directory
  table).
- Tests run on embedded images where mksquashfs is absent, like the existing
  fixtures: big-root.sqsh and xattr-symlink.sqsh embedded, extended-inodes.sqsh
  re-embedded with mixed.bin (a compressed, a stored, a sparse and a compressed
  block, then a fragment) for the block-start calculation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Olof-Lagerkvist

Copy link
Copy Markdown
Member

Thanks, this now looks very close to ready for merge. The extended symlink handling, embedded fixtures, and mixed block-layout regression test address the issues I raised earlier.

I found one small correctness issue that I would like fixed before merging:

"Symlink.MaxTargetLength" is currently "65536", but upstream SquashFS defines "SQUASHFS_SYMLINK_MAX" as "65535". Since this check is intended to validate the on-disk format, please use the actual format limit rather than the size of the temporary buffer used by tooling.

You can feed this back to Claude:

Please make one final narrow cleanup to PR #79:

  • Change "Symlink.MaxTargetLength" from "65536" to "65535".
  • Update the accompanying XML comment so it cites the SquashFS-defined maximum target length, rather than describing 65536 as the maximum valid target.
  • Re-check the upstream SquashFS definitions while making the change, but do not otherwise broaden or redesign the implementation.
  • Update the PR description because it still says the new tests are no-ops when "mksquashfs" is unavailable. They now use embedded fallback images, so the description should reflect the current behavior.
  • Run the existing test suite and leave the PR unmerged for final maintainer review.

Unless that re-check finds something unexpected, I do not currently see any other substantive blocker.

@0xorial

0xorial commented Sep 10, 2026

Copy link
Copy Markdown
Author

On it ! Btw I really appreciate your openness and responsiveness on this repo!

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Olof-Lagerkvist
Olof-Lagerkvist marked this pull request as ready for review September 10, 2026 20:14
@Olof-Lagerkvist
Olof-Lagerkvist merged commit c4c4ac8 into LTRData:LTRData.DiscUtils-initial Sep 10, 2026
3 checks passed
@Olof-Lagerkvist

Copy link
Copy Markdown
Member

Thanks a lot for your contribution!

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