Preserve cleanup after daemon restarts and harden OCI and tar imports against filenames that debugfs cannot encode safely. Mirror tap, loop, and dm teardown identity onto VM.Runtime, teach cleanup and reconcile to fall back to those persisted fields when handles.json is missing or corrupt, and clear the recovery state on stop, error, and delete paths. Reject debugfs-hostile entry names during flattening and in ApplyOwnership itself, then add regression coverage for corrupt handles.json recovery and unsafe import paths. Verified with targeted go tests, make lint-go, make lint-shell, and make build.
124 lines
3.7 KiB
Go
124 lines
3.7 KiB
Go
package imagepull
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"banger/internal/system"
|
|
)
|
|
|
|
// ApplyOwnership rewrites the ext4 image's per-file uid/gid/mode to match
|
|
// the tar-header values Flatten captured. `mkfs.ext4 -d` preserves the
|
|
// on-disk ownership of the source tree — which is the runner's uid/gid,
|
|
// since we extracted as a regular user — so without this pass setuid
|
|
// binaries become setuid-nonroot and root-owned config files are
|
|
// readable by the runner's group.
|
|
//
|
|
// Implementation: stream a "set_inode_field" script to `debugfs -w`.
|
|
// One invocation handles tens of thousands of files; the bottleneck is
|
|
// debugfs's one-inode-at-a-time disk I/O, not process startup.
|
|
func ApplyOwnership(ctx context.Context, runner system.CommandRunner, ext4File string, meta Metadata) error {
|
|
if len(meta.Entries) == 0 {
|
|
return nil
|
|
}
|
|
script, err := buildOwnershipScript(meta)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if script.Len() == 0 {
|
|
return nil
|
|
}
|
|
stdinRunner, ok := runner.(system.StdinRunner)
|
|
if !ok {
|
|
return fmt.Errorf("ownership fixup requires a runner that supports stdin (got %T)", runner)
|
|
}
|
|
out, err := stdinRunner.RunStdin(ctx, script, "debugfs", "-w", "-f", "-", ext4File)
|
|
if err != nil {
|
|
return fmt.Errorf("debugfs ownership fixup: %w: %s", err, string(out))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// buildOwnershipScript emits one `set_inode_field` block per entry.
|
|
// Paths are prefixed with "/" so debugfs resolves them from the ext4
|
|
// root. Entries are sorted for deterministic output (helps testing and
|
|
// makes debugfs's internal caching slightly more cache-friendly).
|
|
func buildOwnershipScript(meta Metadata) (*bytes.Buffer, error) {
|
|
var buf bytes.Buffer
|
|
paths := make([]string, 0, len(meta.Entries))
|
|
for p := range meta.Entries {
|
|
paths = append(paths, p)
|
|
}
|
|
sort.Strings(paths)
|
|
for _, p := range paths {
|
|
m := meta.Entries[p]
|
|
mode := debugfsMode(m.Type, m.Mode)
|
|
if mode == 0 {
|
|
continue // hardlinks or unsupported types (skip)
|
|
}
|
|
if err := validateDebugFSPath(p); err != nil {
|
|
return nil, err
|
|
}
|
|
escaped := escapeDebugfsPath(p)
|
|
fmt.Fprintf(&buf, "set_inode_field %s uid %d\n", escaped, m.Uid)
|
|
fmt.Fprintf(&buf, "set_inode_field %s gid %d\n", escaped, m.Gid)
|
|
fmt.Fprintf(&buf, "set_inode_field %s mode 0%o\n", escaped, mode)
|
|
}
|
|
return &buf, nil
|
|
}
|
|
|
|
// debugfsMode composes the full i_mode word (file-type bits +
|
|
// permission bits) that debugfs' `set_inode_field ... mode` expects.
|
|
// Returns 0 for types we don't set (hardlinks, unknown).
|
|
func debugfsMode(typ byte, hdrMode int64) uint32 {
|
|
perm := uint32(hdrMode) & 0o7777
|
|
switch typ {
|
|
case tar.TypeReg:
|
|
return 0o100000 | perm
|
|
case tar.TypeDir:
|
|
return 0o040000 | perm
|
|
case tar.TypeSymlink:
|
|
return 0o120000 | perm
|
|
case tar.TypeChar:
|
|
return 0o020000 | perm
|
|
case tar.TypeBlock:
|
|
return 0o060000 | perm
|
|
case tar.TypeFifo:
|
|
return 0o010000 | perm
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
var errUnsafeDebugFSPath = errors.New("unsafe path for debugfs ownership script")
|
|
|
|
func validateDebugFSPath(rel string) error {
|
|
for i := 0; i < len(rel); i++ {
|
|
switch c := rel[i]; {
|
|
case c == '"':
|
|
return fmt.Errorf("%w: %q contains '\"'", errUnsafeDebugFSPath, rel)
|
|
case c == '\\':
|
|
return fmt.Errorf("%w: %q contains '\\\\'", errUnsafeDebugFSPath, rel)
|
|
case c < 0x20 || c == 0x7f:
|
|
return fmt.Errorf("%w: %q contains control byte 0x%02x", errUnsafeDebugFSPath, rel, c)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// escapeDebugfsPath prepends "/" and wraps in double quotes if the path
|
|
// contains spaces. validateDebugFSPath rejects debugfs-hostile bytes
|
|
// before this runs, so the only quoting we need is the simple
|
|
// whitespace case debugfs already handles.
|
|
func escapeDebugfsPath(rel string) string {
|
|
abs := "/" + rel
|
|
if strings.ContainsRune(abs, ' ') {
|
|
return `"` + abs + `"`
|
|
}
|
|
return abs
|
|
}
|