Skip to content
 
 

Latest commit

 

History

186 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-erofs

A Go library for reading and creating EROFS filesystem images using the standard fs.FS interface.

About this fork

A fork of github.com/erofs/go-erofs, hardened against untrusted images, corrected on several on-disk paths, and made substantially faster to read. Each fix below ships with a regression test, and the security-relevant ones were confirmed to fail against the code they replace.

Untrusted input. CopyFrom sized allocations directly from superblock and inode fields, so a few edited bytes in an 8 KiB image could request a terabyte-scale buffer — which Go reports as an unrecoverable fatal OOM, not a returned error. The directory walk also had no cycle detection and never terminated on an image whose directories reach themselves.

Path resolution bounded symlink hops but not the work done per hop, so an 8 KiB image could hold a single Open for 88 seconds across 33 million reads; the total walk is now bounded. nid and chunk block addresses are validated before they become file offsets, since the overflowed values were handed to the caller's io.ReaderAt as negative offsets — an error for *bytes.Reader and *os.File, a panic for a slice-backed reader. Chunk-index buffers are no longer allocated from a declared size before the read that would show the data is absent. Dirent names carrying a path separator are rejected, so the standard extract pattern (fs.WalkDir plus filepath.Join) cannot be walked out of its destination directory, and in merge mode a crafted name can no longer collapse to a whiteout that erases every prior layer. Superblock geometry is bounded at Open, and xattrs with an undefined name-index prefix or a duplicate key are rejected rather than silently taken last-one-wins, which is what let security.capability be spoofed.

Correctness. Chunk extents are mapped at block granularity and holes are preserved across a re-index; previously a region declared as a hole could read back as real bytes from the backing device, at an offset the image never referenced. Short reads are no longer silently zero-filled, entries can no longer be attached beneath a regular file and then vanish from the image, two files can no longer be opened for writing at once and share one data region, and xattr names and values are checked against their on-disk field widths instead of being truncated into them.

Several defects were images this library wrote but could not read back. The device table was stamped with an offset 128 bytes from where it was written. At the maximum block size a full directory block listed every entry but could not open the last one in each block, because the name-end offset was computed in a uint16 that 65536 truncates to zero. Symlink("", …) produced a link that resolved to the root, so any path through it silently discarded everything to its left. Chunk indexes carried address bits the declared format does not cover, which the kernel reads as phys mod 2^32. A metadata-only copy could remap a chunk onto the destination's own data file and serve an unrelated layer's bytes; a source reporting a negative Size() produced a plausible-looking image whose every inode sat one byte off its slot; and a pre-existing data file that did not start on a block boundary put the first file's chunk on an earlier block. fs.FileInfo.Mode and Sys().(*Stat).Mode took the file type from two different on-disk fields and could disagree; both now come from the inode.

Compatibility. Images produced by the reference mkfs.erofs are readable. Any inode carrying a shared xattr — which is how SELinux labels are stored — used to fail outright, because the shared xattr area sits in the image's final block and a whole-block read there runs past the end of the file. The reader also now honours the fs.FS path contract: rooted paths, . and empty elements, and .. traversal are rejected, so fstest.TestFS passes. Names that are not valid UTF-8 remain readable, since EROFS stores names as byte strings. Note that a symlink target is resolved from the image root, as it is under a kernel mount, so fs.Sub bounds the paths a caller may write but not where a link inside the subtree may lead.

Performance. Reading a whole file no longer costs one ReadAt per filesystem block, and io.Copy goes through io.WriterTo. Inodes, dirents, xattr headers, the superblock and device slots are all encoded and decoded directly rather than through reflection, and a pooled block that was being stranded on every inode read is returned. ReadDir converts each block's name region once and sub-slices per entry instead of allocating a string and a heap entry apiece, and the writer carves its entries from one slab and emits a dirent block per Write rather than one per name.

Measured against the fork point (44d5e74), medians of six runs on an Apple M5 Pro with go1.25, through the public API only:

operation upstream this fork
fs.Stat by path 1638 ns, 4701 B, 9 allocs 477 ns, 528 B, 5 allocs
Open by path 1467 ns, 4437 B, 6 allocs 427 ns, 288 B, 4 allocs
Read a 500-entry directory 30.9 µs, 1015 allocs 10.3 µs, 18 allocs
Read an 8 MiB file 346 µs, 2051 reads 241 µs, 4 reads
Write a 500-entry directory 1.41 ms, 4348 allocs 1.37 ms, 3319 allocs

The read count is ReadAt calls per whole-file fs.ReadFile, covering the lookup and the inode as well as the data. Writing is bounded by serialization rather than by allocation, so fewer allocations there buy little wall time.

