filesystem_ext4

package module
v0.4.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: 26 Imported by: 0

README

go-filesystems/ext4

ext4 filesystem

Go Reference CI

Package ext4 test helpers and journal

This package contains an in-memory ext4 implementation and test helpers used by the unit tests. The recent change shortens the critical section in Transaction.Commit() and adds a regression test exercising concurrent commits against the sidecar journal.

See the tests under test/ for usage examples.

Overview

Pure-Go read/write access to ext4 filesystem images.

  • No root privileges required.
  • No external tools required for normal operations (some cross-check tests use mke2fs).
  • No CGO.

This package supports extents, 64-bit block numbers, flex_bg, directory htree indexing, CRC32c metadata checksums, and automatic partition detection. It also reads the classic ext2/ext3 indirect block map (single-, double- and triple-indirect), inline data (small files and directories stored in the inode/xattr area), and sparse files (unallocated holes read back as zeros) — verified against real mke2fs-produced ext2/ext3/ext4 images across block sizes (see testdata/).

Production-readiness improvements (recent)

  • Re-entrant locking moved from a getGID() hack to explicit owner-token reentrantMutex APIs, enabling safe reentrancy without relying on runtime internals.
  • Metadata serialization for block-group descriptors (BGD), block and inode bitmaps and inode-table writes is implemented; metadata_csum support is available for checksum-protected metadata.
  • Online resize: Grow() implements expanding images and adding block groups.
  • IOCTLs: basic GetFlags/SetFlags ioctl paths are implemented where useful for tooling.
  • Filesystem inspection and repair hooks: CheckImage() and RepairImage() provide programmatic fsck/repair entry points for integration tests.
  • Concurrency hardening: bitmap/BGD/inode-table updates are serialized with per-group locks to avoid concurrent write corruption.
  • Tests: unit and integration tests were added for Grow, repair paths and concurrent bitmap updates; many code paths are covered under the test build tag.

Important notes

  • Production builds compile without the test build tag; test-only tracing and write hooks are guarded by //go:build test and are excluded from production binaries.
  • The previous getGID() test-only helper has been replaced with an owner-token based reentrantMutex API. Code should prefer LockOwner/UnlockOwner with an explicit token (see package docs and tests).
  • Test wrappers used by //go:build test expose an UnderlyingFile() helper (for example the loggingRW wrapper) so that lock keying can share the underlying descriptor without using unsafe reflection into unexported fields.

References

https://docs.kernel.org/filesystems/ext4/

Support summary

Feature Status Notes
Open / Close ✅ Supports partitioned images (MBR/GPT auto-detect)
Format ✅ Creates ext4 images; some cross-check tests require mke2fs
ReadFile / WriteFile ✅ Full file I/O (extents and ext2/3 block map); sparse-file holes read back as zeros
Read at an offset ✅ OpenFile(path) → io.ReaderAt + Size() (filesystem.Opener / filesystem.File). Snapshots the inode's block map — extents, ext2/3 indirect map, or inline data — and reads no file data at open, so a 4 KiB read out of a 4 GiB file costs 4 KiB. Holes read as zeros, as ReadFile produces them
Write at an offset ✅ OpenFile(path) → io.WriterAt + Truncate + Sync (filesystem.WritableFile), for extent-mapped inodes. Writes only the bytes given, allocates only the blocks the range touches, and keeps the file sparse: a write far past the end leaves the gap a hole. Inline-data and ext2/3 block-map inodes come back as a plain read-only File, so a caller falls back rather than failing
Inline data ✅ Reads small files and directories stored inline in the inode/xattr area
MkDir / Delete / Rename ✅ Directory and rename operations implemented
ReadLink / Symlinks ✅ Fast (in-inode) and slow (out-of-line) targets
Metadata serialization (BGD/bitmaps/inode-table) ✅ Writes + metadata_csum supported
Online resize (Grow / Shrink / Resize) ✅ Grow adds block groups; Shrink frees already-free trailing groups (non-relocating, resize2fs-style); Resize dispatches to whichever direction the new size requires. Open/Format return filesystem.Filesystem, so callers reach these by type-asserting to an interface exposing the method, e.g. fs.(interface{ Resize(int64) error })
IOCTLs (GetFlags/SetFlags) ✅ Tooling-level ioctl support implemented
CheckImage / RepairImage hooks ✅ Programmatic fsck/repair entry points
Concurrency hardening ✅ Per-group locks for bitmap/BGD/inode-table
Test-only tracing ✅ Heavy tracing under //go:build test for diagnostics

Untrusted input

An image is attacker-controlled bytes. Every allocation and every walk the driver sizes from a value read off the image is checked against what the image can hold before it happens (tests assert < 1 MiB allocated for each refused road):

Input Bound
file i_size for ReadFile the smaller of the declared filesystem size and DefaultMaxReadSize (1 GiB); fs.(interface{ SetMaxReadSize(int64) }) changes it; larger files are read with OpenFile; refused with ErrTooLarge
ext2/ext3 indirect block map walked as a tree (holes cost nothing); every pointer, indirect blocks included, is charged against the blocks the device holds; an indirect block reached twice is refused
extent leaves overlapping leaves (logical or physical) are refused: each would make a reader decode the same blocks again, so a few KiB of leaves could cost gigabytes
sidecar journal descriptor count and each data length are checked against the bytes the journal file still holds before any buffer is sized from them
extent tree depth ≤ 5, strictly decreasing, no block visited twice

Fuzz targets: FuzzBlockMapExtents, FuzzExtentList, FuzzJournalSidecar (these assert the allocation stays proportional to the input), plus the older FuzzOpenAndRead, FuzzParseExtentNode, FuzzParseDirBlock, FuzzReadSuperblock.

