Files
Paweł Gronowski a8a1cfd111 daemon: Add embedded containerd mode
Run containerd inside dockerd when the experimental
`embedded-containerd` feature is enabled through `--feature` or the
daemon configuration.

Serve containerd's gRPC API on a Unix socket, or a named pipe on
Windows, for the plugin executor and external tools.
Use an in-memory listener for dockerd's own containerd client. Serve
TTRPC on a platform endpoint so task shims can publish events.

Register only the containerd plugins dockerd needs. Leave CRI, sandbox,
streaming, transfer, NRI, and the restart monitor out of the embedded
server.
Reject `--cri-containerd` when embedded mode is enabled instead of
silently ignoring the requested CRI support.

Check the feature before `ContainerdAddr` so it can override the default
containerd socket supplied by packaged service units. Continue to use
the configured external containerd when the feature is disabled.

Derive the Windows named-pipe address from the daemon state directory so
multiple daemons can run on the same host. Restrict the pipes to the
built-in Administrators group and LocalSystem with the same protected
DACL as dockerd's API pipe, as the default security descriptor depends
on the process token and can expose the containerd endpoints more
broadly than intended. Treat embedded mode as a containerd runtime in
the Windows test environment.

Add the `no_embedded_containerd` build tag so distributors can omit the
embedded plugin graph and its dependencies.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-07-24 16:39:06 +02:00

1352 lines
35 KiB
Go