Ported from upstream since the fork. The system.posix_acl_access and system.posix_acl_default xattr prefixes, which were spelled with a trailing dot so a stored ACL could never be matched by name (upstream 03d68d8). Upstream's Link, Remove, RemoveAll and hardlink detection in CopyFrom arrived here independently and are not ports.

Requires Go 1.26.

Features

  • Read EROFS images through Go's fs.FS interface
  • Create EROFS images from directories or any fs.FS
  • Merge multiple filesystem sources with overlay whiteout support
  • Metadata-only mode for container layer indexing (chunk-based references to original data)
  • Pure Go, no CGO — uses only the standard library

Status

  • Read erofs files created with default mkfs.erofs options
  • Read chunk-based erofs files with indexes
  • Xattr support including long xattr prefixes
  • Extra devices for chunked data
  • Create erofs files from any fs.FS
  • Directory to erofs packing
  • AUFS whiteout to overlayfs conversion
  • Merge multiple filesystem layers with whiteout processing
  • Read erofs files with compression

Reading an EROFS image

f, err := os.Open("image.erofs")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

img, err := erofs.Open(f)
if err != nil {
    log.Fatal(err)
}

fs.WalkDir(img, ".", func(path string, d fs.DirEntry, err error) error {
    fmt.Println(path)
    return nil
})

Merging multiple layers

Combine multiple filesystem sources into one image. The Merge option enables overlay semantics — AUFS-style whiteout files (.wh.<name>) delete entries from prior layers:

outFile, _ := os.Create("merged.erofs")
w := erofs.Create(outFile)

w.CopyFrom(baseLayer)
w.CopyFrom(overlayLayer, erofs.Merge())
w.Close()

Merge can also be combined with MetadataOnly to build a merged index without copying data:

w := erofs.Create(outFile)
w.CopyFrom(layer1, erofs.MetadataOnly())
w.CopyFrom(layer2, erofs.MetadataOnly(), erofs.Merge())
w.Close()

Building an image programmatically

outFile, _ := os.Create("image.erofs")
w := erofs.Create(outFile)

f, _ := w.Create("/hello.txt")
f.Write([]byte("hello world\n"))
f.Close()

w.Mkdir("/dir", 0o755)
w.Symlink("hello.txt", "/link")

w.Close()
outFile.Close()

Close each file before creating the next one. File data is appended to a single stream, so two open writers would interleave their bytes; Create returns an error rather than let that corrupt the image silently.

The writer's surface

Beyond Create, Mkdir, Symlink and CopyFrom:

  • Link adds a name for an existing inode; Mknod creates a device, FIFO or socket from an fs.FileMode.
  • Remove and RemoveAll drop entries, with unlink(2) semantics for hard-linked names; together with CopyFrom(..., Merge()) this is how layers are merged programmatically.
  • Chmod, Chown, Chtimes, Setxattr and SetNlink edit metadata by path; File.Chmod and File.Chown do the same on an open file.
  • Stat and Open read entries back before Close.
  • Create options: WithBlockSize, WithBuildTime, WithDataFile (an external data file for metadata-only images) and WithTempDir (where the spool lives otherwise). CopyFrom options: MetadataOnly and Merge.

Paths given to the writer are cleaned: x, /x and ./x name the same entry. The reader is stricter and follows the fs.FS convention exactly.

CopyFrom takes ownership, times and link identity from Sys() when it is a *syscall.Stat_t (linux, darwin and freebsd) or a *builder.Entry; a source with neither still contributes mode, size and ModTime. Two names that share a source inode become one inode with two names in the image, and a file's link count is computed from the names the image holds.

Limits

The reader accepts block sizes from 512 bytes to 64 KiB. Compressed inodes, 48-bit chunk addressing and extended inode slots are rejected with ErrNotImplemented. The bounds that protect a caller from a hostile image report ErrInvalid: symlink targets and paths of at most 4096 bytes, 255 symlink hops per resolution, a chunk-index map of at most 64 MiB per file, and fs.ReadFile refusing files over 128 MiB (open and io.Copy those). The writer stores xattr names of at most 255 bytes after their prefix and values of at most 65535 bytes. Chown refuses negative ids.

The fs.FS from Open is safe for concurrent use; the files it opens and a Writer are not.

Releasing

Versions are signed annotated tags following semantic versioning, cut from main; the library has no build artefacts. Before tagging: just lint and just test green on CI, CHANGELOG.md moved from Unreleased to the version with the date, and the tag message naming the headline changes. Changes to exported names or documented behaviour after 1.0 are a major version.

About

A pure-Go library for reading and creating EROFS filesystem images through the standard fs.FS interface — no CGO. Supports overlay whiteout merging and a metadata-only mode for container layer indexing. Fork of erofs/go-erofs.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages