ufs

package module
v0.0.0-...-58ed63e Latest Latest
Warning

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

Go to latest
Published: Jun 22, 2026 License: BSD-3-Clause Imports: 11 Imported by: 0

README

go-filesystems/ufs

ufs

Go Reference CI

Pure-Go driver for the FreeBSD UFS2 on-disk format. Reads and writes both supported, with Mkfs to format a fresh image from scratch.

UFS is the FreeBSD name for the Berkeley Fast File System (FFS); the on-disk format is shared across the BSDs (FFSv1 = UFS1, FFSv2 = UFS2). This driver therefore also reads NetBSD / OpenBSD FFS images — verified by TestInterop_MakefsFFS, which builds FFSv1 and FFSv2 images with NetBSD's makefs and reads them back.

Status

Through sprint 2D: read + write + Mkfs + double-indirect on top of the read-only surface from sprint 2A. Cross-validated against three parallel UFS2 sources (pure-Go Mkfs, real FreeBSD raw.xz extraction, docker oracle). Used by cloud-boot/tamago-uefi Phase 3 to build the in-memory UFS2 partition consumed by FreeBSD's loader.efi over Go-side EFI_BLOCK_IO_PROTOCOL + EFI_SIMPLE_FILE_SYSTEM_PROTOCOL shims.

  • No CGO.
  • No external tools.
  • No root privileges required.
  • Compatible with FreeBSD 14.x UFS2 images (little-endian, 64-bit inode pointers).

Supported on-disk features

Area Status Notes
Superblock decode yes Primary superblock at SBLOCK_UFS2 (65536). UFS1 magic rejected with a clear error.
Inode decode yes ufs2_dinode (256 bytes) — mode, size, link count, direct/indirect pointers.
Directory walk yes Variable-length direct entries; 4-byte aligned reclen; vacant slot skipping.
File data yes Direct (12) + single indirect block traversal. Double / triple indirect return ErrUnsupportedIndirect.
Sparse holes yes Zero block pointer materialises as implicit zero-fill.
Symlinks (inline) yes "Fast" symlinks decoded directly from the inode's block-pointer area.
Symlinks (spilled) yes Falls back to reading a data block when di_blocks > 0.
Cycle protection yes maxSymlinkHops = 32 before ErrTooManyLinks.

Write support

Sprint 2C-A adds a coherent-image writer on top of the read-only surface from sprint 2A. The writer satisfies the same filesystem.Filesystem interface — WriteFile, MkDir, DeleteFile, DeleteDir, Rename — plus the optional Symlinker capability, and exposes a Mkfs(w, sizeBytes) entry point that formats a fresh UFS2 image into an empty backing store.

Constructors:

  • Open(rs, size) — read-only handle.
  • OpenRW(rs, wa, size) — read + write on an existing image.
  • Mkfs(rw, sizeBytes) — fresh format; returns an *FS already open for read+write.

Operations are flushed coherently — each individual call leaves the on-disk image in a well-formed state (superblock + per-cg counters

  • inode bitmap + block bitmap all consistent). A crash mid-call leaves a fresh-but-potentially-leaky filesystem rather than a torn one.

Soft updates / journaling are deliberately absent. The on-disk format does not require them — soft updates are a runtime optimisation. FreeBSD's fsck_ufs will recover any UFS2 image we produce on first mount. For our use case (producing a UFS2 boot partition in-process from tamago-uefi) that is the correct trade-off.

Out of scope (sprint 2C-A)
  • Double / triple indirect blocks. Files larger than ~2 MiB at the default 4 KiB block size return ErrFileTooLarge. loader.efi plus a FreeBSD /boot/kernel tree fits comfortably below this limit.
  • Cluster summary updates (fs_clustersumoff). These are an allocator-locality hint, not a correctness requirement.
  • Extended attributes (di_extb / di_extsize).
  • Snapshots, gjournal.
