aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/landlock
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-04-10 23:56:45 +0900
committerOphestra <cat@gensokyo.uk>2026-04-10 23:56:45 +0900
commitb39064037625d1757a65913086ec3a247ad5bb29 (patch)
tree7fd59329e779e195aaadab8846996844e5f45974 /internal/landlock
parentad2c9f36cddfd8adcd06fbe9238d176a8eb89614 (diff)
internal/landlock: relocate from package container
This is not possible to use directly, so remove it from the public API. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/landlock')
-rw-r--r--internal/landlock/landlock.go259
-rw-r--r--internal/landlock/landlock_test.go65
2 files changed, 324 insertions, 0 deletions
diff --git a/internal/landlock/landlock.go b/internal/landlock/landlock.go
new file mode 100644
index 00000000..b65e3ebe
--- /dev/null
+++ b/internal/landlock/landlock.go
@@ -0,0 +1,259 @@
+package landlock
+
+import (
+ "strings"
+ "syscall"
+ "unsafe"
+
+ "hakurei.app/ext"
+)
+
+// include/uapi/linux/landlock.h
+
+const (
+ LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
+)
+
+// AccessFS is bitmask of handled filesystem actions.
+type AccessFS uint64
+
+const (
+ LANDLOCK_ACCESS_FS_EXECUTE AccessFS = 1 << iota
+ LANDLOCK_ACCESS_FS_WRITE_FILE
+ LANDLOCK_ACCESS_FS_READ_FILE
+ LANDLOCK_ACCESS_FS_READ_DIR
+ LANDLOCK_ACCESS_FS_REMOVE_DIR
+ LANDLOCK_ACCESS_FS_REMOVE_FILE
+ LANDLOCK_ACCESS_FS_MAKE_CHAR
+ LANDLOCK_ACCESS_FS_MAKE_DIR
+ LANDLOCK_ACCESS_FS_MAKE_REG
+ LANDLOCK_ACCESS_FS_MAKE_SOCK
+ LANDLOCK_ACCESS_FS_MAKE_FIFO
+ LANDLOCK_ACCESS_FS_MAKE_BLOCK
+ LANDLOCK_ACCESS_FS_MAKE_SYM
+ LANDLOCK_ACCESS_FS_REFER
+ LANDLOCK_ACCESS_FS_TRUNCATE
+ LANDLOCK_ACCESS_FS_IOCTL_DEV
+
+ _LANDLOCK_ACCESS_FS_DELIM
+)
+
+// String returns a space-separated string of [AccessFS] flags.
+func (f AccessFS) String() string {
+ switch f {
+ case LANDLOCK_ACCESS_FS_EXECUTE:
+ return "execute"
+
+ case LANDLOCK_ACCESS_FS_WRITE_FILE:
+ return "write_file"
+
+ case LANDLOCK_ACCESS_FS_READ_FILE:
+ return "read_file"
+
+ case LANDLOCK_ACCESS_FS_READ_DIR:
+ return "read_dir"
+
+ case LANDLOCK_ACCESS_FS_REMOVE_DIR:
+ return "remove_dir"
+
+ case LANDLOCK_ACCESS_FS_REMOVE_FILE:
+ return "remove_file"
+
+ case LANDLOCK_ACCESS_FS_MAKE_CHAR:
+ return "make_char"
+
+ case LANDLOCK_ACCESS_FS_MAKE_DIR:
+ return "make_dir"
+
+ case LANDLOCK_ACCESS_FS_MAKE_REG:
+ return "make_reg"
+
+ case LANDLOCK_ACCESS_FS_MAKE_SOCK:
+ return "make_sock"
+
+ case LANDLOCK_ACCESS_FS_MAKE_FIFO:
+ return "make_fifo"
+
+ case LANDLOCK_ACCESS_FS_MAKE_BLOCK:
+ return "make_block"
+
+ case LANDLOCK_ACCESS_FS_MAKE_SYM:
+ return "make_sym"
+
+ case LANDLOCK_ACCESS_FS_REFER:
+ return "fs_refer"
+
+ case LANDLOCK_ACCESS_FS_TRUNCATE:
+ return "fs_truncate"
+
+ case LANDLOCK_ACCESS_FS_IOCTL_DEV:
+ return "fs_ioctl_dev"
+
+ default:
+ var c []AccessFS
+ for i := AccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
+ if f&i != 0 {
+ c = append(c, i)
+ }
+ }
+ if len(c) == 0 {
+ return "NULL"
+ }
+ s := make([]string, len(c))
+ for i, v := range c {
+ s[i] = v.String()
+ }
+ return strings.Join(s, " ")
+ }
+}
+
+// AccessNet is bitmask of handled network actions.
+type AccessNet uint64
+
+const (
+ LANDLOCK_ACCESS_NET_BIND_TCP AccessNet = 1 << iota
+ LANDLOCK_ACCESS_NET_CONNECT_TCP
+
+ _LANDLOCK_ACCESS_NET_DELIM
+)
+
+// String returns a space-separated string of [AccessNet] flags.
+func (f AccessNet) String() string {
+ switch f {
+ case LANDLOCK_ACCESS_NET_BIND_TCP:
+ return "bind_tcp"
+
+ case LANDLOCK_ACCESS_NET_CONNECT_TCP:
+ return "connect_tcp"
+
+ default:
+ var c []AccessNet
+ for i := AccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
+ if f&i != 0 {
+ c = append(c, i)
+ }
+ }
+ if len(c) == 0 {
+ return "NULL"
+ }
+ s := make([]string, len(c))
+ for i, v := range c {
+ s[i] = v.String()
+ }
+ return strings.Join(s, " ")
+ }
+}
+
+// Scope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
+type Scope uint64
+
+const (
+ LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET Scope = 1 << iota
+ LANDLOCK_SCOPE_SIGNAL
+
+ _LANDLOCK_SCOPE_DELIM
+)
+
+// String returns a space-separated string of [Scope] flags.
+func (f Scope) String() string {
+ switch f {
+ case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET:
+ return "abstract_unix_socket"
+
+ case LANDLOCK_SCOPE_SIGNAL:
+ return "signal"
+
+ default:
+ var c []Scope
+ for i := Scope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
+ if f&i != 0 {
+ c = append(c, i)
+ }
+ }
+ if len(c) == 0 {
+ return "NULL"
+ }
+ s := make([]string, len(c))
+ for i, v := range c {
+ s[i] = v.String()
+ }
+ return strings.Join(s, " ")
+ }
+}
+
+// RulesetAttr is equivalent to struct landlock_ruleset_attr.
+type RulesetAttr struct {
+ // Bitmask of handled filesystem actions.
+ HandledAccessFS AccessFS
+ // Bitmask of handled network actions.
+ HandledAccessNet AccessNet
+ // Bitmask of scopes restricting a Landlock domain from accessing outside
+ // resources (e.g. IPCs).
+ Scoped Scope
+}
+
+// String returns a user-facing description of [RulesetAttr].
+func (rulesetAttr *RulesetAttr) String() string {
+ if rulesetAttr == nil {
+ return "NULL"
+ }
+ elems := make([]string, 0, 3)
+ if rulesetAttr.HandledAccessFS > 0 {
+ elems = append(elems, "fs: "+rulesetAttr.HandledAccessFS.String())
+ }
+ if rulesetAttr.HandledAccessNet > 0 {
+ elems = append(elems, "net: "+rulesetAttr.HandledAccessNet.String())
+ }
+ if rulesetAttr.Scoped > 0 {
+ elems = append(elems, "scoped: "+rulesetAttr.Scoped.String())
+ }
+ if len(elems) == 0 {
+ return "0"
+ }
+ return strings.Join(elems, ", ")
+}
+
+// Create loads the ruleset into the kernel.
+func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
+ var pointer, size uintptr
+ // NULL needed for abi version
+ if rulesetAttr != nil {
+ pointer = uintptr(unsafe.Pointer(rulesetAttr))
+ size = unsafe.Sizeof(*rulesetAttr)
+ }
+
+ rulesetFd, _, errno := syscall.Syscall(
+ ext.SYS_LANDLOCK_CREATE_RULESET,
+ pointer, size,
+ flags,
+ )
+ fd = int(rulesetFd)
+ err = errno
+ if fd < 0 {
+ return
+ }
+
+ if rulesetAttr != nil { // not a fd otherwise
+ syscall.CloseOnExec(fd)
+ }
+ return fd, nil
+}
+
+// GetABI returns the ABI version supported by the kernel.
+func GetABI() (int, error) {
+ return (*RulesetAttr)(nil).Create(LANDLOCK_CREATE_RULESET_VERSION)
+}
+
+// RestrictSelf applies a loaded ruleset to the calling thread.
+func RestrictSelf(rulesetFd int, flags uintptr) error {
+ r, _, errno := syscall.Syscall(
+ ext.SYS_LANDLOCK_RESTRICT_SELF,
+ uintptr(rulesetFd),
+ flags,
+ 0,
+ )
+ if r != 0 {
+ return errno
+ }
+ return nil
+}
diff --git a/internal/landlock/landlock_test.go b/internal/landlock/landlock_test.go
new file mode 100644
index 00000000..6f30fc6b
--- /dev/null
+++ b/internal/landlock/landlock_test.go
@@ -0,0 +1,65 @@
+package landlock_test
+
+import (
+ "testing"
+ "unsafe"
+
+ "hakurei.app/internal/landlock"
+)
+
+func TestLandlockString(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ rulesetAttr *landlock.RulesetAttr
+ want string
+ }{
+ {"nil", nil, "NULL"},
+ {"zero", new(landlock.RulesetAttr), "0"},
+ {"some", &landlock.RulesetAttr{Scoped: landlock.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
+ {"set", &landlock.RulesetAttr{
+ HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_MAKE_SYM | landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV | landlock.LANDLOCK_ACCESS_FS_WRITE_FILE,
+ HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP,
+ Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | landlock.LANDLOCK_SCOPE_SIGNAL,
+ }, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
+ {"all", &landlock.RulesetAttr{
+ HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_EXECUTE |
+ landlock.LANDLOCK_ACCESS_FS_WRITE_FILE |
+ landlock.LANDLOCK_ACCESS_FS_READ_FILE |
+ landlock.LANDLOCK_ACCESS_FS_READ_DIR |
+ landlock.LANDLOCK_ACCESS_FS_REMOVE_DIR |
+ landlock.LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ landlock.LANDLOCK_ACCESS_FS_MAKE_CHAR |
+ landlock.LANDLOCK_ACCESS_FS_MAKE_DIR |
+ landlock.LANDLOCK_ACCESS_FS_MAKE_REG |
+ landlock.LANDLOCK_ACCESS_FS_MAKE_SOCK |
+ landlock.LANDLOCK_ACCESS_FS_MAKE_FIFO |
+ landlock.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
+ landlock.LANDLOCK_ACCESS_FS_MAKE_SYM |
+ landlock.LANDLOCK_ACCESS_FS_REFER |
+ landlock.LANDLOCK_ACCESS_FS_TRUNCATE |
+ landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV,
+ HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP |
+ landlock.LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
+ landlock.LANDLOCK_SCOPE_SIGNAL,
+ }, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ if got := tc.rulesetAttr.String(); got != tc.want {
+ t.Errorf("String: %s, want %s", got, tc.want)
+ }
+ })
+ }
+}
+
+func TestLandlockAttrSize(t *testing.T) {
+ t.Parallel()
+ want := 24
+ if got := unsafe.Sizeof(landlock.RulesetAttr{}); got != uintptr(want) {
+ t.Errorf("Sizeof: %d, want %d", got, want)
+ }
+}