diff --git a/pkg/sentry/control/fs.go b/pkg/sentry/control/fs.go index 6260fad2ff..74a1292cd6 100644 --- a/pkg/sentry/control/fs.go +++ b/pkg/sentry/control/fs.go @@ -146,3 +146,57 @@ func cat(k *kernel.Kernel, path string, output *os.File) error { _, err = io.Copy(output, &fdReader{ctx: ctx, fd: fd}) return err } + +// ReadOpts contains options for the Read RPC call. +type ReadOpts struct { + // ContainerID identifies which container's filesystem to read from. + ContainerID string `json:"container_id"` + + // Path is the filesystem path for the file to read. + Path string `json:"path"` + + // Size is the maximum number of bytes to read (0 means unlimited). + Size int64 `json:"size"` + + // FilePayload contains the destination for output. + urpc.FilePayload +} + +// Read is a RPC stub which prints out and returns the content of the file up to the specified size. +func (f *Fs) Read(o *ReadOpts, _ *struct{}) error { + if len(o.FilePayload.Files) != 1 { + return ErrInvalidFiles + } + + output := o.FilePayload.Files[0] + ctx := f.Kernel.SupervisorContext() + mntns, err := f.mountNamespaceForContainer(o.ContainerID) + if err != nil { + return err + } + defer mntns.DecRef(ctx) + + creds := auth.NewRootCredentials(f.Kernel.RootUserNamespace()) + root := mntns.Root(ctx) + defer root.DecRef(ctx) + + fd, err := f.Kernel.VFS().OpenAt(ctx, creds, &vfs.PathOperation{ + Root: root, + Start: root, + Path: fspath.Parse(o.Path), + }, &vfs.OpenOptions{ + Flags: linux.O_RDONLY, + }) + if err != nil { + return fmt.Errorf("failed to open file %s: %v", o.Path, err) + } + defer fd.DecRef(ctx) + + reader := &fdReader{ctx: ctx, fd: fd} + if o.Size > 0 { + _, err = io.Copy(output, io.LimitReader(reader, o.Size)) + } else { + _, err = io.Copy(output, reader) + } + return err +} diff --git a/runsc/boot/controller.go b/runsc/boot/controller.go index 324b35e1ad..4ff205dfd0 100644 --- a/runsc/boot/controller.go +++ b/runsc/boot/controller.go @@ -193,6 +193,7 @@ const ( // FS-related commands (see fs.go for more details). const ( FsTarRootfsUpperLayer = "Fs.TarRootfsUpperLayer" + FsRead = "Fs.Read" ) // controller holds the control server, and is used for communication into the diff --git a/runsc/cli/maincli/maincli.go b/runsc/cli/maincli/maincli.go index a8d71cfa36..6ca55d3f1c 100644 --- a/runsc/cli/maincli/maincli.go +++ b/runsc/cli/maincli/maincli.go @@ -63,6 +63,7 @@ func commands() (map[util.SubCommand]string, []subcommands.Command) { new(cmd.Do): userGroup, new(cmd.FSCheckpoint): userGroup, new(cmd.PortForward): userGroup, + new(cmd.Read): userGroup, new(cmd.SandboxExec): userGroup, new(cmd.Tar): userGroup, diff --git a/runsc/cmd/BUILD b/runsc/cmd/BUILD index c80cb7c6ad..67c3340d1d 100644 --- a/runsc/cmd/BUILD +++ b/runsc/cmd/BUILD @@ -64,6 +64,7 @@ go_library( "platforms.go", "portforward.go", "ps.go", + "read.go", "read_control.go", "restore.go", "resume.go", @@ -161,6 +162,7 @@ go_test( "list_test.go", "mitigate_test.go", "pidfile_test.go", + "read_test.go", "sandboxexec_test.go", "spec_test.go", ], diff --git a/runsc/cmd/read.go b/runsc/cmd/read.go new file mode 100644 index 0000000000..18b64276dd --- /dev/null +++ b/runsc/cmd/read.go @@ -0,0 +1,94 @@ +// Copyright 2026 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/google/subcommands" + specs "github.com/opencontainers/runtime-spec/specs-go" + "gvisor.dev/gvisor/runsc/cmd/util" + "gvisor.dev/gvisor/runsc/config" + "gvisor.dev/gvisor/runsc/container" + "gvisor.dev/gvisor/runsc/flag" +) + +// Read implements subcommands.Command for the "read" command. +type Read struct { + containerLoader + size int64 +} + +// Name implements subcommands.Command.Name. +func (*Read) Name() string { + return "read" +} + +// Synopsis implements subcommands.Command.Synopsis. +func (*Read) Synopsis() string { + return "read a file of the sandbox given the path" +} + +// Usage implements subcommands.Command.Usage. +func (*Read) Usage() string { + return `read [flags] - read a file of the sandbox given the path + +Where "" is the name for the instance of the container, and +"" is the path to the file in the sandbox to read. Size can be specified via the --size flag. + +EXAMPLE: + # runsc read --size 4096 /etc/passwd + # runsc read /etc/passwd +` +} + +// SetFlags implements subcommands.Command.SetFlags. +func (r *Read) SetFlags(f *flag.FlagSet) { + f.Int64Var(&r.size, "size", 0, "maximum size to read (0 means unlimited)") +} + +// FetchSpec implements util.SubCommand.FetchSpec. +func (r *Read) FetchSpec(conf *config.Config, f *flag.FlagSet) (string, *specs.Spec, error) { + c, err := r.loadContainer(conf, f, container.LoadOpts{SkipCheck: true}) + if err != nil { + return "", nil, fmt.Errorf("loading container: %w", err) + } + return c.ID, c.Spec, nil +} + +// Execute implements subcommands.Command.Execute. +func (r *Read) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcommands.ExitStatus { + if f.NArg() != 2 { + f.Usage() + return subcommands.ExitUsageError + } + + path := f.Arg(1) + size := r.size + + conf := args[0].(*config.Config) + c, err := r.loadContainer(conf, f, container.LoadOpts{SkipCheck: true}) + if err != nil { + util.Fatalf("loading container: %v", err) + } + + if err := c.Sandbox.ReadFile(c.ID, path, size, os.Stdout); err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) + return subcommands.ExitFailure + } + return subcommands.ExitSuccess +} diff --git a/runsc/cmd/read_test.go b/runsc/cmd/read_test.go new file mode 100644 index 0000000000..6e30fb1d70 --- /dev/null +++ b/runsc/cmd/read_test.go @@ -0,0 +1,34 @@ +// Copyright 2026 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" + + "gvisor.dev/gvisor/runsc/flag" +) + +func TestReadFlags(t *testing.T) { + r := Read{} + f := flag.NewFlagSet("read", flag.ContinueOnError) + r.SetFlags(f) + + if err := f.Parse([]string{"--size", "4096"}); err != nil { + t.Fatalf("f.Parse failed: %v", err) + } + if r.size != 4096 { + t.Errorf("expected size 4096, got %d", r.size) + } +} diff --git a/runsc/container/container_test.go b/runsc/container/container_test.go index 4db1cdca72..ef3de7f807 100644 --- a/runsc/container/container_test.go +++ b/runsc/container/container_test.go @@ -4791,3 +4791,65 @@ func TestIPv6DisableAllSysctl(t *testing.T) { } } } + +func TestReadFile(t *testing.T) { + conf := testutil.TestConfig(t) + spec, _ := sleepSpecConf(t) + _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer cleanup() + + args := Args{ + ID: testutil.RandomContainerID(), + Spec: spec, + BundleDir: bundleDir, + } + cont, err := New(conf, args) + if err != nil { + t.Fatalf("error creating container: %v", err) + } + defer cont.Destroy() + if err := cont.Start(conf); err != nil { + t.Fatalf("error starting container: %v", err) + } + + // Test reading /proc/version without size limit. + tmpFile1, err := os.CreateTemp(testutil.TmpDir(), "readfile-1-*.txt") + if err != nil { + t.Fatalf("error creating temp file: %v", err) + } + defer os.Remove(tmpFile1.Name()) + defer tmpFile1.Close() + + if err := cont.Sandbox.ReadFile(cont.ID, "/proc/version", 0, tmpFile1); err != nil { + t.Fatalf("ReadFile failed: %v", err) + } + content1, err := os.ReadFile(tmpFile1.Name()) + if err != nil { + t.Fatalf("error reading temp file: %v", err) + } + if !strings.Contains(string(content1), "Linux") { + t.Errorf("expected /proc/version to contain 'Linux', got %q", string(content1)) + } + + // Test reading /proc/version with a size limit of 5 bytes. + tmpFile2, err := os.CreateTemp(testutil.TmpDir(), "readfile-2-*.txt") + if err != nil { + t.Fatalf("error creating temp file: %v", err) + } + defer os.Remove(tmpFile2.Name()) + defer tmpFile2.Close() + + if err := cont.Sandbox.ReadFile(cont.ID, "/proc/version", 5, tmpFile2); err != nil { + t.Fatalf("ReadFile failed: %v", err) + } + content2, err := os.ReadFile(tmpFile2.Name()) + if err != nil { + t.Fatalf("error reading temp file: %v", err) + } + if string(content2) != "Linux" { + t.Errorf("expected exactly 'Linux' (5 bytes), got %q (%d bytes)", string(content2), len(content2)) + } +} diff --git a/runsc/sandbox/sandbox.go b/runsc/sandbox/sandbox.go index 275d538bb2..b80f8037b3 100644 --- a/runsc/sandbox/sandbox.go +++ b/runsc/sandbox/sandbox.go @@ -2466,6 +2466,21 @@ func (s *Sandbox) TarRootfsUpperLayer(containerID string, outFD *os.File) error return nil } +// ReadFile reads a file of the sandbox from the given container (or root container if containerID is empty) up to the specified size. +func (s *Sandbox) ReadFile(containerID, path string, size int64, outFD *os.File) error { + log.Debugf("ReadFile, sandbox: %q, container: %q, path: %q, size: %d", s.ID, containerID, path, size) + opts := control.ReadOpts{ + ContainerID: containerID, + Path: path, + Size: size, + FilePayload: urpc.FilePayload{Files: []*os.File{outFD}}, + } + if err := s.call(boot.FsRead, &opts, nil); err != nil { + return fmt.Errorf("reading file %q: %w", path, err) + } + return nil +} + func setCloExeOnAllFDs() error { f, err := os.Open("/proc/self/fd") if err != nil {