Skip to content

Commit 1ea84d6

Browse files
EtiennePerotgvisor-bot
authored andcommitted
Add test that runs runsc do inside a non-gVisor container.
This is used in contexts such as Dangerzone: https://gvisor.dev/blog/2024/09/23/safe-ride-into-the-dangerzone/ Updates issue #10944. PiperOrigin-RevId: 682454284
1 parent b89f53b commit 1ea84d6

File tree

6 files changed

+273
-4
lines changed

6 files changed

+273
-4
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ docker-tests: load-basic $(RUNTIME_BIN)
346346
@$(call install_runtime,$(RUNTIME)-dcache,--fdlimit=2000 --dcache=100) # Used by TestDentryCacheLimit.
347347
@$(call install_runtime,$(RUNTIME)-host-uds,--host-uds=all) # Used by TestHostSocketConnect.
348348
@$(call install_runtime,$(RUNTIME)-overlay,--overlay2=all:self) # Used by TestOverlay*.
349-
@$(call test_runtime,$(RUNTIME),$(INTEGRATION_TARGETS) //test/e2e:integration_runtime_test)
349+
@$(call test_runtime,$(RUNTIME),$(INTEGRATION_TARGETS) //test/e2e:integration_runtime_test //test/e2e:runtime_in_docker_test)
350350
.PHONY: docker-tests
351351

352352
plugin-network-tests: load-basic $(RUNTIME_BIN)

images/basic/integrationtest/Dockerfile

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,21 @@ WORKDIR /root
44
COPY . .
55
RUN chmod +x *.sh
66

7-
RUN apt-get update && apt-get install -y gcc iputils-ping iproute2
7+
RUN apt-get update && apt-get install -y \
8+
build-essential iputils-ping iproute2 iptables
89

910
# Compilation Steps.
1011
RUN gcc -O2 -o test_copy_up test_copy_up.c
1112
RUN gcc -O2 -o test_rewinddir test_rewinddir.c
1213
RUN gcc -O2 -o link_test link_test.c
1314
RUN gcc -O2 -o test_sticky test_sticky.c
1415
RUN gcc -O2 -o host_fd host_fd.c
15-
RUN gcc -O2 -o host_connect host_connect.c
16+
RUN gcc -O2 -o host_connect host_connect.c
17+
18+
# Add nonprivileged regular user named "nonroot".
19+
RUN groupadd --gid 1337 nonroot && \
20+
useradd --uid 1337 --gid 1337 \
21+
--create-home \
22+
--shell $(which bash) \
23+
--password '' \
24+
nonroot

pkg/test/dockerutil/container.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ type RunOpts struct {
116116

117117
Devices []container.DeviceMapping
118118

119+
// SecurityOpts are security options to set on the container.
120+
SecurityOpts []string
121+
119122
// sniffGPUOpts, if set, sets the rules for GPU sniffing during this test.
120123
// Must be set via `RunOpts.SniffGPU`.
121124
sniffGPUOpts *SniffGPUOpts
@@ -347,6 +350,7 @@ func (c *Container) hostConfig(r RunOpts) *container.HostConfig {
347350
CapAdd: r.CapAdd,
348351
CapDrop: r.CapDrop,
349352
Privileged: r.Privileged,
353+
SecurityOpt: r.SecurityOpts,
350354
ReadonlyRootfs: r.ReadOnly,
351355
NetworkMode: container.NetworkMode(r.NetworkMode),
352356
Resources: container.Resources{

test/e2e/BUILD

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,26 @@ go_test(
5252
],
5353
)
5454

55+
go_test(
56+
name = "runtime_in_docker_test",
57+
size = "large",
58+
srcs = [
59+
"runtime_in_docker_test.go",
60+
],
61+
library = ":integration",
62+
tags = [
63+
# Requires docker and runsc to be configured before the test runs.
64+
"local",
65+
"manual",
66+
],
67+
visibility = ["//:sandbox"],
68+
deps = [
69+
"//pkg/test/dockerutil",
70+
"//pkg/test/testutil",
71+
"@com_github_docker_docker//api/types/mount:go_default_library",
72+
],
73+
)
74+
5575
go_library(
5676
name = "integration",
5777
srcs = ["integration.go"],

test/e2e/integration_runtime_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func TestDentryCacheLimit(t *testing.T) {
104104
}
105105
}
106106

107-
// Run the container. Open a bunch of files simutaneously and sleep a bit
107+
// Run the container. Open a bunch of files simultaneously and sleep a bit
108108
// to give time for everything to start. We shouldn't hit the FD limit
109109
// because the dentry cache is small.
110110
cmd := `for file in /tmp/foo/*; do (cat > "${file}") & done && sleep 10`

test/e2e/runtime_in_docker_test.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// Copyright 2024 The gVisor Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package integration provides end-to-end integration tests for runsc.
16+
package integration
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"slices"
22+
"strings"
23+
"testing"
24+
25+
"github.com/docker/docker/api/types/mount"
26+
"gvisor.dev/gvisor/pkg/test/dockerutil"
27+
"gvisor.dev/gvisor/pkg/test/testutil"
28+
)
29+
30+
// testVariant is a variant of the gVisor in Docker test.
31+
type testVariant struct {
32+
Name string
33+
User string
34+
WorkDir string
35+
CapAdd []string
36+
Args []string
37+
MountCgroupfs bool
38+
}
39+
40+
// run runs the test variant.
41+
func (test testVariant) run(ctx context.Context, logger testutil.Logger, runscPath string) (string, error) {
42+
d := dockerutil.MakeNativeContainer(ctx, logger)
43+
defer d.CleanUp(ctx)
44+
opts := dockerutil.RunOpts{
45+
Image: "basic/integrationtest",
46+
User: test.User,
47+
WorkDir: test.WorkDir,
48+
SecurityOpts: []string{
49+
// Disable default seccomp filter which blocks `mount(2)` and others.
50+
"seccomp=unconfined",
51+
52+
// Disable AppArmor which also blocks mounts.
53+
"apparmor=unconfined",
54+
55+
// Set correct SELinux label; this allows ptrace.
56+
"label=type:container_engine_t",
57+
},
58+
CapAdd: test.CapAdd,
59+
Mounts: []mount.Mount{
60+
// Mount the runtime binary.
61+
{
62+
Type: mount.TypeBind,
63+
Source: runscPath,
64+
Target: "/runtime",
65+
ReadOnly: true,
66+
},
67+
},
68+
}
69+
if test.MountCgroupfs {
70+
opts.Mounts = append(opts.Mounts, mount.Mount{
71+
Type: mount.TypeBind,
72+
Source: "/sys/fs/cgroup",
73+
Target: "/sys/fs/cgroup",
74+
ReadOnly: false,
75+
})
76+
}
77+
// Mount an unobstructed view of procfs at /proc2 so that the runtime
78+
// can mount a fresh procfs.
79+
// TODO(gvisor.dev/issue/10944): Remove this once issue is fixed.
80+
opts.Mounts = append(opts.Mounts, mount.Mount{
81+
Type: mount.TypeBind,
82+
Source: "/proc",
83+
Target: "/proc2",
84+
ReadOnly: false,
85+
BindOptions: &mount.BindOptions{
86+
NonRecursive: true,
87+
},
88+
})
89+
const wantMessage = "It became a jumble of words, a litany, almost a kind of glossolalia."
90+
args := []string{
91+
"/runtime",
92+
"--debug=true",
93+
"--debug-log=/dev/stderr",
94+
}
95+
args = append(args, test.Args...)
96+
args = append(args, "do", "/bin/echo", wantMessage)
97+
logger.Logf("Running: %v", args)
98+
got, err := d.Run(ctx, opts, args...)
99+
got = strings.TrimSpace(got)
100+
if err != nil {
101+
return got, err
102+
}
103+
if !strings.Contains(got, wantMessage) {
104+
return got, fmt.Errorf("did not observe substring %q in logs", wantMessage)
105+
}
106+
return got, nil
107+
}
108+
109+
// failureCases returns modified versions of this same test that are expected
110+
// to fail. Verifying that these variants fail ensures that each test variant
111+
// runs with the minimal amount of deviations from the default configuration.
112+
func (test testVariant) failureCases() []testVariant {
113+
failureCase := func(name string) testVariant {
114+
copy := test
115+
copy.Name = name
116+
return copy
117+
}
118+
var failureCases []testVariant
119+
if test.MountCgroupfs {
120+
copy := failureCase("without cgroupfs mounted")
121+
copy.MountCgroupfs = false
122+
failureCases = append(failureCases, copy)
123+
}
124+
for i, capAdd := range test.CapAdd {
125+
copy := failureCase(fmt.Sprintf("without capability %s", capAdd))
126+
copy.CapAdd = append(append([]string(nil), test.CapAdd[:i]...), test.CapAdd[i+1:]...)
127+
failureCases = append(failureCases, copy)
128+
}
129+
for _, tryRemoveArg := range []string{
130+
"--rootless=true",
131+
"--ignore-cgroups=true",
132+
} {
133+
if index := slices.Index(test.Args, tryRemoveArg); index != -1 {
134+
copy := failureCase(fmt.Sprintf("without argument %s", tryRemoveArg))
135+
copy.Args = append(append([]string(nil), test.Args[:index]...), test.Args[index+1:]...)
136+
failureCases = append(failureCases, copy)
137+
}
138+
}
139+
return failureCases
140+
}
141+
142+
// TestGVisorInDocker runs `runsc` inside a non-gVisor container.
143+
// This is used in contexts such as Dangerzone:
144+
// https://gvisor.dev/blog/2024/09/23/safe-ride-into-the-dangerzone/
145+
func TestGVisorInDocker(t *testing.T) {
146+
ctx := context.Background()
147+
runscPath, err := dockerutil.RuntimePath()
148+
if err != nil {
149+
t.Fatalf("Cannot locate runtime path: %v", err)
150+
}
151+
for _, test := range []testVariant{
152+
{
153+
Name: "Rootful",
154+
User: "root",
155+
CapAdd: []string{
156+
// Necessary to set up networking (creating veth devices).
157+
"NET_ADMIN",
158+
// Necessary to set up networking, which calls `ip netns add` which
159+
// calls `mount(2)`.
160+
"SYS_ADMIN",
161+
},
162+
// Mount cgroupfs as writable, otherwise the runtime won't be able to
163+
// set up cgroups.
164+
MountCgroupfs: true,
165+
},
166+
{
167+
Name: "Rootful without networking",
168+
User: "root",
169+
CapAdd: []string{
170+
// "Can't run sandbox process in minimal chroot since we don't have CAP_SYS_ADMIN"
171+
"SYS_ADMIN",
172+
},
173+
Args: []string{
174+
"--network=none",
175+
},
176+
MountCgroupfs: true,
177+
},
178+
{
179+
Name: "Rootful with host networking",
180+
User: "root",
181+
CapAdd: []string{
182+
// Necessary to set up networking (creating veth devices).
183+
"NET_ADMIN",
184+
// Necessary to set up networking, which calls `ip netns add` which
185+
// calls `mount(2)`.
186+
"SYS_ADMIN",
187+
},
188+
Args: []string{
189+
"--network=host",
190+
},
191+
MountCgroupfs: true,
192+
},
193+
{
194+
Name: "Rootful without networking and cgroupfs",
195+
User: "root",
196+
CapAdd: []string{
197+
// "Can't run sandbox process in minimal chroot since we don't have CAP_SYS_ADMIN"
198+
"SYS_ADMIN",
199+
},
200+
Args: []string{
201+
"--network=none",
202+
"--ignore-cgroups=true",
203+
},
204+
},
205+
{
206+
Name: "Rootless",
207+
User: "nonroot",
208+
WorkDir: "/home/nonroot",
209+
Args: []string{
210+
"--rootless=true",
211+
},
212+
},
213+
{
214+
Name: "Rootless without networking",
215+
User: "nonroot",
216+
WorkDir: "/home/nonroot",
217+
Args: []string{
218+
"--rootless=true",
219+
"--network=none",
220+
},
221+
},
222+
} {
223+
t.Run(test.Name, func(t *testing.T) {
224+
if logs, err := test.run(ctx, t, runscPath); err != nil {
225+
t.Fatalf("Error: %v; logs:\n%s", err, logs)
226+
}
227+
for _, failureCase := range test.failureCases() {
228+
t.Run(failureCase.Name, func(t *testing.T) {
229+
if logs, err := failureCase.run(ctx, t, runscPath); err == nil {
230+
t.Fatalf("Failure case unexpectedly succeeded; logs:\n%s", logs)
231+
}
232+
})
233+
}
234+
})
235+
}
236+
}

0 commit comments

Comments
 (0)