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