mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user