Out of scope (sprint 2A — still applies)
  • UFS1 (deferred to sprint 3 if needed for older NetBSD / OpenBSD).
  • Metadata-checksum verification.

Module

github.com/go-filesystems/ufs

Usage

import (
    "log"

    "github.com/go-filesystems/ufs"
)

func main() {
    fs, err := ufs.OpenFile("/path/to/rootfs.img")
    if err != nil {
        log.Fatal(err)
    }
    defer fs.Close()

    entries, _ := fs.ListDir("/boot")
    for _, e := range entries {
        log.Printf("%s (ino %d, type %d)", e.Name(), e.Inode(), e.FileType())
    }

    kernel, err := fs.ReadFile("/boot/kernel/kernel")
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("kernel image: %d bytes", len(kernel))
}

Open(rs io.ReaderAt, size int64) is the lower-level read-only constructor when the caller already owns a backing store (e.g. an EFI block-IO shim). OpenRW adds the writer side; Mkfs formats a fresh image.

Building a boot partition from scratch:

img := make([]byte, 16*1024*1024)
ba := newBackingArray(img) // any io.ReaderAt+io.WriterAt
fs, err := ufs.Mkfs(ba, int64(len(img)))
if err != nil {
    log.Fatal(err)
}
_ = fs.MkDir("/boot", 0o755)
_ = fs.MkDir("/boot/kernel", 0o755)
_ = fs.WriteFile("/boot/loader.conf", []byte("kernel=\"kernel\"\n"), 0o644)
_ = fs.WriteFile("/boot/kernel/kernel", kernelBytes, 0o755)

Errors

All errors returned by the driver are wrapped sentinels — compare with errors.Is:

  • ErrReadOnly — write-side methods (sprint 2A is read-only).
  • ErrNotFound — path component missing.
  • ErrInvalidPath — empty or relative path passed in.
  • ErrBadSuperblock — magic mismatch, UFS1, or sanity-check failure.
  • ErrUnsupportedIndirect — file requires double / triple indirect traversal (deferred).
  • ErrNotDirectory / ErrNotRegular / ErrNotSymlink — type mismatch.
  • ErrTooManyLinks — symlink chain exceeded 32 hops.
  • ErrNoSpace — no free inode or block in any cylinder group.
  • ErrExists — write-side target path already exists.
  • ErrNotEmptyDeleteDir on a non-empty directory.
  • ErrFileTooLarge — file would require double/triple indirect blocks the writer does not implement.

Tests

go test -race -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1

Coverage as of release: ~96%. The remaining 4% is defensive unreachable code and OS-fault paths (e.g. os.File.Stat failure on an already-opened descriptor).

The test image is built deterministically in Go via buildFixture() (see fixture_test.go) — no external newfs / makefs dependency and no binary blob in the repo. Generation procedure for a real-FreeBSD image is documented in testdata/README.md for future sprints.

References

  • FreeBSD source — sys/ufs/ffs/fs.h, sys/ufs/ufs/dinode.h, sys/ufs/ufs/dir.h.
  • The 4.4BSD design discussion of the Fast File System by Marshall McKusick et al.

Documentation

Overview

Package ufs is a pure-Go driver for the FreeBSD UFS on-disk format. It reads both UFS2 and UFS1 images (the latter with 128-byte dinodes and 32-bit block pointers); the write/Mkfs surface targets UFS2 only. Sprint 2A targets the surface that the FreeBSD loader.efi needs to read a kernel + modules off a UFS root partition; unsupported write operations from the filesystem.Filesystem interface are stubbed out with ErrReadOnly.

Index

Constants

View Source
const (
	DtUnknown uint8 = 0
	DtFifo    uint8 = 1
	DtChr     uint8 = 2
	DtDir     uint8 = 4
	DtBlk     uint8 = 6
	DtReg     uint8 = 8
	DtLnk     uint8 = 10
	DtSock    uint8 = 12
	DtWht     uint8 = 14
)

d_type values exposed by the UFS direntry. Mirrors the DT_* constants in sys/ufs/ufs/dir.h.

View Source
const (
	IFMT  uint16 = 0o170000
	IFIFO uint16 = 0o010000
	IFCHR uint16 = 0o020000
	IFDIR uint16 = 0o040000
	IFBLK uint16 = 0o060000
	IFREG uint16 = 0o100000
	IFLNK uint16 = 0o120000
	IFSCK uint16 = 0o140000
)

File type bits in di_mode (UFS uses BSD-style octal constants shared with ext2/POSIX). The IFMT mask isolates the type.

View Source
const (
	// SblockUFS2 is the byte offset of the primary UFS2 superblock on
	// the backing device. Each UFS2 cylinder group additionally holds
	// a copy for crash recovery, but the read-only driver only ever
	// consults the primary.
	SblockUFS2 = 65536

	// SblockUFS1 is the byte offset of the primary UFS1 superblock on
	// the backing device (SBLOCK_UFS1 in fs.h). UFS1 places its
	// superblock 8 KiB into the partition, ahead of the UFS2 location.
	SblockUFS1 = 8192

	// SblockSize is the on-disk size reserved for the superblock
	// region (8 KiB). The struct fs payload is ~1376 bytes; the rest
	// is padding.
	SblockSize = 8192

	// MagicUFS2 identifies a valid UFS2 superblock.
	MagicUFS2 uint32 = 0x19540119
	// MagicUFS1 identifies a UFS1 superblock.
	MagicUFS1 uint32 = 0x00011954

	// RootInode is the inode number of the filesystem root directory
	// in both UFS1 and UFS2 (UFS_ROOTINO in dinode.h).
	RootInode = 2

	// InodeSize is the size of a UFS2 on-disk dinode in bytes
	// (struct ufs2_dinode). It is also the inode density used by the
	// UFS2-only Mkfs/write path.
	InodeSize = 256

	// UFS1InodeSize is the size of a UFS1 on-disk dinode in bytes
	// (struct ufs1_dinode). UFS1 inodes are half the size of UFS2
	// inodes and carry 32-bit block pointers.
	UFS1InodeSize = 128

	// NumDirect is the count of direct block pointers in a UFS2
	// inode (UFS_NDADDR).
	NumDirect = 12
	// NumIndirect is the count of indirect block-pointer levels
	// (UFS_NIADDR): single, double, triple.
	NumIndirect = 3
)

On-disk superblock layout constants. See sys/ufs/ffs/fs.h in the FreeBSD source tree for the canonical definitions.

View Source
const (

	// CgMagic is the magic constant in a UFS cylinder group header.
	CgMagic uint32 = 0x090255
)

Cylinder-group header field offsets within the on-disk struct cg. Mirrors sys/ufs/ffs/fs.h (FreeBSD 14.x amd64). We only decode the pointers and counters the writer actually mutates; everything else stays untouched in the raw image bytes.

Variables

