package system import ( "bytes" "context" "errors" "io" "os" "path/filepath" "strings" "testing" ) // stdinFuncRunner is funcRunner extended with a RunStdin hook so we // can assert the exact debugfs batch script that callers stream in. type stdinFuncRunner struct { funcRunner runStdin func(ctx context.Context, stdin io.Reader, name string, args ...string) ([]byte, error) } func (r stdinFuncRunner) RunStdin(ctx context.Context, stdin io.Reader, name string, args ...string) ([]byte, error) { if r.runStdin == nil { return nil, errors.New("unexpected RunStdin call") } return r.runStdin(ctx, stdin, name, args...) } // userOwnedImage writes a zero-length regular file at a tempdir and // returns its path. Regular files trigger extfsRun's non-sudo branch, // which is the whole point of the new toolkit. func userOwnedImage(t *testing.T) string { t.Helper() path := filepath.Join(t.TempDir(), "work.ext4") if err := os.WriteFile(path, []byte{}, 0o644); err != nil { t.Fatalf("write image: %v", err) } return path } func TestExt4PathExists(t *testing.T) { image := userOwnedImage(t) t.Run("path found", func(t *testing.T) { r := funcRunner{ run: func(_ context.Context, name string, args ...string) ([]byte, error) { if name != "debugfs" { t.Fatalf("name = %q, want debugfs", name) } want := []string{"-R", "stat /root/.ssh", image} for i := range want { if args[i] != want[i] { t.Fatalf("args[%d] = %q, want %q (full %v)", i, args[i], want[i], args) } } return []byte("Inode: 12 Type: directory"), nil }, } ok, err := Ext4PathExists(context.Background(), r, image, "/root/.ssh") if err != nil { t.Fatalf("Ext4PathExists: %v", err) } if !ok { t.Fatal("expected exists = true") } }) t.Run("path missing", func(t *testing.T) { r := funcRunner{ run: func(context.Context, string, ...string) ([]byte, error) { // debugfs prints the "File not found" message to stdout // on lookup miss. No exit error (debugfs exits 0 for // soft misses on `stat`). return []byte("stat: File not found by ext2_lookup while starting pathname"), nil }, } ok, err := Ext4PathExists(context.Background(), r, image, "/root/.ssh") if err != nil { t.Fatalf("Ext4PathExists: %v", err) } if ok { t.Fatal("expected exists = false") } }) t.Run("rejects hostile path", func(t *testing.T) { r := funcRunner{} if _, err := Ext4PathExists(context.Background(), r, image, `/evil"path`); err == nil { t.Fatal("expected rejection for path containing double-quote") } }) } func TestReadExt4File(t *testing.T) { image := userOwnedImage(t) r := funcRunner{ run: func(_ context.Context, name string, args ...string) ([]byte, error) { if name != "debugfs" { t.Fatalf("name = %q, want debugfs", name) } if args[0] != "-R" || args[1] != "cat /etc/fstab" { t.Fatalf("args = %v, want -R \"cat /etc/fstab\" ...", args) } return []byte("tmpfs /tmp tmpfs defaults 0 0\n"), nil }, } got, err := ReadExt4File(context.Background(), r, image, "/etc/fstab") if err != nil { t.Fatalf("ReadExt4File: %v", err) } if !bytes.Contains(got, []byte("tmpfs /tmp")) { t.Fatalf("got = %q, want contains tmpfs line", got) } } func TestMkdirExt4_BatchesStatMkdirAndMetadata(t *testing.T) { image := userOwnedImage(t) var capturedScript string r := stdinFuncRunner{ funcRunner: funcRunner{ run: func(_ context.Context, name string, args ...string) ([]byte, error) { // The only non-stdin call should be the existence check. if name == "debugfs" && len(args) >= 2 && args[0] == "-R" && strings.HasPrefix(args[1], "stat ") { return []byte("stat: File not found"), nil } t.Fatalf("unexpected Run(%q, %v)", name, args) return nil, nil }, }, runStdin: func(_ context.Context, stdin io.Reader, name string, args ...string) ([]byte, error) { if name != "debugfs" { t.Fatalf("stdin runner name = %q, want debugfs", name) } want := []string{"-w", "-f", "-", image} for i, w := range want { if args[i] != w { t.Fatalf("stdin args[%d] = %q, want %q", i, args[i], w) } } b, _ := io.ReadAll(stdin) capturedScript = string(b) return nil, nil }, } if err := MkdirExt4(context.Background(), r, image, "/.ssh", 0o700, 0, 0); err != nil { t.Fatalf("MkdirExt4: %v", err) } // mkdir line must be present (path didn't exist). if !strings.Contains(capturedScript, "mkdir /.ssh") { t.Fatalf("script missing mkdir line:\n%s", capturedScript) } // Mode must include the directory file-type nibble (040000 | 0700 = 040700). if !strings.Contains(capturedScript, "set_inode_field /.ssh mode 040700") { t.Fatalf("script missing mode line with S_IFDIR+0700:\n%s", capturedScript) } if !strings.Contains(capturedScript, "set_inode_field /.ssh uid 0") { t.Fatalf("script missing uid line:\n%s", capturedScript) } if !strings.Contains(capturedScript, "set_inode_field /.ssh gid 0") { t.Fatalf("script missing gid line:\n%s", capturedScript) } } func TestMkdirExt4_SkipsMkdirWhenDirectoryExists(t *testing.T) { image := userOwnedImage(t) var capturedScript string r := stdinFuncRunner{ funcRunner: funcRunner{ run: func(_ context.Context, name string, args ...string) ([]byte, error) { // First call: existence probe. Return success. if name == "debugfs" && args[0] == "-R" && strings.HasPrefix(args[1], "stat ") { return []byte("Inode: 12 Type: directory Mode: 0700"), nil } t.Fatalf("unexpected Run(%q, %v)", name, args) return nil, nil }, }, runStdin: func(_ context.Context, stdin io.Reader, _ string, _ ...string) ([]byte, error) { b, _ := io.ReadAll(stdin) capturedScript = string(b) return nil, nil }, } if err := MkdirExt4(context.Background(), r, image, "/.ssh", 0o700, 0, 0); err != nil { t.Fatalf("MkdirExt4: %v", err) } // Directory existed — no mkdir line, but metadata lines still fire. if strings.Contains(capturedScript, "mkdir ") { t.Fatalf("script should not contain mkdir for pre-existing path:\n%s", capturedScript) } if !strings.Contains(capturedScript, "set_inode_field /.ssh mode") { t.Fatalf("script missing metadata reset for pre-existing dir:\n%s", capturedScript) } } func TestWriteExt4FileOwned_StagesTempfileAndBatchesOwnership(t *testing.T) { image := userOwnedImage(t) var observedTemp string var capturedScript string r := stdinFuncRunner{ funcRunner: funcRunner{ run: func(_ context.Context, name string, args ...string) ([]byte, error) { switch name { case "e2rm": // First non-stdin call — best-effort, we don't // verify the target since e2rm on a missing path // returns a visible error but the caller ignores it. return nil, nil case "e2cp": if len(args) != 2 { t.Fatalf("e2cp args = %v, want 2 (src, dst)", args) } observedTemp = args[0] // Assert the dst uses the image:path form. if args[1] != image+":/root/.ssh/authorized_keys" { t.Fatalf("e2cp dst = %q, want image:path", args[1]) } // Assert the temp file was populated with our data // BEFORE e2cp was called. data, err := os.ReadFile(args[0]) if err != nil { t.Fatalf("temp missing at e2cp time: %v", err) } if !bytes.Equal(data, []byte("managed-key\n")) { t.Fatalf("temp contents = %q, want managed-key", data) } return nil, nil } t.Fatalf("unexpected Run(%q, %v)", name, args) return nil, nil }, }, runStdin: func(_ context.Context, stdin io.Reader, _ string, _ ...string) ([]byte, error) { b, _ := io.ReadAll(stdin) capturedScript = string(b) return nil, nil }, } err := WriteExt4FileOwned( context.Background(), r, image, "/root/.ssh/authorized_keys", 0o600, 0, 0, []byte("managed-key\n"), ) if err != nil { t.Fatalf("WriteExt4FileOwned: %v", err) } // Temp cleanup ran — we saved observedTemp while it still existed; // by now it should be gone. if observedTemp == "" { t.Fatal("e2cp source path was never captured") } if _, err := os.Stat(observedTemp); !os.IsNotExist(err) { t.Fatalf("temp file not cleaned up: stat err = %v", err) } // Mode line must bake in S_IFREG (0100000) + 0600 = 0100600. if !strings.Contains(capturedScript, "set_inode_field /root/.ssh/authorized_keys mode 0100600") { t.Fatalf("script missing regular-file mode line:\n%s", capturedScript) } } func TestEnsureExt4RootPerms_UsesRootInodeLiteral(t *testing.T) { image := userOwnedImage(t) var capturedScript string r := stdinFuncRunner{ funcRunner: funcRunner{}, runStdin: func(_ context.Context, stdin io.Reader, _ string, _ ...string) ([]byte, error) { b, _ := io.ReadAll(stdin) capturedScript = string(b) return nil, nil }, } if err := EnsureExt4RootPerms(context.Background(), r, image, 0o755, 0, 0); err != nil { t.Fatalf("EnsureExt4RootPerms: %v", err) } // Must address inode 2 — the ext4 root directory — with the // FULL i_mode word (S_IFDIR | 0755 = 040755). debugfs's // set_inode_field doesn't preserve the type nibble, so passing // just the permission bits (0755) would reset the root inode // to regular-file shape and break the next kernel mount. if !strings.Contains(capturedScript, "set_inode_field <2> mode 040755") { t.Fatalf("script missing root-inode mode line with S_IFDIR+0755:\n%s", capturedScript) } if !strings.Contains(capturedScript, "set_inode_field <2> uid 0") { t.Fatalf("script missing root-inode uid line:\n%s", capturedScript) } } func TestRejectDebugfsUnsafePath(t *testing.T) { for _, tc := range []struct { name string path string wantErr bool }{ {"empty", "", true}, {"relative", "relative/path", true}, {"absolute plain", "/ok", false}, {"absolute with space", "/ok path", false}, {"contains double-quote", `/a"b`, true}, {"contains backslash", `/a\b`, true}, {"contains newline", "/a\nb", true}, } { t.Run(tc.name, func(t *testing.T) { err := rejectDebugfsUnsafePath(tc.path) if (err != nil) != tc.wantErr { t.Fatalf("rejectDebugfsUnsafePath(%q) err = %v, wantErr = %v", tc.path, err, tc.wantErr) } }) } }