Reading part of a file

ReadFile returns the whole file, which is unusable for anything serving reads on demand — a mount, an NFS or 9P export — where a 4 KiB request out of a 4 GiB file must not allocate 4 GiB. The driver implements the optional filesystem.Opener capability:

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

    buf := make([]byte, 4096)
    n, err := f.ReadAt(buf, 1<<30) // only the bytes asked for
    _, _ = n, err
    _ = f.Size()                   // i_size, read at open; touches no data
}

OpenFile snapshots the inode's block map — extent leaves for ext4, the synthesised equivalent for the ext2/ext3 indirect map, or the inline bytes for an inline-data inode — using the same inode.readExtents the ReadFile path uses, under the same inode lock. ReadAt binary-searches that map, reads whole contiguous runs in one go, and serves holes as zeros without touching the device. It follows io.ReaderAt exactly (n < len(p) only with a non-nil error, io.EOF at the end) and is safe to call concurrently.

A read-only File is a snapshot: it describes the file as it was when opened. A file rewritten through the same Filesystem afterwards must be reopened.

Writing part of a file

WriteFile replaces a whole file — and this driver's WriteFile frees every block of it and reallocates them — so a positional write expressed as ReadFile + splice

  • WriteFile costs O(filesize) per request, and a client writing a file in fixed-size blocks pays that per block. Measured over a real Linux kernel NFS mount against a driver without a positional write, 2 MiB in 64 KiB blocks took 23 s (90 kB/s), and a soft,timeo=50 mount gave up with EIO partway through.

The File returned by OpenFile is therefore also a filesystem.WritableFile — when the inode's layout allows it:

if o, ok := fs.(filesystem.Opener); ok {
    f, _ := o.OpenFile("/var/lib/big.img")
    defer f.Close()
    if w, ok := f.(filesystem.WritableFile); ok {
        _, err := w.WriteAt(buf, 1<<30) // io.WriterAt semantics, exactly
        _ = w.Truncate(1 << 20)         // grow is free; shrink frees blocks
        _ = w.Sync()                    // fsync(2) on a file-backed image
        _ = err
    }
    // otherwise: ReadFile, splice, WriteFile — correct, and quadratic.
}

WriteAt reuses the extent map resolved at open, turning an offset into a physical block by binary search, and allocates only for the blocks the written range actually covers. Sparseness is preserved: a write far past the end allocates nothing for the gap, which stays a hole, costs no space, and reads back as zeros through both ReadAt and ReadFile. That is what ext4 means by a sparse file, and it is the one place this driver's WriteAt deliberately differs from FAT's, which has to allocate the gap.

Two layouts come back as a plain, read-only File: an inline-data inode, and the classic ext2/ext3 indirect block map, whose growth would mean allocating indirect blocks that no write path in this package has ever produced. They are refused at the probe rather than from WriteAt, because the type assertion is a caller's only chance to learn the truth before it commits to a strategy — a File that satisfied the interface and then failed every call would force a server to discover the limitation one failed request at a time. Refusing at the probe is the difference between "slower" and "broken".

Limitations & cautions

  • This package is intended primarily for tooling and tests (image creation, inspection, repair, and offline modification). It is not a kernel filesystem driver and does not aim to be a drop-in replacement for the in-kernel ext4 implementation.
  • Not every kernel ioctl or obscure ext4 feature is implemented; check tests and source for the supported surface.
  • Integration tests that cross-validate behavior against e2fsprogs (mke2fs, e2fsck) are provided but may be skipped on platforms lacking those tools.
  • Performance and large-scale concurrency behavior are still subjects for benchmarking; see BENCHMARKS.md and the "Backoff tuning summary" section below.

Module

github.com/go-filesystems/ext4

Volume label

The driver implements the optional filesystem.Labeller interface so callers can read and update the ext2/3/4 superblock's s_volume_name in place:

fs, _ := ext4.Open("disk.img", -1)
defer fs.Close()
if l, ok := fs.(filesystem.Labeller); ok {
    fmt.Println("label:", l.Label())
    _ = l.SetLabel("cloudimg-rootfs")
}

SetLabel is offline-safe only — it takes the FS write lock, re-reads the on-disk superblock, zero-fills bytes [120:136], writes the new label, recomputes the superblock CRC-32C if metadata_csum is enabled, and WriteAt+Syncs the 1024-byte block back. Concurrent writers may produce a torn superblock; use it on a filesystem no other process is actively mutating.

The superblock checksum uses the kernel-canonical CRC-32C convention — crc32c(~0, sb[:0x3FC]), no final XOR, no seed — distinct from every other metadata structure in ext4 (which is seeded from s_checksum_seed or crc32c(~0, uuid)). Mismatching the convention produces "Found ext4 filesystem with invalid superblock checksum" on the next Linux mount.

Quick start — validation and best practices

1. Static analysis
go vet ./...
# staticcheck ./...  # run if staticcheck is installed
2. Run tests with the race detector (test-only tracing requires -tags test)

Recommended quick run (from the repo root):

go test -race -tags test ./... -v

Run the test harness package (shorter):

go test -race -tags test ./test -v

For the whole repository (long), saving output to a local log:

go test -race ./... 2>&1 | tee all_tests_race.log

To check coverage for the package:

go test -tags test -coverprofile=cover.out . && go tool cover -html=cover.out
3. Build without test tags to ensure no test-only dependencies leak
go build .

What's changed (high level)

  • Replaced getGID()-based reentrancy with owner-token reentrantMutex APIs.

  • Implemented metadata serialization for BGD / bitmaps / inode-table and metadata_csum support.

  • Added Grow() to perform online image expansions (adds block groups, updates superblock/BGD tables).

  • Added basic ioctl support for common tooling flags.

  • Exposed CheckImage() / RepairImage() hooks for programmatic fsck/repair.

  • Hardened concurrency around bitmap/BGD/inode-table updates and added tests that exercise concurrent bitmap updates.

  • Local fixes (developer notes):

    • Cached debug/lock flags to avoid hot-path os.Getenv calls.
    • Added a configurable idle timeout for commitWorker goroutines to prevent unbounded worker proliferation under stress (EXT4_COMMIT_WORKER_IDLE_SEC).
    • Fixed commit-sequence stall: Transaction.Commit() now guarantees applySeq always advances (via mustAdvanceApplySeq deferred flag) even when apply returns an error, preventing all goroutines from blocking indefinitely on dirML.Lock().
    • Stress harness adjusted: removed inter-test t.Parallel() and reduced default workload (workers, ops, write_hammer) to complete within the standard go test 10-minute timeout.
    • Verified targeted and package-level tests under -race -tags test after changes.

Backoff tuning summary

We performed a small parameter sweep to reduce lock-order contention and blocking fallbacks observed under the TestStress_Concurrent harness. The table below summarizes the long-run (32 workers × 500 ops) fallback counts for a few candidate (attempts, base_us) values. Results are noisy and depend on workload timing; treat them as guidance for tuning.

Attempts Base (µs) Total fallbacks (32×500) Top hot-group Top count Notes
4 250 435 fd:4:inode:g:3:block:false 265 Lower peak but higher total than 6,250
6 250 425 fd:4:inode:g:3:block:false 294 Recommended: lowest total fallbacks
8 250 437 fd:4:inode:g:3:block:false 341 Higher total, higher peak

Tradeoffs:

  • Lower attempts can reduce contention bursts (lower peak on hot-group), but may increase total blocking fallbacks across the run.
  • Higher attempts with jitter spreads retries but can increase aggregate attempts and CPU; balance to minimize total fallbacks for your workload.

Reproduce a single data point locally (example: attempts=6, base=250µs) from the repo root:

EXT4_BACKOFF_MAX_ATTEMPTS=6 EXT4_BACKOFF_BASE_US=250 \
  EXT4_LOCK_DEBUG=1 EXT4_STRESS_WORKERS=32 EXT4_STRESS_OPS_PER_WORKER=500 \
  go test -count=1 -run 'TestStress_Concurrent/debian/concurrent_rw' -tags test ./test -v

The sweep above was produced by varying EXT4_BACKOFF_MAX_ATTEMPTS / EXT4_BACKOFF_BASE_US across the same command and comparing the printed fallback counts; no grid-running script ships in this repository, so a full sweep needs to be scripted by the caller.

Next tuning options:

  • Run a denser grid around (6,250) to refine the minimum.
  • Increase allocation dispersion (allocGroupCursor) or subdivide BGD locks to reduce serialization on very hot groups.

For questions or to request additional test runs (benchmarks, long-running integration), open an issue on this repository.

Documentation

Overview

Package ext4 provides read/write access to ext4 filesystem images without requiring root privileges. It supports raw disk images (MBR/GPT) as well as bare filesystem images and handles modern ext4 features: extents, 64-bit block numbers, flex_bg, dir_htree and metadata_csum (CRC32c checksums).

Package ext4 provides read/write access to ext4 filesystem images. See ext4.go for exported types and constants.

tryMutex implements a mutex with TryLock semantics using a buffered channel of size 1. It provides Lock/Unlock (blocking) and TryLock

Index

Constants

View Source
const (
	FeatIncompatFiletype   = 0x0002
	FeatIncompatExtents    = 0x0040
	FeatIncompat64bit      = 0x0080
	FeatIncompatFlexBg     = 0x0200
	FeatIncompatCsumSeed   = 0x2000
	FeatIncompatInlineData = 0x8000
)

Incompatible feature flags (must understand to read/write).

View Source
const (
	FeatROCompatSparseSuper  = 0x0001
	FeatROCompatLargeFile    = 0x0002
	FeatROCompatGdtCsum      = 0x0010
	FeatROCompatMetadataCsum = 0x0400
)

Read-only compatible feature flags.

View Source
const (
	InodeFlagHashIndex  = 0x00001000 // directory uses htree index
	InodeFlagExtents    = 0x00080000 // inode uses extent tree
	InodeFlagInlineData = 0x10000000 // data stored inline in inode
)

Inode flags.

View Source
const (
	FtUnknown = 0
	FtRegFile = 1
	FtDir     = 2
	FtSymlink = 7
	FtDirTail = 0xDE // fake tail entry carrying checksum
)

Directory entry file types.

View Source
const DefaultMaxReadSize int64 = 1 << 30

DefaultMaxReadSize is the largest file ReadFile will materialise in memory unless SetMaxReadSize says otherwise: 1 GiB.

A hole costs no bytes on disk, so a sparse file's i_size is bounded by nothing in the image; only the caller knows how much memory it is willing to spend. Anything larger should be read through OpenFile, which allocates nothing proportional to the file.

View Source
const ExtentMagic uint16 = 0xF30A

Extent tree magic number.

View Source
const MaxLabelLen = 16

MaxLabelLen is the on-disk size of the ext4 volume label (s_volume_name, at superblock offset 0x78).

View Source
const RootIno uint32 = 2

Well-known inode numbers.

Variables

