daemon: persist teardown fallbacks and reject unsafe import paths

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.
This commit is contained in:
Thales Maciel 2026-04-23 16:21:59 -03:00
parent 86a56fedb3
commit d743a8ba4b
No known key found for this signature in database
GPG key ID: 33112E6833C34679
15 changed files with 272 additions and 81 deletions

View file

@ -254,6 +254,30 @@ func TestFlattenRejectsPathTraversal(t *testing.T) {
}
}
func TestFlattenRejectsDebugFSHostilePath(t *testing.T) {
img, err := mutate.AppendLayers(empty.Image,
makeLayer(t, []tarMember{
{name: `etc/bad"name`, body: []byte("bad")},
}),
)
if err != nil {
t.Fatalf("AppendLayers: %v", err)
}
pulled := PulledImage{
Reference: "test/debugfs-hostile",
Digest: "sha256:test",
Platform: "linux/amd64",
Image: img,
}
_, err = Flatten(context.Background(), pulled, t.TempDir())
if !errors.Is(err, errUnsafeDebugFSPath) {
t.Fatalf("Flatten hostile path: err=%v, want %v", err, errUnsafeDebugFSPath)
}
if !strings.Contains(err.Error(), `etc/bad\"name`) {
t.Fatalf("Flatten hostile path: err=%v, want offending path", err)
}
}
func TestFlattenAcceptsAbsoluteSymlink(t *testing.T) {
// Container layers regularly contain absolute symlinks like
// /usr/bin/mawk — they're interpreted relative to the rootfs at
@ -303,6 +327,19 @@ func TestFlattenRejectsRelativeSymlinkEscape(t *testing.T) {
}
}
func TestFlattenTarRejectsDebugFSHostilePath(t *testing.T) {
tarData := buildTar(t, []tarMember{
{name: "etc/bad\tname", body: []byte("bad")},
})
_, err := FlattenTar(context.Background(), bytes.NewReader(tarData), t.TempDir())
if !errors.Is(err, errUnsafeDebugFSPath) {
t.Fatalf("FlattenTar hostile path: err=%v, want %v", err, errUnsafeDebugFSPath)
}
if !strings.Contains(err.Error(), `etc/bad\tname`) {
t.Fatalf("FlattenTar hostile path: err=%v, want offending path", err)
}
}
func TestBuildExt4ProducesValidImage(t *testing.T) {
if _, err := exec.LookPath("mkfs.ext4"); err != nil {
t.Skip("mkfs.ext4 not available; skipping")
@ -412,13 +449,30 @@ func TestApplyOwnershipRewritesUidGidMode(t *testing.T) {
}
}
func TestApplyOwnershipRejectsUnsafeMetadataPath(t *testing.T) {
meta := Metadata{Entries: map[string]FileMeta{
"bad\nname": {Uid: 0, Gid: 0, Mode: 0o644, Type: tar.TypeReg},
}}
err := ApplyOwnership(context.Background(), system.NewRunner(), filepath.Join(t.TempDir(), "rootfs.ext4"), meta)
if !errors.Is(err, errUnsafeDebugFSPath) {
t.Fatalf("ApplyOwnership hostile path: err=%v, want %v", err, errUnsafeDebugFSPath)
}
if !strings.Contains(err.Error(), `bad\nname`) {
t.Fatalf("ApplyOwnership hostile path: err=%v, want offending path", err)
}
}
func TestBuildOwnershipScriptDeterministic(t *testing.T) {
meta := Metadata{Entries: map[string]FileMeta{
"b": {Uid: 0, Gid: 0, Mode: 0o755, Type: tar.TypeReg},
"a": {Uid: 0, Gid: 0, Mode: 0o755, Type: tar.TypeReg},
"a/x": {Uid: 0, Gid: 0, Mode: 0o644, Type: tar.TypeReg},
}}
got := buildOwnershipScript(meta).String()
gotBuf, err := buildOwnershipScript(meta)
if err != nil {
t.Fatalf("buildOwnershipScript: %v", err)
}
got := gotBuf.String()
// sorted: a, a/x, b
want := "set_inode_field /a uid 0\nset_inode_field /a gid 0\nset_inode_field /a mode 0100755\n" +
"set_inode_field /a/x uid 0\nset_inode_field /a/x gid 0\nset_inode_field /a/x mode 0100644\n" +