squashfs

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: BSD-3-Clause Imports: 22 Imported by: 3

README

go-filesystems/squashfs

squashfs

Go Reference CI

Pure-Go, read-only access to SquashFS 4.0 filesystem images — no root, no external tools, no CGO.

SquashFS is a compressed, read-only archive format used widely for live media, container/app images (snap, AppImage) and embedded/initramfs roots. This driver parses an image produced by mksquashfs and exposes it through the shared github.com/go-filesystems/interface Filesystem API.

Support summary

Feature Status Notes
Open / Close ✅ SquashFS 4.0 superblock; validates magic + version
ReadFile ✅ Data blocks + tail-end fragments + sparse blocks
Read at an offset ✅ (*FS).OpenFile(path) → io.ReaderAt + Size() (filesystem.Opener / filesystem.File); decompresses only the blocks a range touches, through the shared block cache
ListDir ✅ Multi-header directories (no ./.. — SquashFS omits them)
Stat ✅ mode (type + perms), size, inode number
ReadLink / Symlinks ✅ Targets read; followed during path resolution
Compression — gzip / xz / zstd / lzo / lz4 ✅ gzip (zlib), xz (LZMA2, Delta and BCJ filters), zstd, LZO1X, LZ4
Compression — lzma (legacy) ✅ Standalone LZMA1 stream decoded
Create image (BuildFromDir) ✅ Build a SquashFS 4.0 image from a tree; gzip or uncompressed; unsquashfs-readable
In-place writes (WriteFile/MkDir/…) ❌ The archive is immutable once written; mutators return ErrReadOnly

References

Module

github.com/go-filesystems/squashfs

Usage

fs, err := squashfs.OpenFile("image.squashfs")
if err != nil { /* ... */ }
defer fs.Close()

data, err := fs.ReadFile("/etc/hostname")
entries, err := fs.ListDir("/")

// Create an image from a directory tree (gzip by default).
err = squashfs.BuildFromDir("out.squashfs", "/path/to/tree", squashfs.BuildOptions{})
Reading part of a file

ReadFile returns the whole file, which is no use to anything serving reads on demand — a mount, an NFS or 9P export — where a 4 KiB request out of a multi-gigabyte image must not decompress the lot. The driver implements the optional filesystem.Opener capability:

var generic filesystem.Filesystem = fs
if o, ok := generic.(filesystem.Opener); ok {
    f, err := o.OpenFile("/usr/lib/big.so")
    if err != nil { /* ... */ }
    defer f.Close()

    buf := make([]byte, 4096)
    n, err := f.ReadAt(buf, 1<<30) // only the blocks that range touches
    _, _ = n, err
    _ = f.Size()                   // from the inode; decompresses nothing
}

Compression does not rule random access out. A SquashFS file is cut into fixed-size logical blocks (block_size, 128 KiB by default) compressed independently, and the inode carries each one's on-disk length. So the block holding byte N is N/block_size, and its position on disk is the running sum of the preceding lengths — a prefix sum computed once at OpenFile over metadata already decoded. Serving a range costs the one or two blocks it touches. Decompression goes through the same block cache ReadFile uses, which matters because a fragment block is shared between many files' tails.

ReadAt follows io.ReaderAt exactly (n < len(p) only with a non-nil error, io.EOF at the end) and is safe to call concurrently.

Note the method (*FS).OpenFile(path) (filesystem.File, error) is distinct from the package-level squashfs.OpenFile(path) (*FS, error), which opens an image from the host filesystem.

Untrusted images

An image is attacker-controlled input, and in SquashFS almost every size is a claim: a 4-byte sparse block entry stands for a whole block of zeros, metadata is compressed, and a fragment of a header can name gigabytes. Every input-driven allocation is therefore bounded before it is made (bombs_test.go measures each road with runtime.MemStats):

Claim in the image Bound
block_size / block_log at most 1 MiB / 20, the reference limits (Linux super.c)
a data block's on-disk length at most block_size, compressed or stored, and inside the image (Linux block.c)
a file's size ReadFile refuses more than DefaultMaxReadFile (1 GiB) with ErrTooLarge; ReadFileLimit(path, max) picks the ceiling, OpenFile + ReadAt stream any size
a file's block list read in chunks as the metadata supplies it, never sized from the claim
a directory at most 4 Mi entries (ErrTooLarge)
an xattr list the id index is read one pointer at a time; names and values are capped at 16 MiB per inode
zstd output the shared decoder stops at 1 MiB (the format maximum), not 64 GiB
lzma dictionary the dictionary the header declares (up to 4 GiB) is never allocated: the decoder (go-compressions/xz) grows its window with the data it produces and refuses to grow it beyond MaxDictionary, set to the block's output limit; a block that outgrows it is ErrCorrupt
xz dictionary the same: the block header's dictionary is a claim, not an allocation, and MaxDictionary is the block's output limit (a planted 1.5 GiB claim over a 44-byte stream costs about 23 KB; it cost 1.6 GB with ulikunitz/xz). The stream must also be one block of one stream

Fuzz targets: FuzzOpenRead, FuzzReadFile, FuzzReadMetaBlock, FuzzReadDir, FuzzDecompress, FuzzReadInode, FuzzReadXattr.

Limitations

  • Reading: gzip, xz, zstd, LZO, lz4 and legacy standalone lzma blocks are all decoded; xz blocks may carry the Delta and BCJ filters.
  • Writing (BuildFromDir): produces gzip or uncompressed images; files are stored as full data blocks (no tail-end fragment packing yet), all owned by uid/gid 0, no xattrs. Once written, an image is immutable (no in-place edits).
  • Extended attributes (xattr table) are not surfaced.
  • Intended for tooling and testing.

Documentation

Overview

Package squashfs is a pure-Go, read-only driver for the SquashFS 4.0 on-disk format produced by mksquashfs. It parses the superblock, metadata-backed inode and directory tables, file data blocks and tail-end fragments, and decompresses gzip/zlib-compressed (and uncompressed) metadata and data. SquashFS is an inherently read-only archive format; every mutating method of filesystem.Filesystem returns ErrReadOnly.

Index

Constants

View Source
const DefaultMaxReadFile int64 = 1 << 30

DefaultMaxReadFile is the largest file ReadFile will assemble in memory, 1 GiB. A SquashFS file can legitimately expand a very long way (a sparse block costs 4 bytes of block list, and the block list itself is compressed), so no structural bound relates the image's size to the file's; a ceiling is the only protection. Larger files stream through OpenFile and ReadAt, or raise the ceiling with ReadFileLimit.

View Source
const Magic = 0x73717368

Magic is the SquashFS superblock magic, "hsqs" in little-endian.

Variables

View Source
var (
	// ErrReadOnly is returned by every mutating method (WriteFile, MkDir,
	// DeleteFile, DeleteDir, Rename). SquashFS is a read-only archive.
	ErrReadOnly = errors.New("squashfs: filesystem is read-only")

	// ErrBadMagic is returned when the superblock magic is not "hsqs".
	ErrBadMagic = errors.New("squashfs: bad superblock magic")

	// ErrUnsupportedVersion is returned for any on-disk version other than 4.0.
	ErrUnsupportedVersion = errors.New("squashfs: unsupported version (only 4.0)")

	// ErrUnsupportedCompression is returned when the image uses a compressor
	// this driver cannot yet decode.
	ErrUnsupportedCompression = errors.New("squashfs: unsupported compression")

	// ErrNotFound is returned when a path component cannot be located.
	ErrNotFound = fmt.Errorf("squashfs: path not found: %w", iofs.ErrNotExist)

	// ErrNotDirectory is returned when ListDir targets a non-directory.
	ErrNotDirectory = errors.New("squashfs: not a directory")

	// ErrNotRegular is returned when ReadFile targets a non-regular file.
	ErrNotRegular = errors.New("squashfs: not a regular file")

	// ErrNotSymlink is returned when ReadLink targets a non-symlink.
	ErrNotSymlink = errors.New("squashfs: not a symbolic link")

	// ErrTooManyLinks is returned when path resolution exceeds the symlink hop
	// limit, indicating a loop.
	ErrTooManyLinks = errors.New("squashfs: too many symbolic link traversals")

	// ErrTooLarge is returned when a structure the image declares (a file's
	// contents for ReadFile, a directory listing, an attribute list) would
	// exceed a configured or built-in ceiling. It is returned before the
	// memory is allocated. Use OpenFile / ReadAt to stream a large file, or
	// ReadFileLimit to choose the ceiling.
	ErrTooLarge = errors.New("squashfs: declared size exceeds the limit")

	// ErrCorrupt is returned when an on-disk structure fails a sanity check.
	ErrCorrupt = errors.New("squashfs: corrupt image")
)