View Source
var (
	InodeOffMode       = inodeOffMode
	InodeOffSizeLo     = inodeOffSizeLo
	InodeOffLinksCount = inodeOffLinksCount
	InodeOffFlags      = inodeOffFlags
	InodeOffBlock      = inodeOffBlock
	InodeOffGeneration = inodeOffGeneration
	InodeOffFilACLLo   = inodeOffFilACLLo
	InodeOffSizeHi     = inodeOffSizeHi
	InodeOffBlocksHi   = inodeOffBlocksHi
	InodeOffCsumLo     = inodeOffCsumLo
	InodeOffExtraIsize = inodeOffExtraIsize
	InodeOffCsumHi     = inodeOffCsumHi
)

Export inode offset constants used by tests.

View Source
var CommitHook func(seq uint64, entries int)

CommitHook, when set (tests only), is invoked with the transaction sequence number each time a transaction is committed. This allows tests to observe/measure commit behavior without parsing debug logs. CommitHook, when set (tests only), is invoked with the transaction sequence number and the number of entries each time a transaction is committed. This allows tests to observe/measure commit behavior without parsing debug logs.

View Source
var ErrReadOnlyReader = errors.New("ext4: this filesystem was opened over a reader that cannot be written")

ErrReadOnlyReader is what a write gets when the filesystem was opened over something that can only be read.

View Source
var ErrTooLarge = safeio.ErrTooLarge

ErrTooLarge is returned, wrapped, when an on-disk size or count would make the driver allocate or walk more than the image can legitimately hold. It is safeio.ErrTooLarge, so errors.Is works against either name.

View Source
var LinuxPartTypeGPT = gpt.LinuxFilesystemGUID

Expose partition constants for tests. These now come from the shared go-volumes/gpt parser (partition.go migrated to it).

View Source
var SectorSize = int64(gpt.SectorSize)

Functions

func AddDirEntry

func AddDirEntry(f ReaderWriterAt, fsOffset int64, sb *Superblock, dir *Inode, child uint32, name string, fileType uint8) error

AddDirEntry exposes addDirEntry.

func AddFastSymlink(t testing.TB, fs *Ext4FS, parentPath, name, target string)

AddFastSymlink creates a symlink inode and dir entry in the live filesystem (used by tests that need a quick symlink creation helper).

func AddRangeToTx

func AddRangeToTx(tx *Transaction, f ReaderWriterAt, fsOffset int64, sb *Superblock, startAbs int64, data []byte) error

AddRangeToTx exposes addRangeToTx so external tests can prepare transaction entries using the same locking semantics as production callers. This prevents test-only preparations from bypassing per-block locking and triggering lost-update races.

func AllocBlocks

func AllocBlocks(f ReaderWriterAt, fsOffset int64, sb *Superblock, n uint32) ([]uint64, error)

AllocBlocks exposes allocBlocks.

func AllocInode

func AllocInode(f ReaderWriterAt, fsOffset int64, sb *Superblock, isDir bool) (uint32, error)

AllocInode exposes allocInode.

func BgdOffset

func BgdOffset(sb *Superblock, g uint32) int64

BgdOffset exposes bgdOffset.

func BgdRaw

func BgdRaw(d *Bgd) []byte

func BgdTableBlock

func BgdTableBlock(sb *Superblock) uint64

func CRC32c

func CRC32c(crc uint32, data []byte) uint32

CRC32c exposes crc32c.

func ClearBit

func ClearBit(b []byte, i int)

func CloneFSImageFromPath

func CloneFSImageFromPath(path string) ([]byte, error)

CloneFSImageFromPath reads the image file into a buffer copy for in-memory tests.

func ComputeInodeCsum

func ComputeInodeCsum(sb *Superblock, in *Inode)

computeInodeCsum wrapper

func CreateFullWorker

func CreateFullWorker(f ReaderWriterAt, fsOffset int64, sb *Superblock, group uint32, capacity int) error

CreateFullWorker inserts a commitWorker for the provided file/group with a tasks channel of the given capacity and pre-fills it so enqueue attempts will hit the queue-full fallback path. Returns an error if a worker already exists for the computed key.

func CsumSeed

func CsumSeed(sb *Superblock) uint32

func DecrementAndFreeInode

func DecrementAndFreeInode(f ReaderWriterAt, fsOffset int64, sb *Superblock, in *Inode) error

DecrementAndFreeInode exposes decrementAndFreeInode.

func DirBlockForPath

func DirBlockForPath(t testing.TB, rw ReaderWriterAt, sb *Superblock, path string) uint64

DirBlockForPath returns the first data block for the given path.

func DumpBlockingFallbacks

func DumpBlockingFallbacks()

func DumpCommitTrace

func DumpCommitTrace()

func DumpDebugLog

func DumpDebugLog(w io.Writer)

DumpDebugLog writes the accumulated debug buffer to the provided writer.

func EncodeBGD

func EncodeBGD(d *Bgd, sb *Superblock) []byte

EncodeBGD encodes a decoded BGD into its raw descriptor bytes and returns the raw byte slice for inspection by external tests.

func EnqueueRawCommitOps

func EnqueueRawCommitOps(f ReaderWriterAt, fsOffset int64, sb *Superblock, group uint32, starts []int64, datas [][]byte) (chan error, uint64, error)

EnqueueRawCommitOps exposes a thin wrapper allowing tests to enqueue a multi-op commit where each op is specified by a start offset (relative to fsOffset) and a data slice. This is useful for constructing a single commit that contains both a bitmap write and a BGD write so the worker's `lastBitmap` optimization can be exercised in tests.

func FindFreeBit

