Add a "runsc read" command to read files from a sandbox.

This change introduces a new "runsc read" command that allows users to read the
contents of a file within a running sandbox. The command takes a container ID,
a file path, and an optional size limit. The file content is streamed from the
sentry to runsc and printed to standard output. A new Fs.Read RPC is added to
the sentry control server to handle the file reading.

PiperOrigin-RevId: 944745364
This commit is contained in:
Xin Zhong
2026-07-08 15:53:06 -07:00
committed by gVisor bot
parent a1687571bc
commit a379968976
8 changed files with 263 additions and 0 deletions
+54
View File
@@ -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
}
+1
View File
@@ -192,6 +192,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
+1
View File
@@ -65,6 +65,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,
+2
View File
@@ -62,6 +62,7 @@ go_library(
"platforms.go",
"portforward.go",
"ps.go",
"read.go",
"read_control.go",
"restore.go",
"resume.go",
@@ -159,6 +160,7 @@ go_test(
"list_test.go",
"mitigate_test.go",
"pidfile_test.go",
"read_test.go",
"sandboxexec_test.go",
"spec_test.go",
],
+94
View File
@@ -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] <container-id> <path> - read a file of the sandbox given the path
Where "<container-id>" is the name for the instance of the container, and
"<path>" is the path to the file in the sandbox to read. Size can be specified via the --size flag.
EXAMPLE:
# runsc read --size 4096 <container-id> /etc/passwd
# runsc read <container-id> /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("Failed to load 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
}
+34
View File
@@ -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("Failed to parse flags: %v", err)
}
if r.size != 4096 {
t.Errorf("Got size %d, want 4096", r.size)
}
}
+62
View File
@@ -5531,3 +5531,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("Failed to setup container: %v", err)
}
defer cleanup()
args := Args{
ID: testutil.RandomContainerID(),
Spec: spec,
BundleDir: bundleDir,
}
cont, err := New(conf, args)
if err != nil {
t.Fatalf("Failed to create container: %v", err)
}
defer cont.Destroy()
if err := cont.Start(conf); err != nil {
t.Fatalf("Failed to start container: %v", err)
}
// Test reading /proc/version without size limit.
tmpFile1, err := os.CreateTemp(testutil.TmpDir(), "readfile-1-*.txt")
if err != nil {
t.Fatalf("Failed to create 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("Failed to read /proc/version: %v", err)
}
content1, err := os.ReadFile(tmpFile1.Name())
if err != nil {
t.Fatalf("Failed to read temp file: %v", err)
}
if !strings.Contains(string(content1), "Linux") {
t.Errorf("Got %q, want string containing 'Linux'", 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("Failed to create 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("Failed to read temp file: %v", err)
}
if string(content2) != "Linux" {
t.Errorf("Got %q (%d bytes), want 'Linux' (5 bytes)", string(content2), len(content2))
}
}
+15
View File
@@ -2534,6 +2534,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 {