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()
}
|