View Source
var (
	// ErrReadOnly is returned by every mutating method on the
	// filesystem.Filesystem interface (WriteFile, MkDir, DeleteFile,
	// DeleteDir, Rename). The sprint-2A driver is read-only by
	// construction; writes will land in a later sprint.
	ErrReadOnly = errors.New("ufs: filesystem is read-only")

	// ErrNotFound is returned when a path component cannot be located
	// in its parent directory.
	ErrNotFound = errors.New("ufs: path not found")

	// ErrInvalidPath is returned when a caller-supplied path is
	// syntactically invalid (e.g. empty, or containing an inode jump
	// the driver cannot honour).
	ErrInvalidPath = errors.New("ufs: invalid path")

	// ErrBadSuperblock is returned by Open when the on-disk
	// superblock at SBLOCK_UFS2 fails validation (wrong magic, wrong
	// fs_inodefmt, or numeric fields outside their sane ranges).
	ErrBadSuperblock = errors.New("ufs: superblock validation failed")

	// ErrUnsupportedIndirect is returned when a read maps to an LBN
	// beyond the triple-indirect tier (i.e. beyond any addressing the
	// UFS on-disk format provides). Direct and single/double/triple
	// indirect reads are all supported. The write path still rejects
	// triple-indirect allocation with ErrFileTooLarge.
	ErrUnsupportedIndirect = errors.New("ufs: indirect block beyond triple-indirect reach not supported")

	// ErrNotDirectory is returned when ListDir is called on an inode
	// that is not a directory.
	ErrNotDirectory = errors.New("ufs: not a directory")

	// ErrNotRegular is returned when ReadFile is called on an inode
	// that is neither a regular file nor a symbolic link.
	ErrNotRegular = errors.New("ufs: not a regular file")

	// ErrNotSymlink is returned when ReadLink is called on a
	// non-symlink inode.
	ErrNotSymlink = errors.New("ufs: not a symbolic link")

	// ErrTooManyLinks is returned when path resolution loops through
	// more than maxSymlinkHops symbolic links, indicating either a
	// genuine cycle or a pathological tree.
	ErrTooManyLinks = errors.New("ufs: too many symbolic link traversals")

	// ErrNoSpace is returned when an allocation cannot find a free
	// inode or block in any cylinder group.
	ErrNoSpace = errors.New("ufs: no space left on device")

	// ErrExists is returned when a mutating operation would overwrite
	// an existing path (WriteFile/MkDir/Symlink/Rename target).
	ErrExists = errors.New("ufs: path already exists")

	// ErrNotEmpty is returned when DeleteDir is called on a directory
	// that still contains entries other than "." and "..".
	ErrNotEmpty = errors.New("ufs: directory not empty")

	// ErrFileTooLarge is returned when a write would require
	// double/triple indirect blocks the writer does not yet implement.
	ErrFileTooLarge = errors.New("ufs: file too large for single-indirect")
)

Sentinel errors returned by the UFS2 driver. Callers should compare with errors.Is rather than == so wrapped errors continue to match.

Functions

func DirentReclen

func DirentReclen(namlen int) int

DirentReclen returns the minimum reclen (rounded up to 4) required to encode a directory entry whose name is `namlen` bytes long. Equivalent to FreeBSD's DIRECTSIZ macro.

func EncodeDirent

func EncodeDirent(buf []byte, ino uint32, dtype uint8, name string, reclen uint16) int

EncodeDirent writes one variable-length directory record into buf and returns the number of bytes written (= reclen). It is used by the in-process fixture builder; the read-side driver never calls it at runtime. The caller is responsible for picking a reclen that is 4-byte aligned and at least direntHeaderSize+namlen.

func ReadFileAll

func ReadFileAll(rs io.ReaderAt, sb *Superblock, in *Inode) ([]byte, error)

ReadFileAll is a convenience wrapper that reads the entire file.

func ReadFileBody

func ReadFileBody(rs io.ReaderAt, sb *Superblock, in *Inode, off int64, n int) ([]byte, error)

ReadFileBody reads up to `n` bytes starting at `off` from the file described by `in`. It honours the UFS2 layout — direct fragments for the first 12 logical blocks, then single-, double-, and triple-indirect for the next fs_nindir, fs_nindir², and fs_nindir³ logical blocks respectively. An LBN beyond triple-indirect reach (which cannot occur in a valid filesystem) yields ErrUnsupportedIndirect.

The function caps `n` at di_size − off so callers never observe trailing slack from the underlying fragment.

Types

type Dirent

type Dirent struct {
	// Ino is the inode number of the named file. Zero marks a
	// vacant slot inside the directory block — callers must skip
	// these.
	Ino uint32
	// Reclen is the on-disk size of this record, including header,
	// name and 4-byte padding.
	Reclen uint16
	// Type is the file-type byte (DT_DIR, DT_REG, etc.). May be
	// DT_UNKNOWN on very old images; consumers should fall back to
	// stat-ing the inode in that case.
	Type uint8
	// Namlen is the byte length of Name, NOT including any
	// trailing NUL.
	Namlen uint8
	// Name is the entry's name. Not NUL-terminated.
	Name string
}

