Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions pkg/sentry/control/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions runsc/boot/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions runsc/cli/maincli/maincli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
2 changes: 2 additions & 0 deletions runsc/cmd/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ go_library(
"platforms.go",
"portforward.go",
"ps.go",
"read.go",
"read_control.go",
"restore.go",
"resume.go",
Expand Down Expand Up @@ -161,6 +162,7 @@ go_test(
"list_test.go",
"mitigate_test.go",
"pidfile_test.go",
"read_test.go",
"sandboxexec_test.go",
"spec_test.go",
],
Expand Down
94 changes: 94 additions & 0 deletions runsc/cmd/read.go
Original file line number Diff line number Diff line change
@@ -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("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
}
34 changes: 34 additions & 0 deletions runsc/cmd/read_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
62 changes: 62 additions & 0 deletions runsc/container/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
15 changes: 15 additions & 0 deletions runsc/sandbox/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading