Skip to content

Commit 356d1be

Browse files
fvoznikashentubot
authored andcommitted
Allow 'runsc do' to run without root
'--rootless' flag lets a non-root user execute 'runsc do'. The drawback is that the sandbox and gofer processes will run as root inside a user namespace that is mapped to the caller's user, intead of nobody. And network is defaulted to '--network=host' inside the root network namespace. On the bright side, it's very convenient for testing: runsc --rootless do ls runsc --rootless do curl www.google.com PiperOrigin-RevId: 252840970
1 parent df110ad commit 356d1be

File tree

15 files changed

+212
-154
lines changed

15 files changed

+212
-154
lines changed

runsc/boot/config.go

+7
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,12 @@ type Config struct {
226226
// to the same underlying network device. This allows netstack to better
227227
// scale for high throughput use cases.
228228
NumNetworkChannels int
229+
230+
// Rootless allows the sandbox to be started with a user that is not root.
231+
// Defense is depth measures are weaker with rootless. Specifically, the
232+
// sandbox and Gofer process run as root inside a user namespace with root
233+
// mapped to the caller's user.
234+
Rootless bool
229235
}
230236

231237
// ToFlags returns a slice of flags that correspond to the given Config.
@@ -250,6 +256,7 @@ func (c *Config) ToFlags() []string {
250256
"--profile=" + strconv.FormatBool(c.ProfileEnable),
251257
"--net-raw=" + strconv.FormatBool(c.EnableRaw),
252258
"--num-network-channels=" + strconv.Itoa(c.NumNetworkChannels),
259+
"--rootless=" + strconv.FormatBool(c.Rootless),
253260
}
254261
if c.TestOnlyAllowRunAsCurrentUserWithoutChroot {
255262
// Only include if set since it is never to be used by users.

runsc/cmd/boot.go

+12-10
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})
130130
// Ensure that if there is a panic, all goroutine stacks are printed.
131131
debug.SetTraceback("all")
132132

133+
conf := args[0].(*boot.Config)
134+
133135
if b.setUpRoot {
134136
if err := setUpChroot(b.pidns); err != nil {
135137
Fatalf("error setting up chroot: %v", err)
@@ -143,14 +145,16 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})
143145
args = append(args, arg)
144146
}
145147
}
146-
// Note that we've already read the spec from the spec FD, and
147-
// we will read it again after the exec call. This works
148-
// because the ReadSpecFromFile function seeks to the beginning
149-
// of the file before reading.
150-
if err := callSelfAsNobody(args); err != nil {
151-
Fatalf("%v", err)
148+
if !conf.Rootless {
149+
// Note that we've already read the spec from the spec FD, and
150+
// we will read it again after the exec call. This works
151+
// because the ReadSpecFromFile function seeks to the beginning
152+
// of the file before reading.
153+
if err := callSelfAsNobody(args); err != nil {
154+
Fatalf("%v", err)
155+
}
156+
panic("callSelfAsNobody must never return success")
152157
}
153-
panic("callSelfAsNobody must never return success")
154158
}
155159
}
156160

@@ -163,9 +167,6 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})
163167
}
164168
specutils.LogSpec(spec)
165169

166-
conf := args[0].(*boot.Config)
167-
waitStatus := args[1].(*syscall.WaitStatus)
168-
169170
if b.applyCaps {
170171
caps := spec.Process.Capabilities
171172
if caps == nil {
@@ -251,6 +252,7 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})
251252

252253
ws := l.WaitExit()
253254
log.Infof("application exiting with %+v", ws)
255+
waitStatus := args[1].(*syscall.WaitStatus)
254256
*waitStatus = syscall.WaitStatus(ws.Status())
255257
l.Destroy()
256258
return subcommands.ExitSuccess

runsc/cmd/capability_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,6 @@ func TestCapabilities(t *testing.T) {
116116
}
117117

118118
func TestMain(m *testing.M) {
119-
testutil.RunAsRoot()
119+
specutils.MaybeRunAsRoot()
120120
os.Exit(m.Run())
121121
}

runsc/cmd/create.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -82,21 +82,25 @@ func (c *Create) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}
8282
id := f.Arg(0)
8383
conf := args[0].(*boot.Config)
8484

85+
if conf.Rootless {
86+
return Errorf("Rootless mode not supported with %q", c.Name())
87+
}
88+
8589
bundleDir := c.bundleDir
8690
if bundleDir == "" {
8791
bundleDir = getwdOrDie()
8892
}
8993
spec, err := specutils.ReadSpec(bundleDir)
9094
if err != nil {
91-
Fatalf("reading spec: %v", err)
95+
return Errorf("reading spec: %v", err)
9296
}
9397
specutils.LogSpec(spec)
9498

9599
// Create the container. A new sandbox will be created for the
96100
// container unless the metadata specifies that it should be run in an
97101
// existing container.
98102
if _, err := container.Create(id, spec, conf, bundleDir, c.consoleSocket, c.pidFile, c.userLog); err != nil {
99-
Fatalf("creating container: %v", err)
103+
return Errorf("creating container: %v", err)
100104
}
101105
return subcommands.ExitSuccess
102106
}

runsc/cmd/do.go

+27-12
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,9 @@ import (
3939
// Do implements subcommands.Command for the "do" command. It sets up a simple
4040
// sandbox and executes the command inside it. See Usage() for more details.
4141
type Do struct {
42-
root string
43-
cwd string
44-
ip string
45-
networkNamespace bool
42+
root string
43+
cwd string
44+
ip string
4645
}
4746

4847
// Name implements subcommands.Command.Name.
@@ -72,7 +71,6 @@ func (c *Do) SetFlags(f *flag.FlagSet) {
7271
f.StringVar(&c.root, "root", "/", `path to the root directory, defaults to "/"`)
7372
f.StringVar(&c.cwd, "cwd", ".", "path to the current directory, defaults to the current directory")
7473
f.StringVar(&c.ip, "ip", "192.168.10.2", "IPv4 address for the sandbox")
75-
f.BoolVar(&c.networkNamespace, "netns", true, "run in a new network namespace")
7674
}
7775

7876
// Execute implements subcommands.Command.Execute.
@@ -85,15 +83,21 @@ func (c *Do) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) su
8583
conf := args[0].(*boot.Config)
8684
waitStatus := args[1].(*syscall.WaitStatus)
8785

88-
// Map the entire host file system, but make it readonly with a writable
89-
// overlay on top (ignore --overlay option).
90-
conf.Overlay = true
86+
if conf.Rootless {
87+
if err := specutils.MaybeRunAsRoot(); err != nil {
88+
return Errorf("Error executing inside namespace: %v", err)
89+
}
90+
// Execution will continue here if no more capabilities are needed...
91+
}
9192

9293
hostname, err := os.Hostname()
9394
if err != nil {
9495
return Errorf("Error to retrieve hostname: %v", err)
9596
}
9697

98+
// Map the entire host file system, but make it readonly with a writable
99+
// overlay on top (ignore --overlay option).
100+
conf.Overlay = true
97101
absRoot, err := resolvePath(c.root)
98102
if err != nil {
99103
return Errorf("Error resolving root: %v", err)
@@ -119,11 +123,22 @@ func (c *Do) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) su
119123
specutils.LogSpec(spec)
120124

121125
cid := fmt.Sprintf("runsc-%06d", rand.Int31n(1000000))
122-
if !c.networkNamespace {
123-
if conf.Network != boot.NetworkHost {
124-
Fatalf("The current network namespace can be used only if --network=host is set", nil)
126+
if conf.Network == boot.NetworkNone {
127+
netns := specs.LinuxNamespace{
128+
Type: specs.NetworkNamespace,
129+
}
130+
if spec.Linux != nil {
131+
panic("spec.Linux is not nil")
125132
}
126-
} else if conf.Network != boot.NetworkNone {
133+
spec.Linux = &specs.Linux{Namespaces: []specs.LinuxNamespace{netns}}
134+
135+
} else if conf.Rootless {
136+
if conf.Network == boot.NetworkSandbox {
137+
fmt.Println("*** Rootless requires changing network type to host ***")
138+
conf.Network = boot.NetworkHost
139+
}
140+
141+
} else {
127142
clean, err := c.setupNet(cid, spec)
128143
if err != nil {
129144
return Errorf("Error setting up network: %v", err)

runsc/cmd/restore.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -80,25 +80,29 @@ func (r *Restore) Execute(_ context.Context, f *flag.FlagSet, args ...interface{
8080
conf := args[0].(*boot.Config)
8181
waitStatus := args[1].(*syscall.WaitStatus)
8282

83+
if conf.Rootless {
84+
return Errorf("Rootless mode not supported with %q", r.Name())
85+
}
86+
8387
bundleDir := r.bundleDir
8488
if bundleDir == "" {
8589
bundleDir = getwdOrDie()
8690
}
8791
spec, err := specutils.ReadSpec(bundleDir)
8892
if err != nil {
89-
Fatalf("reading spec: %v", err)
93+
return Errorf("reading spec: %v", err)
9094
}
9195
specutils.LogSpec(spec)
9296

9397
if r.imagePath == "" {
94-
Fatalf("image-path flag must be provided")
98+
return Errorf("image-path flag must be provided")
9599
}
96100

97101
conf.RestoreFile = filepath.Join(r.imagePath, checkpointFileName)
98102

99103
ws, err := container.Run(id, spec, conf, bundleDir, r.consoleSocket, r.pidFile, r.userLog, r.detach)
100104
if err != nil {
101-
Fatalf("running container: %v", err)
105+
return Errorf("running container: %v", err)
102106
}
103107
*waitStatus = ws
104108

runsc/cmd/run.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,23 @@ func (r *Run) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) s
6767
conf := args[0].(*boot.Config)
6868
waitStatus := args[1].(*syscall.WaitStatus)
6969

70+
if conf.Rootless {
71+
return Errorf("Rootless mode not supported with %q", r.Name())
72+
}
73+
7074
bundleDir := r.bundleDir
7175
if bundleDir == "" {
7276
bundleDir = getwdOrDie()
7377
}
7478
spec, err := specutils.ReadSpec(bundleDir)
7579
if err != nil {
76-
Fatalf("reading spec: %v", err)
80+
return Errorf("reading spec: %v", err)
7781
}
7882
specutils.LogSpec(spec)
7983

8084
ws, err := container.Run(id, spec, conf, bundleDir, r.consoleSocket, r.pidFile, r.userLog, r.detach)
8185
if err != nil {
82-
Fatalf("running container: %v", err)
86+
return Errorf("running container: %v", err)
8387
}
8488

8589
*waitStatus = ws

runsc/container/container_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"gvisor.googlesource.com/gvisor/pkg/sentry/control"
3737
"gvisor.googlesource.com/gvisor/pkg/sentry/kernel/auth"
3838
"gvisor.googlesource.com/gvisor/runsc/boot"
39+
"gvisor.googlesource.com/gvisor/runsc/specutils"
3940
"gvisor.googlesource.com/gvisor/runsc/test/testutil"
4041
)
4142

@@ -1853,7 +1854,7 @@ func TestMain(m *testing.M) {
18531854
if err := testutil.ConfigureExePath(); err != nil {
18541855
panic(err.Error())
18551856
}
1856-
testutil.RunAsRoot()
1857+
specutils.MaybeRunAsRoot()
18571858

18581859
os.Exit(m.Run())
18591860
}

runsc/main.go

+34-29
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,19 @@ var (
6161
straceLogSize = flag.Uint("strace-log-size", 1024, "default size (in bytes) to log data argument blobs")
6262

6363
// Flags that control sandbox runtime behavior.
64-
platform = flag.String("platform", "ptrace", "specifies which platform to use: ptrace (default), kvm")
65-
network = flag.String("network", "sandbox", "specifies which network to use: sandbox (default), host, none. Using network inside the sandbox is more secure because it's isolated from the host network.")
66-
gso = flag.Bool("gso", true, "enable generic segmenation offload")
67-
fileAccess = flag.String("file-access", "exclusive", "specifies which filesystem to use for the root mount: exclusive (default), shared. Volume mounts are always shared.")
68-
overlay = flag.Bool("overlay", false, "wrap filesystem mounts with writable overlay. All modifications are stored in memory inside the sandbox.")
69-
watchdogAction = flag.String("watchdog-action", "log", "sets what action the watchdog takes when triggered: log (default), panic.")
70-
panicSignal = flag.Int("panic-signal", -1, "register signal handling that panics. Usually set to SIGUSR2(12) to troubleshoot hangs. -1 disables it.")
71-
profile = flag.Bool("profile", false, "prepares the sandbox to use Golang profiler. Note that enabling profiler loosens the seccomp protection added to the sandbox (DO NOT USE IN PRODUCTION).")
72-
netRaw = flag.Bool("net-raw", false, "enable raw sockets. When false, raw sockets are disabled by removing CAP_NET_RAW from containers (`runsc exec` will still be able to utilize raw sockets). Raw sockets allow malicious containers to craft packets and potentially attack the network.")
73-
numNetworkChannels = flag.Int("num-network-channels", 1, "number of underlying channels(FDs) to use for network link endpoints.")
64+
platform = flag.String("platform", "ptrace", "specifies which platform to use: ptrace (default), kvm")
65+
network = flag.String("network", "sandbox", "specifies which network to use: sandbox (default), host, none. Using network inside the sandbox is more secure because it's isolated from the host network.")
66+
gso = flag.Bool("gso", true, "enable generic segmenation offload")
67+
fileAccess = flag.String("file-access", "exclusive", "specifies which filesystem to use for the root mount: exclusive (default), shared. Volume mounts are always shared.")
68+
overlay = flag.Bool("overlay", false, "wrap filesystem mounts with writable overlay. All modifications are stored in memory inside the sandbox.")
69+
watchdogAction = flag.String("watchdog-action", "log", "sets what action the watchdog takes when triggered: log (default), panic.")
70+
panicSignal = flag.Int("panic-signal", -1, "register signal handling that panics. Usually set to SIGUSR2(12) to troubleshoot hangs. -1 disables it.")
71+
profile = flag.Bool("profile", false, "prepares the sandbox to use Golang profiler. Note that enabling profiler loosens the seccomp protection added to the sandbox (DO NOT USE IN PRODUCTION).")
72+
netRaw = flag.Bool("net-raw", false, "enable raw sockets. When false, raw sockets are disabled by removing CAP_NET_RAW from containers (`runsc exec` will still be able to utilize raw sockets). Raw sockets allow malicious containers to craft packets and potentially attack the network.")
73+
numNetworkChannels = flag.Int("num-network-channels", 1, "number of underlying channels(FDs) to use for network link endpoints.")
74+
rootless = flag.Bool("rootless", false, "it allows the sandbox to be started with a user that is not root. Sandbox and Gofer processes may run with same privileges as current user.")
75+
76+
// Test flags, not to be used outside tests, ever.
7477
testOnlyAllowRunAsCurrentUserWithoutChroot = flag.Bool("TESTONLY-unsafe-nonroot", false, "TEST ONLY; do not ever use! This skips many security measures that isolate the host from the sandbox.")
7578
)
7679

@@ -166,26 +169,28 @@ func main() {
166169

167170
// Create a new Config from the flags.
168171
conf := &boot.Config{
169-
RootDir: *rootDir,
170-
Debug: *debug,
171-
LogFilename: *logFilename,
172-
LogFormat: *logFormat,
173-
DebugLog: *debugLog,
174-
DebugLogFormat: *debugLogFormat,
175-
FileAccess: fsAccess,
176-
Overlay: *overlay,
177-
Network: netType,
178-
GSO: *gso,
179-
LogPackets: *logPackets,
180-
Platform: platformType,
181-
Strace: *strace,
182-
StraceLogSize: *straceLogSize,
183-
WatchdogAction: wa,
184-
PanicSignal: *panicSignal,
185-
ProfileEnable: *profile,
186-
EnableRaw: *netRaw,
172+
RootDir: *rootDir,
173+
Debug: *debug,
174+
LogFilename: *logFilename,
175+
LogFormat: *logFormat,
176+
DebugLog: *debugLog,
177+
DebugLogFormat: *debugLogFormat,
178+
FileAccess: fsAccess,
179+
Overlay: *overlay,
180+
Network: netType,
181+
GSO: *gso,
182+
LogPackets: *logPackets,
183+
Platform: platformType,
184+
Strace: *strace,
185+
StraceLogSize: *straceLogSize,
186+
WatchdogAction: wa,
187+
PanicSignal: *panicSignal,
188+
ProfileEnable: *profile,
189+
EnableRaw: *netRaw,
190+
NumNetworkChannels: *numNetworkChannels,
191+
Rootless: *rootless,
192+
187193
TestOnlyAllowRunAsCurrentUserWithoutChroot: *testOnlyAllowRunAsCurrentUserWithoutChroot,
188-
NumNetworkChannels: *numNetworkChannels,
189194
}
190195
if len(*straceSyscalls) != 0 {
191196
conf.StraceSyscalls = strings.Split(*straceSyscalls, ",")

0 commit comments

Comments
 (0)