package compactext4
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"path"
"sort"
"strings"
"time"
"github.com/Microsoft/hcsshim/ext4/internal/format"
"github.com/Microsoft/hcsshim/internal/memory"
)
// Writer writes a compact ext4 file system.
//
// It expects all paths to use directory separator '/', even on Windows.
type Writer struct {
f io.ReadWriteSeeker
bw *bufio.Writer
inodes []*inode
curName string
curInode *inode
pos int64
dataWritten, dataMax int64
err error
initialized bool
supportInlineData bool
maxDiskSize int64
gdBlocks uint32
}
// Mode flags for Linux files.
const (
S_IXOTH = format.S_IXOTH
S_IWOTH = format.S_IWOTH
S_IROTH = format.S_IROTH
S_IXGRP = format.S_IXGRP
S_IWGRP = format.S_IWGRP
S_IRGRP = format.S_IRGRP
S_IXUSR = format.S_IXUSR
S_IWUSR = format.S_IWUSR
S_IRUSR = format.S_IRUSR
S_ISVTX = format.S_ISVTX
S_ISGID = format.S_ISGID
S_ISUID = format.S_ISUID
S_IFIFO = format.S_IFIFO
S_IFCHR = format.S_IFCHR
S_IFDIR = format.S_IFDIR
S_IFBLK = format.S_IFBLK
S_IFREG = format.S_IFREG
S_IFLNK = format.S_IFLNK
S_IFSOCK = format.S_IFSOCK
TypeMask = format.TypeMask
)
type inode struct {
Size int64
Atime, Ctime, Mtime, Crtime uint64
Number format.InodeNumber
Mode uint16
Uid, Gid uint32
LinkCount uint32
XattrBlock uint32
BlockCount uint32
Devmajor, Devminor uint32
Flags format.InodeFlag
Data []byte
XattrInline []byte
Children directory
}
func (node *inode) FileType() uint16 {
return node.Mode & format.TypeMask
}
func (node *inode) IsDir() bool {
return node.FileType() == S_IFDIR
}
// A File represents a file to be added to an ext4 file system.
type File struct {
Linkname string
Size int64
Mode uint16
Uid, Gid uint32
Atime, Ctime, Mtime, Crtime time.Time
Devmajor, Devminor uint32
Xattrs map[string][]byte
}
const (
inodeFirst = 11
inodeLostAndFound = inodeFirst
BlockSize = 4096
blocksPerGroup = BlockSize * 8
inodeSize = 256
maxInodesPerGroup = BlockSize * 8 // Limited by the inode bitmap
inodesPerGroupIncrement = BlockSize / inodeSize
defaultMaxDiskSize = 16 * memory.GiB // 16GB
maxMaxDiskSize = 16 * 1024 * 1024 * 1024 * 1024 // 16TB
groupDescriptorSize = 32 // Use the small group descriptor
groupsPerDescriptorBlock = BlockSize / groupDescriptorSize
maxFileSize = 128 * memory.GiB // 128GB file size maximum for now
smallSymlinkSize = 59 // max symlink size that goes directly in the inode
maxBlocksPerExtent = 0x8000 // maximum number of blocks in an extent
inodeDataSize = 60
inodeUsedSize = 152 // fields through CrtimeExtra
inodeExtraSize = inodeSize - inodeUsedSize
xattrInodeOverhead = 4 + 4 // magic number + empty next entry value
xattrBlockOverhead = 32 + 4 // header + empty next entry value
inlineDataXattrOverhead = xattrInodeOverhead + 16 + 4 // entry + "data"
inlineDataSize = inodeDataSize + inodeExtraSize - inlineDataXattrOverhead
)
type exceededMaxSizeError struct {
Size int64
}
func (err exceededMaxSizeError) Error() string {
return fmt.Sprintf("disk exceeded maximum size of %d bytes", err.Size)
}
var directoryEntrySize = binary.Size(format.DirectoryEntry{})
var extraIsize = uint16(inodeUsedSize - 128)
type directory map[string]*inode
func splitFirst(p string) (string, string) {
n := strings.IndexByte(p, '/')
if n >= 0 {
return p[:n], p[n+1:]
}
return p, ""
}
func (w *Writer) findPath(root *inode, p string) *inode {
inode := root
for inode != nil && len(p) != 0 {
name, rest := splitFirst(p)
p = rest
inode = inode.Children[name]
}
return inode
}
func timeToFsTime(t time.Time) uint64 {
if t.IsZero() {
return 0
}
s := t.Unix()
if s < -0x80000000 {
return 0x80000000
}
if s > 0x37fffffff {
return 0x37fffffff
}
return uint64(s) | uint64(t.Nanosecond())<<34
}
func fsTimeToTime(t uint64) time.Time {
if t == 0 {
return time.Time{}
}
s := int64(t & 0x3ffffffff)
if s > 0x7fffffff && s < 0x100000000 {
s = int64(int32(uint32(s)))
}
return time.Unix(s, int64(t>>34))
}
func (w *Writer) getInode(i format.InodeNumber) *inode {
if i == 0 || int(i) > len(w.inodes) {
return nil
}
return w.inodes[i-1]
}
var xattrPrefixes = []struct {
Index uint8
Prefix string
}{
{2, "system.posix_acl_access"},
{3, "system.posix_acl_default"},
{8, "system.richacl"},
{7, "system."},
{1, "user."},
{4, "trusted."},
{6, "security."},
}
func compressXattrName(name string) (uint8, string) {
for _, p := range xattrPrefixes {
if strings.HasPrefix(name, p.Prefix) {
return p.Index, name[len(p.Prefix):]
}
}
return 0, name
}
func decompressXattrName(index uint8, name string) string {
for _, p := range xattrPrefixes {
if index == p.Index {
return p.Prefix + name
}
}
return name
}
func hashXattrEntry(name string, value []byte) uint32 {
var hash uint32
for i := 0; i < len(name); i++ {
hash = (hash << 5) ^ (hash >> 27) ^ uint32(name[i])
}
for i := 0; i+3 < len(value); i += 4 {
hash = (hash << 16) ^ (hash >> 16) ^ binary.LittleEndian.Uint32(value[i:i+4])
}
if len(value)%4 != 0 {
var last [4]byte
copy(last[:], value[len(value)&^3:])
hash = (hash << 16) ^ (hash >> 16) ^ binary.LittleEndian.Uint32(last[:])
}
return hash
}
type xattr struct {
Name string
Index uint8
Value []byte
}
func (x *xattr) EntryLen() int {
return (len(x.Name)+3)&^3 + 16
}
func (x *xattr) ValueLen() int {
return (len(x.Value) + 3) &^ 3
}
type xattrState struct {
inode, block []xattr
inodeLeft, blockLeft int
}
func (s *xattrState) init() {
s.inodeLeft = inodeExtraSize - xattrInodeOverhead
s.blockLeft = BlockSize - xattrBlockOverhead
}
func (s *xattrState) addXattr(name string, value []byte) bool {
index, name := compressXattrName(name)
x := xattr{
Index: index,
Name: name,
Value: value,
}
length := x.EntryLen() + x.ValueLen()
if s.inodeLeft >= length {
s.inode = append(s.inode, x)
s.inodeLeft -= length
} else if s.blockLeft >= length {
s.block = append(s.block, x)
s.blockLeft -= length
} else {
return false
}
return true
}
func putXattrs(xattrs []xattr, b []byte, offsetDelta uint16) {
offset := uint16(len(b)) + offsetDelta
eb := b
db := b
for _, xattr := range xattrs {
vl := xattr.ValueLen()
offset -= uint16(vl)
eb[0] = uint8(len(xattr.Name))
eb[1] = xattr.Index
binary.LittleEndian.PutUint16(eb[2:], offset)
binary.LittleEndian.PutUint32(eb[8:], uint32(len(xattr.Value)))
binary.LittleEndian.PutUint32(eb[12:], hashXattrEntry(xattr.Name, xattr.Value))
copy(eb[16:], xattr.Name)
eb = eb[xattr.EntryLen():]
copy(db[len(db)-vl:], xattr.Value)
db = db[:len(db)-vl]
}
}
func getXattrs(b []byte, xattrs map[string][]byte, offsetDelta uint16) {
eb := b
for len(eb) != 0 {
nameLen := eb[0]
if nameLen == 0 {
break
}
index := eb[1]
offset := binary.LittleEndian.Uint16(eb[2:]) - offsetDelta
valueLen := binary.LittleEndian.Uint32(eb[8:])
attr := xattr{
Index: index,
Name: string(eb[16 : 16+nameLen]),
Value: b[offset : uint32(offset)+valueLen],
}
xattrs[decompressXattrName(index, attr.Name)] = attr.Value
eb = eb[attr.EntryLen():]
}
}
func (w *Writer) writeXattrs(inode *inode, state *xattrState) error {
// Write the inline attributes.
if len(state.inode) != 0 {
inode.XattrInline = make([]byte, inodeExtraSize)
binary.LittleEndian.PutUint32(inode.XattrInline[0:], format.XAttrHeaderMagic) // Magic
putXattrs(state.inode, inode.XattrInline[4:], 0)
}
// Write the block attributes. If there was previously an xattr block, then
// rewrite it even if it is now empty.
if len(state.block) != 0 || inode.XattrBlock != 0 {
sort.Slice(state.block, func(i, j int) bool {
return state.block[i].Index < state.block[j].Index ||
len(state.block[i].Name) < len(state.block[j].Name) ||
state.block[i].Name < state.block[j].Name
})
var b [BlockSize]byte
binary.LittleEndian.PutUint32(b[0:], format.XAttrHeaderMagic) // Magic
binary.LittleEndian.PutUint32(b[4:], 1) // ReferenceCount
binary.LittleEndian.PutUint32(b[8:], 1) // Blocks
putXattrs(state.block, b[32:], 32)
orig := w.block()
if inode.XattrBlock == 0 {
inode.XattrBlock = orig
inode.BlockCount++
} else {
// Reuse the original block.
w.seekBlock(inode.XattrBlock)
defer w.seekBlock(orig)
}
if _, err := w.write(b[:]); err != nil {
return err
}
}
return nil
}
func (w *Writer) write(b []byte) (int, error) {
if w.err != nil {
return 0, w.err
}
if w.pos+int64(len(b)) > w.maxDiskSize {
w.err = exceededMaxSizeError{w.maxDiskSize}
return 0, w.err
}
n, err := w.bw.Write(b)
w.pos += int64(n)
w.err = err
return n, err
}
func (w *Writer) zero(n int64) (int64, error) {
if w.err != nil {
return 0, w.err
}
if w.pos+int64(n) > w.maxDiskSize {
w.err = exceededMaxSizeError{w.maxDiskSize}
return 0, w.err
}
n, err := io.CopyN(w.bw, zero, n)
w.pos += n
w.err = err
return n, err
}
func (w *Writer) makeInode(f *File, node *inode) (*inode, error) {
mode := f.Mode
if mode&format.TypeMask == 0 {
mode |= format.S_IFREG
}
typ := mode & format.TypeMask
ino := format.InodeNumber(len(w.inodes) + 1)
if node == nil {
node = &inode{
Number: ino,
}
if typ == S_IFDIR {
node.Children = make(directory)
node.LinkCount = 1 // A directory is linked to itself.
}
} else if node.Flags&format.InodeFlagExtents != 0 {
// Since we cannot deallocate or reuse blocks, don't allow updates that
// would invalidate data that has already been written.
return nil, errors.New("cannot overwrite file with non-inline data")
}
node.Mode = mode
node.Uid = f.Uid
node.Gid = f.Gid
node.Flags = format.InodeFlagHugeFile
node.Atime = timeToFsTime(f.Atime)
node.Ctime = timeToFsTime(f.Ctime)
node.Mtime = timeToFsTime(f.Mtime)
node.Crtime = timeToFsTime(f.Crtime)
node.Devmajor = f.Devmajor
node.Devminor = f.Devminor
node.Data = nil
if f.Xattrs == nil {
f.Xattrs = make(map[string][]byte)
}
// copy over existing xattrs first, we need to merge existing xattrs and the passed xattrs.
existingXattrs := make(map[string][]byte)
if len(node.XattrInline) > 0 {
getXattrs(node.XattrInline[4:], existingXattrs, 0)
}
node.XattrInline = nil
var xstate xattrState
xstate.init()
var size int64
switch typ {
case format.S_IFREG:
size = f.Size
if f.Size > maxFileSize {
return nil, fmt.Errorf("file too big: %d > %d", f.Size, int64(maxFileSize))
}
if f.Size <= inlineDataSize && w.supportInlineData {
node.Data = make([]byte, f.Size)
extra := 0
if f.Size > inodeDataSize {
extra = int(f.Size - inodeDataSize)
}
// Add a dummy entry for now.
if !xstate.addXattr("system.data", node.Data[:extra]) {
panic("not enough room for inline data")
}
node.Flags |= format.InodeFlagInlineData
}
case format.S_IFLNK:
node.Mode |= 0777 // Symlinks should appear as ugw rwx
size = int64(len(f.Linkname))
if size <= smallSymlinkSize {
// Special case: small symlinks go directly in Block without setting
// an inline data flag.
node.Data = make([]byte, len(f.Linkname))
copy(node.Data, f.Linkname)
}
case format.S_IFDIR, format.S_IFIFO, format.S_IFSOCK, format.S_IFCHR, format.S_IFBLK:
default:
return nil, fmt.Errorf("invalid mode %o", mode)
}
// merge xattrs but prefer currently passed over existing
for name, data := range existingXattrs {
if _, ok := f.Xattrs[name]; !ok {
f.Xattrs[name] = data
}
}
// Accumulate the extended attributes.
if len(f.Xattrs) != 0 {
// Sort the xattrs to avoid non-determinism in map iteration.
var xattrs []string
for name := range f.Xattrs {
xattrs = append(xattrs, name)
}
sort.Strings(xattrs)
for _, name := range xattrs {
if !xstate.addXattr(name, f.Xattrs[name]) {
return nil, fmt.Errorf("could not fit xattr %s", name)
}
}
}
if err := w.writeXattrs(node, &xstate); err != nil {
return nil, err
}
node.Size = size
if typ == format.S_IFLNK && size > smallSymlinkSize {
// Write the link name as data.
w.startInode("", node, size)
if _, err := w.Write([]byte(f.Linkname)); err != nil {
return nil, err
}
if err := w.finishInode(); err != nil {
return nil, err
}
}
if int(node.Number-1) >= len(w.inodes) {
w.inodes = append(w.inodes, node)
}
return node, nil
}
func (w *Writer) root() *inode {
return w.getInode(format.InodeRoot)
}
func (w *Writer) lookup(name string, mustExist bool) (*inode, *inode, string, error) {
root := w.root()
cleanname := path.Clean("/" + name)[1:]
if len(cleanname) == 0 {
return root, root, "", nil
}
dirname, childname := path.Split(cleanname)
if len(childname) == 0 || len(childname) > 0xff {
return nil, nil, "", fmt.Errorf("%s: invalid name", name)
}
dir := w.findPath(root, dirname)
if dir == nil || !dir.IsDir() {
return nil, nil, "", fmt.Errorf("%s: path not found", name)
}
child := dir.Children[childname]
if child == nil && mustExist {
return nil, nil, "", fmt.Errorf("%s: file not found", name)
}
return dir, child, childname, nil
}
// MakeParents ensures that all the parent directories in the path specified by `name` exists. If
// they don't exist it creates them (like `mkdir -p`). These non existing parent directories are created
// with the same permissions as that of it's parent directory. It is expected that the a
// call to make these parent directories will be made at a later point with the correct
// permissions, at that time the permissions of these directories will be updated.
// We treat Atime, Mtime, Ctime, and Crtime in the same way.
func (w *Writer) MakeParents(name string) error {
if err := w.finishInode(); err != nil {
return err
}
// go through the directories in the path one by one and create the
// parent directories if they don't exist.
cleanname := path.Clean("/" + name)[1:]
parentDirs, _ := path.Split(cleanname)
currentPath := ""
root := w.root()
dirname := ""
for parentDirs != "" {
dirname, parentDirs = splitFirst(parentDirs)
currentPath += "/" + dirname
if _, ok := root.Children[dirname]; !ok {
f := &File{
Mode: root.Mode,
Atime: fsTimeToTime(root.Atime),
Mtime: fsTimeToTime(root.Mtime),
Ctime: fsTimeToTime(root.Ctime),
Crtime: fsTimeToTime(root.Crtime),
Size: 0,
Uid: root.Uid,
Gid: root.Gid,
Devmajor: root.Devmajor,
Devminor: root.Devminor,
Xattrs: make(map[string][]byte),
}
if err := w.Create(currentPath, f); err != nil {
return fmt.Errorf("failed while creating parent directories: %w", err)
}
}
root = root.Children[dirname]
}
return nil
}
// Create adds a file to the file system.
func (w *Writer) Create(name string, f *File) error {
if err := w.finishInode(); err != nil {
return err
}
dir, existing, childname, err := w.lookup(name, false)
if err != nil {
return err
}
var reuse *inode
if existing != nil {
if existing.IsDir() {
if f.Mode&TypeMask != S_IFDIR {
return fmt.Errorf("%s: cannot replace a directory with a file", name)
}
reuse = existing
} else if f.Mode&TypeMask == S_IFDIR {
return fmt.Errorf("%s: cannot replace a file with a directory", name)
} else if existing.LinkCount < 2 {
reuse = existing
}
} else {
if f.Mode&TypeMask == S_IFDIR && dir.LinkCount >= format.MaxLinks {
return fmt.Errorf("%s: exceeded parent directory maximum link count", name)
}
}
child, err := w.makeInode(f, reuse)
if err != nil {
return fmt.Errorf("%s: %w", name, err)
}
if existing != child {
if existing != nil {
existing.LinkCount--
}
dir.Children[childname] = child
child.LinkCount++
if child.IsDir() {
dir.LinkCount++
}
}
if child.Mode&format.TypeMask == format.S_IFREG {
w.startInode(name, child, f.Size)
}
return nil
}
// Link adds a hard link to the file system.
// We support creating hardlinks to symlinks themselves instead of what
// the symlinks link to, as this is what containerd does upstream.
func (w *Writer) Link(oldname, newname string) error {
if err := w.finishInode(); err != nil {
return err
}
newdir, existing, newchildname, err := w.lookup(newname, false)
if err != nil {
return err
}
if existing != nil && (existing.IsDir() || existing.LinkCount < 2) {
return fmt.Errorf("%s: cannot orphan existing file or directory", newname)
}
_, oldfile, _, err := w.lookup(oldname, true)
if err != nil {
return err
}
switch oldfile.Mode & format.TypeMask {
case format.S_IFDIR:
return fmt.Errorf("%s: link target cannot be a directory: %s", newname, oldname)
}
if existing != oldfile && oldfile.LinkCount >= format.MaxLinks {
return fmt.Errorf("%s: link target would exceed maximum link count: %s", newname, oldname)
}
if existing != nil {
existing.LinkCount--
}
oldfile.LinkCount++
newdir.Children[newchildname] = oldfile
return nil
}
// Stat returns information about a file that has been written.
func (w *Writer) Stat(name string) (*File, error) {
if err := w.finishInode(); err != nil {
return nil, err
}
_, node, _, err := w.lookup(name, true)
if err != nil {
return nil, err
}
f := &File{
Size: node.Size,
Mode: node.Mode,
Uid: node.Uid,
Gid: node.Gid,
Atime: fsTimeToTime(node.Atime),
Ctime: fsTimeToTime(node.Ctime),
Mtime: fsTimeToTime(node.Mtime),
Crtime: fsTimeToTime(node.Crtime),
Devmajor: node.Devmajor,
Devminor: node.Devminor,
}
f.Xattrs = make(map[string][]byte)
if node.XattrBlock != 0 || len(node.XattrInline) != 0 {
if node.XattrBlock != 0 {
orig := w.block()
w.seekBlock(node.XattrBlock)
if w.err != nil {
return nil, w.err
}
var b [BlockSize]byte
_, err := w.f.Read(b[:])
w.seekBlock(orig)
if err != nil {
return nil, err
}
getXattrs(b[32:], f.Xattrs, 32)
}
if len(node.XattrInline) != 0 {
getXattrs(node.XattrInline[4:], f.Xattrs, 0)
delete(f.Xattrs, "system.data")
}
}
if node.FileType() == S_IFLNK {
if node.Size > smallSymlinkSize {
return nil, fmt.Errorf("%s: cannot retrieve link information", name)
}
f.Linkname = string(node.Data)
}
return f, nil
}
func (w *Writer) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
if w.dataWritten+int64(len(b)) > w.dataMax {
return 0, fmt.Errorf("%s: wrote too much: %d > %d", w.curName, w.dataWritten+int64(len(b)), w.dataMax)
}
if w.curInode.Flags&format.InodeFlagInlineData != 0 {
copy(w.curInode.Data[w.dataWritten:], b)
w.dataWritten += int64(len(b))
return len(b), nil
}
n, err := w.write(b)
w.dataWritten += int64(n)
return n, err
}
func (w *Writer) startInode(name string, inode *inode, size int64) {
if w.curInode != nil {
panic("inode already in progress")
}
w.curName = name
w.curInode = inode
w.dataWritten = 0
w.dataMax = size
}
func (w *Writer) block() uint32 {
return uint32(w.pos / BlockSize)
}
func (w *Writer) seekBlock(block uint32) {
w.pos = int64(block) * BlockSize
if w.err != nil {
return
}
w.err = w.bw.Flush()
if w.err != nil {
return
}
_, w.err = w.f.Seek(w.pos, io.SeekStart)
}
func (w *Writer) nextBlock() {
if w.pos%BlockSize != 0 {
// Simplify callers; w.err is updated on failure.
_, _ = w.zero(BlockSize - w.pos%BlockSize)
}
}
func fillExtents(hdr *format.ExtentHeader, extents []format.ExtentLeafNode, startBlock, offset, inodeSize uint32) {
*hdr = format.ExtentHeader{
Magic: format.ExtentHeaderMagic,
Entries: uint16(len(extents)),
Max: uint16(cap(extents)),
Depth: 0,
}
for i := range extents {
block := offset + uint32(i)*maxBlocksPerExtent
length := inodeSize - block
if length > maxBlocksPerExtent {
length = maxBlocksPerExtent
}
start := startBlock + block
extents[i] = format.ExtentLeafNode{
Block: block,
Length: uint16(length),
StartLow: start,
}
}
}
func (w *Writer) writeExtents(inode *inode) error {
start := w.pos - w.dataWritten
if start%BlockSize != 0 {
panic("unaligned")
}
w.nextBlock()
startBlock := uint32(start / BlockSize)
blocks := w.block() - startBlock
usedBlocks := blocks
const extentNodeSize = 12
const extentsPerBlock = BlockSize/extentNodeSize - 1
extents := (blocks + maxBlocksPerExtent - 1) / maxBlocksPerExtent
var b bytes.Buffer
if extents == 0 {
// Nothing to do.
} else if extents <= 4 {
var root struct {
hdr format.ExtentHeader
extents [4]format.ExtentLeafNode
}
fillExtents(&root.hdr, root.extents[:extents], startBlock, 0, blocks)
_ = binary.Write(&b, binary.LittleEndian, root)
} else if extents <= 4*extentsPerBlock {
const extentsPerBlock = BlockSize/extentNodeSize - 1
extentBlocks := extents/extentsPerBlock + 1
usedBlocks += extentBlocks
var b2 bytes.Buffer
var root struct {
hdr format.ExtentHeader
nodes [4]format.ExtentIndexNode
}
root.hdr = format.ExtentHeader{
Magic: format.ExtentHeaderMagic,
Entries: uint16(extentBlocks),
Max: 4,
Depth: 1,
}
for i := uint32(0); i < extentBlocks; i++ {
root.nodes[i] = format.ExtentIndexNode{
Block: i * extentsPerBlock * maxBlocksPerExtent,
LeafLow: w.block(),
}
extentsInBlock := extents - i*extentBlocks
if extentsInBlock > extentsPerBlock {
extentsInBlock = extentsPerBlock
}
var node struct {
hdr format.ExtentHeader
extents [extentsPerBlock]format.ExtentLeafNode
_ [BlockSize - (extentsPerBlock+1)*extentNodeSize]byte
}
offset := i * extentsPerBlock * maxBlocksPerExtent
fillExtents(&node.hdr, node.extents[:extentsInBlock], startBlock+offset, offset, blocks)
_ = binary.Write(&b2, binary.LittleEndian, node)
if _, err := w.write(b2.Next(BlockSize)); err != nil {
return err
}
}
_ = binary.Write(&b, binary.LittleEndian, root)
} else {
panic("file too big")
}
inode.Data = b.Bytes()
inode.Flags |= format.InodeFlagExtents
inode.BlockCount += usedBlocks
return w.err
}
func (w *Writer) finishInode() error {
if !w.initialized {
if err := w.init(); err != nil {
return err
}
}
if w.curInode == nil {
return nil
}
if w.dataWritten != w.dataMax {
return fmt.Errorf("did not write the right amount: %d != %d", w.dataWritten, w.dataMax)
}
if w.dataMax != 0 && w.curInode.Flags&format.InodeFlagInlineData == 0 {
if err := w.writeExtents(w.curInode); err != nil {
return err
}
}
w.dataWritten = 0
w.dataMax = 0
w.curInode = nil
return w.err
}
func modeToFileType(mode uint16) format.FileType {
switch mode & format.TypeMask {
default:
return format.FileTypeUnknown
case format.S_IFREG:
return format.FileTypeRegular
case format.S_IFDIR:
return format.FileTypeDirectory
case format.S_IFCHR:
return format.FileTypeCharacter
case format.S_IFBLK:
return format.FileTypeBlock
case format.S_IFIFO:
return format.FileTypeFIFO
case format.S_IFSOCK:
return format.FileTypeSocket
case format.S_IFLNK:
return format.FileTypeSymbolicLink
}
}
type constReader byte
var zero = constReader(0)
func (r constReader) Read(b []byte) (int, error) {
for i := range b {
b[i] = byte(r)
}
return len(b), nil
}
func (w *Writer) writeDirectory(dir, parent *inode) error {
if err := w.finishInode(); err != nil {
return err
}
// The size of the directory is not known yet.
w.startInode("", dir, 0x7fffffffffffffff)
left := BlockSize
finishBlock := func() error {
if left > 0 {
e := format.DirectoryEntry{
RecordLength: uint16(left),
}
err := binary.Write(w, binary.LittleEndian, e)
if err != nil {
return err
}
left -= directoryEntrySize
if left < 4 {
panic("not enough space for trailing entry")
}
_, err = io.CopyN(w, zero, int64(left))
if err != nil {
return err
}
}
left = BlockSize
return nil
}
writeEntry := func(ino format.InodeNumber, name string) error {
rlb := directoryEntrySize + len(name)
rl := (rlb + 3) & ^3
if left < rl+12 {
if err := finishBlock(); err != nil {
return err
}
}
e := format.DirectoryEntry{
Inode: ino,
RecordLength: uint16(rl),
NameLength: uint8(len(name)),
FileType: modeToFileType(w.getInode(ino).Mode),
}
err := binary.Write(w, binary.LittleEndian, e)
if err != nil {
return err
}
_, err = w.Write([]byte(name))
if err != nil {
return err
}
var zero [4]byte
_, err = w.Write(zero[:rl-rlb])
if err != nil {
return err
}
left -= rl
return nil
}
if err := writeEntry(dir.Number, "."); err != nil {
return err
}
if err := writeEntry(parent.Number, ".."); err != nil {
return err
}
// Follow e2fsck's convention and sort the children by inode number.
var children []string
for name := range dir.Children {
children = append(children, name)
}
sort.Slice(children, func(i, j int) bool {
left_num := dir.Children[children[i]].Number
right_num := dir.Children[children[j]].Number
if left_num == right_num {
return children[i] < children[j]
}
return left_num < right_num
})
for _, name := range children {
child := dir.Children[name]
if err := writeEntry(child.Number, name); err != nil {
return err
}
}
if err := finishBlock(); err != nil {
return err
}
w.curInode.Size = w.dataWritten
w.dataMax = w.dataWritten
return nil
}
func (w *Writer) writeDirectoryRecursive(dir, parent *inode) error {
if err := w.writeDirectory(dir, parent); err != nil {
return err
}
// Follow e2fsck's convention and sort the children by inode number.
var children []string
for name := range dir.Children {
children = append(children, name)
}
sort.Slice(children, func(i, j int) bool {
left_num := dir.Children[children[i]].Number
right_num := dir.Children[children[j]].Number
if left_num == right_num {
return children[i] < children[j]
}
return left_num < right_num
})
for _, name := range children {
child := dir.Children[name]
if child.IsDir() {
if err := w.writeDirectoryRecursive(child, dir); err != nil {
return err
}
}
}
return nil
}
func (w *Writer) writeInodeTable(tableSize uint32) error {
var b bytes.Buffer
for _, inode := range w.inodes {
if inode != nil {
binode := format.Inode{
Mode: inode.Mode,
Uid: uint16(inode.Uid & 0xffff),
Gid: uint16(inode.Gid & 0xffff),
SizeLow: uint32(inode.Size & 0xffffffff),
SizeHigh: uint32(inode.Size >> 32),
LinksCount: uint16(inode.LinkCount),
BlocksLow: inode.BlockCount,
Flags: inode.Flags,
XattrBlockLow: inode.XattrBlock,
UidHigh: uint16(inode.Uid >> 16),
GidHigh: uint16(inode.Gid >> 16),
ExtraIsize: uint16(inodeUsedSize - 128),
Atime: uint32(inode.Atime),
AtimeExtra: uint32(inode.Atime >> 32),
Ctime: uint32(inode.Ctime),
CtimeExtra: uint32(inode.Ctime >> 32),
Mtime: uint32(inode.Mtime),
MtimeExtra: uint32(inode.Mtime >> 32),
Crtime: uint32(inode.Crtime),
CrtimeExtra: uint32(inode.Crtime >> 32),
}
switch inode.Mode & format.TypeMask {
case format.S_IFDIR, format.S_IFREG, format.S_IFLNK:
n := copy(binode.Block[:], inode.Data)
if n < len(inode.Data) {
// Rewrite the first xattr with the data.
xattr := [1]xattr{{
Name: "data",
Index: 7, // "system."
Value: inode.Data[n:],
}}
putXattrs(xattr[:], inode.XattrInline[4:], 0)
}
case format.S_IFBLK, format.S_IFCHR:
dev := inode.Devminor&0xff | inode.Devmajor<<8 | (inode.Devminor&0xffffff00)<<12
binary.LittleEndian.PutUint32(binode.Block[4:], dev)
}
_ = binary.Write(&b, binary.LittleEndian, binode)
b.Truncate(inodeUsedSize)
n, _ := b.Write(inode.XattrInline)
_, _ = io.CopyN(&b, zero, int64(inodeExtraSize-n))
} else {
_, _ = io.CopyN(&b, zero, inodeSize)
}
if _, err := w.write(b.Next(inodeSize)); err != nil {
return err
}
}
rest := tableSize - uint32(len(w.inodes)*inodeSize)
if _, err := w.zero(int64(rest)); err != nil {
return err
}
return nil
}
// NewWriter returns a Writer that writes an ext4 file system to the provided
// ReadWriteSeeker.
func NewWriter(f io.ReadWriteSeeker, opts ...Option) *Writer {
w := &Writer{
f: f,
bw: bufio.NewWriterSize(f, 65536*8),
maxDiskSize: defaultMaxDiskSize,
}
for _, opt := range opts {
opt(w)
}
return w
}
// An Option provides extra options to NewWriter.
type Option func(*Writer)
// InlineData instructs the Writer to write small files into the inode
// structures directly. This creates smaller images but currently is not
// compatible with DAX.
func InlineData(w *Writer) {
w.supportInlineData = true
}
// MaximumDiskSize instructs the writer to reserve enough metadata space for the
// specified disk size. If not provided, then 16GB is the default.
func MaximumDiskSize(size int64) Option {
return func(w *Writer) {
if size < 0 || size > maxMaxDiskSize {
w.maxDiskSize = maxMaxDiskSize
} else if size == 0 {
w.maxDiskSize = defaultMaxDiskSize
} else {
w.maxDiskSize = (size + BlockSize - 1) &^ (BlockSize - 1)
}
}
}
func (w *Writer) init() error {
// Skip the defective block inode.
w.inodes = make([]*inode, 1, 32)
// Create the root directory.
root, _ := w.makeInode(&File{
Mode: format.S_IFDIR | 0755,
}, nil)
root.LinkCount++ // The root is linked to itself.
// Skip until the first non-reserved inode.
w.inodes = append(w.inodes, make([]*inode, inodeFirst-len(w.inodes)-1)...)
maxBlocks := (w.maxDiskSize-1)/BlockSize + 1
maxGroups := (maxBlocks-1)/blocksPerGroup + 1
w.gdBlocks = uint32((maxGroups-1)/groupsPerDescriptorBlock + 1)
// Skip past the superblock and block descriptor table.
w.seekBlock(1 + w.gdBlocks)
w.initialized = true
// The lost+found directory is required to exist for e2fsck to pass.
if err := w.Create("lost+found", &File{Mode: format.S_IFDIR | 0700}); err != nil {
return err
}
return w.err
}
func groupCount(blocks uint32, inodes uint32, inodesPerGroup uint32) uint32 {
inodeBlocksPerGroup := inodesPerGroup * inodeSize / BlockSize
dataBlocksPerGroup := blocksPerGroup - inodeBlocksPerGroup - 2 // save room for the bitmaps
// Increase the block count to ensure there are enough groups for all the
// inodes.
minBlocks := (inodes-1)/inodesPerGroup*dataBlocksPerGroup + 1
if blocks < minBlocks {
blocks = minBlocks
}
return (blocks + dataBlocksPerGroup - 1) / dataBlocksPerGroup
}
func bestGroupCount(blocks uint32, inodes uint32) (groups uint32, inodesPerGroup uint32) {
groups = 0xffffffff
for ipg := uint32(inodesPerGroupIncrement); ipg <= maxInodesPerGroup; ipg += inodesPerGroupIncrement {
g := groupCount(blocks, inodes, ipg)
if g < groups {
groups = g
inodesPerGroup = ipg
}
}
return
}
func (w *Writer) Close() error {
if err := w.finishInode(); err != nil {
return err
}
root := w.root()
if err := w.writeDirectoryRecursive(root, root); err != nil {
return err
}
// Finish the last inode (probably a directory).
if err := w.finishInode(); err != nil {
return err
}
// Write the inode table
inodeTableOffset := w.block()
groups, inodesPerGroup := bestGroupCount(inodeTableOffset, uint32(len(w.inodes)))
err := w.writeInodeTable(groups * inodesPerGroup * inodeSize)
if err != nil {
return err
}
// Write the bitmaps.
bitmapOffset := w.block()
bitmapSize := groups * 2
validDataSize := bitmapOffset + bitmapSize
diskSize := validDataSize
minSize := (groups-1)*blocksPerGroup + 1
if diskSize < minSize {
diskSize = minSize
}
usedGdBlocks := (groups-1)/groupsPerDescriptorBlock + 1
if usedGdBlocks > w.gdBlocks {
return exceededMaxSizeError{w.maxDiskSize}
}
gds := make([]format.GroupDescriptor, w.gdBlocks*groupsPerDescriptorBlock)
inodeTableSizePerGroup := inodesPerGroup * inodeSize / BlockSize
var totalUsedBlocks, totalUsedInodes uint32
for g := uint32(0); g < groups; g++ {
var b [BlockSize * 2]byte
var dirCount, usedInodeCount, usedBlockCount uint16
// Block bitmap
if (g+1)*blocksPerGroup <= validDataSize {
// This group is fully allocated.
for j := range b[:BlockSize] {
b[j] = 0xff
}
usedBlockCount = blocksPerGroup
} else if g*blocksPerGroup < validDataSize {
for j := uint32(0); j < validDataSize-g*blocksPerGroup; j++ {
b[j/8] |= 1 << (j % 8)
usedBlockCount++
}
}
if g == 0 {
// Unused group descriptor blocks should be cleared.
for j := 1 + usedGdBlocks; j < 1+w.gdBlocks; j++ {
b[j/8] &^= 1 << (j % 8)
usedBlockCount--
}
}
if g == groups-1 && diskSize%blocksPerGroup != 0 {
// Blocks that aren't present in the disk should be marked as
// allocated.
for j := diskSize % blocksPerGroup; j < blocksPerGroup; j++ {
b[j/8] |= 1 << (j % 8)
usedBlockCount++
}
}
// Inode bitmap
for j := uint32(0); j < inodesPerGroup; j++ {
ino := format.InodeNumber(1 + g*inodesPerGroup + j)
inode := w.getInode(ino)
if ino < inodeFirst || inode != nil {
b[BlockSize+j/8] |= 1 << (j % 8)
usedInodeCount++
}
if inode != nil && inode.Mode&format.TypeMask == format.S_IFDIR {
dirCount++
}
}
_, err := w.write(b[:])
if err != nil {
return err
}
gds[g] = format.GroupDescriptor{
BlockBitmapLow: bitmapOffset + 2*g,
InodeBitmapLow: bitmapOffset + 2*g + 1,
InodeTableLow: inodeTableOffset + g*inodeTableSizePerGroup,
UsedDirsCountLow: dirCount,
FreeInodesCountLow: uint16(inodesPerGroup) - usedInodeCount,
FreeBlocksCountLow: blocksPerGroup - usedBlockCount,
}
totalUsedBlocks += uint32(usedBlockCount)
totalUsedInodes += uint32(usedInodeCount)
}
// Zero up to the disk size.
_, err = w.zero(int64(diskSize-bitmapOffset-bitmapSize) * BlockSize)
if err != nil {
return err
}
// Write the block descriptors
w.seekBlock(1)
if w.err != nil {
return w.err
}
err = binary.Write(w.bw, binary.LittleEndian, gds)
if err != nil {
return err
}
// Write the super block
var blk [BlockSize]byte
b := bytes.NewBuffer(blk[:1024])
sb := &format.SuperBlock{
InodesCount: inodesPerGroup * groups,
BlocksCountLow: diskSize,
FreeBlocksCountLow: blocksPerGroup*groups - totalUsedBlocks,
FreeInodesCount: inodesPerGroup*groups - totalUsedInodes,
FirstDataBlock: 0,
LogBlockSize: 2, // 2^(10 + 2)
LogClusterSize: 2,
BlocksPerGroup: blocksPerGroup,
ClustersPerGroup: blocksPerGroup,
InodesPerGroup: inodesPerGroup,
Magic: format.SuperBlockMagic,
State: 1, // cleanly unmounted
Errors: 1, // continue on error?
CreatorOS: 0, // Linux
RevisionLevel: 1, // dynamic inode sizes
FirstInode: inodeFirst,
LpfInode: inodeLostAndFound,
InodeSize: inodeSize,
FeatureCompat: format.CompatSparseSuper2 | format.CompatExtAttr,
FeatureIncompat: format.IncompatFiletype | format.IncompatExtents | format.IncompatFlexBg,
FeatureRoCompat: format.RoCompatLargeFile | format.RoCompatHugeFile | format.RoCompatExtraIsize | format.RoCompatReadonly,
MinExtraIsize: extraIsize,
WantExtraIsize: extraIsize,
LogGroupsPerFlex: 31,
}
if w.supportInlineData {
sb.FeatureIncompat |= format.IncompatInlineData
}
_ = binary.Write(b, binary.LittleEndian, sb)
w.seekBlock(0)
if _, err := w.write(blk[:]); err != nil {
return err
}
w.seekBlock(diskSize)
return w.err
}