func FindFreeBit(b []byte, maxBits int) (int, bool)

func FindFreeRun

func FindFreeRun(b []byte, maxBits, n int) ([]int, bool)

func Format

func Format(path string, sizeBytes int64, cfg FormatConfig) (filesystem.Filesystem, error)

Format creates a new ext4 filesystem in the file at path. The file is created (or truncated) and formatted. sizeBytes must be a multiple of 4096 and large enough to hold filesystem metadata plus at least one data block.

On success the newly formatted filesystem is opened and returned; the caller must Close it when done.

func FreeBlock

func FreeBlock(f ReaderWriterAt, fsOffset int64, sb *Superblock, block uint64) error

FreeBlock exposes freeBlock.

func FreeInodeBlocks

func FreeInodeBlocks(f ReaderWriterAt, fsOffset int64, sb *Superblock, in *Inode) error

FreeInodeBlocks exposes freeInodeBlocks.

func FreeInodeSlot

func FreeInodeSlot(f ReaderWriterAt, fsOffset int64, sb *Superblock, inodeNum uint32) error

FreeInodeSlot exposes freeInodeSlot.

func GetCommitWorkerIdleSecs

func GetCommitWorkerIdleSecs() int

Get/Set commit worker idle duration (seconds) for tests.

func GetSeqForAck

func GetSeqForAck(ack chan error) (uint64, bool)

Test-only helpers to observe and control ack->seq mapping behavior. GetSeqForAck returns the registered seq for the provided ack channel and a boolean indicating whether the mapping exists.

func HookMemFileBuf

func HookMemFileBuf(h *HookMemFile) []byte

HookMemFileBuf returns the underlying buffer for tests that need to mutate or inspect the raw bytes inside a HookMemFile.

func InodeNum

func InodeNum(in *Inode) uint32

func InodeOffsetFor

func InodeOffsetFor(r ReaderWriterAt, sb *Superblock, inodeNum uint32) int64

InodeOffsetFor computes inode byte offset (helper for tests).

func InodeRaw

func InodeRaw(in *Inode) []byte

func InodeSizeVal

func InodeSizeVal(in *Inode) uint64

func IsDir

func IsDir(in *Inode) bool

func IsRegular

func IsRegular(in *Inode) bool

func IsSeqAcked

func IsSeqAcked(seq uint64) bool
func IsSymlink(in *Inode) bool

func MakeDir

func MakeDir(f ReaderWriterAt, fsOffset int64, sb *Superblock, path string, perm os.FileMode) error

MakeDir exposes the internal makeDir helper for external tests.

func MarkSeqAcked

func MarkSeqAcked(seq uint64)

Test helpers for commit dispatcher internals.

func MinDirentSize

func MinDirentSize(nameLen int) int

Dirent helpers.

func NewOwner

func NewOwner() uint64

NewOwner returns a new unique owner token for locking purposes.

func NumBlockGroups

func NumBlockGroups(sb *Superblock) uint32

Exported superblock convenience accessors for external tests.

func Open

func Open(imagePath string, partIndex int) (filesystem.Filesystem, error)

Open opens an ext4 filesystem image at imagePath, automatically detecting the partition table (MBR / GPT) and using the first Linux partition. Pass partIndex = -1 for auto-detection (first Linux partition).

func OpenFromDevice

func OpenFromDevice(dev blockDevice, partIndex int) (filesystem.Filesystem, error)

OpenFromDevice opens an ext4 filesystem backed by an arbitrary block device. dev must remain valid until the returned Filesystem is closed.

func OpenReader added in v0.3.0

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

OpenReader opens the ext4 filesystem that starts at offset zero of r.

It exists so this driver fits the one shape the rest of go-filesystems agrees on: github.com/go-filesystems/detect's Opener is func(io.ReaderAt, int64) (filesystem.Filesystem, error). OpenFromDevice was already close, but its parameter is an UNEXPORTED interface -- no caller outside this package can name a type that satisfies it, so in practice the only way in was a path.

There is no partition index: r is expected to begin at the filesystem, which is what detect has just established by reading the magic there. Open remains the way to look through a partition table.

WRITES need an io.WriterAt. A reader that is only a reader gives a filesystem that serves every read and refuses every write with ErrReadOnlyReader -- rather than one that accepts a write and loses it.

The reader is NOT closed by Close: the caller opened it and still owns it. That is the same division as iso9660, squashfs, hfsplus and ufs, whose reader-based constructors have always worked this way.

func OwnerFrom

func OwnerFrom(ctx context.Context) (uint64, bool)

OwnerFrom extracts an owner token from context.

func PartitionOffset

func PartitionOffset(r io.ReaderAt, partIndex int) (int64, error)

func PartitionOffsetSized

func PartitionOffsetSized(r io.ReaderAt, partIndex int, deviceSize int64) (int64, error)

PartitionOffsetSized exposes partitionOffset with an explicit device size so tests can exercise the go-volumes/gpt bounds checks against a known size.

func ReadBitmap

func ReadBitmap(f ReaderWriterAt, fsOffset int64, sb *Superblock, bitmapBlock uint64) ([]byte, error)

ReadBitmap exposes readBitmap.

func ReadFileData

func ReadFileData(f ReaderWriterAt, fsOffset int64, sb *Superblock, in *Inode) ([]byte, error)

ReadFileData exposes readFileData.

func ReadRawBlock

func ReadRawBlock(f ReaderWriterAt, fsOffset int64, sb *Superblock, blockNum uint64) ([]byte, error)

ReadRawBlock exposes readRawBlock.

func ReadSymlink(f ReaderWriterAt, fsOffset int64, sb *Superblock, in *Inode) (string, error)

ReadSymlink exposes readSymlink.

func RemoveDir

func RemoveDir(f ReaderWriterAt, fsOffset int64, sb *Superblock, path string) error

RemoveDir exposes removeDir for tests that operate on low-level images.

func RemoveFile

func RemoveFile(f ReaderWriterAt, fsOffset int64, sb *Superblock, path string) error

RemoveFile exposes removeFile for tests that operate on low-level images.

func RemoveWorkerFor

func RemoveWorkerFor(f ReaderWriterAt, fsOffset int64, sb *Superblock, group uint32)

RemoveWorkerFor removes any injected worker for the given file/group.

func Rename

func Rename(f ReaderWriterAt, fsOffset int64, sb *Superblock, oldPath, newPath string) error

Rename exposes the internal rename helper for external tests.

func RootDirBlockFor

func RootDirBlockFor(t testing.TB, rw ReaderWriterAt, sb *Superblock) uint64

RootDirBlockFor returns the first data block for the root directory.

func SetAckToSeqDeleteGraceMS

func SetAckToSeqDeleteGraceMS(ms int)

SetAckToSeqDeleteGraceMS sets the test-only ack->seq deletion grace duration in milliseconds. Useful for exercising TTL behavior in tests.

func SetAddDirEntryAllocBlocks

func SetAddDirEntryAllocBlocks(fn func(ReaderWriterAt, int64, *Superblock, uint32) ([]uint64, error)) func(ReaderWriterAt, int64, *Superblock, uint32) ([]uint64, error)

Setters for addDirEntry package-level hooks so external tests can inject failures or behavior.

func SetAddDirEntrySetInlineExtents

func SetAddDirEntrySetInlineExtents(fn func(*Inode, []extentLeaf) error) func(*Inode, []extentLeaf) error

func SetAddDirEntryWriteBlock

func SetAddDirEntryWriteBlock(fn func(ReaderWriterAt, int64, *Superblock, uint64, []byte) error) func(ReaderWriterAt, int64, *Superblock, uint64, []byte) error

func SetBit

func SetBit(b []byte, i int)

Bitmap/bit helpers.

func SetCommitWorkerIdleSecs

func SetCommitWorkerIdleSecs(s int)

func SetFormatOpenFS

func SetFormatOpenFS(fn func(string, int) (filesystem.Filesystem, error)) func(string, int) (filesystem.Filesystem, error)

func SetFormatOpenFile

func SetFormatOpenFile(fn func(string) (FormatFile, error)) func(string) (FormatFile, error)

Format hook setters - allow external tests to inject file/open/rand behavior.

func SetFormatRandRead

func SetFormatRandRead(fn func([]byte) (int, error)) func([]byte) (int, error)

func SetInlineExtents

func SetInlineExtents(in *Inode, exts []ExtentLeaf) error

SetInlineExtents sets inline extents on an inode (wrapped method).

func SetMakeDirAllocBlocks

func SetMakeDirAllocBlocks(fn func(ReaderWriterAt, int64, *Superblock, uint32) ([]uint64, error)) func(ReaderWriterAt, int64, *Superblock, uint32) ([]uint64, error)

func SetMakeDirAllocInode

func SetMakeDirAllocInode(fn func(ReaderWriterAt, int64, *Superblock, bool) (uint32, error)) func(ReaderWriterAt, int64, *Superblock, bool) (uint32, error)

func SetMode

func SetMode(in *Inode, mode uint16, links uint16)

func SetReadLinkReadInode

func SetReadLinkReadInode(fn func(ReaderWriterAt, int64, *Superblock, uint32) (*Inode, error)) func(ReaderWriterAt, int64, *Superblock, uint32) (*Inode, error)

func SetRemoveDirChildDir

func SetRemoveDirChildDir(fn func(ReaderWriterAt, int64, *Superblock, string) error) func(ReaderWriterAt, int64, *Superblock, string) error

Setter helpers for package-level hooks used by tests.

func SetRemoveDirChildFile

func SetRemoveDirChildFile(fn func(ReaderWriterAt, int64, *Superblock, string) error) func(ReaderWriterAt, int64, *Superblock, string) error

func SetRemoveDirFreeBlocks

func SetRemoveDirFreeBlocks(fn func(ReaderWriterAt, int64, *Superblock, *Inode) error) func(ReaderWriterAt, int64, *Superblock, *Inode) error

func SetRemoveDirFreeSlot

func SetRemoveDirFreeSlot(fn func(ReaderWriterAt, int64, *Superblock, uint32) error) func(ReaderWriterAt, int64, *Superblock, uint32) error

func SetSize

func SetSize(in *Inode, size uint64)

func SetWriteFileSetInlineExtents

func SetWriteFileSetInlineExtents(fn func(*Inode, []extentLeaf) error) func(*Inode, []extentLeaf) error

func TryInsertDirEntry

func TryInsertDirEntry(buf []byte, childIno uint32, name string, fileType uint8, needed int) bool

func UpdateDirBlockCsum

func UpdateDirBlockCsum(buf []byte, sb *Superblock, dir *Inode)

UpdateDirBlockCsum exposes updateDirBlockCsum for tests.

func UpdateDotDot

func UpdateDotDot(f ReaderWriterAt, fsOffset int64, sb *Superblock, dirIno *Inode, newParentIno uint32) error

UpdateDotDot exposes the internal updateDotDot helper for tests that need to exercise updating '..' directory entries.

func WithOwner

func WithOwner(ctx context.Context) (context.Context, uint64)

WithOwner returns a new context that carries a unique owner token and the token itself. Useful for propagating an ownership token through call chains.

func WorkerExistsFor

func WorkerExistsFor(f ReaderWriterAt, fsOffset int64, sb *Superblock, group uint32) bool

WorkerExistsFor returns true if a worker is registered for the file/group.

func WriteBGD

func WriteBGD(f ReaderWriterAt, fsOffset int64, sb *Superblock, g uint32, d *Bgd) error

WriteBGD exposes writeBGD.

func WriteBitmapBuf

func WriteBitmapBuf(f ReaderWriterAt, fsOffset int64, sb *Superblock, g uint32, d *Bgd, isBlock bool, bitmapBlock uint64, bmap []byte) error

WriteBitmapBuf exposes writeBitmapBuf for low-level bitmap testing.

func WriteBitmapWithCsum

func WriteBitmapWithCsum(f ReaderWriterAt, fsOffset int64, sb *Superblock, g uint32, d *Bgd, isBlockBitmap bool) error

WriteBitmapWithCsum exposes writeBitmapWithCsum.

func WriteDirEntry

func WriteDirEntry(buf []byte, off int, ino uint32, recLen uint16, name string, fileType uint8)

func WriteFileRaw

func WriteFileRaw(f ReaderWriterAt, fsOffset int64, sb *Superblock, path string, data []byte, perm os.FileMode) error

WriteFileRaw exposes the internal writeFile helper used by package tests.

func WriteInode

func WriteInode(f ReaderWriterAt, fsOffset int64, sb *Superblock, in *Inode) error

WriteInode exposes writeInode.

func WriteRawBlock

func WriteRawBlock(f ReaderWriterAt, fsOffset int64, sb *Superblock, blockNum uint64, data []byte) error

WriteRawBlock exposes writeRawBlock.

func WriteSuperblock

func WriteSuperblock(f ReaderWriterAt, fsOffset int64, sb *Superblock) error

func ZeroDirEntry

func ZeroDirEntry(f ReaderWriterAt, fsOffset int64, sb *Superblock, dirIno *Inode, name string) error

ZeroDirEntry exposes zeroDirEntry.

func ZeroEntryInBlock

func ZeroEntryInBlock(buf []byte, name string, le binary.ByteOrder) bool

ZeroEntryInBlock exposes zeroEntryInBlock.

Types

type Bgd

type Bgd = bgd

func DecodeBGD

func DecodeBGD(raw []byte, sb *Superblock) *Bgd

DecodeBGD exposes decodeBGD.

func ReadBGD

func ReadBGD(f ReaderWriterAt, fsOffset int64, sb *Superblock, g uint32) (*Bgd, error)

ReadBGD exposes readBGD.

type BlockDevice

type BlockDevice = blockDevice

BlockDevice is the exported alias of blockDevice, lets external packages satisfy the interface and pass instances to OpenFromDevice. Without it, OpenFromDevice's signature `(dev blockDevice, …)` references an unexported type — only code inside this package could call it. Use cases for implementing BlockDevice externally:

  • LUKS / dm-crypt: wrap a github.com/go-fde/luks.Device so ext4.OpenFromDevice reads the plaintext payload.
  • In-memory testing: feed a *bytes.Reader-backed fixture into the FS without writing a tempfile.
  • qcow2 / DMG / other image formats from the go-diskimages family: surface the unpacked block view as a BlockDevice.

type DirEntry

type DirEntry struct {
	Inode    uint32
	Name     string
	FileType uint8
}

DirEntry is a parsed directory entry.

func ParseDirBlock

func ParseDirBlock(buf []byte) []DirEntry

func ReadDir

func ReadDir(f ReaderWriterAt, fsOffset int64, sb *Superblock, dirIno *Inode) ([]DirEntry, error)

ReadDir exposes readDir for tests that need to enumerate directory entries from a single inode.

type Ext4FS

type Ext4FS = ext4FS

Expose the concrete FS type as an exported alias for tests.

func NewTempFS

func NewTempFS(t testing.TB) (*Ext4FS, func())

NewTempFS creates and returns a temporary formatted ext4 filesystem for tests. The returned cleanup function should be deferred by the caller.

func NewTempFSWithSize

func NewTempFSWithSize(t testing.TB, sizeBytes int64) (*Ext4FS, func())

NewTempFSWithSize creates and returns a temporary formatted ext4 filesystem for tests with a custom image size in bytes. The returned cleanup function should be deferred by the caller.

type ExtentLeaf

type ExtentLeaf = extentLeaf

func BuildExtents

func BuildExtents(phys []uint64) []ExtentLeaf

BuildExtents converts phys block list into exported ExtentLeaf slice.

func ParseExtentNode

func ParseExtentNode(f ReaderWriterAt, fsOffset int64, sb *Superblock, buf []byte, inodeNum uint32, inodeRaw []byte) ([]ExtentLeaf, error)

ParseExtentNode exposes the hardened extent-tree entry point (parseExtentRoot), which seeds depth and cycle tracking before recursing.

type FS

type FS = ext4FS

FS is a public alias for the concrete ext4 filesystem type. Used by downstream packages (apple-vz/grub) that need to refer to the concrete type for method receivers. The Ext4FS alias (same target) is gated behind `//go:build test` for test-only helpers; FS is always available so production code can hold a typed pointer.

type FormatConfig

type FormatConfig struct {
	// UUID is the filesystem UUID. A random v4 UUID is generated when all bytes are zero.
	UUID [16]byte
	// Label is the volume label stored in the superblock (trimmed to 16 bytes).
	Label string
}

FormatConfig holds optional parameters for Format. All fields are optional; sensible defaults are used when left at their zero value.