Dirent is a decoded directory entry.

func ParseDirents

func ParseDirents(buf []byte) ([]Dirent, error)

ParseDirents walks a directory data buffer and returns every directory entry it finds. UFS directories are arrays of variable-length records; each record's d_reclen advances the cursor. We skip records whose d_ino is zero (vacant) but still honour their reclen so the iteration stays aligned.

The function tolerates a trailing partial record (the kernel pads to a fragment boundary) by stopping once the next reclen would overrun the buffer.

type FS

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

FS is an opened, read-only UFS2 filesystem.

The struct deliberately holds only an io.ReaderAt plus the decoded superblock. All on-disk traversal is recomputed from the superblock geometry on every call — no caches, no locks, no background goroutines. That keeps the driver trivially safe for concurrent readers and trivially correct against a frozen image (the only environment a read-only client cares about).

func Mkfs

func Mkfs(w interface {
	io.ReaderAt
	io.WriterAt
}, sizeBytes int64) (*FS, error)

Mkfs writes a fresh UFS2 filesystem onto a backing ReadWriterAt with the given byte-size geometry. The returned *FS is open for read+write. The legacy sprint-2C-A defaults (4 KiB block, 4 KiB fragment, single-indirect reach) are preserved for backward compatibility; callers that need the 29 MiB-kernel double-indirect surface should use MkfsWith with BlockSize=32768.

Layout per cylinder group (offsets in fragments):

[0 .. dblkno)            metadata (boot area + sb copy + cg header +
                          inode table) — bitmap marks these allocated
[dblkno .. fpg)          data area — bitmap initially zero (free)