Sentinel errors returned by the SquashFS driver. Compare with errors.Is so wrapped errors continue to match.

Functions

func BuildFromDir

func BuildFromDir(imgPath, srcDir string, opts BuildOptions) error

BuildFromDir creates a SquashFS image at imgPath from the directory tree rooted at srcDir. It is the write-side counterpart of the reader: the result is a standard SquashFS 4.0 image that unsquashfs (and this package) can read.

func OpenReader added in v0.3.0

func OpenReader(r io.ReaderAt, size int64) (filesystem.Filesystem, error)

OpenReader opens the SquashFS filesystem at the start of r, exactly as Open does, and returns it as a filesystem.Filesystem.

It exists so every driver in go-filesystems answers to ONE name for the same thing. github.com/go-filesystems/detect's Opener is func(io.ReaderAt, int64) (filesystem.Filesystem, error): Open here is the right shape but returns a concrete *FS, which is a different type and does not fit. The drivers that could only be opened from a PATH have grown this function; this one is that same function, so a caller can write

detect.Register(detect.T, driver.OpenReader)

for every driver instead of a closure per driver.

The concrete type is still there for a caller that wants what it has beyond the common interface: Open returns it.

Types

type BuildOptions

type BuildOptions struct {
	// Uncompressed stores metadata and data blocks without compression.
	// When set it overrides Compressor.
	Uncompressed bool

	// Compressor selects the block compressor. The zero value is gzip
	// (zlib), matching mksquashfs's default. Ignored when Uncompressed is set.
	Compressor Compressor

	// NoFragments disables fragment packing, reverting to the legacy behaviour
	// where every regular file is stored as full data blocks. When false
	// (default) the tails of files smaller than one block, and whole sub-block
	// files, are packed into shared fragment blocks to shrink the image.
	NoFragments bool
}

BuildOptions configures image creation.

type Compressor

type Compressor uint16

Compressor selects the block compressor used by BuildFromDir. It mirrors the compression ids stored in the superblock. Only encoders the package can also decode are offered; LZO is read-only (no Go encoder available) and LZMA (legacy) is not produced.

const (
	CompressGZIP Compressor = compGZIP // raw zlib stream (mksquashfs default)
	CompressZSTD Compressor = compZSTD
	CompressXZ   Compressor = compXZ
	CompressLZ4  Compressor = compLZ4
)

Supported write compressors.

type FS

type FS struct {
	// contains filtered or unexported fields
}

FS is an opened, read-only SquashFS 4.0 filesystem.

The struct holds an io.ReaderAt, the decoded superblock and the block decompressor. All traversal is recomputed per call from the on-disk tables — no caches, no locks — which keeps the reader trivially safe for concurrent use against a frozen image.

func Open

func Open(rs io.ReaderAt, size int64) (*FS, error)

Open parses the SquashFS superblock at offset 0 and returns a read-only handle. The caller retains ownership of rs unless it also implements io.Closer, in which case Close is forwarded. Pass size = -1 if unknown.

func OpenFile

func OpenFile(path string) (*FS, error)

OpenFile opens path read-only and wires it into Open. The returned FS owns the file handle and closes it on Close.

func (*FS) Close

func (fs *FS) Close() error

Close releases the backing file handle if FS opened one.

func (*FS) DeleteDir

func (fs *FS) DeleteDir(string) error

func (*FS) DeleteFile

func (fs *FS) DeleteFile(string) error

func (*FS) ListDir

func (fs *FS) ListDir(path string) ([]filesystem.DirEntry, error)

ListDir enumerates the entries of the directory at path. SquashFS does not store "." or ".." entries, so they are absent from the result.

func (*FS) MkDir

func (fs *FS) MkDir(string, os.FileMode) error

func (*FS) OpenFile added in v0.2.0

func (fs *FS) OpenFile(path string) (filesystem.File, error)

OpenFile opens the regular file at path for random access, following symlinks exactly as ReadFile does.

It decodes the inode and computes the prefix sum of the block list — the map — but reads and decompresses no file data.

func (*FS) ReadFile

func (fs *FS) ReadFile(path string) ([]byte, error)

ReadFile returns the full contents of the regular file at path. Symlinks, including a final-component symlink, are followed.

A file larger than DefaultMaxReadFile (1 GiB) is refused with ErrTooLarge before any memory is allocated: the image declares the size, and a few kilobytes of sparse block list can declare terabytes. Use ReadFileLimit to choose the ceiling, or OpenFile to stream.

func (*FS) ReadFileLimit added in v0.5.0

func (fs *FS) ReadFileLimit(path string, max int64) ([]byte, error)

ReadFileLimit is ReadFile with a caller-chosen ceiling on the file's size, in bytes; a file the inode declares larger returns ErrTooLarge without allocating. A negative max disables the ceiling.

func (fs *FS) ReadLink(path string) (string, error)

ReadLink returns the target of the symbolic link at path; the final component is not followed.

func (*FS) Rename

func (fs *FS) Rename(string, string) error

func (*FS) Stat

func (fs *FS) Stat(path string) (filesystem.Stat, error)

Stat resolves path (following symlinks) and returns mode, size and inode.

func (*FS) Superblock

func (fs *FS) Superblock() *Superblock

Superblock returns the decoded superblock. The pointer is owned by FS.

func (*FS) WriteFile

func (fs *FS) WriteFile(string, []byte, os.FileMode) error

func (*FS) Xattrs

func (fs *FS) Xattrs(path string) (map[string][]byte, error)

Xattrs resolves path (following symlinks) and returns its extended attributes, keyed by fully-qualified name (with the namespace prefix, e.g. "user.comment") and mapped to the raw value bytes.

A nil map (and nil error) is returned when the node has no attributes or the image carries no xattr table. Only the extended inode variants store an xattr index, so basic inodes always report none.

type Superblock

type Superblock struct {
	Magic            uint32 // 0x00
	InodeCount       uint32 // 0x04
	ModTime          uint32 // 0x08 (unix seconds)
	BlockSize        uint32 // 0x0C
	FragCount        uint32 // 0x10
	Compression      uint16 // 0x14
	BlockLog         uint16 // 0x16 (log2 BlockSize)
	Flags            uint16 // 0x18
	IDCount          uint16 // 0x1A
	VersionMajor     uint16 // 0x1C
	VersionMinor     uint16 // 0x1E
	RootInode        uint64 // 0x20 (inode reference)
	BytesUsed        uint64 // 0x28
	IDTableStart     uint64 // 0x30
	XattrTableStart  uint64 // 0x38
	InodeTableStart  uint64 // 0x40
	DirTableStart    uint64 // 0x48
	FragTableStart   uint64 // 0x50
	LookupTableStart uint64 // 0x58
}

Superblock is the decoded SquashFS 4.0 superblock (little-endian on disk). Field order and offsets mirror struct squashfs_super_block.

Directories

Path Synopsis
benchmarks module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL