unix: add listns syscall introduced in Linux 6.19

Add support for the listns(2) system call available since Linux 6.19.
The system call enumerates namespace IDs with optional filtering by
namespace type and owning user namespace.

Change-Id: I8d880a51bcd97004ab81d09ba0f877aae967f3ca
GitHub-Last-Rev: 43d1dc0ae5ac2789a666cdf6eec92ca2554ef7c7
GitHub-Pull-Request: golang/sys#279
Reviewed-on: https://go-review.googlesource.com/c/sys/+/781686
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
Reviewed-by: Florian Lehner <lhnr.flrn@gmail.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Auto-Submit: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Tobias Klauser <tobias.klauser@gmail.com>
diff --git a/unix/linux/types.go b/unix/linux/types.go
index 8c9a907..130eaa1 100644
--- a/unix/linux/types.go
+++ b/unix/linux/types.go
@@ -153,6 +153,7 @@
 #include <linux/nexthop.h>
 #include <linux/nfc.h>
 #include <linux/nl80211.h>
+#include <linux/nsfs.h>
 #include <linux/openat2.h>
 #include <linux/perf_event.h>
 #include <linux/pps.h>
@@ -568,6 +569,16 @@
 	unsigned int flags;
 	struct ptp_clock_time on;
 };
+
+// the one defined in linux/nsfs.h has anonymous nested struct
+struct my_ns_id_req {
+	__u32 size;
+	__u32 spare;
+	__u64 ns_id;
+	__u32 ns_type;
+	__u32 spare2;
+	__u64 user_ns_id;
+};
 */
 import "C"
 
@@ -1121,6 +1132,22 @@
 
 const SizeofOpenHow = C.sizeof_struct_open_how
 
+type NsIdReq C.struct_my_ns_id_req
+
+const (
+	NS_ID_REQ_SIZE_VER0 = C.NS_ID_REQ_SIZE_VER0
+	LISTNS_CURRENT_USER = C.LISTNS_CURRENT_USER
+
+	TIME_NS   = C.TIME_NS
+	MNT_NS    = C.MNT_NS
+	CGROUP_NS = C.CGROUP_NS
+	UTS_NS    = C.UTS_NS
+	IPC_NS    = C.IPC_NS
+	USER_NS   = C.USER_NS
+	PID_NS    = C.PID_NS
+	NET_NS    = C.NET_NS
+)
+
 const (
 	RESOLVE_BENEATH       = C.RESOLVE_BENEATH
 	RESOLVE_IN_ROOT       = C.RESOLVE_IN_ROOT
diff --git a/unix/syscall_linux.go b/unix/syscall_linux.go
index 8fbfb0d..e54d20a 100644
--- a/unix/syscall_linux.go
+++ b/unix/syscall_linux.go
@@ -2050,6 +2050,7 @@
 //sysnb	Kill(pid int, sig syscall.Signal) (err error)
 //sys	Klogctl(typ int, buf []byte) (n int, err error) = SYS_SYSLOG
 //sys	Lgetxattr(path string, attr string, dest []byte) (sz int, err error)
+//sys	Listns(req *NsIdReq, nsIds []uint64, flags uint) (n int, err error)
 //sys	Listxattr(path string, dest []byte) (sz int, err error)
 //sys	Llistxattr(path string, dest []byte) (sz int, err error)
 //sys	Lremovexattr(path string, attr string) (err error)
diff --git a/unix/syscall_linux_test.go b/unix/syscall_linux_test.go
index 7bf78e6..03f3ebf 100644
--- a/unix/syscall_linux_test.go
+++ b/unix/syscall_linux_test.go
@@ -141,6 +141,108 @@
 	}
 }
 
+func TestListns(t *testing.T) {
+	callListns := func(tb testing.TB, req *unix.NsIdReq, nsIDs []uint64, flags uint, context string) int {
+		tb.Helper()
+		n, err := unix.Listns(req, nsIDs, flags)
+		if err != nil {
+			if errors.Is(err, unix.ENOSYS) {
+				tb.Skip("listns syscall is not available (need Linux >= 6.19), skipping test")
+			}
+			if errors.Is(err, unix.EPERM) || errors.Is(err, unix.EACCES) {
+				tb.Skipf("listns requires additional privileges in this environment, skipping test: %v", err)
+			}
+			tb.Fatalf("%s: Listns failed: %v", context, err)
+		}
+		if n < 0 || n > len(nsIDs) {
+			tb.Fatalf("%s: Listns returned unexpected count %d, want [0, %d]", context, n, len(nsIDs))
+		}
+		return n
+	}
+
+	t.Run("all namespaces", func(t *testing.T) {
+		req := &unix.NsIdReq{
+			Size:       unix.NS_ID_REQ_SIZE_VER0,
+			Ns_id:      0,
+			Ns_type:    0,
+			User_ns_id: 0,
+		}
+		ids := make([]uint64, 64)
+		callListns(t, req, ids, 0, "all namespaces")
+	})
+
+	t.Run("network namespaces only", func(t *testing.T) {
+		req := &unix.NsIdReq{
+			Size:       unix.NS_ID_REQ_SIZE_VER0,
+			Ns_id:      0,
+			Ns_type:    uint32(unix.NET_NS),
+			User_ns_id: 0,
+		}
+		ids := make([]uint64, 64)
+		callListns(t, req, ids, 0, "network namespaces only")
+	})
+
+	t.Run("current user namespace owner", func(t *testing.T) {
+		req := &unix.NsIdReq{
+			Size:       unix.NS_ID_REQ_SIZE_VER0,
+			Ns_id:      0,
+			Ns_type:    0,
+			User_ns_id: unix.LISTNS_CURRENT_USER,
+		}
+		ids := make([]uint64, 64)
+		callListns(t, req, ids, 0, "current user namespace owner")
+	})
+
+	t.Run("network and mount namespaces", func(t *testing.T) {
+		req := &unix.NsIdReq{
+			Size:       unix.NS_ID_REQ_SIZE_VER0,
+			Ns_id:      0,
+			Ns_type:    uint32(unix.NET_NS | unix.MNT_NS),
+			User_ns_id: 0,
+		}
+		ids := make([]uint64, 64)
+		callListns(t, req, ids, 0, "network and mount namespaces")
+	})
+
+	t.Run("pagination via ns_id", func(t *testing.T) {
+		const pageSize = 10
+		req := &unix.NsIdReq{
+			Size:       unix.NS_ID_REQ_SIZE_VER0,
+			Ns_id:      0,
+			Ns_type:    0,
+			User_ns_id: 0,
+		}
+		ids := make([]uint64, pageSize)
+
+		total := 0
+		for {
+			n := callListns(t, req, ids, 0, "pagination")
+			if n == 0 {
+				break
+			}
+
+			// Continue from the last seen namespace ID.
+			last := ids[n-1]
+			if last == req.Ns_id {
+				t.Fatalf("pagination did not advance: last=%d", last)
+			}
+			total += n
+			req.Ns_id = last
+
+			// Partial batch means we've exhausted all results.
+			if n < pageSize {
+				break
+			}
+		}
+
+		t.Logf("total ns: %d", total)
+		if total == 0 {
+			t.Fatalf("pagination should return at least one namespace")
+		}
+	})
+
+}
+
 func TestIoctlGetRTCTime(t *testing.T) {
 	f, err := os.Open("/dev/rtc0")
 	if err != nil {
diff --git a/unix/zsyscall_linux.go b/unix/zsyscall_linux.go
index 86d3b56..ad47efc 100644
--- a/unix/zsyscall_linux.go
+++ b/unix/zsyscall_linux.go
@@ -1167,6 +1167,23 @@
 
 // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
 
+func Listns(req *NsIdReq, nsIds []uint64, flags uint) (n int, err error) {
+	var _p0 unsafe.Pointer
+	if len(nsIds) > 0 {
+		_p0 = unsafe.Pointer(&nsIds[0])
+	} else {
+		_p0 = unsafe.Pointer(&_zero)
+	}
+	r0, _, e1 := Syscall6(SYS_LISTNS, uintptr(unsafe.Pointer(req)), uintptr(_p0), uintptr(len(nsIds)), uintptr(flags), 0, 0)
+	n = int(r0)
+	if e1 != 0 {
+		err = errnoErr(e1)
+	}
+	return
+}
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
 func Listxattr(path string, dest []byte) (sz int, err error) {
 	var _p0 *byte
 	_p0, err = BytePtrFromString(path)
diff --git a/unix/ztypes_linux.go b/unix/ztypes_linux.go
index 538a18f..18da7aa 100644
--- a/unix/ztypes_linux.go
+++ b/unix/ztypes_linux.go
@@ -933,6 +933,29 @@
 
 const SizeofOpenHow = 0x18
 
+type NsIdReq struct {
+	Size       uint32
+	Spare      uint32
+	Ns_id      uint64
+	Ns_type    uint32
+	Spare2     uint32
+	User_ns_id uint64
+}
+
+const (
+	NS_ID_REQ_SIZE_VER0 = 0x20
+	LISTNS_CURRENT_USER = 0xffffffffffffffff
+
+	TIME_NS   = 0x80
+	MNT_NS    = 0x20000
+	CGROUP_NS = 0x2000000
+	UTS_NS    = 0x4000000
+	IPC_NS    = 0x8000000
+	USER_NS   = 0x10000000
+	PID_NS    = 0x20000000
+	NET_NS    = 0x40000000
+)
+
 const (
 	RESOLVE_BENEATH       = 0x8
 	RESOLVE_IN_ROOT       = 0x10