Phase B-1: ownership fixup via debugfs pass
imagepull.Flatten now captures per-file uid/gid/mode/type from the tar headers as it walks layers, returning a Metadata map alongside the extracted tree. Whiteouts correctly drop the victim's metadata. The returned Metadata feeds the new imagepull.ApplyOwnership, which pipes a batched `set_inode_field` script to `debugfs -w -f -`. Why: mkfs.ext4 -d copies the runner's on-disk uids verbatim, so without this pass setuid binaries become setuid-nonroot and sshd refuses to start on the resulting image. With the pass, a pulled debian:bookworm has /usr/bin/sudo with uid=0 + setuid bit surviving intact. imagepull.BuildExt4 signature unchanged; ownership is applied as a separate step by the daemon orchestrator between BuildExt4 and StageBootArtifacts, keeping each helper focused. The seam (d.pullAndFlatten) now returns (Metadata, error) for test stubs to feed synthetic metadata. StdinRunner is a new duck-typed extension next to CommandRunner; the real system.Runner implements RunStdin, test mocks don't need to unless they exercise stdin. Prevents every existing mock from growing a new method. Tests: - TestFlattenCapturesHeaderMetadata: setuid bit + mode survive the tar-header walk - TestApplyOwnershipRewritesUidGidMode: real debugfs round-trip — create ext4 with runner's uid, apply synthetic metadata setting uid=0 + setuid mode, verify via `debugfs -R stat` that the inode now has uid=0 and mode 04755 - TestBuildOwnershipScriptDeterministic: sorted, well-formed sif script output Debugfs and mkfs.ext4 tests skip if the binaries aren't on PATH. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2e4d4b14da
commit
43982a4ae3
7 changed files with 334 additions and 32 deletions
|
|
@ -19,6 +19,7 @@ import (
|
|||
"banger/internal/buildinfo"
|
||||
"banger/internal/config"
|
||||
"banger/internal/daemon/opstate"
|
||||
"banger/internal/imagepull"
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
"banger/internal/rpc"
|
||||
|
|
@ -50,7 +51,7 @@ type Daemon struct {
|
|||
vmDNS *vmdns.Server
|
||||
vmCaps []vmCapability
|
||||
imageBuild func(context.Context, imageBuildSpec) error
|
||||
pullAndFlatten func(ctx context.Context, ref, cacheDir, destDir string) error
|
||||
pullAndFlatten func(ctx context.Context, ref, cacheDir, destDir string) (imagepull.Metadata, error)
|
||||
requestHandler func(context.Context, rpc.Request) rpc.Response
|
||||
guestWaitForSSH func(context.Context, string, string, time.Duration) error
|
||||
guestDial func(context.Context, string, string) (guestSSHClient, error)
|
||||
|
|
|
|||
|
|
@ -86,7 +86,8 @@ func (d *Daemon) PullImage(ctx context.Context, params api.ImagePullParams) (ima
|
|||
}
|
||||
defer os.RemoveAll(rootfsTree)
|
||||
|
||||
if err := d.runPullAndFlatten(ctx, ref, d.layout.OCICacheDir, rootfsTree); err != nil {
|
||||
meta, err := d.runPullAndFlatten(ctx, ref, d.layout.OCICacheDir, rootfsTree)
|
||||
if err != nil {
|
||||
return model.Image{}, fmt.Errorf("pull oci image: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +107,9 @@ func (d *Daemon) PullImage(ctx context.Context, params api.ImagePullParams) (ima
|
|||
if err := imagepull.BuildExt4(ctx, d.runner, rootfsTree, rootfsExt4, sizeBytes); err != nil {
|
||||
return model.Image{}, fmt.Errorf("build rootfs ext4: %w", err)
|
||||
}
|
||||
if err := imagepull.ApplyOwnership(ctx, d.runner, rootfsExt4, meta); err != nil {
|
||||
return model.Image{}, fmt.Errorf("apply ownership: %w", err)
|
||||
}
|
||||
|
||||
stagedKernel, stagedInitrd, stagedModules, err := imagemgr.StageBootArtifacts(ctx, d.runner, stagingDir, kernelPath, initrdPath, modulesDir)
|
||||
if err != nil {
|
||||
|
|
@ -138,13 +142,13 @@ func (d *Daemon) PullImage(ctx context.Context, params api.ImagePullParams) (ima
|
|||
}
|
||||
|
||||
// runPullAndFlatten is the seam tests substitute. nil → real implementation.
|
||||
func (d *Daemon) runPullAndFlatten(ctx context.Context, ref, cacheDir, destDir string) error {
|
||||
func (d *Daemon) runPullAndFlatten(ctx context.Context, ref, cacheDir, destDir string) (imagepull.Metadata, error) {
|
||||
if d.pullAndFlatten != nil {
|
||||
return d.pullAndFlatten(ctx, ref, cacheDir, destDir)
|
||||
}
|
||||
pulled, err := imagepull.Pull(ctx, ref, cacheDir)
|
||||
if err != nil {
|
||||
return err
|
||||
return imagepull.Metadata{}, err
|
||||
}
|
||||
return imagepull.Flatten(ctx, pulled, destDir)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"banger/internal/api"
|
||||
"banger/internal/imagepull"
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
"banger/internal/system"
|
||||
|
|
@ -40,14 +41,19 @@ func writeFakeKernelTriple(t *testing.T) (kernelPath, initrdPath, modulesDir str
|
|||
|
||||
// stubPullAndFlatten writes a fixed file tree into destDir, simulating a
|
||||
// successful OCI pull without the network or tarball machinery.
|
||||
func stubPullAndFlatten(_ context.Context, _ string, _ string, destDir string) error {
|
||||
func stubPullAndFlatten(_ context.Context, _ string, _ string, destDir string) (imagepull.Metadata, error) {
|
||||
if err := os.MkdirAll(filepath.Join(destDir, "etc"), 0o755); err != nil {
|
||||
return err
|
||||
return imagepull.Metadata{}, err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(destDir, "etc", "hello"), []byte("world"), 0o644); err != nil {
|
||||
return err
|
||||
return imagepull.Metadata{}, err
|
||||
}
|
||||
return os.WriteFile(filepath.Join(destDir, "marker"), []byte("ok"), 0o644)
|
||||
if err := os.WriteFile(filepath.Join(destDir, "marker"), []byte("ok"), 0o644); err != nil {
|
||||
return imagepull.Metadata{}, err
|
||||
}
|
||||
// Tiny synthetic metadata — daemon-level tests exercise the seam
|
||||
// plumbing, not the ownership pass itself.
|
||||
return imagepull.Metadata{Entries: map[string]imagepull.FileMeta{}}, nil
|
||||
}
|
||||
|
||||
func TestPullImageHappyPath(t *testing.T) {
|
||||
|
|
@ -146,8 +152,8 @@ func TestPullImageRequiresKernel(t *testing.T) {
|
|||
func TestPullImageCleansStagingOnFailure(t *testing.T) {
|
||||
imagesDir := t.TempDir()
|
||||
kernel, _, _ := writeFakeKernelTriple(t)
|
||||
failureSeam := func(_ context.Context, _ string, _ string, _ string) error {
|
||||
return errors.New("network borked")
|
||||
failureSeam := func(_ context.Context, _ string, _ string, _ string) (imagepull.Metadata, error) {
|
||||
return imagepull.Metadata{}, errors.New("network borked")
|
||||
}
|
||||
|
||||
d := &Daemon{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue