diff --git a/Makefile b/Makefile index 14e79bd..c83bf16 100644 --- a/Makefile +++ b/Makefile @@ -16,14 +16,18 @@ clean: test: test-local set -eu; \ for p in $(PACKAGES); do \ - (cd $$p; go test $(RUN_VIA_SUDO) -v .); \ + if $p = user && go version | grep -qv go1.18; then \ + (cd $$p; go test $(RUN_VIA_SUDO) -v .); \ + fi \ done .PHONY: tidy tidy: set -eu; \ - for p in $(PACKAGES); do \ - (cd $$p; go mod tidy); \ + for p in $(PACKAGES); do \ + if $p = user && go version | grep -qv go1.18; then \ + (cd $$p; go mod tidy); \ + fi \ done # Test the mount module against the local mountinfo source code instead of the @@ -42,9 +46,11 @@ lint: $(BINDIR)/golangci-lint $(BINDIR)/golangci-lint version set -eu; \ for p in $(PACKAGES); do \ - (cd $$p; \ - go mod download; \ - ../$(BINDIR)/golangci-lint run); \ + if $p = user && go version | grep -qv go1.18; then \ + (cd $$p; \ + go mod download; \ + ../$(BINDIR)/golangci-lint run); \ + fi \ done $(BINDIR)/golangci-lint: $(BINDIR) diff --git a/user/go.mod b/user/go.mod index 94e2d1f..d4ad72f 100644 --- a/user/go.mod +++ b/user/go.mod @@ -1,5 +1,5 @@ module github.com/moby/sys/user -go 1.18 +go 1.21 require golang.org/x/sys v0.1.0 diff --git a/user/userns/userns.go b/user/userns/userns.go new file mode 100644 index 0000000..56b24c4 --- /dev/null +++ b/user/userns/userns.go @@ -0,0 +1,16 @@ +// Package userns provides utilities to detect whether we are currently running +// in a Linux user namespace. +// +// This code was migrated from [libcontainer/runc], which based its implementation +// on code from [lcx/incus]. +// +// [libcontainer/runc]: https://github.com/opencontainers/runc/blob/3778ae603c706494fd1e2c2faf83b406e38d687d/libcontainer/userns/userns_linux.go#L12-L49 +// [lcx/incus]: https://github.com/lxc/incus/blob/e45085dd42f826b3c8c3228e9733c0b6f998eafe/shared/util.go#L678-L700 +package userns + +// RunningInUserNS detects whether we are currently running in a Linux +// user namespace and memoizes the result. It returns false on non-Linux +// platforms. +func RunningInUserNS() bool { + return inUserNS() +} diff --git a/user/userns/userns_linux.go b/user/userns/userns_linux.go new file mode 100644 index 0000000..87c1c38 --- /dev/null +++ b/user/userns/userns_linux.go @@ -0,0 +1,53 @@ +package userns + +import ( + "bufio" + "fmt" + "os" + "sync" +) + +var inUserNS = sync.OnceValue(runningInUserNS) + +// runningInUserNS detects whether we are currently running in a user namespace. +// +// This code was migrated from [libcontainer/runc] and based on an implementation +// from [lcx/incus]. +// +// [libcontainer/runc]: https://github.com/opencontainers/runc/blob/3778ae603c706494fd1e2c2faf83b406e38d687d/libcontainer/userns/userns_linux.go#L12-L49 +// [lcx/incus]: https://github.com/lxc/incus/blob/e45085dd42f826b3c8c3228e9733c0b6f998eafe/shared/util.go#L678-L700 +func runningInUserNS() bool { + file, err := os.Open("/proc/self/uid_map") + if err != nil { + // This kernel-provided file only exists if user namespaces are supported. + return false + } + defer file.Close() + + buf := bufio.NewReader(file) + l, _, err := buf.ReadLine() + if err != nil { + return false + } + + return uidMapInUserNS(string(l)) +} + +func uidMapInUserNS(uidMap string) bool { + if uidMap == "" { + // File exist but empty (the initial state when userns is created, + // see user_namespaces(7)). + return true + } + + var a, b, c int64 + if _, err := fmt.Sscanf(uidMap, "%d %d %d", &a, &b, &c); err != nil { + // Assume we are in a regular, non user namespace. + return false + } + + // As per user_namespaces(7), /proc/self/uid_map of + // the initial user namespace shows 0 0 4294967295. + initNS := a == 0 && b == 0 && c == 4294967295 + return !initNS +} diff --git a/user/userns/userns_linux_fuzzer.go b/user/userns/userns_linux_fuzzer.go new file mode 100644 index 0000000..26ba2e1 --- /dev/null +++ b/user/userns/userns_linux_fuzzer.go @@ -0,0 +1,8 @@ +//go:build linux && gofuzz + +package userns + +func FuzzUIDMap(uidmap []byte) int { + _ = uidMapInUserNS(string(uidmap)) + return 1 +} diff --git a/user/userns/userns_linux_test.go b/user/userns/userns_linux_test.go new file mode 100644 index 0000000..25c4ac3 --- /dev/null +++ b/user/userns/userns_linux_test.go @@ -0,0 +1,34 @@ +package userns + +import "testing" + +func TestUIDMapInUserNS(t *testing.T) { + cases := []struct { + s string + expected bool + }{ + { + s: " 0 0 4294967295\n", + expected: false, + }, + { + s: " 0 0 1\n", + expected: true, + }, + { + s: " 0 1001 1\n 1 231072 65536\n", + expected: true, + }, + { + // file exist but empty (the initial state when userns is created. see man 7 user_namespaces) + s: "", + expected: true, + }, + } + for _, c := range cases { + actual := uidMapInUserNS(c.s) + if c.expected != actual { + t.Fatalf("expected %v, got %v for %q", c.expected, actual, c.s) + } + } +} diff --git a/user/userns/userns_unsupported.go b/user/userns/userns_unsupported.go new file mode 100644 index 0000000..8ed8307 --- /dev/null +++ b/user/userns/userns_unsupported.go @@ -0,0 +1,6 @@ +//go:build !linux + +package userns + +// inUserNS is a stub for non-Linux systems. Always returns false. +func inUserNS() bool { return false }