github.com/opencontainers/runtime-tools@v0.9.0/validation/linux_ns_path/linux_ns_path.go (about)

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