type FormatFile

type FormatFile = formatFile

FormatFile is an alias to the internal formatFile interface used by Format.

type HookMemFile

type HookMemFile struct {

	// Exported hook fields are convenient for external tests.
	ReadHook  func(off int64, p []byte) error
	WriteHook func(off int64, p []byte) error
	// contains filtered or unexported fields
}

HookMemFile is a small in-memory reader/writer used by tests.

func CloneFSImage

func CloneFSImage(t testing.TB, fs *Ext4FS) *HookMemFile

CloneFSImage reads the backing file for an open FS into a HookMemFile copy.

func NewHookMemFile

func NewHookMemFile(buf []byte) *HookMemFile

NewHookMemFile returns a HookMemFile backed by the provided buffer. This is a convenience for external tests that need a HookMemFile with an initial backing store.

func (*HookMemFile) ReadAt

func (m *HookMemFile) ReadAt(p []byte, off int64) (int, error)

func (*HookMemFile) WriteAt

func (m *HookMemFile) WriteAt(p []byte, off int64) (int, error)

type Inode

type Inode = inode

func LookupParent

func LookupParent(f ReaderWriterAt, fsOffset int64, sb *Superblock, path string) (*Inode, string, error)

LookupParent exposes lookupParent for tests validating parent resolution.

func LookupPath

func LookupPath(f ReaderWriterAt, fsOffset int64, sb *Superblock, path string) (*Inode, error)

LookupPath exposes lookupPath.

func NewTestInode

func NewTestInode(num uint32, inodeSize uint16) *Inode

Inode helpers and accessors for tests.

func ReadInode

func ReadInode(f ReaderWriterAt, fsOffset int64, sb *Superblock, inodeNum uint32) (*Inode, error)

ReadInode exposes readInode.

type Journal

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

Journal represents an on-disk sidecar journal attached to a filesystem image. When enabled it persists transactions to the sidecar and can replay committed transactions at Open time.

func OpenJournal

func OpenJournal(f readerWriterAt, fsOffset int64, blockSize int, sb *superblock) (*Journal, error)

OpenJournal opens/creates a sidecar journal for the provided backing reader/writer. It returns a Journal that is enabled when a sidecar file can be created; otherwise it returns a disabled journal (no-op).

func (*Journal) Close

func (j *Journal) Close() error

Close cleanly shuts down the journal including sync workers and closes the underlying journal file. It also unregisters the journal.

Each Transaction.Commit applies its entries to the backing image synchronously before returning, so at Close all committed entries are already persisted on disk. Truncate the journal file so the next Open does not replay a growing log of already-applied transactions (which would otherwise turn ReplayOnOpen into an O(N) tax on every reopen).

func (*Journal) ReplayOnOpen

func (j *Journal) ReplayOnOpen() error

ReplayOnOpen scans the sidecar journal for committed transactions and applies them to the backing image. Partial or uncommitted transactions are ignored.

func (*Journal) StartTx

func (j *Journal) StartTx() (*Transaction, error)

StartTx creates a new transaction associated with the journal.

type ReaderWriterAt

type ReaderWriterAt = readerWriterAt

Exported aliases to internal types so test packages can refer to them

type Stat

type Stat struct {
	Mode  uint16 // Unix permission bits
	Size  uint64
	Inode uint32
}

Stat holds basic metadata about a filesystem path.

type Superblock

type Superblock = superblock

func CloneSuperblock

func CloneSuperblock(sb *Superblock) *Superblock

CloneSuperblock returns a deep copy of a superblock.

func CloneSuperblockFromFS

func CloneSuperblockFromFS(fs *Ext4FS) *Superblock

CloneSuperblockFromFS returns a copy of the open filesystem's superblock.

func NewTestSuperblock

func NewTestSuperblock(inodesPerGroup, blocksPerGroup, numGroups uint32, blockSize uint32) *Superblock

NewTestSuperblock builds an in-memory superblock for tests.

func ReadSuperblock

func ReadSuperblock(f ReaderWriterAt, fsOffset int64) (*Superblock, error)

Superblock helpers.

type Transaction

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

Transaction collects metadata blocks to be journaled and committed.

func (*Transaction) Abort

func (tx *Transaction) Abort() error

Abort cancels the transaction. For this implementation this is a no-op unless the transaction has been committed.

func (*Transaction) AddBlock

func (tx *Transaction) AddBlock(blockNum uint64, data []byte) error

AddBlock appends a metadata block to the transaction. The data is copied. AddBlock adds or updates the entry for blockNum in the transaction. If an entry for blockNum already exists it is replaced so each block appears at most once in the journal record. Callers that want to build on a previous in-tx modification of the same block must first call GetBlock to retrieve the current in-tx state before overlaying their changes and calling AddBlock.

func (*Transaction) AddCommitCallback

func (tx *Transaction) AddCommitCallback(cb func(uint64)) error

AddCommitCallback registers a callback to be invoked when this transaction is applied. The callback receives the transaction sequence number. It is an error to register callbacks after the transaction has been committed.

func (*Transaction) Commit

func (tx *Transaction) Commit() error

Commit writes the transaction to the sidecar journal and then applies the blocks to the backing filesystem image (write-through). This provides a straightforward durability model and simplifies replay semantics.

func (*Transaction) GetBlock

func (tx *Transaction) GetBlock(blockNum uint64) []byte

GetBlock returns the current in-transaction data for blockNum, or nil if the block has not been prepared into this transaction yet. The returned slice is a copy and safe to modify.

Directories

Path Synopsis
benchmarks module

Jump to

Keyboard shortcuts

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