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
- Variables
- func DirentReclen(namlen int) int
- func EncodeDirent(buf []byte, ino uint32, dtype uint8, name string, reclen uint16) int
- func ReadFileAll(rs io.ReaderAt, sb *Superblock, in *Inode) ([]byte, error)
- func ReadFileBody(rs io.ReaderAt, sb *Superblock, in *Inode, off int64, n int) ([]byte, error)
- type Dirent
- type FS
- func Mkfs(w interface{ ... }, sizeBytes int64) (*FS, error)
- func MkfsWith(w interface{ ... }, sizeBytes int64, opts MkfsOptions) (*FS, error)
- func Open(rs io.ReaderAt, size int64) (*FS, error)
- func OpenFile(path string) (*FS, error)
- func OpenRW(rs io.ReaderAt, wa io.WriterAt, size int64) (*FS, error)
- func (fs *FS) Chmod(path string, perm os.FileMode) error
- func (fs *FS) Chown(path string, uid, gid uint32) error
- func (fs *FS) Chtimes(path string, atime, mtime time.Time) error
- func (fs *FS) Close() error
- func (fs *FS) DeleteDir(p string) error
- func (fs *FS) DeleteFile(p string) error
- func (fs *FS) Link(oldPath, newPath string) error
- func (fs *FS) ListDir(path string) ([]filesystem.DirEntry, error)
- func (fs *FS) MkDir(p string, perm os.FileMode) error
- func (fs *FS) ReadFile(path string) ([]byte, error)
- func (fs *FS) ReadLink(path string) (string, error)
- func (fs *FS) Rename(oldPath, newPath string) error
- func (fs *FS) Stat(path string) (filesystem.Stat, error)
- func (fs *FS) Superblock() *Superblock
- func (fs *FS) Symlink(target, linkPath string) error
- func (fs *FS) WriteFile(p string, data []byte, perm os.FileMode) error
- type Inode
- type MkfsOptions
- type Superblock
Constants ¶
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.
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
ReadFileAll is a convenience wrapper that reads the entire file.
func ReadFileBody ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Chmod replaces the permission + setuid/setgid/sticky bits at path, preserving the file-type bits. ctime is refreshed.
func (*FS) Chown ¶
Chown updates uid/gid at path. ctime is refreshed; mode, body and the other timestamps are left alone.
func (*FS) Chtimes ¶
Chtimes sets di_atime and di_mtime at path. ctime is refreshed to now per POSIX; birth time is left untouched.
func (*FS) Close ¶
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 ¶
DeleteDir removes an empty directory at `p`. Returns ErrNotEmpty if the directory still contains entries other than "." and "..".
func (*FS) DeleteFile ¶
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) Link ¶
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 ¶
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 ¶
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) ReadLink ¶
ReadLink returns the target string of the symbolic link at `path`. The path's last component is NOT followed.
func (*FS) Rename ¶
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) Symlink ¶
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 ¶
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 ¶
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 ¶
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) Shortlink ¶
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).
