Skip to content

Commit 9b65419

Browse files
author
Zhou Hao
authored
Merge pull request #628 from kinvolk/dongsu/NSProcInPath
validation: add test for NSProcInPath
2 parents f0f4727 + ad0e97e commit 9b65419

File tree

1 file changed

+189
-0
lines changed

1 file changed

+189
-0
lines changed

validation/linux_ns_path.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"runtime"
8+
"syscall"
9+
"time"
10+
11+
"github.com/mndrix/tap-go"
12+
rspec "github.com/opencontainers/runtime-spec/specs-go"
13+
"github.com/opencontainers/runtime-tools/specerror"
14+
"github.com/opencontainers/runtime-tools/validation/util"
15+
)
16+
17+
func getRuntimeToolsNamespace(ns string) string {
18+
// Deal with exceptional cases of "net" and "mnt", because those strings
19+
// cannot be recognized by mapStrToNamespace(), which actually expects
20+
// "network" and "mount" respectively.
21+
switch ns {
22+
case "net":
23+
return "network"
24+
case "mnt":
25+
return "mount"
26+
}
27+
28+
// In other cases, return just the original string
29+
return ns
30+
}
31+
32+
func waitForState(stateCheckFunc func() error) error {
33+
timeout := 3 * time.Second
34+
alarm := time.After(timeout)
35+
ticker := time.Tick(200 * time.Millisecond)
36+
for {
37+
select {
38+
case <-alarm:
39+
return fmt.Errorf("failed to reach expected state within %v", timeout)
40+
case <-ticker:
41+
if err := stateCheckFunc(); err == nil {
42+
return nil
43+
}
44+
}
45+
}
46+
}
47+
48+
func checkNamespacePath(unsharePid int, ns string) error {
49+
testNsPath := fmt.Sprintf("/proc/%d/ns/%s", os.Getpid(), ns)
50+
testNsInode, err := os.Readlink(testNsPath)
51+
if err != nil {
52+
out, err2 := exec.Command("sh", "-c", fmt.Sprintf("ls -la /proc/%d/ns/", os.Getpid())).CombinedOutput()
53+
return fmt.Errorf("cannot read namespace link for the test process: %s\n%v\n%v", err, err2, string(out))
54+
}
55+
56+
var errNsPath error
57+
58+
unshareNsPath := ""
59+
unshareNsInode := ""
60+
61+
doCheckNamespacePath := func() error {
62+
specialChildren := ""
63+
if ns == "pid" {
64+
// Unsharing pidns does not move the process into the new
65+
// pidns but the next forked process. 'unshare' is called with
66+
// '--fork' so the pidns will be fully created and populated
67+
// with a pid 1.
68+
//
69+
// However, finding out the pid of the child process is not
70+
// trivial: it would require to parse
71+
// /proc/$pid/task/$tid/children but that only works on kernels
72+
// with CONFIG_PROC_CHILDREN (not all distros have that).
73+
//
74+
// It is easier to look at /proc/$pid/ns/pid_for_children on
75+
// the parent process. Available since Linux 4.12.
76+
specialChildren = "_for_children"
77+
}
78+
unshareNsPath = fmt.Sprintf("/proc/%d/ns/%s", unsharePid, ns+specialChildren)
79+
unshareNsInode, err = os.Readlink(unshareNsPath)
80+
if err != nil {
81+
errNsPath = fmt.Errorf("cannot read namespace link for the unshare process: %s", err)
82+
return errNsPath
83+
}
84+
85+
if testNsInode == unshareNsInode {
86+
errNsPath = fmt.Errorf("expected: %q, found: %q", testNsInode, unshareNsInode)
87+
return errNsPath
88+
}
89+
90+
return nil
91+
}
92+
93+
// Since it takes some time until unshare switched to the new namespace,
94+
// we should make a loop to check for the result up to 3 seconds.
95+
if err := waitForState(doCheckNamespacePath); err != nil {
96+
// we should return errNsPath instead of err, because errNsPath is what
97+
// returned from the actual test function doCheckNamespacePath(), not
98+
// waitForState().
99+
return errNsPath
100+
}
101+
102+
g, err := util.GetDefaultGenerator()
103+
if err != nil {
104+
return fmt.Errorf("cannot get the default generator: %v", err)
105+
}
106+
107+
rtns := getRuntimeToolsNamespace(ns)
108+
g.AddOrReplaceLinuxNamespace(rtns, unshareNsPath)
109+
110+
return util.RuntimeOutsideValidate(g, func(config *rspec.Spec, state *rspec.State) error {
111+
containerNsPath := fmt.Sprintf("/proc/%d/ns/%s", state.Pid, ns)
112+
containerNsInode, err := os.Readlink(containerNsPath)
113+
if err != nil {
114+
out, err2 := exec.Command("sh", "-c", fmt.Sprintf("ls -la /proc/%d/ns/", state.Pid)).CombinedOutput()
115+
return fmt.Errorf("cannot read namespace link for the container process: %s\n%v\n%v", err, err2, out)
116+
}
117+
if containerNsInode != unshareNsInode {
118+
return fmt.Errorf("expected: %q, found: %q", unshareNsInode, containerNsInode)
119+
}
120+
return nil
121+
})
122+
}
123+
124+
func testNamespacePath(t *tap.T, ns string, unshareOpt string) error {
125+
// Calling 'unshare' (part of util-linux) is easier than doing it from
126+
// Golang: mnt namespaces cannot be unshared from multithreaded
127+
// programs.
128+
cmd := exec.Command("unshare", unshareOpt, "--fork", "sleep", "10000")
129+
// We shoud set Setpgid to true, to be able to allow the unshare process
130+
// as well as its child processes to be killed by a single kill command.
131+
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
132+
err := cmd.Start()
133+
if err != nil {
134+
return fmt.Errorf("cannot run unshare: %s", err)
135+
}
136+
defer func() {
137+
if cmd.Process != nil {
138+
cmd.Process.Kill()
139+
}
140+
cmd.Wait()
141+
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
142+
}()
143+
if cmd.Process == nil {
144+
return fmt.Errorf("process failed to start")
145+
}
146+
147+
return checkNamespacePath(cmd.Process.Pid, ns)
148+
}
149+
150+
func main() {
151+
t := tap.New()
152+
t.Header(0)
153+
154+
cases := []struct {
155+
name string
156+
unshareOpt string
157+
}{
158+
{"ipc", "--ipc"},
159+
{"mnt", "--mount"},
160+
{"net", "--net"},
161+
{"pid", "--pid"},
162+
{"uts", "--uts"},
163+
}
164+
165+
for _, c := range cases {
166+
if "linux" != runtime.GOOS {
167+
t.Skip(1, fmt.Sprintf("linux-specific namespace test: %s", c))
168+
}
169+
170+
err := testNamespacePath(t, c.name, c.unshareOpt)
171+
t.Ok(err == nil, fmt.Sprintf("set %s namespace by path", c.name))
172+
if err != nil {
173+
rfcError, errRfc := specerror.NewRFCError(specerror.NSProcInPath, err, rspec.Version)
174+
if errRfc != nil {
175+
continue
176+
}
177+
diagnostic := map[string]string{
178+
"actual": fmt.Sprintf("err == %v", err),
179+
"expected": "err == nil",
180+
"namespace type": c.name,
181+
"level": rfcError.Level.String(),
182+
"reference": rfcError.Reference,
183+
}
184+
t.YAML(diagnostic)
185+
}
186+
}
187+
188+
t.AutoPlan()
189+
}

0 commit comments

Comments
 (0)