The primary superblock (cg 0's copy) lives at byte offset 65536 so FreeBSD's loader / mount path can find it at the canonical UFS2 location.

func MkfsWith

func MkfsWith(w interface {
	io.ReaderAt
	io.WriterAt
}, sizeBytes int64, opts MkfsOptions) (*FS, error)

MkfsWith is the explicit-options form of Mkfs. Callers pass an MkfsOptions to dial BlockSize / FragmentSize / inode density. Zero fields fall back to FreeBSD newfs(8) defaults (BlockSize 4096 canonical default — pass 32768 to match a typical real-world FreeBSD root partition).

func Open

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

Open parses the UFS2 superblock at SblockUFS2 and returns a ready filesystem handle. The caller retains ownership of rs unless they pass a value that also implements io.Closer — in that case Close will be forwarded.

`size` is the total addressable byte size of the backing image. It is currently only used for diagnostics; pass -1 if unknown.

func OpenFile

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

OpenFile is a convenience constructor that opens `path` read-only and wires it into Open. The returned FS owns the file handle and will close it on Close.

func OpenRW

func OpenRW(rs io.ReaderAt, wa io.WriterAt, size int64) (*FS, error)

OpenRW parses the superblock at SblockUFS2 from `rs`, loads the cylinder-group allocator, and wires the writer side through `wa`. Use this when the caller wants both read and write access — e.g. mutating an existing UFS2 image. The caller retains ownership of the backing handle unless it satisfies io.Closer (in which case Close will be forwarded).

func (*FS) Chmod

func (fs *FS) Chmod(path string, perm os.FileMode) error

Chmod replaces the permission + setuid/setgid/sticky bits at path, preserving the file-type bits. ctime is refreshed.

func (*FS) Chown

func (fs *FS) Chown(path string, uid, gid uint32) error

Chown updates uid/gid at path. ctime is refreshed; mode, body and the other timestamps are left alone.

func (*FS) Chtimes

func (fs *FS) Chtimes(path string, atime, mtime time.Time) error

Chtimes sets di_atime and di_mtime at path. ctime is refreshed to now per POSIX; birth time is left untouched.

func (*FS) Close

func (fs *FS) Close() error

Close releases the backing file handle if FS opened one. Calling Close on a filesystem built from a caller-owned io.ReaderAt is a no-op; the caller stays responsible for releasing their handle.

func (*FS) DeleteDir

func (fs *FS) DeleteDir(p string) error

DeleteDir removes an empty directory at `p`. Returns ErrNotEmpty if the directory still contains entries other than "." and "..".

func (*FS) DeleteFile

func (fs *FS) DeleteFile(p string) error

DeleteFile removes the regular-file (or symlink) entry at `p`, frees the inode and any blocks it referenced, and updates the parent directory.

func (fs *FS) Link(oldPath, newPath string) error

Link adds a new directory entry at newPath referencing the same inode as oldPath and bumps that inode's link count. oldPath must not be a directory; newPath must not already exist. Requires a read-write filesystem.

func (*FS) ListDir

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

ListDir enumerates the entries of the directory at `path`. The special "." and ".." entries are included as the on-disk format stores them; callers that want a POSIX-clean view should filter them out.

func (*FS) MkDir

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

MkDir creates a new directory inode at `p`, populates it with canonical "." and ".." entries, links it into the parent, and bumps the parent's nlink count by 1 (UFS counts subdirs via the "..") backpointer).

func (*FS) ReadFile

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

ReadFile loads the entire contents of the regular file at `path`. Symlinks along the path are followed transparently; if `path` itself resolves to a symlink, the link is followed too.

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

ReadLink returns the target string of the symbolic link at `path`. The path's last component is NOT followed.

func (*FS) Rename

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

Rename moves the entry at oldPath to newPath. The implementation is not strictly atomic across the disk — it removes the old dirent, then adds a new one — but it is coherent: each individual write leaves the filesystem in a well-formed state.

Cross-directory renames are supported. If newPath already exists the call fails with ErrExists; callers wanting overwrite must DeleteFile first.

func (*FS) Stat

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

Stat resolves `path` and returns a Stat carrying mode, size and inode number. Symlinks are followed.

func (*FS) Superblock

func (fs *FS) Superblock() *Superblock

Superblock returns a pointer to the decoded superblock so callers (e.g. EFI_SIMPLE_FILE_SYSTEM_PROTOCOL plumbing) can introspect the on-disk geometry without re-reading it. The returned pointer is owned by FS; do not mutate.

func (fs *FS) Symlink(target, linkPath string) error

Symlink creates a symbolic link at linkPath whose target is `target`. Inline ("fast") symlinks are used when the target fits in the inode's block-pointer area; longer targets spill into a single data block.

func (*FS) WriteFile

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

WriteFile creates a new regular file at `path`, links it into the parent directory, and writes `data`. It is an error for the path to already exist; callers that want overwrite semantics should DeleteFile first. perm is masked to 0o7777 (the high bits are reserved for file-type and are forced to IFREG).

type Inode

type Inode struct {
	// Mode is di_mode: high four bits are the file type (IFMT),
	// low twelve are the permission bits.
	Mode uint16
	// Nlink is the POSIX link count.
	Nlink uint16
	// UID is the owner uid.
	UID uint32
	// GID is the owner gid.
	GID uint32
	// Size is the file size in bytes (di_size).
	Size uint64
	// Blocks is the number of 512-byte disk blocks actually
	// allocated to the file (di_blocks).
	Blocks uint64
	// Flags is the BSD chflags(2) word (di_flags).
	Flags uint32
	// Extsize is the size of the extended-attribute area in bytes;
	// non-zero means the inode carries di_extb[] block pointers we
	// currently ignore.
	Extsize uint32

	// Direct holds the 12 direct fragment pointers (di_db).
	Direct [NumDirect]uint64
	// Indirect holds the 3 indirect-block pointers (di_ib): single,
	// double, triple.
	Indirect [NumIndirect]uint64

	// Raw is the full on-disk inode image, useful for callers that
	// need to peek at fields we don't decode (e.g. the embedded
	// shortlink target). For UFS2 the whole 256 bytes are populated;
	// for UFS1 only the first 128 bytes carry meaningful data.
	Raw [InodeSize]byte
	// contains filtered or unexported fields
}

Inode mirrors the fields of struct ufs2_dinode that the read-only driver consults. We carry the raw 256 bytes alongside the decoded view so callers that need the inline shortlink can reach for it without a re-read.

func ReadInode

func ReadInode(rs io.ReaderAt, sb *Superblock, ino uint64) (*Inode, error)

ReadInode pulls one inode out of the on-disk inode table using the superblock's geometry, decodes the fields the driver needs and returns the result.

func (*Inode) FileType

func (in *Inode) FileType() uint16

FileType returns the four high bits of di_mode, useful when the caller wants to compare against IFREG/IFDIR/IFLNK without the perm bits in the way.

func (*Inode) IsDir

func (in *Inode) IsDir() bool

IsDir reports whether the inode is a directory (IFDIR set in di_mode).

func (*Inode) IsRegular

func (in *Inode) IsRegular() bool

IsRegular reports whether the inode is a regular file (IFREG).

func (in *Inode) IsSymlink() bool

IsSymlink reports whether the inode is a symbolic link (IFLNK).

func (in *Inode) Shortlink(sb *Superblock) (string, bool)

Shortlink returns the target of an inline ("fast") symbolic link embedded in the 120-byte block-pointer area. Returns false if the inode is not a symlink or the target spills into a data block.

The UFS2 "fast symlink" optimisation stores the link target in the inode itself when the target is short enough to fit in the (UFS_NDADDR + UFS_NIADDR) * sizeof(ufs2_daddr_t) = 120 bytes otherwise used for block pointers.

type MkfsOptions

type MkfsOptions struct {
	// BlockSize is the filesystem block size in bytes. Must be a
	// power of two in [4096, 65536]. Default 4096; FreeBSD newfs
	// defaults to 32768 on devices ≥ 2 GiB and the cloud-boot live
	// pipeline uses 32768 so the 29 MiB FreeBSD kernel fits via
	// double-indirect.
	BlockSize int
	// FragmentSize is the per-fragment size in bytes. Must divide
	// BlockSize evenly. Default = BlockSize / 8 (matching FreeBSD
	// newfs(8)); pass an explicit equal-to-BlockSize value to force
	// frag == 1 (no sub-block allocation).
	FragmentSize int
	// InodeDensity is the per-inode byte budget — i.e. one inode is
	// reserved per InodeDensity bytes of data area. Default 4096.
	InodeDensity int
	// Label is reserved for future use; currently ignored.
	Label string
}

MkfsOptions controls the geometry of a freshly-minted UFS2 filesystem. All fields are optional; zero values are replaced with sprint-2C-A-compatible defaults (4 KiB block, 4 KiB fragment, one inode per 4 KiB). Sprint 2D introduced this struct so callers that need to store ≥ 2 MiB files can dial the block size up to 32 KiB (matching FreeBSD newfs(8)) — extending single-indirect reach to 16 MiB and engaging double-indirect (8 GiB) on top.

type Superblock

type Superblock struct {
	// Sblkno is the offset of the super-block within a cylinder
	// group, measured in fragments.
	Sblkno int32
	// Cblkno is the offset of the cylinder-group block within a
	// cylinder group, measured in fragments.
	Cblkno int32
	// Iblkno is the offset of the inode table within a cylinder
	// group, measured in fragments.
	Iblkno int32
	// Dblkno is the offset of the first data area within a cylinder
	// group, measured in fragments.
	Dblkno int32

	// Ncg is the total number of cylinder groups in the filesystem.
	Ncg uint32

	// Bsize is the filesystem block size in bytes (e.g. 32768).
	Bsize int32
	// Fsize is the fragment size in bytes (e.g. 4096).
	Fsize int32
	// Frag is the number of fragments per block (Bsize / Fsize).
	Frag int32

	// Bshift is log2(Bsize); used to convert byte offsets to logical
	// block numbers (file-relative).
	Bshift int32
	// Fshift is log2(Fsize); used to convert byte offsets to
	// fragment counts.
	Fshift int32

	// Fsbtodb is the shift constant for converting filesystem
	// fragments to 512-byte disk blocks. A fragment occupies
	// (1 << Fsbtodb) sectors.
	Fsbtodb int32

	// Sbsize is the actual on-disk superblock size in bytes.
	Sbsize int32

	// Nindir is the number of pointers per indirect block
	// (Bsize / 8 on UFS2).
	Nindir int32

	// Inopb is the number of inodes per filesystem block
	// (Bsize / InodeSize).
	Inopb uint32

	// Ipg is the number of inodes per cylinder group.
	Ipg uint32
	// Fpg is the number of fragments per cylinder group.
	Fpg int32

	// Flags is the FS_ flag bitfield (see fs.h FS_UNCLEAN etc.).
	Flags int32

	// Maxsymlinklen is the maximum length of an inline ("fast")
	// symbolic link whose target is stored in the inode's direct
	// block pointers rather than in a data block.
	Maxsymlinklen int32

	// OldInodefmt is FS_44INODEFMT (2) for any UFS2 image; the
	// "old" naming is historical (the field predates the UFS1 vs
	// UFS2 distinction).
	OldInodefmt int32

	// Maxfilesize is the largest representable file size.
	Maxfilesize uint64

	// Magic is FS_UFS2_MAGIC (0x19540119) for a UFS2 image or
	// FS_UFS1_MAGIC (0x00011954) for a UFS1 image.
	Magic uint32

	// IsUFS1 reports whether the image is in the UFS1 on-disk format.
	// It drives the on-disk inode size (128 vs 256 bytes) and the
	// block-pointer width (32-bit vs 64-bit) used by the read paths.
	IsUFS1 bool
}

Superblock holds the decoded UFS2 superblock fields needed by the driver. We intentionally decode only the read-side subset; many fields (allocation hints, snapshot lists, journal pointers) are not consulted by a read-only client.

func ReadSuperblock

func ReadSuperblock(rs io.ReaderAt) (*Superblock, error)

ReadSuperblock pulls the primary UFS2 superblock off the backing device at SblockUFS2, decodes the fields the driver needs, and validates magic/inodefmt/sizing invariants. Returns ErrBadSuperblock on any failure so callers can distinguish "not a UFS2 image" from a generic I/O error.

func (*Superblock) CgBase

func (sb *Superblock) CgBase(cg uint32) int64

CgBase returns the byte offset of cylinder-group cg within the backing device. UFS lays out cylinder groups at multiples of Fpg fragments from the start of the partition, each fragment being Fsize bytes wide.

func (*Superblock) FragOffset

func (sb *Superblock) FragOffset(frag uint64) int64

FragOffset returns the absolute byte offset of fragment `frag` (UFS "fs block number" — a daddr_t — addresses fragments, not full blocks).

func (*Superblock) ImageBytes

func (sb *Superblock) ImageBytes() int64

ImageBytes returns an upper bound on the addressable byte size of the backing image, derived purely from the validated superblock geometry: Ncg cylinder groups × Fpg fragments/group × Fsize bytes/fragment. The fields are all validated (Fpg > 0, the product fits in int64) by validate(), so this never overflows for an accepted superblock. It is used as the ceiling for bounded allocations so a crafted di_size cannot drive a multi-terabyte make([]byte, …) (class A OOM). A file cannot be larger than the image that contains it.

func (*Superblock) InodeOffset

func (sb *Superblock) InodeOffset(ino uint64) int64

InodeOffset returns the absolute byte offset of inode `ino` (1-based, per UFS convention; inode 0 is reserved).

Jump to

Keyboard shortcuts

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