From 1b5ecd9eaf3289d164d8ed1bce6013e0e4ef8e86 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Thu, 3 Jul 2025 02:59:43 +0900 Subject: container: move out of toplevel This allows slightly easier use of the vanity url. This also provides some disambiguation between low level containers and hakurei app containers. Signed-off-by: Ophestra --- .../internal/app/instance/common/container.go | 54 +-- .../internal/app/internal/setuid/app_nixos_test.go | 26 +- .../internal/app/internal/setuid/app_pd_test.go | 78 ++-- .../internal/app/internal/setuid/app_test.go | 6 +- .../internal/app/internal/setuid/export_test.go | 4 +- .../internal/app/internal/setuid/process.go | 4 +- cmd/hakurei/internal/app/internal/setuid/seal.go | 10 +- cmd/hakurei/internal/app/internal/setuid/shim.go | 30 +- cmd/hakurei/main.go | 6 +- cmd/planterette/app.go | 2 +- cmd/planterette/with.go | 2 +- container.go | 229 ---------- container/container.go | 229 ++++++++++ container/container_test.go | 281 ++++++++++++ container/executable.go | 26 ++ container/executable_test.go | 17 + container/init.go | 364 ++++++++++++++++ container/mount.go | 123 ++++++ container/msg.go | 43 ++ container/ops.go | 482 +++++++++++++++++++++ container/output.go | 26 ++ container/params.go | 47 ++ container/path.go | 94 ++++ container/seccomp/libseccomp-helper.c | 130 ++++++ container/seccomp/libseccomp-helper.h | 24 + container/seccomp/libseccomp.go | 188 ++++++++ container/seccomp/libseccomp_test.go | 147 +++++++ container/seccomp/mksysnum_linux.pl | 83 ++++ container/seccomp/presets.go | 229 ++++++++++ container/seccomp/presets_clone_backwards2.go | 7 + container/seccomp/presets_clone_generic.go | 6 + container/seccomp/proc.go | 78 ++++ container/seccomp/seccomp.go | 60 +++ container/seccomp/seccomp_test.go | 65 +++ container/seccomp/syscall.go | 28 ++ container/seccomp/syscall_extra_linux_amd64.go | 54 +++ container/seccomp/syscall_linux_amd64.go | 459 ++++++++++++++++++++ container/seccomp/syscall_test.go | 20 + container/syscall.go | 81 ++++ container/sysctl.go | 47 ++ container/vfs/mangle.go | 30 ++ container/vfs/mangle_test.go | 27 ++ container/vfs/mountinfo.go | 260 +++++++++++ container/vfs/mountinfo_test.go | 404 +++++++++++++++++ container/vfs/unfold.go | 107 +++++ container/vfs/unfold_test.go | 93 ++++ container_test.go | 281 ------------ executable.go | 26 -- executable_test.go | 17 - helper/container.go | 8 +- helper/container_test.go | 16 +- hst/container.go | 2 +- hst/template.go | 2 +- init.go | 364 ---------------- internal/output.go | 4 +- internal/sys/std.go | 4 +- ldd/exec.go | 26 +- mount.go | 123 ------ msg.go | 43 -- ops.go | 482 --------------------- output.go | 26 -- params.go | 47 -- path.go | 94 ---- seccomp/libseccomp-helper.c | 130 ------ seccomp/libseccomp-helper.h | 24 - seccomp/libseccomp.go | 188 -------- seccomp/libseccomp_test.go | 147 ------- seccomp/mksysnum_linux.pl | 83 ---- seccomp/presets.go | 229 ---------- seccomp/presets_clone_backwards2.go | 7 - seccomp/presets_clone_generic.go | 6 - seccomp/proc.go | 78 ---- seccomp/seccomp.go | 60 --- seccomp/seccomp_test.go | 65 --- seccomp/syscall.go | 28 -- seccomp/syscall_extra_linux_amd64.go | 54 --- seccomp/syscall_linux_amd64.go | 459 -------------------- seccomp/syscall_test.go | 20 - syscall.go | 81 ---- sysctl.go | 47 -- system/dbus/dbus_test.go | 12 +- system/dbus/proc.go | 26 +- system/output.go | 8 +- vfs/mangle.go | 30 -- vfs/mangle_test.go | 27 -- vfs/mountinfo.go | 260 ----------- vfs/mountinfo_test.go | 404 ----------------- vfs/unfold.go | 107 ----- vfs/unfold_test.go | 93 ---- 89 files changed, 4523 insertions(+), 4525 deletions(-) delete mode 100644 container.go create mode 100644 container/container.go create mode 100644 container/container_test.go create mode 100644 container/executable.go create mode 100644 container/executable_test.go create mode 100644 container/init.go create mode 100644 container/mount.go create mode 100644 container/msg.go create mode 100644 container/ops.go create mode 100644 container/output.go create mode 100644 container/params.go create mode 100644 container/path.go create mode 100644 container/seccomp/libseccomp-helper.c create mode 100644 container/seccomp/libseccomp-helper.h create mode 100644 container/seccomp/libseccomp.go create mode 100644 container/seccomp/libseccomp_test.go create mode 100755 container/seccomp/mksysnum_linux.pl create mode 100644 container/seccomp/presets.go create mode 100644 container/seccomp/presets_clone_backwards2.go create mode 100644 container/seccomp/presets_clone_generic.go create mode 100644 container/seccomp/proc.go create mode 100644 container/seccomp/seccomp.go create mode 100644 container/seccomp/seccomp_test.go create mode 100644 container/seccomp/syscall.go create mode 100644 container/seccomp/syscall_extra_linux_amd64.go create mode 100644 container/seccomp/syscall_linux_amd64.go create mode 100644 container/seccomp/syscall_test.go create mode 100644 container/syscall.go create mode 100644 container/sysctl.go create mode 100644 container/vfs/mangle.go create mode 100644 container/vfs/mangle_test.go create mode 100644 container/vfs/mountinfo.go create mode 100644 container/vfs/mountinfo_test.go create mode 100644 container/vfs/unfold.go create mode 100644 container/vfs/unfold_test.go delete mode 100644 container_test.go delete mode 100644 executable.go delete mode 100644 executable_test.go delete mode 100644 init.go delete mode 100644 mount.go delete mode 100644 msg.go delete mode 100644 ops.go delete mode 100644 output.go delete mode 100644 params.go delete mode 100644 path.go delete mode 100644 seccomp/libseccomp-helper.c delete mode 100644 seccomp/libseccomp-helper.h delete mode 100644 seccomp/libseccomp.go delete mode 100644 seccomp/libseccomp_test.go delete mode 100755 seccomp/mksysnum_linux.pl delete mode 100644 seccomp/presets.go delete mode 100644 seccomp/presets_clone_backwards2.go delete mode 100644 seccomp/presets_clone_generic.go delete mode 100644 seccomp/proc.go delete mode 100644 seccomp/seccomp.go delete mode 100644 seccomp/seccomp_test.go delete mode 100644 seccomp/syscall.go delete mode 100644 seccomp/syscall_extra_linux_amd64.go delete mode 100644 seccomp/syscall_linux_amd64.go delete mode 100644 seccomp/syscall_test.go delete mode 100644 syscall.go delete mode 100644 sysctl.go delete mode 100644 vfs/mangle.go delete mode 100644 vfs/mangle_test.go delete mode 100644 vfs/mountinfo.go delete mode 100644 vfs/mountinfo_test.go delete mode 100644 vfs/unfold.go delete mode 100644 vfs/unfold_test.go diff --git a/cmd/hakurei/internal/app/instance/common/container.go b/cmd/hakurei/internal/app/instance/common/container.go index b7e26317..3e4740da 100644 --- a/cmd/hakurei/internal/app/instance/common/container.go +++ b/cmd/hakurei/internal/app/instance/common/container.go @@ -8,10 +8,10 @@ import ( "path" "syscall" - "git.gensokyo.uk/security/hakurei" + "git.gensokyo.uk/security/hakurei/container" + "git.gensokyo.uk/security/hakurei/container/seccomp" "git.gensokyo.uk/security/hakurei/hst" "git.gensokyo.uk/security/hakurei/internal/sys" - "git.gensokyo.uk/security/hakurei/seccomp" "git.gensokyo.uk/security/hakurei/system/dbus" ) @@ -21,12 +21,12 @@ const preallocateOpsCount = 1 << 5 // NewContainer initialises [sandbox.Params] via [hst.ContainerConfig]. // Note that remaining container setup must be queued by the caller. -func NewContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*hakurei.Params, map[string]string, error) { +func NewContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*container.Params, map[string]string, error) { if s == nil { return nil, nil, syscall.EBADE } - container := &hakurei.Params{ + params := &container.Params{ Hostname: s.Hostname, SeccompFlags: s.SeccompFlags, SeccompPresets: s.SeccompPresets, @@ -35,47 +35,47 @@ func NewContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*hakurei } { - ops := make(hakurei.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link)+len(s.Cover)) - container.Ops = &ops + ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link)+len(s.Cover)) + params.Ops = &ops } if s.Multiarch { - container.SeccompFlags |= seccomp.AllowMultiarch + params.SeccompFlags |= seccomp.AllowMultiarch } if !s.SeccompCompat { - container.SeccompPresets |= seccomp.PresetExt + params.SeccompPresets |= seccomp.PresetExt } if !s.Devel { - container.SeccompPresets |= seccomp.PresetDenyDevel + params.SeccompPresets |= seccomp.PresetDenyDevel } if !s.Userns { - container.SeccompPresets |= seccomp.PresetDenyNS + params.SeccompPresets |= seccomp.PresetDenyNS } if !s.Tty { - container.SeccompPresets |= seccomp.PresetDenyTTY + params.SeccompPresets |= seccomp.PresetDenyTTY } if s.MapRealUID { /* some programs fail to connect to dbus session running as a different uid so this workaround is introduced to map priv-side caller uid in container */ - container.Uid = os.Getuid() - *uid = container.Uid - container.Gid = os.Getgid() - *gid = container.Gid + params.Uid = os.Getuid() + *uid = params.Uid + params.Gid = os.Getgid() + *gid = params.Gid } else { - *uid = hakurei.OverflowUid() - *gid = hakurei.OverflowGid() + *uid = container.OverflowUid() + *gid = container.OverflowGid() } - container. + params. Proc("/proc"). Tmpfs(hst.Tmp, 1<<12, 0755) if !s.Device { - container.Dev("/dev").Mqueue("/dev/mqueue") + params.Dev("/dev").Mqueue("/dev/mqueue") } else { - container.Bind("/dev", "/dev", hakurei.BindWritable|hakurei.BindDevice) + params.Bind("/dev", "/dev", container.BindWritable|container.BindDevice) } /* retrieve paths and hide them if they're made available in the sandbox; @@ -154,29 +154,29 @@ func NewContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*hakurei var flags int if c.Write { - flags |= hakurei.BindWritable + flags |= container.BindWritable } if c.Device { - flags |= hakurei.BindDevice | hakurei.BindWritable + flags |= container.BindDevice | container.BindWritable } if !c.Must { - flags |= hakurei.BindOptional + flags |= container.BindOptional } - container.Bind(c.Src, dest, flags) + params.Bind(c.Src, dest, flags) } // cover matched paths for i, ok := range hidePathMatch { if ok { - container.Tmpfs(hidePaths[i], 1<<13, 0755) + params.Tmpfs(hidePaths[i], 1<<13, 0755) } } for _, l := range s.Link { - container.Link(l[0], l[1]) + params.Link(l[0], l[1]) } - return container, maps.Clone(s.Env), nil + return params, maps.Clone(s.Env), nil } func evalSymlinks(os sys.State, v *string) error { diff --git a/cmd/hakurei/internal/app/internal/setuid/app_nixos_test.go b/cmd/hakurei/internal/app/internal/setuid/app_nixos_test.go index 29dfdd82..df955af4 100644 --- a/cmd/hakurei/internal/app/internal/setuid/app_nixos_test.go +++ b/cmd/hakurei/internal/app/internal/setuid/app_nixos_test.go @@ -1,10 +1,10 @@ package setuid_test import ( - "git.gensokyo.uk/security/hakurei" "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/app" + "git.gensokyo.uk/security/hakurei/container" + "git.gensokyo.uk/security/hakurei/container/seccomp" "git.gensokyo.uk/security/hakurei/hst" - "git.gensokyo.uk/security/hakurei/seccomp" "git.gensokyo.uk/security/hakurei/system" "git.gensokyo.uk/security/hakurei/system/acl" "git.gensokyo.uk/security/hakurei/system/dbus" @@ -94,7 +94,7 @@ var testCasesNixos = []sealTestCase{ }). UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write). UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write), - &hakurei.Params{ + &container.Params{ Uid: 1971, Gid: 100, Dir: "/var/lib/persist/module/hakurei/0/1", @@ -114,7 +114,7 @@ var testCasesNixos = []sealTestCase{ "XDG_SESSION_CLASS=user", "XDG_SESSION_TYPE=tty", }, - Ops: new(hakurei.Ops). + Ops: new(container.Ops). Proc("/proc"). Tmpfs(hst.Tmp, 4096, 0755). Dev("/dev").Mqueue("/dev/mqueue"). @@ -122,18 +122,18 @@ var testCasesNixos = []sealTestCase{ Bind("/usr/bin", "/usr/bin", 0). Bind("/nix/store", "/nix/store", 0). Bind("/run/current-system", "/run/current-system", 0). - Bind("/sys/block", "/sys/block", hakurei.BindOptional). - Bind("/sys/bus", "/sys/bus", hakurei.BindOptional). - Bind("/sys/class", "/sys/class", hakurei.BindOptional). - Bind("/sys/dev", "/sys/dev", hakurei.BindOptional). - Bind("/sys/devices", "/sys/devices", hakurei.BindOptional). + Bind("/sys/block", "/sys/block", container.BindOptional). + Bind("/sys/bus", "/sys/bus", container.BindOptional). + Bind("/sys/class", "/sys/class", container.BindOptional). + Bind("/sys/dev", "/sys/dev", container.BindOptional). + Bind("/sys/devices", "/sys/devices", container.BindOptional). Bind("/run/opengl-driver", "/run/opengl-driver", 0). - Bind("/dev/dri", "/dev/dri", hakurei.BindDevice|hakurei.BindWritable|hakurei.BindOptional). + Bind("/dev/dri", "/dev/dri", container.BindDevice|container.BindWritable|container.BindOptional). Etc("/etc", "8e2c76b066dabe574cf073bdb46eb5c1"). Tmpfs("/run/user", 4096, 0755). - Bind("/tmp/hakurei.1971/runtime/1", "/run/user/1971", hakurei.BindWritable). - Bind("/tmp/hakurei.1971/tmpdir/1", "/tmp", hakurei.BindWritable). - Bind("/var/lib/persist/module/hakurei/0/1", "/var/lib/persist/module/hakurei/0/1", hakurei.BindWritable). + Bind("/tmp/hakurei.1971/runtime/1", "/run/user/1971", container.BindWritable). + Bind("/tmp/hakurei.1971/tmpdir/1", "/tmp", container.BindWritable). + Bind("/var/lib/persist/module/hakurei/0/1", "/var/lib/persist/module/hakurei/0/1", container.BindWritable). Place("/etc/passwd", []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")). Place("/etc/group", []byte("hakurei:x:100:\n")). Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0). diff --git a/cmd/hakurei/internal/app/internal/setuid/app_pd_test.go b/cmd/hakurei/internal/app/internal/setuid/app_pd_test.go index 2236510e..ee1eceb1 100644 --- a/cmd/hakurei/internal/app/internal/setuid/app_pd_test.go +++ b/cmd/hakurei/internal/app/internal/setuid/app_pd_test.go @@ -3,10 +3,10 @@ package setuid_test import ( "os" - "git.gensokyo.uk/security/hakurei" "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/app" + "git.gensokyo.uk/security/hakurei/container" + "git.gensokyo.uk/security/hakurei/container/seccomp" "git.gensokyo.uk/security/hakurei/hst" - "git.gensokyo.uk/security/hakurei/seccomp" "git.gensokyo.uk/security/hakurei/system" "git.gensokyo.uk/security/hakurei/system/acl" "git.gensokyo.uk/security/hakurei/system/dbus" @@ -28,7 +28,7 @@ var testCasesPd = []sealTestCase{ Ensure("/tmp/hakurei.1971/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/0", acl.Read, acl.Write, acl.Execute). Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute). Ensure("/tmp/hakurei.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute), - &hakurei.Params{ + &container.Params{ Dir: "/home/chronos", Path: "/run/current-system/sw/bin/zsh", Args: []string{"/run/current-system/sw/bin/zsh"}, @@ -41,30 +41,30 @@ var testCasesPd = []sealTestCase{ "XDG_SESSION_CLASS=user", "XDG_SESSION_TYPE=tty", }, - Ops: new(hakurei.Ops). + Ops: new(container.Ops). Proc("/proc"). Tmpfs(hst.Tmp, 4096, 0755). Dev("/dev").Mqueue("/dev/mqueue"). - Bind("/bin", "/bin", hakurei.BindWritable). - Bind("/boot", "/boot", hakurei.BindWritable). - Bind("/home", "/home", hakurei.BindWritable). - Bind("/lib", "/lib", hakurei.BindWritable). - Bind("/lib64", "/lib64", hakurei.BindWritable). - Bind("/nix", "/nix", hakurei.BindWritable). - Bind("/root", "/root", hakurei.BindWritable). - Bind("/run", "/run", hakurei.BindWritable). - Bind("/srv", "/srv", hakurei.BindWritable). - Bind("/sys", "/sys", hakurei.BindWritable). - Bind("/usr", "/usr", hakurei.BindWritable). - Bind("/var", "/var", hakurei.BindWritable). - Bind("/dev/kvm", "/dev/kvm", hakurei.BindWritable|hakurei.BindDevice|hakurei.BindOptional). + Bind("/bin", "/bin", container.BindWritable). + Bind("/boot", "/boot", container.BindWritable). + Bind("/home", "/home", container.BindWritable). + Bind("/lib", "/lib", container.BindWritable). + Bind("/lib64", "/lib64", container.BindWritable). + Bind("/nix", "/nix", container.BindWritable). + Bind("/root", "/root", container.BindWritable). + Bind("/run", "/run", container.BindWritable). + Bind("/srv", "/srv", container.BindWritable). + Bind("/sys", "/sys", container.BindWritable). + Bind("/usr", "/usr", container.BindWritable). + Bind("/var", "/var", container.BindWritable). + Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional). Tmpfs("/run/user/1971", 8192, 0755). Tmpfs("/run/dbus", 8192, 0755). Etc("/etc", "4a450b6596d7bc15bd01780eb9a607ac"). Tmpfs("/run/user", 4096, 0755). - Bind("/tmp/hakurei.1971/runtime/0", "/run/user/65534", hakurei.BindWritable). - Bind("/tmp/hakurei.1971/tmpdir/0", "/tmp", hakurei.BindWritable). - Bind("/home/chronos", "/home/chronos", hakurei.BindWritable). + Bind("/tmp/hakurei.1971/runtime/0", "/run/user/65534", container.BindWritable). + Bind("/tmp/hakurei.1971/tmpdir/0", "/tmp", container.BindWritable). + Bind("/home/chronos", "/home/chronos", container.BindWritable). Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). Place("/etc/group", []byte("hakurei:x:65534:\n")). Tmpfs("/var/run/nscd", 8192, 0755), @@ -166,7 +166,7 @@ var testCasesPd = []sealTestCase{ }). UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write). UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write), - &hakurei.Params{ + &container.Params{ Dir: "/home/chronos", Path: "/run/current-system/sw/bin/zsh", Args: []string{"zsh", "-c", "exec chromium "}, @@ -184,31 +184,31 @@ var testCasesPd = []sealTestCase{ "XDG_SESSION_CLASS=user", "XDG_SESSION_TYPE=tty", }, - Ops: new(hakurei.Ops). + Ops: new(container.Ops). Proc("/proc"). Tmpfs(hst.Tmp, 4096, 0755). Dev("/dev").Mqueue("/dev/mqueue"). - Bind("/bin", "/bin", hakurei.BindWritable). - Bind("/boot", "/boot", hakurei.BindWritable). - Bind("/home", "/home", hakurei.BindWritable). - Bind("/lib", "/lib", hakurei.BindWritable). - Bind("/lib64", "/lib64", hakurei.BindWritable). - Bind("/nix", "/nix", hakurei.BindWritable). - Bind("/root", "/root", hakurei.BindWritable). - Bind("/run", "/run", hakurei.BindWritable). - Bind("/srv", "/srv", hakurei.BindWritable). - Bind("/sys", "/sys", hakurei.BindWritable). - Bind("/usr", "/usr", hakurei.BindWritable). - Bind("/var", "/var", hakurei.BindWritable). - Bind("/dev/dri", "/dev/dri", hakurei.BindWritable|hakurei.BindDevice|hakurei.BindOptional). - Bind("/dev/kvm", "/dev/kvm", hakurei.BindWritable|hakurei.BindDevice|hakurei.BindOptional). + Bind("/bin", "/bin", container.BindWritable). + Bind("/boot", "/boot", container.BindWritable). + Bind("/home", "/home", container.BindWritable). + Bind("/lib", "/lib", container.BindWritable). + Bind("/lib64", "/lib64", container.BindWritable). + Bind("/nix", "/nix", container.BindWritable). + Bind("/root", "/root", container.BindWritable). + Bind("/run", "/run", container.BindWritable). + Bind("/srv", "/srv", container.BindWritable). + Bind("/sys", "/sys", container.BindWritable). + Bind("/usr", "/usr", container.BindWritable). + Bind("/var", "/var", container.BindWritable). + Bind("/dev/dri", "/dev/dri", container.BindWritable|container.BindDevice|container.BindOptional). + Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional). Tmpfs("/run/user/1971", 8192, 0755). Tmpfs("/run/dbus", 8192, 0755). Etc("/etc", "ebf083d1b175911782d413369b64ce7c"). Tmpfs("/run/user", 4096, 0755). - Bind("/tmp/hakurei.1971/runtime/9", "/run/user/65534", hakurei.BindWritable). - Bind("/tmp/hakurei.1971/tmpdir/9", "/tmp", hakurei.BindWritable). - Bind("/home/chronos", "/home/chronos", hakurei.BindWritable). + Bind("/tmp/hakurei.1971/runtime/9", "/run/user/65534", container.BindWritable). + Bind("/tmp/hakurei.1971/tmpdir/9", "/tmp", container.BindWritable). + Bind("/home/chronos", "/home/chronos", container.BindWritable). Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). Place("/etc/group", []byte("hakurei:x:65534:\n")). Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0", 0). diff --git a/cmd/hakurei/internal/app/internal/setuid/app_test.go b/cmd/hakurei/internal/app/internal/setuid/app_test.go index 18f5ec35..f95f3711 100644 --- a/cmd/hakurei/internal/app/internal/setuid/app_test.go +++ b/cmd/hakurei/internal/app/internal/setuid/app_test.go @@ -7,9 +7,9 @@ import ( "testing" "time" - "git.gensokyo.uk/security/hakurei" "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/app" "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/app/internal/setuid" + "git.gensokyo.uk/security/hakurei/container" "git.gensokyo.uk/security/hakurei/hst" "git.gensokyo.uk/security/hakurei/internal/sys" "git.gensokyo.uk/security/hakurei/system" @@ -21,7 +21,7 @@ type sealTestCase struct { config *hst.Config id app.ID wantSys *system.I - wantContainer *hakurei.Params + wantContainer *container.Params } func TestApp(t *testing.T) { @@ -32,7 +32,7 @@ func TestApp(t *testing.T) { a := setuid.NewWithID(tc.id, tc.os) var ( gotSys *system.I - gotContainer *hakurei.Params + gotContainer *container.Params ) if !t.Run("seal", func(t *testing.T) { if sa, err := a.Seal(tc.config); err != nil { diff --git a/cmd/hakurei/internal/app/internal/setuid/export_test.go b/cmd/hakurei/internal/app/internal/setuid/export_test.go index 66d16a1c..812215eb 100644 --- a/cmd/hakurei/internal/app/internal/setuid/export_test.go +++ b/cmd/hakurei/internal/app/internal/setuid/export_test.go @@ -1,8 +1,8 @@ package setuid import ( - "git.gensokyo.uk/security/hakurei" . "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/app" + "git.gensokyo.uk/security/hakurei/container" "git.gensokyo.uk/security/hakurei/internal/sys" "git.gensokyo.uk/security/hakurei/system" ) @@ -14,7 +14,7 @@ func NewWithID(id ID, os sys.State) App { return a } -func AppIParams(a App, sa SealedApp) (*system.I, *hakurei.Params) { +func AppIParams(a App, sa SealedApp) (*system.I, *container.Params) { v := a.(*app) seal := sa.(*outcome) if v.outcome != seal || v.id != seal.id { diff --git a/cmd/hakurei/internal/app/internal/setuid/process.go b/cmd/hakurei/internal/app/internal/setuid/process.go index 50d17234..28b4211c 100644 --- a/cmd/hakurei/internal/app/internal/setuid/process.go +++ b/cmd/hakurei/internal/app/internal/setuid/process.go @@ -12,9 +12,9 @@ import ( "syscall" "time" - "git.gensokyo.uk/security/hakurei" . "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/app" "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/state" + "git.gensokyo.uk/security/hakurei/container" "git.gensokyo.uk/security/hakurei/internal" "git.gensokyo.uk/security/hakurei/internal/hlog" "git.gensokyo.uk/security/hakurei/system" @@ -94,7 +94,7 @@ func (seal *outcome) Run(rs *RunState) error { cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) } var e *gob.Encoder - if fd, encoder, err := hakurei.Setup(&cmd.ExtraFiles); err != nil { + if fd, encoder, err := container.Setup(&cmd.ExtraFiles); err != nil { return hlog.WrapErrSuffix(err, "cannot create shim setup pipe:") } else { diff --git a/cmd/hakurei/internal/app/internal/setuid/seal.go b/cmd/hakurei/internal/app/internal/setuid/seal.go index de17dcb5..6c301ff3 100644 --- a/cmd/hakurei/internal/app/internal/setuid/seal.go +++ b/cmd/hakurei/internal/app/internal/setuid/seal.go @@ -16,9 +16,9 @@ import ( "sync/atomic" "syscall" - "git.gensokyo.uk/security/hakurei" . "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/app" "git.gensokyo.uk/security/hakurei/cmd/hakurei/internal/app/instance/common" + "git.gensokyo.uk/security/hakurei/container" "git.gensokyo.uk/security/hakurei/hst" "git.gensokyo.uk/security/hakurei/internal" "git.gensokyo.uk/security/hakurei/internal/hlog" @@ -80,7 +80,7 @@ type outcome struct { sys *system.I ctx context.Context - container *hakurei.Params + container *container.Params env map[string]string sync *os.File @@ -334,7 +334,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co seal.sys.Ensure(runtimeDirInst, 0700) seal.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute) seal.container.Tmpfs("/run/user", 1<<12, 0755) - seal.container.Bind(runtimeDirInst, innerRuntimeDir, hakurei.BindWritable) + seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable) } { @@ -345,7 +345,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co seal.sys.Ensure(tmpdirInst, 01700) seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute) // mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp - seal.container.Bind(tmpdirInst, "/tmp", hakurei.BindWritable) + seal.container.Bind(tmpdirInst, "/tmp", container.BindWritable) } { @@ -357,7 +357,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if seal.user.username != "" { username = seal.user.username } - seal.container.Bind(seal.user.data, homeDir, hakurei.BindWritable) + seal.container.Bind(seal.user.data, homeDir, container.BindWritable) seal.container.Dir = homeDir seal.env["HOME"] = homeDir seal.env["USER"] = username diff --git a/cmd/hakurei/internal/app/internal/setuid/shim.go b/cmd/hakurei/internal/app/internal/setuid/shim.go index 0281f804..264ee0af 100644 --- a/cmd/hakurei/internal/app/internal/setuid/shim.go +++ b/cmd/hakurei/internal/app/internal/setuid/shim.go @@ -10,10 +10,10 @@ import ( "syscall" "time" - "git.gensokyo.uk/security/hakurei" + "git.gensokyo.uk/security/hakurei/container" + "git.gensokyo.uk/security/hakurei/container/seccomp" "git.gensokyo.uk/security/hakurei/internal" "git.gensokyo.uk/security/hakurei/internal/hlog" - "git.gensokyo.uk/security/hakurei/seccomp" ) /* @@ -74,7 +74,7 @@ type shimParams struct { Monitor int // finalised container params - Container *hakurei.Params + Container *container.Params // path to outer home directory Home string @@ -86,7 +86,7 @@ type shimParams struct { func ShimMain() { hlog.Prepare("shim") - if err := hakurei.SetDumpable(hakurei.SUID_DUMP_DISABLE); err != nil { + if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil { log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) } @@ -94,11 +94,11 @@ func ShimMain() { params shimParams closeSetup func() error ) - if f, err := hakurei.Receive(shimEnv, ¶ms, nil); err != nil { - if errors.Is(err, hakurei.ErrInvalid) { + if f, err := container.Receive(shimEnv, ¶ms, nil); err != nil { + if errors.Is(err, container.ErrInvalid) { log.Fatal("invalid config descriptor") } - if errors.Is(err, hakurei.ErrNotSet) { + if errors.Is(err, container.ErrNotSet) { log.Fatal("HAKUREI_SHIM not set") } @@ -149,17 +149,17 @@ func ShimMain() { } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() // unreachable - container := hakurei.New(ctx, name) - container.Params = *params.Container - container.Stdin, container.Stdout, container.Stderr = os.Stdin, os.Stdout, os.Stderr - container.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) } - container.WaitDelay = 2 * time.Second + z := container.New(ctx, name) + z.Params = *params.Container + z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr + z.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) } + z.WaitDelay = 2 * time.Second - if err := container.Start(); err != nil { + if err := z.Start(); err != nil { hlog.PrintBaseError(err, "cannot start container:") os.Exit(1) } - if err := container.Serve(); err != nil { + if err := z.Serve(); err != nil { hlog.PrintBaseError(err, "cannot configure container:") } @@ -170,7 +170,7 @@ func ShimMain() { log.Fatalf("cannot load syscall filter: %v", err) } - if err := container.Wait(); err != nil { + if err := z.Wait(); err != nil { var exitError *exec.ExitError if !errors.As(err, &exitError) { if errors.Is(err, context.Canceled) { diff --git a/cmd/hakurei/main.go b/cmd/hakurei/main.go index ef6b3e55..11259a07 100644 --- a/cmd/hakurei/main.go +++ b/cmd/hakurei/main.go @@ -9,7 +9,7 @@ import ( "log" "os" - "git.gensokyo.uk/security/hakurei" + "git.gensokyo.uk/security/hakurei/container" "git.gensokyo.uk/security/hakurei/internal" "git.gensokyo.uk/security/hakurei/internal/hlog" "git.gensokyo.uk/security/hakurei/internal/sys" @@ -28,9 +28,9 @@ var std sys.State = new(sys.Std) func main() { // early init path, skips root check and duplicate PR_SET_DUMPABLE - hakurei.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) + container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) - if err := hakurei.SetDumpable(hakurei.SUID_DUMP_DISABLE); err != nil { + if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil { log.Printf("cannot set SUID_DUMP_DISABLE: %s", err) // not fatal: this program runs as the privileged user } diff --git a/cmd/planterette/app.go b/cmd/planterette/app.go index a8adcc51..41e1e026 100644 --- a/cmd/planterette/app.go +++ b/cmd/planterette/app.go @@ -6,8 +6,8 @@ import ( "os" "path" + "git.gensokyo.uk/security/hakurei/container/seccomp" "git.gensokyo.uk/security/hakurei/hst" - "git.gensokyo.uk/security/hakurei/seccomp" "git.gensokyo.uk/security/hakurei/system" "git.gensokyo.uk/security/hakurei/system/dbus" ) diff --git a/cmd/planterette/with.go b/cmd/planterette/with.go index a2eb02e3..76e0861f 100644 --- a/cmd/planterette/with.go +++ b/cmd/planterette/with.go @@ -5,9 +5,9 @@ import ( "path" "strings" + "git.gensokyo.uk/security/hakurei/container/seccomp" "git.gensokyo.uk/security/hakurei/hst" "git.gensokyo.uk/security/hakurei/internal" - "git.gensokyo.uk/security/hakurei/seccomp" ) func withNixDaemon( diff --git a/container.go b/container.go deleted file mode 100644 index 5bbc5d3c..00000000 --- a/container.go +++ /dev/null @@ -1,229 +0,0 @@ -// Package hakurei implements unprivileged Linux containers with built-in support for syscall filtering. -package hakurei - -import ( - "context" - "encoding/gob" - "errors" - "fmt" - "io" - "os" - "os/exec" - "path" - "strconv" - . "syscall" - "time" - - "git.gensokyo.uk/security/hakurei/seccomp" -) - -type ( - // Container represents a container environment being prepared or run. - // None of [Container] methods are safe for concurrent use. - Container struct { - // Name of initial process in the container. - name string - // Cgroup fd, nil to disable. - Cgroup *int - // ExtraFiles passed through to initial process in the container, - // with behaviour identical to its [exec.Cmd] counterpart. - ExtraFiles []*os.File - - // Custom [exec.Cmd] initialisation function. - CommandContext func(ctx context.Context) (cmd *exec.Cmd) - - // param encoder for shim and init - setup *gob.Encoder - // cancels cmd - cancel context.CancelFunc - - Stdin io.Reader - Stdout io.Writer - Stderr io.Writer - - Cancel func(cmd *exec.Cmd) error - WaitDelay time.Duration - - cmd *exec.Cmd - ctx context.Context - Params - } - - // Params holds container configuration and is safe to serialise. - Params struct { - // Working directory in the container. - Dir string - // Initial process environment. - Env []string - // Absolute path of initial process in the container. Overrides name. - Path string - // Initial process argv. - Args []string - - // Mapped Uid in user namespace. - Uid int - // Mapped Gid in user namespace. - Gid int - // Hostname value in UTS namespace. - Hostname string - // Sequential container setup ops. - *Ops - // Seccomp system call filter rules. - SeccompRules []seccomp.NativeRule - // Extra seccomp flags. - SeccompFlags seccomp.ExportFlag - // Seccomp presets. Has no effect unless SeccompRules is zero-length. - SeccompPresets seccomp.FilterPreset - // Do not load seccomp program. - SeccompDisable bool - // Permission bits of newly created parent directories. - // The zero value is interpreted as 0755. - ParentPerm os.FileMode - // Do not syscall.Setsid. - RetainSession bool - // Do not [syscall.CLONE_NEWNET]. - HostNet bool - // Retain CAP_SYS_ADMIN. - Privileged bool - } -) - -func (p *Container) Start() error { - if p.cmd != nil { - return errors.New("sandbox: already started") - } - if p.Ops == nil || len(*p.Ops) == 0 { - return errors.New("sandbox: starting an empty container") - } - - ctx, cancel := context.WithCancel(p.ctx) - p.cancel = cancel - - var cloneFlags uintptr = CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP - if !p.HostNet { - cloneFlags |= CLONE_NEWNET - } - - // map to overflow id to work around ownership checks - if p.Uid < 1 { - p.Uid = OverflowUid() - } - if p.Gid < 1 { - p.Gid = OverflowGid() - } - - if !p.RetainSession { - p.SeccompPresets |= seccomp.PresetDenyTTY - } - - if p.CommandContext != nil { - p.cmd = p.CommandContext(ctx) - } else { - p.cmd = exec.CommandContext(ctx, MustExecutable()) - p.cmd.Args = []string{"init"} - } - - p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr - p.cmd.WaitDelay = p.WaitDelay - if p.Cancel != nil { - p.cmd.Cancel = func() error { return p.Cancel(p.cmd) } - } else { - p.cmd.Cancel = func() error { return p.cmd.Process.Signal(SIGTERM) } - } - p.cmd.Dir = "/" - p.cmd.SysProcAttr = &SysProcAttr{ - Setsid: !p.RetainSession, - Pdeathsig: SIGKILL, - Cloneflags: cloneFlags | CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS, - - // remain privileged for setup - AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP}, - - UseCgroupFD: p.Cgroup != nil, - } - if p.cmd.SysProcAttr.UseCgroupFD { - p.cmd.SysProcAttr.CgroupFD = *p.Cgroup - } - - // place setup pipe before user supplied extra files, this is later restored by init - if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil { - return wrapErrSuffix(err, - "cannot create shim setup pipe:") - } else { - p.setup = e - p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)} - } - p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...) - - msg.Verbose("starting container init") - if err := p.cmd.Start(); err != nil { - return msg.WrapErr(err, err.Error()) - } - return nil -} - -func (p *Container) Serve() error { - if p.setup == nil { - panic("invalid serve") - } - - setup := p.setup - p.setup = nil - - if p.Path != "" && !path.IsAbs(p.Path) { - p.cancel() - return msg.WrapErr(EINVAL, - fmt.Sprintf("invalid executable path %q", p.Path)) - } - - if p.Path == "" { - if p.name == "" { - p.Path = os.Getenv("SHELL") - if !path.IsAbs(p.Path) { - p.cancel() - return msg.WrapErr(EBADE, - "no command specified and $SHELL is invalid") - } - p.name = path.Base(p.Path) - } else if path.IsAbs(p.name) { - p.Path = p.name - } else if v, err := exec.LookPath(p.name); err != nil { - p.cancel() - return msg.WrapErr(err, err.Error()) - } else { - p.Path = v - } - } - - if p.SeccompRules == nil { - // do not transmit nil - p.SeccompRules = make([]seccomp.NativeRule, 0) - } - - err := setup.Encode( - &initParams{ - p.Params, - Getuid(), - Getgid(), - len(p.ExtraFiles), - msg.IsVerbose(), - }, - ) - if err != nil { - p.cancel() - } - return err -} - -func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() } - -func (p *Container) String() string { - return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x", - p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets)) -} - -func New(ctx context.Context, name string, args ...string) *Container { - return &Container{name: name, ctx: ctx, - Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)}, - } -} diff --git a/container/container.go b/container/container.go new file mode 100644 index 00000000..62aa16d7 --- /dev/null +++ b/container/container.go @@ -0,0 +1,229 @@ +// Package container implements unprivileged Linux containers with built-in support for syscall filtering. +package container + +import ( + "context" + "encoding/gob" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path" + "strconv" + . "syscall" + "time" + + "git.gensokyo.uk/security/hakurei/container/seccomp" +) + +type ( + // Container represents a container environment being prepared or run. + // None of [Container] methods are safe for concurrent use. + Container struct { + // Name of initial process in the container. + name string + // Cgroup fd, nil to disable. + Cgroup *int + // ExtraFiles passed through to initial process in the container, + // with behaviour identical to its [exec.Cmd] counterpart. + ExtraFiles []*os.File + + // Custom [exec.Cmd] initialisation function. + CommandContext func(ctx context.Context) (cmd *exec.Cmd) + + // param encoder for shim and init + setup *gob.Encoder + // cancels cmd + cancel context.CancelFunc + + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + + Cancel func(cmd *exec.Cmd) error + WaitDelay time.Duration + + cmd *exec.Cmd + ctx context.Context + Params + } + + // Params holds container configuration and is safe to serialise. + Params struct { + // Working directory in the container. + Dir string + // Initial process environment. + Env []string + // Absolute path of initial process in the container. Overrides name. + Path string + // Initial process argv. + Args []string + + // Mapped Uid in user namespace. + Uid int + // Mapped Gid in user namespace. + Gid int + // Hostname value in UTS namespace. + Hostname string + // Sequential container setup ops. + *Ops + // Seccomp system call filter rules. + SeccompRules []seccomp.NativeRule + // Extra seccomp flags. + SeccompFlags seccomp.ExportFlag + // Seccomp presets. Has no effect unless SeccompRules is zero-length. + SeccompPresets seccomp.FilterPreset + // Do not load seccomp program. + SeccompDisable bool + // Permission bits of newly created parent directories. + // The zero value is interpreted as 0755. + ParentPerm os.FileMode + // Do not syscall.Setsid. + RetainSession bool + // Do not [syscall.CLONE_NEWNET]. + HostNet bool + // Retain CAP_SYS_ADMIN. + Privileged bool + } +) + +func (p *Container) Start() error { + if p.cmd != nil { + return errors.New("sandbox: already started") + } + if p.Ops == nil || len(*p.Ops) == 0 { + return errors.New("sandbox: starting an empty container") + } + + ctx, cancel := context.WithCancel(p.ctx) + p.cancel = cancel + + var cloneFlags uintptr = CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP + if !p.HostNet { + cloneFlags |= CLONE_NEWNET + } + + // map to overflow id to work around ownership checks + if p.Uid < 1 { + p.Uid = OverflowUid() + } + if p.Gid < 1 { + p.Gid = OverflowGid() + } + + if !p.RetainSession { + p.SeccompPresets |= seccomp.PresetDenyTTY + } + + if p.CommandContext != nil { + p.cmd = p.CommandContext(ctx) + } else { + p.cmd = exec.CommandContext(ctx, MustExecutable()) + p.cmd.Args = []string{"init"} + } + + p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr + p.cmd.WaitDelay = p.WaitDelay + if p.Cancel != nil { + p.cmd.Cancel = func() error { return p.Cancel(p.cmd) } + } else { + p.cmd.Cancel = func() error { return p.cmd.Process.Signal(SIGTERM) } + } + p.cmd.Dir = "/" + p.cmd.SysProcAttr = &SysProcAttr{ + Setsid: !p.RetainSession, + Pdeathsig: SIGKILL, + Cloneflags: cloneFlags | CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS, + + // remain privileged for setup + AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP}, + + UseCgroupFD: p.Cgroup != nil, + } + if p.cmd.SysProcAttr.UseCgroupFD { + p.cmd.SysProcAttr.CgroupFD = *p.Cgroup + } + + // place setup pipe before user supplied extra files, this is later restored by init + if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil { + return wrapErrSuffix(err, + "cannot create shim setup pipe:") + } else { + p.setup = e + p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)} + } + p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...) + + msg.Verbose("starting container init") + if err := p.cmd.Start(); err != nil { + return msg.WrapErr(err, err.Error()) + } + return nil +} + +func (p *Container) Serve() error { + if p.setup == nil { + panic("invalid serve") + } + + setup := p.setup + p.setup = nil + + if p.Path != "" && !path.IsAbs(p.Path) { + p.cancel() + return msg.WrapErr(EINVAL, + fmt.Sprintf("invalid executable path %q", p.Path)) + } + + if p.Path == "" { + if p.name == "" { + p.Path = os.Getenv("SHELL") + if !path.IsAbs(p.Path) { + p.cancel() + return msg.WrapErr(EBADE, + "no command specified and $SHELL is invalid") + } + p.name = path.Base(p.Path) + } else if path.IsAbs(p.name) { + p.Path = p.name + } else if v, err := exec.LookPath(p.name); err != nil { + p.cancel() + return msg.WrapErr(err, err.Error()) + } else { + p.Path = v + } + } + + if p.SeccompRules == nil { + // do not transmit nil + p.SeccompRules = make([]seccomp.NativeRule, 0) + } + + err := setup.Encode( + &initParams{ + p.Params, + Getuid(), + Getgid(), + len(p.ExtraFiles), + msg.IsVerbose(), + }, + ) + if err != nil { + p.cancel() + } + return err +} + +func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() } + +func (p *Container) String() string { + return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x", + p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets)) +} + +func New(ctx context.Context, name string, args ...string) *Container { + return &Container{name: name, ctx: ctx, + Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)}, + } +} diff --git a/container/container_test.go b/container/container_test.go new file mode 100644 index 00000000..95f53619 --- /dev/null +++ b/container/container_test.go @@ -0,0 +1,281 @@ +package container_test + +import ( + "bytes" + "context" + "encoding/gob" + "log" + "os" + "os/exec" + "strings" + "syscall" + "testing" + "time" + + "git.gensokyo.uk/security/hakurei/container" + "git.gensokyo.uk/security/hakurei/container/seccomp" + "git.gensokyo.uk/security/hakurei/container/vfs" + "git.gensokyo.uk/security/hakurei/hst" + "git.gensokyo.uk/security/hakurei/internal" + "git.gensokyo.uk/security/hakurei/internal/hlog" + "git.gensokyo.uk/security/hakurei/ldd" +) + +const ( + ignore = "\x00" + ignoreV = -1 +) + +func TestContainer(t *testing.T) { + { + oldVerbose := hlog.Load() + oldOutput := container.GetOutput() + internal.InstallOutput(true) + t.Cleanup(func() { hlog.Store(oldVerbose) }) + t.Cleanup(func() { container.SetOutput(oldOutput) }) + } + + testCases := []struct { + name string + filter bool + session bool + net bool + ops *container.Ops + mnt []*vfs.MountInfoEntry + host string + rules []seccomp.NativeRule + flags seccomp.ExportFlag + presets seccomp.FilterPreset + }{ + {"minimal", true, false, false, + new(container.Ops), nil, "test-minimal", + nil, 0, seccomp.PresetStrict}, + {"allow", true, true, true, + new(container.Ops), nil, "test-minimal", + nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel}, + {"no filter", false, true, true, + new(container.Ops), nil, "test-no-filter", + nil, 0, seccomp.PresetExt}, + {"custom rules", true, true, true, + new(container.Ops), nil, "test-no-filter", + []seccomp.NativeRule{ + {seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}, + }, 0, seccomp.PresetExt}, + {"tmpfs", true, false, false, + new(container.Ops). + Tmpfs(hst.Tmp, 0, 0755), + []*vfs.MountInfoEntry{ + e("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), + }, "test-tmpfs", + nil, 0, seccomp.PresetStrict}, + {"dev", true, true /* go test output is not a tty */, false, + new(container.Ops). + Dev("/dev"). + Mqueue("/dev/mqueue"), + []*vfs.MountInfoEntry{ + e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), + e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), + e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), + e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), + e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), + e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), + e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), + e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), + e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), + }, "", + nil, 0, seccomp.PresetStrict}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + + c := container.New(ctx, "/usr/bin/sandbox.test", "-test.v", + "-test.run=TestHelperCheckContainer", "--", "check", tc.host) + c.Uid = 1000 + c.Gid = 100 + c.Hostname = tc.host + c.CommandContext = commandContext + c.Stdout, c.Stderr = os.Stdout, os.Stderr + c.Ops = tc.ops + c.SeccompRules = tc.rules + c.SeccompFlags = tc.flags | seccomp.AllowMultiarch + c.SeccompPresets = tc.presets + c.SeccompDisable = !tc.filter + c.RetainSession = tc.session + c.HostNet = tc.net + if c.Args[5] == "" { + if name, err := os.Hostname(); err != nil { + t.Fatalf("cannot get hostname: %v", err) + } else { + c.Args[5] = name + } + } + + c. + Tmpfs("/tmp", 0, 0755). + Bind(os.Args[0], os.Args[0], 0). + Mkdir("/usr/bin", 0755). + Link(os.Args[0], "/usr/bin/sandbox.test"). + Place("/etc/hostname", []byte(c.Args[5])) + // in case test has cgo enabled + var libPaths []string + if entries, err := ldd.ExecFilter(ctx, + commandContext, + func(v []byte) []byte { + return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1] + }, os.Args[0]); err != nil { + log.Fatalf("ldd: %v", err) + } else { + libPaths = ldd.Path(entries) + } + for _, name := range libPaths { + c.Bind(name, name, 0) + } + // needs /proc to check mountinfo + c.Proc("/proc") + + mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) + mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore)) + mnt = append(mnt, tc.mnt...) + mnt = append(mnt, + e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), + e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore), + e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore), + ) + for _, name := range libPaths { + mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore)) + } + mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw")) + want := new(bytes.Buffer) + if err := gob.NewEncoder(want).Encode(mnt); err != nil { + t.Fatalf("cannot serialise expected mount points: %v", err) + } + c.Stdin = want + + if err := c.Start(); err != nil { + hlog.PrintBaseError(err, "start:") + t.Fatalf("cannot start container: %v", err) + } else if err = c.Serve(); err != nil { + hlog.PrintBaseError(err, "serve:") + t.Errorf("cannot serve setup params: %v", err) + } + if err := c.Wait(); err != nil { + hlog.PrintBaseError(err, "wait:") + t.Fatalf("wait: %v", err) + } + }) + } +} + +func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry { + return &vfs.MountInfoEntry{ + ID: ignoreV, + Parent: ignoreV, + Devno: vfs.DevT{ignoreV, ignoreV}, + Root: root, + Target: target, + VfsOptstr: vfsOptstr, + OptFields: []string{ignore}, + FsType: fsType, + Source: source, + FsOptstr: fsOptstr, + } +} + +func TestContainerString(t *testing.T) { + c := container.New(t.Context(), "ldd", "/usr/bin/env") + c.SeccompFlags |= seccomp.AllowMultiarch + c.SeccompRules = seccomp.Preset( + seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY, + c.SeccompFlags) + c.SeccompPresets = seccomp.PresetStrict + want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf` + if got := c.String(); got != want { + t.Errorf("String: %s, want %s", got, want) + } +} + +func TestHelperInit(t *testing.T) { + if len(os.Args) != 5 || os.Args[4] != "init" { + return + } + container.SetOutput(hlog.Output{}) + container.Init(hlog.Prepare, internal.InstallOutput) +} + +func TestHelperCheckContainer(t *testing.T) { + if len(os.Args) != 6 || os.Args[4] != "check" { + return + } + + t.Run("user", func(t *testing.T) { + if uid := syscall.Getuid(); uid != 1000 { + t.Errorf("Getuid: %d, want 1000", uid) + } + if gid := syscall.Getgid(); gid != 100 { + t.Errorf("Getgid: %d, want 100", gid) + } + }) + t.Run("hostname", func(t *testing.T) { + if name, err := os.Hostname(); err != nil { + t.Fatalf("cannot get hostname: %v", err) + } else if name != os.Args[5] { + t.Errorf("Hostname: %q, want %q", name, os.Args[5]) + } + + if p, err := os.ReadFile("/etc/hostname"); err != nil { + t.Fatalf("%v", err) + } else if string(p) != os.Args[5] { + t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5]) + } + }) + t.Run("mount", func(t *testing.T) { + var mnt []*vfs.MountInfoEntry + if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil { + t.Fatalf("cannot receive expected mount points: %v", err) + } + + var d *vfs.MountInfoDecoder + if f, err := os.Open("/proc/self/mountinfo"); err != nil { + t.Fatalf("cannot open mountinfo: %v", err) + } else { + d = vfs.NewMountInfoDecoder(f) + } + + i := 0 + for cur := range d.Entries() { + if i == len(mnt) { + t.Errorf("got more than %d entries", len(mnt)) + break + } + + // ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags + cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime") + cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime") + mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime") + mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime") + + if !cur.EqualWithIgnore(mnt[i], "\x00") { + t.Errorf("[FAIL] %s", cur) + } else { + t.Logf("[ OK ] %s", cur) + } + + i++ + } + if err := d.Err(); err != nil { + t.Errorf("cannot parse mountinfo: %v", err) + } + + if i != len(mnt) { + t.Errorf("got %d entries, want %d", i, len(mnt)) + } + }) +} + +func commandContext(ctx context.Context) *exec.Cmd { + return exec.CommandContext(ctx, os.Args[0], "-test.v", + "-test.run=TestHelperInit", "--", "init") +} diff --git a/container/executable.go b/container/executable.go new file mode 100644 index 00000000..d19d56f5 --- /dev/null +++ b/container/executable.go @@ -0,0 +1,26 @@ +package container + +import ( + "log" + "os" + "sync" +) + +var ( + executable string + executableOnce sync.Once +) + +func copyExecutable() { + if name, err := os.Executable(); err != nil { + msg.BeforeExit() + log.Fatalf("cannot read executable path: %v", err) + } else { + executable = name + } +} + +func MustExecutable() string { + executableOnce.Do(copyExecutable) + return executable +} diff --git a/container/executable_test.go b/container/executable_test.go new file mode 100644 index 00000000..00e67b79 --- /dev/null +++ b/container/executable_test.go @@ -0,0 +1,17 @@ +package container_test + +import ( + "os" + "testing" + + "git.gensokyo.uk/security/hakurei/container" +) + +func TestExecutable(t *testing.T) { + for i := 0; i < 16; i++ { + if got := container.MustExecutable(); got != os.Args[0] { + t.Errorf("MustExecutable: %q, want %q", + got, os.Args[0]) + } + } +} diff --git a/container/init.go b/container/init.go new file mode 100644 index 00000000..18d38c35 --- /dev/null +++ b/container/init.go @@ -0,0 +1,364 @@ +package container + +import ( + "errors" + "fmt" + "log" + "os" + "os/exec" + "os/signal" + "path" + "runtime" + "strconv" + . "syscall" + "time" + + "git.gensokyo.uk/security/hakurei/container/seccomp" +) + +const ( + // time to wait for linger processes after death of initial process + residualProcessTimeout = 5 * time.Second + + // intermediate tmpfs mount point + basePath = "/tmp" + + // setup params file descriptor + setupEnv = "HAKUREI_SETUP" +) + +type initParams struct { + Params + + HostUid, HostGid int + // extra files count + Count int + // verbosity pass through + Verbose bool +} + +func Init(prepare func(prefix string), setVerbose func(verbose bool)) { + runtime.LockOSThread() + prepare("init") + + if os.Getpid() != 1 { + log.Fatal("this process must run as pid 1") + } + + var ( + params initParams + closeSetup func() error + setupFile *os.File + offsetSetup int + ) + if f, err := Receive(setupEnv, ¶ms, &setupFile); err != nil { + if errors.Is(err, ErrInvalid) { + log.Fatal("invalid setup descriptor") + } + if errors.Is(err, ErrNotSet) { + log.Fatal("HAKUREI_SETUP not set") + } + + log.Fatalf("cannot decode init setup payload: %v", err) + } else { + if params.Ops == nil { + log.Fatal("invalid setup parameters") + } + if params.ParentPerm == 0 { + params.ParentPerm = 0755 + } + + setVerbose(params.Verbose) + msg.Verbose("received setup parameters") + closeSetup = f + offsetSetup = int(setupFile.Fd() + 1) + } + + // write uid/gid map here so parent does not need to set dumpable + if err := SetDumpable(SUID_DUMP_USER); err != nil { + log.Fatalf("cannot set SUID_DUMP_USER: %s", err) + } + if err := os.WriteFile("/proc/self/uid_map", + append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...), + 0); err != nil { + log.Fatalf("%v", err) + } + if err := os.WriteFile("/proc/self/setgroups", + []byte("deny\n"), + 0); err != nil && !os.IsNotExist(err) { + log.Fatalf("%v", err) + } + if err := os.WriteFile("/proc/self/gid_map", + append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...), + 0); err != nil { + log.Fatalf("%v", err) + } + if err := SetDumpable(SUID_DUMP_DISABLE); err != nil { + log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) + } + + oldmask := Umask(0) + if params.Hostname != "" { + if err := Sethostname([]byte(params.Hostname)); err != nil { + log.Fatalf("cannot set hostname: %v", err) + } + } + + // cache sysctl before pivot_root + LastCap() + + if err := Mount("", "/", "", MS_SILENT|MS_SLAVE|MS_REC, ""); err != nil { + log.Fatalf("cannot make / rslave: %v", err) + } + + for i, op := range *params.Ops { + if op == nil { + log.Fatalf("invalid op %d", i) + } + + if err := op.early(¶ms.Params); err != nil { + msg.PrintBaseErr(err, + fmt.Sprintf("cannot prepare op %d:", i)) + msg.BeforeExit() + os.Exit(1) + } + } + + if err := Mount("rootfs", basePath, "tmpfs", MS_NODEV|MS_NOSUID, ""); err != nil { + log.Fatalf("cannot mount intermediate root: %v", err) + } + if err := os.Chdir(basePath); err != nil { + log.Fatalf("cannot enter base path: %v", err) + } + + if err := os.Mkdir(sysrootDir, 0755); err != nil { + log.Fatalf("%v", err) + } + if err := Mount(sysrootDir, sysrootDir, "", MS_SILENT|MS_MGC_VAL|MS_BIND|MS_REC, ""); err != nil { + log.Fatalf("cannot bind sysroot: %v", err) + } + + if err := os.Mkdir(hostDir, 0755); err != nil { + log.Fatalf("%v", err) + } + // pivot_root uncovers basePath in hostDir + if err := PivotRoot(basePath, hostDir); err != nil { + log.Fatalf("cannot pivot into intermediate root: %v", err) + } + if err := os.Chdir("/"); err != nil { + log.Fatalf("%v", err) + } + + for i, op := range *params.Ops { + // ops already checked during early setup + msg.Verbosef("%s %s", op.prefix(), op) + if err := op.apply(¶ms.Params); err != nil { + msg.PrintBaseErr(err, + fmt.Sprintf("cannot apply op %d:", i)) + msg.BeforeExit() + os.Exit(1) + } + } + + // setup requiring host root complete at this point + if err := Mount(hostDir, hostDir, "", MS_SILENT|MS_REC|MS_PRIVATE, ""); err != nil { + log.Fatalf("cannot make host root rprivate: %v", err) + } + if err := Unmount(hostDir, MNT_DETACH); err != nil { + log.Fatalf("cannot unmount host root: %v", err) + } + + { + var fd int + if err := IgnoringEINTR(func() (err error) { + fd, err = Open("/", O_DIRECTORY|O_RDONLY, 0) + return + }); err != nil { + log.Fatalf("cannot open intermediate root: %v", err) + } + if err := os.Chdir(sysrootPath); err != nil { + log.Fatalf("%v", err) + } + + if err := PivotRoot(".", "."); err != nil { + log.Fatalf("cannot pivot into sysroot: %v", err) + } + if err := Fchdir(fd); err != nil { + log.Fatalf("cannot re-enter intermediate root: %v", err) + } + if err := Unmount(".", MNT_DETACH); err != nil { + log.Fatalf("cannot unmount intemediate root: %v", err) + } + if err := os.Chdir("/"); err != nil { + log.Fatalf("%v", err) + } + + if err := Close(fd); err != nil { + log.Fatalf("cannot close intermediate root: %v", err) + } + } + + if _, _, errno := Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 { + log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", errno) + } + + if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 { + log.Fatalf("cannot clear the ambient capability set: %v", errno) + } + for i := uintptr(0); i <= LastCap(); i++ { + if params.Privileged && i == CAP_SYS_ADMIN { + continue + } + if _, _, errno := Syscall(SYS_PRCTL, PR_CAPBSET_DROP, i, 0); errno != 0 { + log.Fatalf("cannot drop capability from bonding set: %v", errno) + } + } + + var keep [2]uint32 + if params.Privileged { + keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN) + + if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_SYS_ADMIN); errno != 0 { + log.Fatalf("cannot raise CAP_SYS_ADMIN: %v", errno) + } + } + if err := capset( + &capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, + &[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}}, + ); err != nil { + log.Fatalf("cannot capset: %v", err) + } + + if !params.SeccompDisable { + rules := params.SeccompRules + if len(rules) == 0 { // non-empty rules slice always overrides presets + msg.Verbosef("resolving presets %#x", params.SeccompPresets) + rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags) + } + if err := seccomp.Load(rules, params.SeccompFlags); err != nil { + log.Fatalf("cannot load syscall filter: %v", err) + } + msg.Verbosef("%d filter rules loaded", len(rules)) + } else { + msg.Verbose("syscall filter not configured") + } + + extraFiles := make([]*os.File, params.Count) + for i := range extraFiles { + // setup fd is placed before all extra files + extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i)) + } + Umask(oldmask) + + cmd := exec.Command(params.Path) + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + cmd.Args = params.Args + cmd.Env = params.Env + cmd.ExtraFiles = extraFiles + cmd.Dir = params.Dir + + if err := cmd.Start(); err != nil { + log.Fatalf("%v", err) + } + msg.Suspend() + + if err := closeSetup(); err != nil { + log.Println("cannot close setup pipe:", err) + // not fatal + } + + type winfo struct { + wpid int + wstatus WaitStatus + } + info := make(chan winfo, 1) + done := make(chan struct{}) + + go func() { + var ( + err error + wpid = -2 + wstatus WaitStatus + ) + + // keep going until no child process is left + for wpid != -1 { + if err != nil { + break + } + + if wpid != -2 { + info <- winfo{wpid, wstatus} + } + + err = EINTR + for errors.Is(err, EINTR) { + wpid, err = Wait4(-1, &wstatus, 0, nil) + } + } + if !errors.Is(err, ECHILD) { + log.Println("unexpected wait4 response:", err) + } + + close(done) + }() + + // handle signals to dump withheld messages + sig := make(chan os.Signal, 2) + signal.Notify(sig, SIGINT, SIGTERM) + + // closed after residualProcessTimeout has elapsed after initial process death + timeout := make(chan struct{}) + + r := 2 + for { + select { + case s := <-sig: + if msg.Resume() { + msg.Verbosef("terminating on %s after process start", s.String()) + } else { + msg.Verbosef("terminating on %s", s.String()) + } + os.Exit(0) + case w := <-info: + if w.wpid == cmd.Process.Pid { + // initial process exited, output is most likely available again + msg.Resume() + + switch { + case w.wstatus.Exited(): + r = w.wstatus.ExitStatus() + msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus()) + case w.wstatus.Signaled(): + r = 128 + int(w.wstatus.Signal()) + msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal()) + default: + r = 255 + msg.Verbosef("initial process exited with status %#x", w.wstatus) + } + + go func() { + time.Sleep(residualProcessTimeout) + close(timeout) + }() + } + case <-done: + msg.BeforeExit() + os.Exit(r) + case <-timeout: + log.Println("timeout exceeded waiting for lingering processes") + msg.BeforeExit() + os.Exit(r) + } + } +} + +// TryArgv0 calls [Init] if the last element of argv0 is "init". +func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) { + if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" { + msg = v + Init(prepare, setVerbose) + msg.BeforeExit() + os.Exit(0) + } +} diff --git a/container/mount.go b/container/mount.go new file mode 100644 index 00000000..eddec2f6 --- /dev/null +++ b/container/mount.go @@ -0,0 +1,123 @@ +package container + +import ( + "errors" + "fmt" + "os" + "path/filepath" + . "syscall" + + "git.gensokyo.uk/security/hakurei/container/vfs" +) + +func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error { + if eq { + msg.Verbosef("resolved %q flags %#x", target, flags) + } else { + msg.Verbosef("resolved %q on %q flags %#x", source, target, flags) + } + + if err := Mount(source, target, "", MS_SILENT|MS_BIND|flags&MS_REC, ""); err != nil { + return wrapErrSuffix(err, + fmt.Sprintf("cannot mount %q on %q:", source, target)) + } + + var targetFinal string + if v, err := filepath.EvalSymlinks(target); err != nil { + return wrapErrSelf(err) + } else { + targetFinal = v + if targetFinal != target { + msg.Verbosef("target resolves to %q", targetFinal) + } + } + + // final target path according to the kernel through proc + var targetKFinal string + { + var destFd int + if err := IgnoringEINTR(func() (err error) { + destFd, err = Open(targetFinal, O_PATH|O_CLOEXEC, 0) + return + }); err != nil { + return wrapErrSuffix(err, + fmt.Sprintf("cannot open %q:", targetFinal)) + } + if v, err := os.Readlink(p.fd(destFd)); err != nil { + return wrapErrSelf(err) + } else if err = Close(destFd); err != nil { + return wrapErrSuffix(err, + fmt.Sprintf("cannot close %q:", targetFinal)) + } else { + targetKFinal = v + } + } + + mf := MS_NOSUID | flags&MS_NODEV | flags&MS_RDONLY + return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error { + n, err := d.Unfold(targetKFinal) + if err != nil { + if errors.Is(err, ESTALE) { + return msg.WrapErr(err, + fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal)) + } + return wrapErrSuffix(err, + "cannot unfold mount hierarchy:") + } + + if err = remountWithFlags(n, mf); err != nil { + return err + } + if flags&MS_REC == 0 { + return nil + } + + for cur := range n.Collective() { + err = remountWithFlags(cur, mf) + if err != nil && !errors.Is(err, EACCES) { + return err + } + } + + return nil + }) +} + +func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error { + kf, unmatched := n.Flags() + if len(unmatched) != 0 { + msg.Verbosef("unmatched vfs options: %q", unmatched) + } + + if kf&mf != mf { + return wrapErrSuffix( + Mount("none", n.Clean, "", MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, ""), + fmt.Sprintf("cannot remount %q:", n.Clean)) + } + return nil +} + +func mountTmpfs(fsname, name string, size int, perm os.FileMode) error { + target := toSysroot(name) + if err := os.MkdirAll(target, parentPerm(perm)); err != nil { + return wrapErrSelf(err) + } + opt := fmt.Sprintf("mode=%#o", perm) + if size > 0 { + opt += fmt.Sprintf(",size=%d", size) + } + return wrapErrSuffix( + Mount(fsname, target, "tmpfs", MS_NOSUID|MS_NODEV, opt), + fmt.Sprintf("cannot mount tmpfs on %q:", name)) +} + +func parentPerm(perm os.FileMode) os.FileMode { + pperm := 0755 + if perm&0070 == 0 { + pperm &= ^0050 + } + if perm&0007 == 0 { + pperm &= ^0005 + } + return os.FileMode(pperm) +} diff --git a/container/msg.go b/container/msg.go new file mode 100644 index 00000000..3f29e99b --- /dev/null +++ b/container/msg.go @@ -0,0 +1,43 @@ +package container + +import ( + "log" + "sync/atomic" +) + +type Msg interface { + IsVerbose() bool + Verbose(v ...any) + Verbosef(format string, v ...any) + WrapErr(err error, a ...any) error + PrintBaseErr(err error, fallback string) + + Suspend() + Resume() bool + + BeforeExit() +} + +type DefaultMsg struct{ inactive atomic.Bool } + +func (msg *DefaultMsg) IsVerbose() bool { return true } +func (msg *DefaultMsg) Verbose(v ...any) { + if !msg.inactive.Load() { + log.Println(v...) + } +} +func (msg *DefaultMsg) Verbosef(format string, v ...any) { + if !msg.inactive.Load() { + log.Printf(format, v...) + } +} + +func (msg *DefaultMsg) WrapErr(err error, a ...any) error { + log.Println(a...) + return err +} +func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) } + +func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) } +func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) } +func (msg *DefaultMsg) BeforeExit() {} diff --git a/container/ops.go b/container/ops.go new file mode 100644 index 00000000..236d1ac3 --- /dev/null +++ b/container/ops.go @@ -0,0 +1,482 @@ +package container + +import ( + "encoding/gob" + "fmt" + "math" + "os" + "path" + "path/filepath" + "slices" + "strings" + . "syscall" + "unsafe" +) + +type ( + Ops []Op + Op interface { + // early is called in host root. + early(params *Params) error + // apply is called in intermediate root. + apply(params *Params) error + + prefix() string + Is(op Op) bool + fmt.Stringer + } +) + +func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) } + +func init() { gob.Register(new(BindMountOp)) } + +// BindMountOp bind mounts host path Source on container path Target. +type BindMountOp struct { + Source, SourceFinal, Target string + + Flags int +} + +const ( + BindOptional = 1 << iota + BindWritable + BindDevice +) + +func (b *BindMountOp) early(*Params) error { + if !path.IsAbs(b.Source) { + return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", b.Source)) + } + + if v, err := filepath.EvalSymlinks(b.Source); err != nil { + if os.IsNotExist(err) && b.Flags&BindOptional != 0 { + b.SourceFinal = "\x00" + return nil + } + return wrapErrSelf(err) + } else { + b.SourceFinal = v + return nil + } +} + +func (b *BindMountOp) apply(*Params) error { + if b.SourceFinal == "\x00" { + if b.Flags&BindOptional == 0 { + // unreachable + return EBADE + } + return nil + } + + if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) { + return msg.WrapErr(EBADE, "path is not absolute") + } + + source := toHost(b.SourceFinal) + target := toSysroot(b.Target) + + // this perm value emulates bwrap behaviour as it clears bits from 0755 based on + // op->perms which is never set for any bind setup op so always results in 0700 + if fi, err := os.Stat(source); err != nil { + return wrapErrSelf(err) + } else if fi.IsDir() { + if err = os.MkdirAll(target, 0700); err != nil { + return wrapErrSelf(err) + } + } else if err = ensureFile(target, 0444, 0700); err != nil { + return err + } + + var flags uintptr = MS_REC + if b.Flags&BindWritable == 0 { + flags |= MS_RDONLY + } + if b.Flags&BindDevice == 0 { + flags |= MS_NODEV + } + + return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target) +} + +func (b *BindMountOp) Is(op Op) bool { vb, ok := op.(*BindMountOp); return ok && *b == *vb } +func (*BindMountOp) prefix() string { return "mounting" } +func (b *BindMountOp) String() string { + if b.Source == b.Target { + return fmt.Sprintf("%q flags %#x", b.Source, b.Flags) + } + return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable) +} +func (f *Ops) Bind(source, target string, flags int) *Ops { + *f = append(*f, &BindMountOp{source, "", target, flags}) + return f +} + +func init() { gob.Register(new(MountProcOp)) } + +// MountProcOp mounts a private instance of proc. +type MountProcOp string + +func (p MountProcOp) early(*Params) error { return nil } +func (p MountProcOp) apply(params *Params) error { + v := string(p) + + if !path.IsAbs(v) { + return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v)) + } + + target := toSysroot(v) + if err := os.MkdirAll(target, params.ParentPerm); err != nil { + return wrapErrSelf(err) + } + return wrapErrSuffix(Mount("proc", target, "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""), + fmt.Sprintf("cannot mount proc on %q:", v)) +} + +func (p MountProcOp) Is(op Op) bool { vp, ok := op.(MountProcOp); return ok && p == vp } +func (MountProcOp) prefix() string { return "mounting" } +func (p MountProcOp) String() string { return fmt.Sprintf("proc on %q", string(p)) } +func (f *Ops) Proc(dest string) *Ops { + *f = append(*f, MountProcOp(dest)) + return f +} + +func init() { gob.Register(new(MountDevOp)) } + +// MountDevOp mounts part of host dev. +type MountDevOp string + +func (d MountDevOp) early(*Params) error { return nil } +func (d MountDevOp) apply(params *Params) error { + v := string(d) + + if !path.IsAbs(v) { + return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v)) + } + target := toSysroot(v) + + if err := mountTmpfs("devtmpfs", v, 0, params.ParentPerm); err != nil { + return err + } + + for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} { + targetPath := toSysroot(path.Join(v, name)) + if err := ensureFile(targetPath, 0444, params.ParentPerm); err != nil { + return err + } + if err := hostProc.bindMount( + toHost("/dev/"+name), + targetPath, + 0, + true, + ); err != nil { + return err + } + } + for i, name := range []string{"stdin", "stdout", "stderr"} { + if err := os.Symlink( + "/proc/self/fd/"+string(rune(i+'0')), + path.Join(target, name), + ); err != nil { + return wrapErrSelf(err) + } + } + for _, pair := range [][2]string{ + {"/proc/self/fd", "fd"}, + {"/proc/kcore", "core"}, + {"pts/ptmx", "ptmx"}, + } { + if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil { + return wrapErrSelf(err) + } + } + + devPtsPath := path.Join(target, "pts") + for _, name := range []string{path.Join(target, "shm"), devPtsPath} { + if err := os.Mkdir(name, params.ParentPerm); err != nil { + return wrapErrSelf(err) + } + } + + if err := Mount("devpts", devPtsPath, "devpts", MS_NOSUID|MS_NOEXEC, + "newinstance,ptmxmode=0666,mode=620"); err != nil { + return wrapErrSuffix(err, + fmt.Sprintf("cannot mount devpts on %q:", devPtsPath)) + } + + if params.RetainSession { + var buf [8]byte + if _, _, errno := Syscall(SYS_IOCTL, 1, TIOCGWINSZ, uintptr(unsafe.Pointer(&buf[0]))); errno == 0 { + consolePath := toSysroot(path.Join(v, "console")) + if err := ensureFile(consolePath, 0444, params.ParentPerm); err != nil { + return err + } + if name, err := os.Readlink(hostProc.stdout()); err != nil { + return wrapErrSelf(err) + } else if err = hostProc.bindMount( + toHost(name), + consolePath, + 0, + false, + ); err != nil { + return err + } + } + } + + return nil +} + +func (d MountDevOp) Is(op Op) bool { vd, ok := op.(MountDevOp); return ok && d == vd } +func (MountDevOp) prefix() string { return "mounting" } +func (d MountDevOp) String() string { return fmt.Sprintf("dev on %q", string(d)) } +func (f *Ops) Dev(dest string) *Ops { + *f = append(*f, MountDevOp(dest)) + return f +} + +func init() { gob.Register(new(MountMqueueOp)) } + +// MountMqueueOp mounts a private mqueue instance on container Path. +type MountMqueueOp string + +func (m MountMqueueOp) early(*Params) error { return nil } +func (m MountMqueueOp) apply(params *Params) error { + v := string(m) + + if !path.IsAbs(v) { + return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v)) + } + + target := toSysroot(v) + if err := os.MkdirAll(target, params.ParentPerm); err != nil { + return wrapErrSelf(err) + } + return wrapErrSuffix(Mount("mqueue", target, "mqueue", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""), + fmt.Sprintf("cannot mount mqueue on %q:", v)) +} + +func (m MountMqueueOp) Is(op Op) bool { vm, ok := op.(MountMqueueOp); return ok && m == vm } +func (MountMqueueOp) prefix() string { return "mounting" } +func (m MountMqueueOp) String() string { return fmt.Sprintf("mqueue on %q", string(m)) } +func (f *Ops) Mqueue(dest string) *Ops { + *f = append(*f, MountMqueueOp(dest)) + return f +} + +func init() { gob.Register(new(MountTmpfsOp)) } + +// MountTmpfsOp mounts tmpfs on container Path. +type MountTmpfsOp struct { + Path string + Size int + Perm os.FileMode +} + +func (t *MountTmpfsOp) early(*Params) error { return nil } +func (t *MountTmpfsOp) apply(*Params) error { + if !path.IsAbs(t.Path) { + return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) + } + if t.Size < 0 || t.Size > math.MaxUint>>1 { + return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size)) + } + return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm) +} + +func (t *MountTmpfsOp) Is(op Op) bool { vt, ok := op.(*MountTmpfsOp); return ok && *t == *vt } +func (*MountTmpfsOp) prefix() string { return "mounting" } +func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) } +func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops { + *f = append(*f, &MountTmpfsOp{dest, size, perm}) + return f +} + +func init() { gob.Register(new(SymlinkOp)) } + +// SymlinkOp creates a symlink in the container filesystem. +type SymlinkOp [2]string + +func (l *SymlinkOp) early(*Params) error { + if strings.HasPrefix(l[0], "*") { + l[0] = l[0][1:] + if !path.IsAbs(l[0]) { + return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[0])) + } + if name, err := os.Readlink(l[0]); err != nil { + return wrapErrSelf(err) + } else { + l[0] = name + } + } + return nil +} +func (l *SymlinkOp) apply(params *Params) error { + // symlink target is an arbitrary path value, so only validate link name here + if !path.IsAbs(l[1]) { + return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[1])) + } + + target := toSysroot(l[1]) + if err := os.MkdirAll(path.Dir(target), params.ParentPerm); err != nil { + return wrapErrSelf(err) + } + if err := os.Symlink(l[0], target); err != nil { + return wrapErrSelf(err) + } + return nil +} + +func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl } +func (*SymlinkOp) prefix() string { return "creating" } +func (l *SymlinkOp) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) } +func (f *Ops) Link(target, linkName string) *Ops { + *f = append(*f, &SymlinkOp{target, linkName}) + return f +} + +func init() { gob.Register(new(MkdirOp)) } + +// MkdirOp creates a directory in the container filesystem. +type MkdirOp struct { + Path string + Perm os.FileMode +} + +func (m *MkdirOp) early(*Params) error { return nil } +func (m *MkdirOp) apply(*Params) error { + if !path.IsAbs(m.Path) { + return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", m.Path)) + } + + if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil { + return wrapErrSelf(err) + } + return nil +} + +func (m *MkdirOp) Is(op Op) bool { vm, ok := op.(*MkdirOp); return ok && m == vm } +func (*MkdirOp) prefix() string { return "creating" } +func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) } +func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops { + *f = append(*f, &MkdirOp{dest, perm}) + return f +} + +func init() { gob.Register(new(TmpfileOp)) } + +// TmpfileOp places a file in container Path containing Data. +type TmpfileOp struct { + Path string + Data []byte +} + +func (t *TmpfileOp) early(*Params) error { return nil } +func (t *TmpfileOp) apply(params *Params) error { + if !path.IsAbs(t.Path) { + return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) + } + + var tmpPath string + if f, err := os.CreateTemp("/", "tmp.*"); err != nil { + return wrapErrSelf(err) + } else if _, err = f.Write(t.Data); err != nil { + return wrapErrSuffix(err, + "cannot write to intermediate file:") + } else if err = f.Close(); err != nil { + return wrapErrSuffix(err, + "cannot close intermediate file:") + } else { + tmpPath = f.Name() + } + + target := toSysroot(t.Path) + if err := ensureFile(target, 0444, params.ParentPerm); err != nil { + return err + } else if err = hostProc.bindMount( + tmpPath, + target, + MS_RDONLY|MS_NODEV, + false, + ); err != nil { + return err + } else if err = os.Remove(tmpPath); err != nil { + return wrapErrSelf(err) + } + return nil +} + +func (t *TmpfileOp) Is(op Op) bool { + vt, ok := op.(*TmpfileOp) + return ok && t.Path == vt.Path && slices.Equal(t.Data, vt.Data) +} +func (*TmpfileOp) prefix() string { return "placing" } +func (t *TmpfileOp) String() string { + return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data)) +} +func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &TmpfileOp{name, data}); return f } +func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops { + t := &TmpfileOp{Path: name} + *dataP = &t.Data + + *f = append(*f, t) + return f +} + +func init() { gob.Register(new(AutoEtcOp)) } + +// AutoEtcOp expands host /etc into a toplevel symlink mirror with /etc semantics. +// This is not a generic setup op. It is implemented here to reduce ipc overhead. +type AutoEtcOp struct{ Prefix string } + +func (e *AutoEtcOp) early(*Params) error { return nil } +func (e *AutoEtcOp) apply(*Params) error { + const target = sysrootPath + "/etc/" + rel := e.hostRel() + "/" + + if err := os.MkdirAll(target, 0755); err != nil { + return wrapErrSelf(err) + } + if d, err := os.ReadDir(toSysroot(e.hostPath())); err != nil { + return wrapErrSelf(err) + } else { + for _, ent := range d { + n := ent.Name() + switch n { + case ".host": + + case "passwd": + case "group": + + case "mtab": + if err = os.Symlink("/proc/mounts", target+n); err != nil { + return wrapErrSelf(err) + } + + default: + if err = os.Symlink(rel+n, target+n); err != nil { + return wrapErrSelf(err) + } + } + } + } + + return nil +} +func (e *AutoEtcOp) hostPath() string { return "/etc/" + e.hostRel() } +func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix } + +func (e *AutoEtcOp) Is(op Op) bool { + ve, ok := op.(*AutoEtcOp) + return ok && ((e == nil && ve == nil) || (e != nil && ve != nil && *e == *ve)) +} +func (*AutoEtcOp) prefix() string { return "setting up" } +func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) } +func (f *Ops) Etc(host, prefix string) *Ops { + e := &AutoEtcOp{prefix} + f.Mkdir("/etc", 0755) + f.Bind(host, e.hostPath(), 0) + *f = append(*f, e) + return f +} diff --git a/container/output.go b/container/output.go new file mode 100644 index 00000000..6bedd55b --- /dev/null +++ b/container/output.go @@ -0,0 +1,26 @@ +package container + +var msg Msg = new(DefaultMsg) + +func GetOutput() Msg { return msg } +func SetOutput(v Msg) { + if v == nil { + msg = new(DefaultMsg) + } else { + msg = v + } +} + +func wrapErrSuffix(err error, a ...any) error { + if err == nil { + return nil + } + return msg.WrapErr(err, append(a, err)...) +} + +func wrapErrSelf(err error) error { + if err == nil { + return nil + } + return msg.WrapErr(err, err.Error()) +} diff --git a/container/params.go b/container/params.go new file mode 100644 index 00000000..e4328a7b --- /dev/null +++ b/container/params.go @@ -0,0 +1,47 @@ +package container + +import ( + "encoding/gob" + "errors" + "os" + "strconv" +) + +var ( + ErrNotSet = errors.New("environment variable not set") + ErrInvalid = errors.New("bad file descriptor") +) + +// Setup appends the read end of a pipe for setup params transmission and returns its fd. +func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) { + if r, w, err := os.Pipe(); err != nil { + return -1, nil, err + } else { + fd := 3 + len(*extraFiles) + *extraFiles = append(*extraFiles, r) + return fd, gob.NewEncoder(w), nil + } +} + +// Receive retrieves setup fd from the environment and receives params. +func Receive(key string, e any, v **os.File) (func() error, error) { + var setup *os.File + + if s, ok := os.LookupEnv(key); !ok { + return nil, ErrNotSet + } else { + if fd, err := strconv.Atoi(s); err != nil { + return nil, err + } else { + setup = os.NewFile(uintptr(fd), "setup") + if setup == nil { + return nil, ErrInvalid + } + if v != nil { + *v = setup + } + } + } + + return setup.Close, gob.NewDecoder(setup).Decode(e) +} diff --git a/container/path.go b/container/path.go new file mode 100644 index 00000000..a3938ff5 --- /dev/null +++ b/container/path.go @@ -0,0 +1,94 @@ +package container + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path" + "strconv" + "strings" + "syscall" + + "git.gensokyo.uk/security/hakurei/container/vfs" +) + +const ( + hostPath = "/" + hostDir + hostDir = "host" + sysrootPath = "/" + sysrootDir + sysrootDir = "sysroot" +) + +func toSysroot(name string) string { + name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' }) + return path.Join(sysrootPath, name) +} + +func toHost(name string) string { + name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' }) + return path.Join(hostPath, name) +} + +func createFile(name string, perm, pperm os.FileMode, content []byte) error { + if err := os.MkdirAll(path.Dir(name), pperm); err != nil { + return wrapErrSelf(err) + } + f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm) + if err != nil { + return wrapErrSelf(err) + } + if content != nil { + _, err = f.Write(content) + if err != nil { + err = wrapErrSelf(err) + } + } + return errors.Join(f.Close(), err) +} + +func ensureFile(name string, perm, pperm os.FileMode) error { + fi, err := os.Stat(name) + if err != nil { + if !os.IsNotExist(err) { + return err + } + return createFile(name, perm, pperm, nil) + } + + if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 { + err = msg.WrapErr(syscall.EISDIR, + fmt.Sprintf("path %q is a directory", name)) + } + return err +} + +var hostProc = newProcPats(hostPath) + +func newProcPats(prefix string) *procPaths { + return &procPaths{prefix + "/proc", prefix + "/proc/self"} +} + +type procPaths struct { + prefix string + self string +} + +func (p *procPaths) stdout() string { return p.self + "/fd/1" } +func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) } +func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error { + if r, err := os.Open(p.self + "/mountinfo"); err != nil { + return wrapErrSelf(err) + } else { + d := vfs.NewMountInfoDecoder(r) + err0 := f(d) + if err = r.Close(); err != nil { + return wrapErrSuffix(err, + "cannot close mountinfo:") + } else if err = d.Err(); err != nil { + return wrapErrSuffix(err, + "cannot parse mountinfo:") + } + return err0 + } +} diff --git a/container/seccomp/libseccomp-helper.c b/container/seccomp/libseccomp-helper.c new file mode 100644 index 00000000..b09c3eb2 --- /dev/null +++ b/container/seccomp/libseccomp-helper.c @@ -0,0 +1,130 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE /* CLONE_NEWUSER */ +#endif + +#include "libseccomp-helper.h" +#include +#include +#include + +#define LEN(arr) (sizeof(arr) / sizeof((arr)[0])) + +int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, + uint32_t multiarch, + struct hakurei_syscall_rule *rules, + size_t rules_sz, hakurei_export_flag flags) { + int i; + int last_allowed_family; + int disallowed; + struct hakurei_syscall_rule *rule; + + int32_t res = 0; /* refer to resPrefix for message */ + + /* Blocklist all but unix, inet, inet6 and netlink */ + struct { + int family; + hakurei_export_flag flags_mask; + } socket_family_allowlist[] = { + /* NOTE: Keep in numerical order */ + {AF_UNSPEC, 0}, + {AF_LOCAL, 0}, + {AF_INET, 0}, + {AF_INET6, 0}, + {AF_NETLINK, 0}, + {AF_CAN, HAKUREI_EXPORT_CAN}, + {AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH}, + }; + + scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); + if (ctx == NULL) { + res = 1; + goto out; + } else + errno = 0; + + /* We only really need to handle arches on multiarch systems. + * If only one arch is supported the default is fine */ + if (arch != 0) { + /* This *adds* the target arch, instead of replacing the + * native one. This is not ideal, because we'd like to only + * allow the target arch, but we can't really disallow the + * native arch at this point, because then bubblewrap + * couldn't continue running. */ + *ret_p = seccomp_arch_add(ctx, arch); + if (*ret_p < 0 && *ret_p != -EEXIST) { + res = 2; + goto out; + } + + if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) { + *ret_p = seccomp_arch_add(ctx, multiarch); + if (*ret_p < 0 && *ret_p != -EEXIST) { + res = 3; + goto out; + } + } + } + + for (i = 0; i < rules_sz; i++) { + rule = &rules[i]; + assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS); + + if (rule->arg) + *ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), + rule->syscall, 1, *rule->arg); + else + *ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), + rule->syscall, 0); + + if (*ret_p == -EFAULT) { + res = 4; + goto out; + } else if (*ret_p < 0) { + res = 5; + goto out; + } + } + + /* Socket filtering doesn't work on e.g. i386, so ignore failures here + * However, we need to user seccomp_rule_add_exact to avoid libseccomp doing + * something else: https://github.com/seccomp/libseccomp/issues/8 */ + last_allowed_family = -1; + for (i = 0; i < LEN(socket_family_allowlist); i++) { + if (socket_family_allowlist[i].flags_mask != 0 && + (socket_family_allowlist[i].flags_mask & flags) != + socket_family_allowlist[i].flags_mask) + continue; + + for (disallowed = last_allowed_family + 1; + disallowed < socket_family_allowlist[i].family; disallowed++) { + /* Blocklist the in-between valid families */ + seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), + SCMP_SYS(socket), 1, + SCMP_A0(SCMP_CMP_EQ, disallowed)); + } + last_allowed_family = socket_family_allowlist[i].family; + } + /* Blocklist the rest */ + seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, + SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1)); + + if (fd < 0) { + *ret_p = seccomp_load(ctx); + if (*ret_p != 0) { + res = 7; + goto out; + } + } else { + *ret_p = seccomp_export_bpf(ctx, fd); + if (*ret_p != 0) { + res = 6; + goto out; + } + } + +out: + if (ctx) + seccomp_release(ctx); + + return res; +} diff --git a/container/seccomp/libseccomp-helper.h b/container/seccomp/libseccomp-helper.h new file mode 100644 index 00000000..330fc99b --- /dev/null +++ b/container/seccomp/libseccomp-helper.h @@ -0,0 +1,24 @@ +#include +#include + +#if (SCMP_VER_MAJOR < 2) || (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \ + (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1) +#error This package requires libseccomp >= v2.5.1 +#endif + +typedef enum { + HAKUREI_EXPORT_MULTIARCH = 1 << 0, + HAKUREI_EXPORT_CAN = 1 << 1, + HAKUREI_EXPORT_BLUETOOTH = 1 << 2, +} hakurei_export_flag; + +struct hakurei_syscall_rule { + int syscall; + int m_errno; + struct scmp_arg_cmp *arg; +}; + +int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, + uint32_t multiarch, + struct hakurei_syscall_rule *rules, + size_t rules_sz, hakurei_export_flag flags); \ No newline at end of file diff --git a/container/seccomp/libseccomp.go b/container/seccomp/libseccomp.go new file mode 100644 index 00000000..a43b8f23 --- /dev/null +++ b/container/seccomp/libseccomp.go @@ -0,0 +1,188 @@ +package seccomp + +/* +#cgo linux pkg-config: --static libseccomp + +#include +*/ +import "C" +import ( + "errors" + "fmt" + "runtime" + "syscall" + "unsafe" +) + +var ( + ErrInvalidRules = errors.New("invalid native rules slice") +) + +// LibraryError represents a libseccomp error. +type LibraryError struct { + Prefix string + Seccomp syscall.Errno + Errno error +} + +func (e *LibraryError) Error() string { + if e.Seccomp == 0 { + if e.Errno == nil { + panic("invalid libseccomp error") + } + return fmt.Sprintf("%s: %s", e.Prefix, e.Errno) + } + if e.Errno == nil { + return fmt.Sprintf("%s: %s", e.Prefix, e.Seccomp) + } + return fmt.Sprintf("%s: %s (%s)", e.Prefix, e.Seccomp, e.Errno) +} + +func (e *LibraryError) Is(err error) bool { + if e == nil { + return err == nil + } + if ef, ok := err.(*LibraryError); ok { + return *e == *ef + } + return (e.Seccomp != 0 && errors.Is(err, e.Seccomp)) || + (e.Errno != nil && errors.Is(err, e.Errno)) +} + +type ( + ScmpSyscall = C.int + ScmpErrno = C.int +) + +// A NativeRule specifies an arch-specific action taken by seccomp under certain conditions. +type NativeRule struct { + // Syscall is the arch-dependent syscall number to act against. + Syscall ScmpSyscall + // Errno is the errno value to return when the condition is satisfied. + Errno ScmpErrno + // Arg is the optional struct scmp_arg_cmp passed to libseccomp. + Arg *ScmpArgCmp +} + +type ExportFlag = C.hakurei_export_flag + +const ( + // AllowMultiarch allows multiarch/emulation. + AllowMultiarch ExportFlag = C.HAKUREI_EXPORT_MULTIARCH + // AllowCAN allows AF_CAN. + AllowCAN ExportFlag = C.HAKUREI_EXPORT_CAN + // AllowBluetooth allows AF_BLUETOOTH. + AllowBluetooth ExportFlag = C.HAKUREI_EXPORT_BLUETOOTH +) + +var resPrefix = [...]string{ + 0: "", + 1: "seccomp_init failed", + 2: "seccomp_arch_add failed", + 3: "seccomp_arch_add failed (multiarch)", + 4: "internal libseccomp failure", + 5: "seccomp_rule_add failed", + 6: "seccomp_export_bpf failed", + 7: "seccomp_load failed", +} + +// Export streams filter contents to fd, or installs it to the current process if fd < 0. +func Export(fd int, rules []NativeRule, flags ExportFlag) error { + if len(rules) == 0 { + return ErrInvalidRules + } + + var ( + arch C.uint32_t = 0 + multiarch C.uint32_t = 0 + ) + switch runtime.GOARCH { + case "386": + arch = C.SCMP_ARCH_X86 + case "amd64": + arch = C.SCMP_ARCH_X86_64 + multiarch = C.SCMP_ARCH_X86 + case "arm": + arch = C.SCMP_ARCH_ARM + case "arm64": + arch = C.SCMP_ARCH_AARCH64 + multiarch = C.SCMP_ARCH_ARM + } + + var ret C.int + + rulesPinner := new(runtime.Pinner) + for i := range rules { + rule := &rules[i] + rulesPinner.Pin(rule) + if rule.Arg != nil { + rulesPinner.Pin(rule.Arg) + } + } + res, err := C.hakurei_export_filter( + &ret, C.int(fd), + arch, multiarch, + (*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])), + C.size_t(len(rules)), + flags, + ) + rulesPinner.Unpin() + + if prefix := resPrefix[res]; prefix != "" { + return &LibraryError{ + prefix, + -syscall.Errno(ret), + err, + } + } + return err +} + +// ScmpCompare is the equivalent of scmp_compare; +// Comparison operators +type ScmpCompare = C.enum_scmp_compare + +const ( + _SCMP_CMP_MIN = C._SCMP_CMP_MIN + + // not equal + SCMP_CMP_NE = C.SCMP_CMP_NE + // less than + SCMP_CMP_LT = C.SCMP_CMP_LT + // less than or equal + SCMP_CMP_LE = C.SCMP_CMP_LE + // equal + SCMP_CMP_EQ = C.SCMP_CMP_EQ + // greater than or equal + SCMP_CMP_GE = C.SCMP_CMP_GE + // greater than + SCMP_CMP_GT = C.SCMP_CMP_GT + // masked equality + SCMP_CMP_MASKED_EQ = C.SCMP_CMP_MASKED_EQ + + _SCMP_CMP_MAX = C._SCMP_CMP_MAX +) + +// ScmpDatum is the equivalent of scmp_datum_t; +// Argument datum +type ScmpDatum uint64 + +// ScmpArgCmp is the equivalent of struct scmp_arg_cmp; +// Argument / Value comparison definition +type ScmpArgCmp struct { + // argument number, starting at 0 + Arg C.uint + // the comparison op, e.g. SCMP_CMP_* + Op ScmpCompare + + DatumA, DatumB ScmpDatum +} + +// only used for testing +func syscallResolveName(s string) (trap int) { + v := C.CString(s) + trap = int(C.seccomp_syscall_resolve_name(v)) + C.free(unsafe.Pointer(v)) + + return +} diff --git a/container/seccomp/libseccomp_test.go b/container/seccomp/libseccomp_test.go new file mode 100644 index 00000000..a8bfcbf2 --- /dev/null +++ b/container/seccomp/libseccomp_test.go @@ -0,0 +1,147 @@ +package seccomp_test + +import ( + "crypto/sha512" + "errors" + "io" + "slices" + "syscall" + "testing" + + . "git.gensokyo.uk/security/hakurei/container/seccomp" +) + +func TestExport(t *testing.T) { + testCases := []struct { + name string + presets FilterPreset + flags ExportFlag + want []byte + wantErr bool + }{ + {"compat", 0, 0, []byte{ + 0x95, 0xec, 0x69, 0xd0, 0x17, 0x73, 0x3e, 0x07, + 0x21, 0x60, 0xe0, 0xda, 0x80, 0xfd, 0xeb, 0xec, + 0xdf, 0x27, 0xae, 0x81, 0x66, 0xf5, 0xe2, 0xa7, + 0x31, 0x27, 0x0c, 0x98, 0xea, 0x2d, 0x29, 0x46, + 0xcb, 0x52, 0x31, 0x02, 0x90, 0x63, 0x66, 0x8a, + 0xf2, 0x15, 0x87, 0x91, 0x55, 0xda, 0x21, 0xac, + 0xa7, 0x9b, 0x07, 0x0e, 0x04, 0xc0, 0xee, 0x9a, + 0xcd, 0xf5, 0x8f, 0x55, 0xcf, 0xa8, 0x15, 0xa5, + }, false}, + {"base", PresetExt, 0, []byte{ + 0xdc, 0x7f, 0x2e, 0x1c, 0x5e, 0x82, 0x9b, 0x79, + 0xeb, 0xb7, 0xef, 0xc7, 0x59, 0x15, 0x0f, 0x54, + 0xa8, 0x3a, 0x75, 0xc8, 0xdf, 0x6f, 0xee, 0x4d, + 0xce, 0x5d, 0xad, 0xc4, 0x73, 0x6c, 0x58, 0x5d, + 0x4d, 0xee, 0xbf, 0xeb, 0x3c, 0x79, 0x69, 0xaf, + 0x3a, 0x07, 0x7e, 0x90, 0xb7, 0x7b, 0xb4, 0x74, + 0x1d, 0xb0, 0x5d, 0x90, 0x99, 0x7c, 0x86, 0x59, + 0xb9, 0x58, 0x91, 0x20, 0x6a, 0xc9, 0x95, 0x2d, + }, false}, + {"everything", PresetExt | + PresetDenyNS | PresetDenyTTY | PresetDenyDevel | + PresetLinux32, AllowMultiarch | AllowCAN | + AllowBluetooth, []byte{ + 0xe9, 0x9d, 0xd3, 0x45, 0xe1, 0x95, 0x41, 0x34, + 0x73, 0xd3, 0xcb, 0xee, 0x07, 0xb4, 0xed, 0x57, + 0xb9, 0x08, 0xbf, 0xa8, 0x9e, 0xa2, 0x07, 0x2f, + 0xe9, 0x34, 0x82, 0x84, 0x7f, 0x50, 0xb5, 0xb7, + 0x58, 0xda, 0x17, 0xe7, 0x4c, 0xa2, 0xbb, 0xc0, + 0x08, 0x13, 0xde, 0x49, 0xa2, 0xb9, 0xbf, 0x83, + 0x4c, 0x02, 0x4e, 0xd4, 0x88, 0x50, 0xbe, 0x69, + 0xb6, 0x8a, 0x9a, 0x4c, 0x5f, 0x53, 0xa9, 0xdb, + }, false}, + {"strict", PresetStrict, 0, []byte{ + 0xe8, 0x80, 0x29, 0x8d, 0xf2, 0xbd, 0x67, 0x51, + 0xd0, 0x04, 0x0f, 0xc2, 0x1b, 0xc0, 0xed, 0x4c, + 0x00, 0xf9, 0x5d, 0xc0, 0xd7, 0xba, 0x50, 0x6c, + 0x24, 0x4d, 0x8b, 0x8c, 0xf6, 0x86, 0x6d, 0xba, + 0x8e, 0xf4, 0xa3, 0x32, 0x96, 0xf2, 0x87, 0xb6, + 0x6c, 0xcc, 0xc1, 0xd7, 0x8e, 0x97, 0x02, 0x65, + 0x97, 0xf8, 0x4c, 0xc7, 0xde, 0xc1, 0x57, 0x3e, + 0x14, 0x89, 0x60, 0xfb, 0xd3, 0x5c, 0xd7, 0x35, + }, false}, + {"strict compat", 0 | + PresetDenyNS | PresetDenyTTY | PresetDenyDevel, 0, []byte{ + 0x39, 0x87, 0x1b, 0x93, 0xff, 0xaf, 0xc8, 0xb9, + 0x79, 0xfc, 0xed, 0xc0, 0xb0, 0xc3, 0x7b, 0x9e, + 0x03, 0x92, 0x2f, 0x5b, 0x02, 0x74, 0x8d, 0xc5, + 0xc3, 0xc1, 0x7c, 0x92, 0x52, 0x7f, 0x6e, 0x02, + 0x2e, 0xde, 0x1f, 0x48, 0xbf, 0xf5, 0x92, 0x46, + 0xea, 0x45, 0x2c, 0x0d, 0x1d, 0xe5, 0x48, 0x27, + 0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd, + 0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa, + }, false}, + {"hakurei default", PresetExt | PresetDenyDevel, 0, []byte{ + 0xc6, 0x98, 0xb0, 0x81, 0xff, 0x95, 0x7a, 0xfe, + 0x17, 0xa6, 0xd9, 0x43, 0x74, 0x53, 0x7d, 0x37, + 0xf2, 0xa6, 0x3f, 0x6f, 0x9d, 0xd7, 0x5d, 0xa7, + 0x54, 0x65, 0x42, 0x40, 0x7a, 0x9e, 0x32, 0x47, + 0x6e, 0xbd, 0xa3, 0x31, 0x2b, 0xa7, 0x78, 0x5d, + 0x7f, 0x61, 0x85, 0x42, 0xbc, 0xfa, 0xf2, 0x7c, + 0xa2, 0x7d, 0xcc, 0x2d, 0xdd, 0xba, 0x85, 0x20, + 0x69, 0xd2, 0x8b, 0xcf, 0xe8, 0xca, 0xd3, 0x9a, + }, false}, + } + + buf := make([]byte, 8) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e := New(Preset(tc.presets, tc.flags), tc.flags) + digest := sha512.New() + + if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr { + t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr) + return + } + if err := e.Close(); err != nil { + t.Errorf("Close: error = %v", err) + } + if got := digest.Sum(nil); !slices.Equal(got, tc.want) { + t.Fatalf("Export() hash = %x, want %x", + got, tc.want) + return + } + }) + } + + t.Run("close without use", func(t *testing.T) { + e := New(Preset(0, 0), 0) + if err := e.Close(); !errors.Is(err, syscall.EINVAL) { + t.Errorf("Close: error = %v", err) + return + } + }) + + t.Run("close partial read", func(t *testing.T) { + e := New(Preset(0, 0), 0) + if _, err := e.Read(nil); err != nil { + t.Errorf("Read: error = %v", err) + return + } + // the underlying implementation uses buffered io, so the outcome of this is nondeterministic; + // that is not harmful however, so both outcomes are checked for here + if err := e.Close(); err != nil && + (!errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF)) { + t.Errorf("Close: error = %v", err) + return + } + }) +} + +func BenchmarkExport(b *testing.B) { + buf := make([]byte, 8) + for i := 0; i < b.N; i++ { + e := New( + Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32, + AllowMultiarch|AllowCAN|AllowBluetooth), + AllowMultiarch|AllowCAN|AllowBluetooth) + if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil { + b.Fatalf("cannot export: %v", err) + } + if err := e.Close(); err != nil { + b.Fatalf("cannot close exporter: %v", err) + } + } +} diff --git a/container/seccomp/mksysnum_linux.pl b/container/seccomp/mksysnum_linux.pl new file mode 100755 index 00000000..50b88b62 --- /dev/null +++ b/container/seccomp/mksysnum_linux.pl @@ -0,0 +1,83 @@ +#!/usr/bin/env perl +# Copyright 2009 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +use strict; + +my $command = "mksysnum_linux.pl ". join(' ', @ARGV); + +print < 999){ + # ignore deprecated syscalls that are no longer implemented + # https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/unistd.h?id=refs/heads/master#n716 + return; + } + (my $name_upper = $name) =~ y/a-z/A-Z/; + $num = $num + $offset; + if($num > 302){ # not wired in Go standard library + if($state < 0){ + print " \"$name\": SYS_$name_upper,\n"; + } + else{ + print " SYS_$name_upper = $num;\n"; + } + } + elsif($state < 0){ + print " \"$name\": SYS_$name_upper,\n"; + } + else{ + return; + } +} + +GENERATE: + +my $prev; +open(GCC, "gcc -E -dD $ARGV[0] |") || die "can't run gcc"; +while(){ + if(/^#define __NR_Linux\s+([0-9]+)/){ + # mips/mips64: extract offset + $offset = $1; + } + elsif(/^#define __NR_syscalls\s+/) { + # ignore redefinitions of __NR_syscalls + } + elsif(/^#define __NR_(\w+)\s+([0-9]+)/){ + $prev = $2; + fmt($1, $2); + } + elsif(/^#define __NR3264_(\w+)\s+([0-9]+)/){ + $prev = $2; + fmt($1, $2); + } + elsif(/^#define __NR_(\w+)\s+\(\w+\+\s*([0-9]+)\)/){ + fmt($1, $prev+$2) + } + elsif(/^#define __NR_(\w+)\s+\(__NR_Linux \+ ([0-9]+)/){ + fmt($1, $2); + } +} + +if($state < 0){ + $state = $state + 1; + print "}\n\nconst (\n"; + goto GENERATE; +} + +print ")"; diff --git a/container/seccomp/presets.go b/container/seccomp/presets.go new file mode 100644 index 00000000..4a01d2b0 --- /dev/null +++ b/container/seccomp/presets.go @@ -0,0 +1,229 @@ +package seccomp + +/* flatpak commit 4c3bf179e2e4a2a298cd1db1d045adaf3f564532 */ + +import ( + . "syscall" +) + +type FilterPreset int + +const ( + // PresetExt are project-specific extensions. + PresetExt FilterPreset = 1 << iota + // PresetDenyNS denies namespace setup syscalls. + PresetDenyNS + // PresetDenyTTY denies faking input. + PresetDenyTTY + // PresetDenyDevel denies development-related syscalls. + PresetDenyDevel + // PresetLinux32 sets PER_LINUX32. + PresetLinux32 +) + +func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) { + allowedPersonality := PER_LINUX + if presets&PresetLinux32 != 0 { + allowedPersonality = PER_LINUX32 + } + presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality)) + + l := len(presetCommon) + if presets&PresetDenyNS != 0 { + l += len(presetNamespace) + } + if presets&PresetDenyTTY != 0 { + l += len(presetTTY) + } + if presets&PresetDenyDevel != 0 { + l += len(presetDevelFinal) + } + if flags&AllowMultiarch == 0 { + l += len(presetEmu) + } + if presets&PresetExt != 0 { + l += len(presetCommonExt) + if presets&PresetDenyNS != 0 { + l += len(presetNamespaceExt) + } + if flags&AllowMultiarch == 0 { + l += len(presetEmuExt) + } + } + + rules = make([]NativeRule, 0, l) + rules = append(rules, presetCommon...) + if presets&PresetDenyNS != 0 { + rules = append(rules, presetNamespace...) + } + if presets&PresetDenyTTY != 0 { + rules = append(rules, presetTTY...) + } + if presets&PresetDenyDevel != 0 { + rules = append(rules, presetDevelFinal...) + } + if flags&AllowMultiarch == 0 { + rules = append(rules, presetEmu...) + } + if presets&PresetExt != 0 { + rules = append(rules, presetCommonExt...) + if presets&PresetDenyNS != 0 { + rules = append(rules, presetNamespaceExt...) + } + if flags&AllowMultiarch == 0 { + rules = append(rules, presetEmuExt...) + } + } + + return +} + +var ( + presetCommon = []NativeRule{ + /* Block dmesg */ + {ScmpSyscall(SYS_SYSLOG), ScmpErrno(EPERM), nil}, + /* Useless old syscall */ + {ScmpSyscall(SYS_USELIB), ScmpErrno(EPERM), nil}, + /* Don't allow disabling accounting */ + {ScmpSyscall(SYS_ACCT), ScmpErrno(EPERM), nil}, + /* Don't allow reading current quota use */ + {ScmpSyscall(SYS_QUOTACTL), ScmpErrno(EPERM), nil}, + + /* Don't allow access to the kernel keyring */ + {ScmpSyscall(SYS_ADD_KEY), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_KEYCTL), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_REQUEST_KEY), ScmpErrno(EPERM), nil}, + + /* Scary VM/NUMA ops */ + {ScmpSyscall(SYS_MOVE_PAGES), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_MBIND), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_GET_MEMPOLICY), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SET_MEMPOLICY), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_MIGRATE_PAGES), ScmpErrno(EPERM), nil}, + } + + /* hakurei: project-specific extensions */ + presetCommonExt = []NativeRule{ + /* system calls for changing the system clock */ + {ScmpSyscall(SYS_ADJTIMEX), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_CLOCK_ADJTIME), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_CLOCK_ADJTIME64), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_CLOCK_SETTIME), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_CLOCK_SETTIME64), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETTIMEOFDAY), ScmpErrno(EPERM), nil}, + + /* loading and unloading of kernel modules */ + {ScmpSyscall(SYS_DELETE_MODULE), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_FINIT_MODULE), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_INIT_MODULE), ScmpErrno(EPERM), nil}, + + /* system calls for rebooting and reboot preparation */ + {ScmpSyscall(SYS_KEXEC_FILE_LOAD), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_KEXEC_LOAD), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_REBOOT), ScmpErrno(EPERM), nil}, + + /* system calls for enabling/disabling swap devices */ + {ScmpSyscall(SYS_SWAPOFF), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SWAPON), ScmpErrno(EPERM), nil}, + } + + presetNamespace = []NativeRule{ + /* Don't allow subnamespace setups: */ + {ScmpSyscall(SYS_UNSHARE), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETNS), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_MOUNT), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_UMOUNT), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_UMOUNT2), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_PIVOT_ROOT), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_CHROOT), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_CLONE), ScmpErrno(EPERM), + &ScmpArgCmp{cloneArg, SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER}}, + + /* seccomp can't look into clone3()'s struct clone_args to check whether + * the flags are OK, so we have no choice but to block clone3(). + * Return ENOSYS so user-space will fall back to clone(). + * (CVE-2021-41133; see also https://github.com/moby/moby/commit/9f6b562d) + */ + {ScmpSyscall(SYS_CLONE3), ScmpErrno(ENOSYS), nil}, + + /* New mount manipulation APIs can also change our VFS. There's no + * legitimate reason to do these in the sandbox, so block all of them + * rather than thinking about which ones might be dangerous. + * (CVE-2021-41133) */ + {ScmpSyscall(SYS_OPEN_TREE), ScmpErrno(ENOSYS), nil}, + {ScmpSyscall(SYS_MOVE_MOUNT), ScmpErrno(ENOSYS), nil}, + {ScmpSyscall(SYS_FSOPEN), ScmpErrno(ENOSYS), nil}, + {ScmpSyscall(SYS_FSCONFIG), ScmpErrno(ENOSYS), nil}, + {ScmpSyscall(SYS_FSMOUNT), ScmpErrno(ENOSYS), nil}, + {ScmpSyscall(SYS_FSPICK), ScmpErrno(ENOSYS), nil}, + {ScmpSyscall(SYS_MOUNT_SETATTR), ScmpErrno(ENOSYS), nil}, + } + + /* hakurei: project-specific extensions */ + presetNamespaceExt = []NativeRule{ + /* changing file ownership */ + {ScmpSyscall(SYS_CHOWN), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_CHOWN32), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_FCHOWN), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_FCHOWN32), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_FCHOWNAT), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_LCHOWN), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_LCHOWN32), ScmpErrno(EPERM), nil}, + + /* system calls for changing user ID and group ID credentials */ + {ScmpSyscall(SYS_SETGID), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETGID32), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETGROUPS), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETGROUPS32), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETREGID), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETREGID32), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETRESGID), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETRESGID32), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETRESUID), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETRESUID32), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETREUID), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETREUID32), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETUID), ScmpErrno(EPERM), nil}, + {ScmpSyscall(SYS_SETUID32), ScmpErrno(EPERM), nil}, + } + + presetTTY = []NativeRule{ + /* Don't allow faking input to the controlling tty (CVE-2017-5226) */ + {ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM), + &ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCSTI}}, + /* In the unlikely event that the controlling tty is a Linux virtual + * console (/dev/tty2 or similar), copy/paste operations have an effect + * similar to TIOCSTI (CVE-2023-28100) */ + {ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM), + &ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCLINUX}}, + } + + presetEmu = []NativeRule{ + /* modify_ldt is a historic source of interesting information leaks, + * so it's disabled as a hardening measure. + * However, it is required to run old 16-bit applications + * as well as some Wine patches, so it's allowed in multiarch. */ + {ScmpSyscall(SYS_MODIFY_LDT), ScmpErrno(EPERM), nil}, + } + + /* hakurei: project-specific extensions */ + presetEmuExt = []NativeRule{ + {ScmpSyscall(SYS_SUBPAGE_PROT), ScmpErrno(ENOSYS), nil}, + {ScmpSyscall(SYS_SWITCH_ENDIAN), ScmpErrno(ENOSYS), nil}, + {ScmpSyscall(SYS_VM86), ScmpErrno(ENOSYS), nil}, + {ScmpSyscall(SYS_VM86OLD), ScmpErrno(ENOSYS), nil}, + } +) + +func presetDevel(allowedPersonality ScmpDatum) []NativeRule { + return []NativeRule{ + /* Profiling operations; we expect these to be done by tools from outside + * the sandbox. In particular perf has been the source of many CVEs. */ + {ScmpSyscall(SYS_PERF_EVENT_OPEN), ScmpErrno(EPERM), nil}, + /* Don't allow you to switch to bsd emulation or whatnot */ + {ScmpSyscall(SYS_PERSONALITY), ScmpErrno(EPERM), + &ScmpArgCmp{0, SCMP_CMP_NE, allowedPersonality, 0}}, + + {ScmpSyscall(SYS_PTRACE), ScmpErrno(EPERM), nil}, + } +} diff --git a/container/seccomp/presets_clone_backwards2.go b/container/seccomp/presets_clone_backwards2.go new file mode 100644 index 00000000..6a7a636e --- /dev/null +++ b/container/seccomp/presets_clone_backwards2.go @@ -0,0 +1,7 @@ +//go:build s390 || s390x + +package seccomp + +/* Architectures with CONFIG_CLONE_BACKWARDS2: the child stack + * and flags arguments are reversed so the flags come second */ +const cloneArg = 1 diff --git a/container/seccomp/presets_clone_generic.go b/container/seccomp/presets_clone_generic.go new file mode 100644 index 00000000..9d20890e --- /dev/null +++ b/container/seccomp/presets_clone_generic.go @@ -0,0 +1,6 @@ +//go:build !s390 && !s390x + +package seccomp + +/* Normally the flags come first */ +const cloneArg = 0 diff --git a/container/seccomp/proc.go b/container/seccomp/proc.go new file mode 100644 index 00000000..e4ce1853 --- /dev/null +++ b/container/seccomp/proc.go @@ -0,0 +1,78 @@ +package seccomp + +import ( + "context" + "errors" + "syscall" + + "git.gensokyo.uk/security/hakurei/helper/proc" +) + +const ( + PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel +) + +// New returns an inactive Encoder instance. +func New(rules []NativeRule, flags ExportFlag) *Encoder { return &Encoder{newExporter(rules, flags)} } + +// Load loads a filter into the kernel. +func Load(rules []NativeRule, flags ExportFlag) error { return Export(-1, rules, flags) } + +/* +An Encoder writes a BPF program to an output stream. + +Methods of Encoder are not safe for concurrent use. + +An Encoder must not be copied after first use. +*/ +type Encoder struct { + *exporter +} + +func (e *Encoder) Read(p []byte) (n int, err error) { + if err = e.prepare(); err != nil { + return + } + return e.r.Read(p) +} + +func (e *Encoder) Close() error { + if e.r == nil { + return syscall.EINVAL + } + + // this hangs if the cgo thread fails to exit + return errors.Join(e.closeWrite(), <-e.exportErr) +} + +// NewFile returns an instance of exporter implementing [proc.File]. +func NewFile(rules []NativeRule, flags ExportFlag) proc.File { + return &File{rules: rules, flags: flags} +} + +// File implements [proc.File] and provides access to the read end of exporter pipe. +type File struct { + rules []NativeRule + flags ExportFlag + proc.BaseFile +} + +func (f *File) ErrCount() int { return 2 } +func (f *File) Fulfill(ctx context.Context, dispatchErr func(error)) error { + e := newExporter(f.rules, f.flags) + if err := e.prepare(); err != nil { + return err + } + f.Set(e.r) + go func() { + select { + case err := <-e.exportErr: + dispatchErr(nil) + dispatchErr(err) + case <-ctx.Done(): + dispatchErr(e.closeWrite()) + dispatchErr(<-e.exportErr) + } + }() + return nil +} diff --git a/container/seccomp/seccomp.go b/container/seccomp/seccomp.go new file mode 100644 index 00000000..664b31c6 --- /dev/null +++ b/container/seccomp/seccomp.go @@ -0,0 +1,60 @@ +// Package seccomp provides high level wrappers around libseccomp. +package seccomp + +import ( + "os" + "runtime" + "sync" +) + +type exporter struct { + rules []NativeRule + flags ExportFlag + r, w *os.File + + prepareOnce sync.Once + prepareErr error + closeOnce sync.Once + closeErr error + exportErr <-chan error +} + +func (e *exporter) prepare() error { + e.prepareOnce.Do(func() { + if r, w, err := os.Pipe(); err != nil { + e.prepareErr = err + return + } else { + e.r, e.w = r, w + } + + ec := make(chan error, 1) + go func(fd uintptr) { + ec <- Export(int(fd), e.rules, e.flags) + close(ec) + _ = e.closeWrite() + runtime.KeepAlive(e.w) + }(e.w.Fd()) + e.exportErr = ec + runtime.SetFinalizer(e, (*exporter).closeWrite) + }) + return e.prepareErr +} + +func (e *exporter) closeWrite() error { + e.closeOnce.Do(func() { + if e.w == nil { + panic("closeWrite called on invalid exporter") + } + e.closeErr = e.w.Close() + + // no need for a finalizer anymore + runtime.SetFinalizer(e, nil) + }) + + return e.closeErr +} + +func newExporter(rules []NativeRule, flags ExportFlag) *exporter { + return &exporter{rules: rules, flags: flags} +} diff --git a/container/seccomp/seccomp_test.go b/container/seccomp/seccomp_test.go new file mode 100644 index 00000000..2b59c9bc --- /dev/null +++ b/container/seccomp/seccomp_test.go @@ -0,0 +1,65 @@ +package seccomp_test + +import ( + "errors" + "runtime" + "syscall" + "testing" + + "git.gensokyo.uk/security/hakurei/container/seccomp" +) + +func TestLibraryError(t *testing.T) { + testCases := []struct { + name string + sample *seccomp.LibraryError + want string + wantIs bool + compare error + }{ + { + "full", + &seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF}, + "seccomp_export_bpf failed: operation canceled (bad file descriptor)", + true, + &seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF}, + }, + { + "errno only", + &seccomp.LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM}, + "seccomp_init failed: cannot allocate memory", + false, + nil, + }, + { + "seccomp only", + &seccomp.LibraryError{Prefix: "internal libseccomp failure", Seccomp: syscall.EFAULT}, + "internal libseccomp failure: bad address", + true, + syscall.EFAULT, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if errors.Is(tc.sample, tc.compare) != tc.wantIs { + t.Errorf("errors.Is(%#v, %#v) did not return %v", + tc.sample, tc.compare, tc.wantIs) + } + + if got := tc.sample.Error(); got != tc.want { + t.Errorf("Error: %q, want %q", + got, tc.want) + } + }) + } + + t.Run("invalid", func(t *testing.T) { + wantPanic := "invalid libseccomp error" + defer func() { + if r := recover(); r != wantPanic { + t.Errorf("panic: %q, want %q", r, wantPanic) + } + }() + runtime.KeepAlive(new(seccomp.LibraryError).Error()) + }) +} diff --git a/container/seccomp/syscall.go b/container/seccomp/syscall.go new file mode 100644 index 00000000..36a988aa --- /dev/null +++ b/container/seccomp/syscall.go @@ -0,0 +1,28 @@ +package seccomp + +import "iter" + +// Syscalls returns an iterator over all wired syscalls. +func Syscalls() iter.Seq2[string, int] { + return func(yield func(string, int) bool) { + for name, num := range syscallNum { + if !yield(name, num) { + return + } + } + for name, num := range syscallNumExtra { + if !yield(name, num) { + return + } + } + } +} + +// SyscallResolveName resolves a syscall number from its string representation. +func SyscallResolveName(name string) (num int, ok bool) { + if num, ok = syscallNum[name]; ok { + return + } + num, ok = syscallNumExtra[name] + return +} diff --git a/container/seccomp/syscall_extra_linux_amd64.go b/container/seccomp/syscall_extra_linux_amd64.go new file mode 100644 index 00000000..4b83a876 --- /dev/null +++ b/container/seccomp/syscall_extra_linux_amd64.go @@ -0,0 +1,54 @@ +package seccomp + +/* +#cgo linux pkg-config: --static libseccomp + +#include +#include +*/ +import "C" + +const ( + PER_LINUX = C.PER_LINUX + PER_LINUX32 = C.PER_LINUX32 +) + +var syscallNumExtra = map[string]int{ + "umount": SYS_UMOUNT, + "subpage_prot": SYS_SUBPAGE_PROT, + "switch_endian": SYS_SWITCH_ENDIAN, + "vm86": SYS_VM86, + "vm86old": SYS_VM86OLD, + "clock_adjtime64": SYS_CLOCK_ADJTIME64, + "clock_settime64": SYS_CLOCK_SETTIME64, + "chown32": SYS_CHOWN32, + "fchown32": SYS_FCHOWN32, + "lchown32": SYS_LCHOWN32, + "setgid32": SYS_SETGID32, + "setgroups32": SYS_SETGROUPS32, + "setregid32": SYS_SETREGID32, + "setresgid32": SYS_SETRESGID32, + "setresuid32": SYS_SETRESUID32, + "setreuid32": SYS_SETREUID32, + "setuid32": SYS_SETUID32, +} + +const ( + SYS_UMOUNT = C.__SNR_umount + SYS_SUBPAGE_PROT = C.__SNR_subpage_prot + SYS_SWITCH_ENDIAN = C.__SNR_switch_endian + SYS_VM86 = C.__SNR_vm86 + SYS_VM86OLD = C.__SNR_vm86old + SYS_CLOCK_ADJTIME64 = C.__SNR_clock_adjtime64 + SYS_CLOCK_SETTIME64 = C.__SNR_clock_settime64 + SYS_CHOWN32 = C.__SNR_chown32 + SYS_FCHOWN32 = C.__SNR_fchown32 + SYS_LCHOWN32 = C.__SNR_lchown32 + SYS_SETGID32 = C.__SNR_setgid32 + SYS_SETGROUPS32 = C.__SNR_setgroups32 + SYS_SETREGID32 = C.__SNR_setregid32 + SYS_SETRESGID32 = C.__SNR_setresgid32 + SYS_SETRESUID32 = C.__SNR_setresuid32 + SYS_SETREUID32 = C.__SNR_setreuid32 + SYS_SETUID32 = C.__SNR_setuid32 +) diff --git a/container/seccomp/syscall_linux_amd64.go b/container/seccomp/syscall_linux_amd64.go new file mode 100644 index 00000000..28dbcb47 --- /dev/null +++ b/container/seccomp/syscall_linux_amd64.go @@ -0,0 +1,459 @@ +// mksysnum_linux.pl /usr/include/asm/unistd_64.h +// Code generated by the command above; DO NOT EDIT. + +package seccomp + +import . "syscall" + +var syscallNum = map[string]int{ + "read": SYS_READ, + "write": SYS_WRITE, + "open": SYS_OPEN, + "close": SYS_CLOSE, + "stat": SYS_STAT, + "fstat": SYS_FSTAT, + "lstat": SYS_LSTAT, + "poll": SYS_POLL, + "lseek": SYS_LSEEK, + "mmap": SYS_MMAP, + "mprotect": SYS_MPROTECT, + "munmap": SYS_MUNMAP, + "brk": SYS_BRK, + "rt_sigaction": SYS_RT_SIGACTION, + "rt_sigprocmask": SYS_RT_SIGPROCMASK, + "rt_sigreturn": SYS_RT_SIGRETURN, + "ioctl": SYS_IOCTL, + "pread64": SYS_PREAD64, + "pwrite64": SYS_PWRITE64, + "readv": SYS_READV, + "writev": SYS_WRITEV, + "access": SYS_ACCESS, + "pipe": SYS_PIPE, + "select": SYS_SELECT, + "sched_yield": SYS_SCHED_YIELD, + "mremap": SYS_MREMAP, + "msync": SYS_MSYNC, + "mincore": SYS_MINCORE, + "madvise": SYS_MADVISE, + "shmget": SYS_SHMGET, + "shmat": SYS_SHMAT, + "shmctl": SYS_SHMCTL, + "dup": SYS_DUP, + "dup2": SYS_DUP2, + "pause": SYS_PAUSE, + "nanosleep": SYS_NANOSLEEP, + "getitimer": SYS_GETITIMER, + "alarm": SYS_ALARM, + "setitimer": SYS_SETITIMER, + "getpid": SYS_GETPID, + "sendfile": SYS_SENDFILE, + "socket": SYS_SOCKET, + "connect": SYS_CONNECT, + "accept": SYS_ACCEPT, + "sendto": SYS_SENDTO, + "recvfrom": SYS_RECVFROM, + "sendmsg": SYS_SENDMSG, + "recvmsg": SYS_RECVMSG, + "shutdown": SYS_SHUTDOWN, + "bind": SYS_BIND, + "listen": SYS_LISTEN, + "getsockname": SYS_GETSOCKNAME, + "getpeername": SYS_GETPEERNAME, + "socketpair": SYS_SOCKETPAIR, + "setsockopt": SYS_SETSOCKOPT, + "getsockopt": SYS_GETSOCKOPT, + "clone": SYS_CLONE, + "fork": SYS_FORK, + "vfork": SYS_VFORK, + "execve": SYS_EXECVE, + "exit": SYS_EXIT, + "wait4": SYS_WAIT4, + "kill": SYS_KILL, + "uname": SYS_UNAME, + "semget": SYS_SEMGET, + "semop": SYS_SEMOP, + "semctl": SYS_SEMCTL, + "shmdt": SYS_SHMDT, + "msgget": SYS_MSGGET, + "msgsnd": SYS_MSGSND, + "msgrcv": SYS_MSGRCV, + "msgctl": SYS_MSGCTL, + "fcntl": SYS_FCNTL, + "flock": SYS_FLOCK, + "fsync": SYS_FSYNC, + "fdatasync": SYS_FDATASYNC, + "truncate": SYS_TRUNCATE, + "ftruncate": SYS_FTRUNCATE, + "getdents": SYS_GETDENTS, + "getcwd": SYS_GETCWD, + "chdir": SYS_CHDIR, + "fchdir": SYS_FCHDIR, + "rename": SYS_RENAME, + "mkdir": SYS_MKDIR, + "rmdir": SYS_RMDIR, + "creat": SYS_CREAT, + "link": SYS_LINK, + "unlink": SYS_UNLINK, + "symlink": SYS_SYMLINK, + "readlink": SYS_READLINK, + "chmod": SYS_CHMOD, + "fchmod": SYS_FCHMOD, + "chown": SYS_CHOWN, + "fchown": SYS_FCHOWN, + "lchown": SYS_LCHOWN, + "umask": SYS_UMASK, + "gettimeofday": SYS_GETTIMEOFDAY, + "getrlimit": SYS_GETRLIMIT, + "getrusage": SYS_GETRUSAGE, + "sysinfo": SYS_SYSINFO, + "times": SYS_TIMES, + "ptrace": SYS_PTRACE, + "getuid": SYS_GETUID, + "syslog": SYS_SYSLOG, + "getgid": SYS_GETGID, + "setuid": SYS_SETUID, + "setgid": SYS_SETGID, + "geteuid": SYS_GETEUID, + "getegid": SYS_GETEGID, + "setpgid": SYS_SETPGID, + "getppid": SYS_GETPPID, + "getpgrp": SYS_GETPGRP, + "setsid": SYS_SETSID, + "setreuid": SYS_SETREUID, + "setregid": SYS_SETREGID, + "getgroups": SYS_GETGROUPS, + "setgroups": SYS_SETGROUPS, + "setresuid": SYS_SETRESUID, + "getresuid": SYS_GETRESUID, + "setresgid": SYS_SETRESGID, + "getresgid": SYS_GETRESGID, + "getpgid": SYS_GETPGID, + "setfsuid": SYS_SETFSUID, + "setfsgid": SYS_SETFSGID, + "getsid": SYS_GETSID, + "capget": SYS_CAPGET, + "capset": SYS_CAPSET, + "rt_sigpending": SYS_RT_SIGPENDING, + "rt_sigtimedwait": SYS_RT_SIGTIMEDWAIT, + "rt_sigqueueinfo": SYS_RT_SIGQUEUEINFO, + "rt_sigsuspend": SYS_RT_SIGSUSPEND, + "sigaltstack": SYS_SIGALTSTACK, + "utime": SYS_UTIME, + "mknod": SYS_MKNOD, + "uselib": SYS_USELIB, + "personality": SYS_PERSONALITY, + "ustat": SYS_USTAT, + "statfs": SYS_STATFS, + "fstatfs": SYS_FSTATFS, + "sysfs": SYS_SYSFS, + "getpriority": SYS_GETPRIORITY, + "setpriority": SYS_SETPRIORITY, + "sched_setparam": SYS_SCHED_SETPARAM, + "sched_getparam": SYS_SCHED_GETPARAM, + "sched_setscheduler": SYS_SCHED_SETSCHEDULER, + "sched_getscheduler": SYS_SCHED_GETSCHEDULER, + "sched_get_priority_max": SYS_SCHED_GET_PRIORITY_MAX, + "sched_get_priority_min": SYS_SCHED_GET_PRIORITY_MIN, + "sched_rr_get_interval": SYS_SCHED_RR_GET_INTERVAL, + "mlock": SYS_MLOCK, + "munlock": SYS_MUNLOCK, + "mlockall": SYS_MLOCKALL, + "munlockall": SYS_MUNLOCKALL, + "vhangup": SYS_VHANGUP, + "modify_ldt": SYS_MODIFY_LDT, + "pivot_root": SYS_PIVOT_ROOT, + "_sysctl": SYS__SYSCTL, + "prctl": SYS_PRCTL, + "arch_prctl": SYS_ARCH_PRCTL, + "adjtimex": SYS_ADJTIMEX, + "setrlimit": SYS_SETRLIMIT, + "chroot": SYS_CHROOT, + "sync": SYS_SYNC, + "acct": SYS_ACCT, + "settimeofday": SYS_SETTIMEOFDAY, + "mount": SYS_MOUNT, + "umount2": SYS_UMOUNT2, + "swapon": SYS_SWAPON, + "swapoff": SYS_SWAPOFF, + "reboot": SYS_REBOOT, + "sethostname": SYS_SETHOSTNAME, + "setdomainname": SYS_SETDOMAINNAME, + "iopl": SYS_IOPL, + "ioperm": SYS_IOPERM, + "create_module": SYS_CREATE_MODULE, + "init_module": SYS_INIT_MODULE, + "delete_module": SYS_DELETE_MODULE, + "get_kernel_syms": SYS_GET_KERNEL_SYMS, + "query_module": SYS_QUERY_MODULE, + "quotactl": SYS_QUOTACTL, + "nfsservctl": SYS_NFSSERVCTL, + "getpmsg": SYS_GETPMSG, + "putpmsg": SYS_PUTPMSG, + "afs_syscall": SYS_AFS_SYSCALL, + "tuxcall": SYS_TUXCALL, + "security": SYS_SECURITY, + "gettid": SYS_GETTID, + "readahead": SYS_READAHEAD, + "setxattr": SYS_SETXATTR, + "lsetxattr": SYS_LSETXATTR, + "fsetxattr": SYS_FSETXATTR, + "getxattr": SYS_GETXATTR, + "lgetxattr": SYS_LGETXATTR, + "fgetxattr": SYS_FGETXATTR, + "listxattr": SYS_LISTXATTR, + "llistxattr": SYS_LLISTXATTR, + "flistxattr": SYS_FLISTXATTR, + "removexattr": SYS_REMOVEXATTR, + "lremovexattr": SYS_LREMOVEXATTR, + "fremovexattr": SYS_FREMOVEXATTR, + "tkill": SYS_TKILL, + "time": SYS_TIME, + "futex": SYS_FUTEX, + "sched_setaffinity": SYS_SCHED_SETAFFINITY, + "sched_getaffinity": SYS_SCHED_GETAFFINITY, + "set_thread_area": SYS_SET_THREAD_AREA, + "io_setup": SYS_IO_SETUP, + "io_destroy": SYS_IO_DESTROY, + "io_getevents": SYS_IO_GETEVENTS, + "io_submit": SYS_IO_SUBMIT, + "io_cancel": SYS_IO_CANCEL, + "get_thread_area": SYS_GET_THREAD_AREA, + "lookup_dcookie": SYS_LOOKUP_DCOOKIE, + "epoll_create": SYS_EPOLL_CREATE, + "epoll_ctl_old": SYS_EPOLL_CTL_OLD, + "epoll_wait_old": SYS_EPOLL_WAIT_OLD, + "remap_file_pages": SYS_REMAP_FILE_PAGES, + "getdents64": SYS_GETDENTS64, + "set_tid_address": SYS_SET_TID_ADDRESS, + "restart_syscall": SYS_RESTART_SYSCALL, + "semtimedop": SYS_SEMTIMEDOP, + "fadvise64": SYS_FADVISE64, + "timer_create": SYS_TIMER_CREATE, + "timer_settime": SYS_TIMER_SETTIME, + "timer_gettime": SYS_TIMER_GETTIME, + "timer_getoverrun": SYS_TIMER_GETOVERRUN, + "timer_delete": SYS_TIMER_DELETE, + "clock_settime": SYS_CLOCK_SETTIME, + "clock_gettime": SYS_CLOCK_GETTIME, + "clock_getres": SYS_CLOCK_GETRES, + "clock_nanosleep": SYS_CLOCK_NANOSLEEP, + "exit_group": SYS_EXIT_GROUP, + "epoll_wait": SYS_EPOLL_WAIT, + "epoll_ctl": SYS_EPOLL_CTL, + "tgkill": SYS_TGKILL, + "utimes": SYS_UTIMES, + "vserver": SYS_VSERVER, + "mbind": SYS_MBIND, + "set_mempolicy": SYS_SET_MEMPOLICY, + "get_mempolicy": SYS_GET_MEMPOLICY, + "mq_open": SYS_MQ_OPEN, + "mq_unlink": SYS_MQ_UNLINK, + "mq_timedsend": SYS_MQ_TIMEDSEND, + "mq_timedreceive": SYS_MQ_TIMEDRECEIVE, + "mq_notify": SYS_MQ_NOTIFY, + "mq_getsetattr": SYS_MQ_GETSETATTR, + "kexec_load": SYS_KEXEC_LOAD, + "waitid": SYS_WAITID, + "add_key": SYS_ADD_KEY, + "request_key": SYS_REQUEST_KEY, + "keyctl": SYS_KEYCTL, + "ioprio_set": SYS_IOPRIO_SET, + "ioprio_get": SYS_IOPRIO_GET, + "inotify_init": SYS_INOTIFY_INIT, + "inotify_add_watch": SYS_INOTIFY_ADD_WATCH, + "inotify_rm_watch": SYS_INOTIFY_RM_WATCH, + "migrate_pages": SYS_MIGRATE_PAGES, + "openat": SYS_OPENAT, + "mkdirat": SYS_MKDIRAT, + "mknodat": SYS_MKNODAT, + "fchownat": SYS_FCHOWNAT, + "futimesat": SYS_FUTIMESAT, + "newfstatat": SYS_NEWFSTATAT, + "unlinkat": SYS_UNLINKAT, + "renameat": SYS_RENAMEAT, + "linkat": SYS_LINKAT, + "symlinkat": SYS_SYMLINKAT, + "readlinkat": SYS_READLINKAT, + "fchmodat": SYS_FCHMODAT, + "faccessat": SYS_FACCESSAT, + "pselect6": SYS_PSELECT6, + "ppoll": SYS_PPOLL, + "unshare": SYS_UNSHARE, + "set_robust_list": SYS_SET_ROBUST_LIST, + "get_robust_list": SYS_GET_ROBUST_LIST, + "splice": SYS_SPLICE, + "tee": SYS_TEE, + "sync_file_range": SYS_SYNC_FILE_RANGE, + "vmsplice": SYS_VMSPLICE, + "move_pages": SYS_MOVE_PAGES, + "utimensat": SYS_UTIMENSAT, + "epoll_pwait": SYS_EPOLL_PWAIT, + "signalfd": SYS_SIGNALFD, + "timerfd_create": SYS_TIMERFD_CREATE, + "eventfd": SYS_EVENTFD, + "fallocate": SYS_FALLOCATE, + "timerfd_settime": SYS_TIMERFD_SETTIME, + "timerfd_gettime": SYS_TIMERFD_GETTIME, + "accept4": SYS_ACCEPT4, + "signalfd4": SYS_SIGNALFD4, + "eventfd2": SYS_EVENTFD2, + "epoll_create1": SYS_EPOLL_CREATE1, + "dup3": SYS_DUP3, + "pipe2": SYS_PIPE2, + "inotify_init1": SYS_INOTIFY_INIT1, + "preadv": SYS_PREADV, + "pwritev": SYS_PWRITEV, + "rt_tgsigqueueinfo": SYS_RT_TGSIGQUEUEINFO, + "perf_event_open": SYS_PERF_EVENT_OPEN, + "recvmmsg": SYS_RECVMMSG, + "fanotify_init": SYS_FANOTIFY_INIT, + "fanotify_mark": SYS_FANOTIFY_MARK, + "prlimit64": SYS_PRLIMIT64, + "name_to_handle_at": SYS_NAME_TO_HANDLE_AT, + "open_by_handle_at": SYS_OPEN_BY_HANDLE_AT, + "clock_adjtime": SYS_CLOCK_ADJTIME, + "syncfs": SYS_SYNCFS, + "sendmmsg": SYS_SENDMMSG, + "setns": SYS_SETNS, + "getcpu": SYS_GETCPU, + "process_vm_readv": SYS_PROCESS_VM_READV, + "process_vm_writev": SYS_PROCESS_VM_WRITEV, + "kcmp": SYS_KCMP, + "finit_module": SYS_FINIT_MODULE, + "sched_setattr": SYS_SCHED_SETATTR, + "sched_getattr": SYS_SCHED_GETATTR, + "renameat2": SYS_RENAMEAT2, + "seccomp": SYS_SECCOMP, + "getrandom": SYS_GETRANDOM, + "memfd_create": SYS_MEMFD_CREATE, + "kexec_file_load": SYS_KEXEC_FILE_LOAD, + "bpf": SYS_BPF, + "execveat": SYS_EXECVEAT, + "userfaultfd": SYS_USERFAULTFD, + "membarrier": SYS_MEMBARRIER, + "mlock2": SYS_MLOCK2, + "copy_file_range": SYS_COPY_FILE_RANGE, + "preadv2": SYS_PREADV2, + "pwritev2": SYS_PWRITEV2, + "pkey_mprotect": SYS_PKEY_MPROTECT, + "pkey_alloc": SYS_PKEY_ALLOC, + "pkey_free": SYS_PKEY_FREE, + "statx": SYS_STATX, + "io_pgetevents": SYS_IO_PGETEVENTS, + "rseq": SYS_RSEQ, + "uretprobe": SYS_URETPROBE, + "pidfd_send_signal": SYS_PIDFD_SEND_SIGNAL, + "io_uring_setup": SYS_IO_URING_SETUP, + "io_uring_enter": SYS_IO_URING_ENTER, + "io_uring_register": SYS_IO_URING_REGISTER, + "open_tree": SYS_OPEN_TREE, + "move_mount": SYS_MOVE_MOUNT, + "fsopen": SYS_FSOPEN, + "fsconfig": SYS_FSCONFIG, + "fsmount": SYS_FSMOUNT, + "fspick": SYS_FSPICK, + "pidfd_open": SYS_PIDFD_OPEN, + "clone3": SYS_CLONE3, + "close_range": SYS_CLOSE_RANGE, + "openat2": SYS_OPENAT2, + "pidfd_getfd": SYS_PIDFD_GETFD, + "faccessat2": SYS_FACCESSAT2, + "process_madvise": SYS_PROCESS_MADVISE, + "epoll_pwait2": SYS_EPOLL_PWAIT2, + "mount_setattr": SYS_MOUNT_SETATTR, + "quotactl_fd": SYS_QUOTACTL_FD, + "landlock_create_ruleset": SYS_LANDLOCK_CREATE_RULESET, + "landlock_add_rule": SYS_LANDLOCK_ADD_RULE, + "landlock_restrict_self": SYS_LANDLOCK_RESTRICT_SELF, + "memfd_secret": SYS_MEMFD_SECRET, + "process_mrelease": SYS_PROCESS_MRELEASE, + "futex_waitv": SYS_FUTEX_WAITV, + "set_mempolicy_home_node": SYS_SET_MEMPOLICY_HOME_NODE, + "cachestat": SYS_CACHESTAT, + "fchmodat2": SYS_FCHMODAT2, + "map_shadow_stack": SYS_MAP_SHADOW_STACK, + "futex_wake": SYS_FUTEX_WAKE, + "futex_wait": SYS_FUTEX_WAIT, + "futex_requeue": SYS_FUTEX_REQUEUE, + "statmount": SYS_STATMOUNT, + "listmount": SYS_LISTMOUNT, + "lsm_get_self_attr": SYS_LSM_GET_SELF_ATTR, + "lsm_set_self_attr": SYS_LSM_SET_SELF_ATTR, + "lsm_list_modules": SYS_LSM_LIST_MODULES, + "mseal": SYS_MSEAL, +} + +const ( + SYS_NAME_TO_HANDLE_AT = 303 + SYS_OPEN_BY_HANDLE_AT = 304 + SYS_CLOCK_ADJTIME = 305 + SYS_SYNCFS = 306 + SYS_SENDMMSG = 307 + SYS_SETNS = 308 + SYS_GETCPU = 309 + SYS_PROCESS_VM_READV = 310 + SYS_PROCESS_VM_WRITEV = 311 + SYS_KCMP = 312 + SYS_FINIT_MODULE = 313 + SYS_SCHED_SETATTR = 314 + SYS_SCHED_GETATTR = 315 + SYS_RENAMEAT2 = 316 + SYS_SECCOMP = 317 + SYS_GETRANDOM = 318 + SYS_MEMFD_CREATE = 319 + SYS_KEXEC_FILE_LOAD = 320 + SYS_BPF = 321 + SYS_EXECVEAT = 322 + SYS_USERFAULTFD = 323 + SYS_MEMBARRIER = 324 + SYS_MLOCK2 = 325 + SYS_COPY_FILE_RANGE = 326 + SYS_PREADV2 = 327 + SYS_PWRITEV2 = 328 + SYS_PKEY_MPROTECT = 329 + SYS_PKEY_ALLOC = 330 + SYS_PKEY_FREE = 331 + SYS_STATX = 332 + SYS_IO_PGETEVENTS = 333 + SYS_RSEQ = 334 + SYS_URETPROBE = 335 + SYS_PIDFD_SEND_SIGNAL = 424 + SYS_IO_URING_SETUP = 425 + SYS_IO_URING_ENTER = 426 + SYS_IO_URING_REGISTER = 427 + SYS_OPEN_TREE = 428 + SYS_MOVE_MOUNT = 429 + SYS_FSOPEN = 430 + SYS_FSCONFIG = 431 + SYS_FSMOUNT = 432 + SYS_FSPICK = 433 + SYS_PIDFD_OPEN = 434 + SYS_CLONE3 = 435 + SYS_CLOSE_RANGE = 436 + SYS_OPENAT2 = 437 + SYS_PIDFD_GETFD = 438 + SYS_FACCESSAT2 = 439 + SYS_PROCESS_MADVISE = 440 + SYS_EPOLL_PWAIT2 = 441 + SYS_MOUNT_SETATTR = 442 + SYS_QUOTACTL_FD = 443 + SYS_LANDLOCK_CREATE_RULESET = 444 + SYS_LANDLOCK_ADD_RULE = 445 + SYS_LANDLOCK_RESTRICT_SELF = 446 + SYS_MEMFD_SECRET = 447 + SYS_PROCESS_MRELEASE = 448 + SYS_FUTEX_WAITV = 449 + SYS_SET_MEMPOLICY_HOME_NODE = 450 + SYS_CACHESTAT = 451 + SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 + SYS_STATMOUNT = 457 + SYS_LISTMOUNT = 458 + SYS_LSM_GET_SELF_ATTR = 459 + SYS_LSM_SET_SELF_ATTR = 460 + SYS_LSM_LIST_MODULES = 461 + SYS_MSEAL = 462 +) diff --git a/container/seccomp/syscall_test.go b/container/seccomp/syscall_test.go new file mode 100644 index 00000000..933f060b --- /dev/null +++ b/container/seccomp/syscall_test.go @@ -0,0 +1,20 @@ +package seccomp + +import ( + "testing" +) + +func TestSyscallResolveName(t *testing.T) { + for name, want := range Syscalls() { + t.Run(name, func(t *testing.T) { + if got := syscallResolveName(name); got != want { + t.Errorf("syscallResolveName(%q) = %d, want %d", + name, got, want) + } + if got, ok := SyscallResolveName(name); !ok || got != want { + t.Errorf("SyscallResolveName(%q) = %d, want %d", + name, got, want) + } + }) + } +} diff --git a/container/syscall.go b/container/syscall.go new file mode 100644 index 00000000..791cffa6 --- /dev/null +++ b/container/syscall.go @@ -0,0 +1,81 @@ +package container + +import ( + "syscall" + "unsafe" +) + +const ( + O_PATH = 0x200000 + + PR_SET_NO_NEW_PRIVS = 0x26 + + CAP_SYS_ADMIN = 0x15 + CAP_SETPCAP = 0x8 +) + +const ( + SUID_DUMP_DISABLE = iota + SUID_DUMP_USER +) + +func SetDumpable(dumpable uintptr) error { + // linux/sched/coredump.h + if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 { + return errno + } + + return nil +} + +const ( + _LINUX_CAPABILITY_VERSION_3 = 0x20080522 + + PR_CAP_AMBIENT = 0x2f + PR_CAP_AMBIENT_RAISE = 0x2 + PR_CAP_AMBIENT_CLEAR_ALL = 0x4 +) + +type ( + capHeader struct { + version uint32 + pid int32 + } + + capData struct { + effective uint32 + permitted uint32 + inheritable uint32 + } +) + +// See CAP_TO_INDEX in linux/capability.h: +func capToIndex(cap uintptr) uintptr { return cap >> 5 } + +// See CAP_TO_MASK in linux/capability.h: +func capToMask(cap uintptr) uint32 { return 1 << uint(cap&31) } + +func capset(hdrp *capHeader, datap *[2]capData) error { + if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET, + uintptr(unsafe.Pointer(hdrp)), + uintptr(unsafe.Pointer(&datap[0])), 0); errno != 0 { + return errno + } + return nil +} + +// IgnoringEINTR makes a function call and repeats it if it returns an +// EINTR error. This appears to be required even though we install all +// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846. +// Also #20400 and #36644 are issues in which a signal handler is +// installed without setting SA_RESTART. None of these are the common case, +// but there are enough of them that it seems that we can't avoid +// an EINTR loop. +func IgnoringEINTR(fn func() error) error { + for { + err := fn() + if err != syscall.EINTR { + return err + } + } +} diff --git a/container/sysctl.go b/container/sysctl.go new file mode 100644 index 00000000..7fcb94bd --- /dev/null +++ b/container/sysctl.go @@ -0,0 +1,47 @@ +package container + +import ( + "bytes" + "log" + "os" + "strconv" + "sync" +) + +var ( + kernelOverflowuid int + kernelOverflowgid int + kernelCapLastCap int + + sysctlOnce sync.Once +) + +const ( + kernelOverflowuidPath = "/proc/sys/kernel/overflowuid" + kernelOverflowgidPath = "/proc/sys/kernel/overflowgid" + kernelCapLastCapPath = "/proc/sys/kernel/cap_last_cap" +) + +func mustReadSysctl() { + if v, err := os.ReadFile(kernelOverflowuidPath); err != nil { + log.Fatalf("cannot read %q: %v", kernelOverflowuidPath, err) + } else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { + log.Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err) + } + + if v, err := os.ReadFile(kernelOverflowgidPath); err != nil { + log.Fatalf("cannot read %q: %v", kernelOverflowgidPath, err) + } else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { + log.Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err) + } + + if v, err := os.ReadFile(kernelCapLastCapPath); err != nil { + log.Fatalf("cannot read %q: %v", kernelCapLastCapPath, err) + } else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { + log.Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err) + } +} + +func OverflowUid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowuid } +func OverflowGid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowgid } +func LastCap() uintptr { sysctlOnce.Do(mustReadSysctl); return uintptr(kernelCapLastCap) } diff --git a/container/vfs/mangle.go b/container/vfs/mangle.go new file mode 100644 index 00000000..83aba589 --- /dev/null +++ b/container/vfs/mangle.go @@ -0,0 +1,30 @@ +package vfs + +import "strings" + +func Unmangle(s string) string { + if !strings.ContainsRune(s, '\\') { + return s + } + + v := make([]byte, len(s)) + var ( + j int + c byte + ) + for i := 0; i < len(s); i++ { + c = s[i] + if c == '\\' && len(s) > i+3 && + (s[i+1] == '0' || s[i+1] == '1') && + (s[i+2] >= '0' && s[i+2] <= '7') && + (s[i+3] >= '0' && s[i+3] <= '7') { + c = ((s[i+1] - '0') << 6) | + ((s[i+2] - '0') << 3) | + (s[i+3] - '0') + i += 3 + } + v[j] = c + j++ + } + return string(v[:j]) +} diff --git a/container/vfs/mangle_test.go b/container/vfs/mangle_test.go new file mode 100644 index 00000000..d9444a84 --- /dev/null +++ b/container/vfs/mangle_test.go @@ -0,0 +1,27 @@ +package vfs_test + +import ( + "testing" + + "git.gensokyo.uk/security/hakurei/container/vfs" +) + +func TestUnmangle(t *testing.T) { + testCases := []struct { + want string + sample string + }{ + {`\, `, `\134\054\040`}, + {`(10) source -- maybe empty string`, `(10)\040source\040--\040maybe empty string`}, + } + + for _, tc := range testCases { + t.Run(tc.want, func(t *testing.T) { + got := vfs.Unmangle(tc.sample) + if got != tc.want { + t.Errorf("Unmangle: %q, want %q", + got, tc.want) + } + }) + } +} diff --git a/container/vfs/mountinfo.go b/container/vfs/mountinfo.go new file mode 100644 index 00000000..bcb3063f --- /dev/null +++ b/container/vfs/mountinfo.go @@ -0,0 +1,260 @@ +// Package vfs provides bindings and iterators over proc_pid_mountinfo(5). +package vfs + +import ( + "bufio" + "errors" + "fmt" + "io" + "iter" + "slices" + "strconv" + "strings" + "syscall" +) + +const ( + MS_NOSYMFOLLOW = 0x100 +) + +var ( + ErrMountInfoFields = errors.New("unexpected field count") + ErrMountInfoEmpty = errors.New("unexpected empty field") + ErrMountInfoDevno = errors.New("bad maj:min field") + ErrMountInfoSep = errors.New("bad optional fields separator") +) + +type ( + // A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream. + MountInfoDecoder struct { + s *bufio.Scanner + m *MountInfo + + current *MountInfo + parseErr error + complete bool + } + + // MountInfo represents the contents of a proc_pid_mountinfo(5) document. + MountInfo struct { + Next *MountInfo + MountInfoEntry + } + + // MountInfoEntry represents a proc_pid_mountinfo(5) entry. + MountInfoEntry struct { + // mount ID: a unique ID for the mount (may be reused after umount(2)). + ID int `json:"id"` + // parent ID: the ID of the parent mount (or of self for the root of this mount namespace's mount tree). + Parent int `json:"parent"` + // major:minor: the value of st_dev for files on this filesystem (see stat(2)). + Devno DevT `json:"devno"` + // root: the pathname of the directory in the filesystem which forms the root of this mount. + Root string `json:"root"` + // mount point: the pathname of the mount point relative to the process's root directory. + Target string `json:"target"` + // mount options: per-mount options (see mount(2)). + VfsOptstr string `json:"vfs_optstr"` + // optional fields: zero or more fields of the form "tag[:value]"; see below. + // separator: the end of the optional fields is marked by a single hyphen. + OptFields []string `json:"opt_fields"` + // filesystem type: the filesystem type in the form "type[.subtype]". + FsType string `json:"fstype"` + // mount source: filesystem-specific information or "none". + Source string `json:"source"` + // super options: per-superblock options (see mount(2)). + FsOptstr string `json:"fs_optstr"` + } + + DevT [2]int +) + +// Flags interprets VfsOptstr and returns the resulting flags and unmatched options. +func (e *MountInfoEntry) Flags() (flags uintptr, unmatched []string) { + for _, s := range strings.Split(e.VfsOptstr, ",") { + switch s { + case "rw": + case "ro": + flags |= syscall.MS_RDONLY + case "nosuid": + flags |= syscall.MS_NOSUID + case "nodev": + flags |= syscall.MS_NODEV + case "noexec": + flags |= syscall.MS_NOEXEC + case "nosymfollow": + flags |= MS_NOSYMFOLLOW + case "noatime": + flags |= syscall.MS_NOATIME + case "nodiratime": + flags |= syscall.MS_NODIRATIME + case "relatime": + flags |= syscall.MS_RELATIME + default: + unmatched = append(unmatched, s) + } + } + return +} + +// NewMountInfoDecoder returns a new decoder that reads from r. +// +// The decoder introduces its own buffering and may read data from r beyond the mountinfo entries requested. +func NewMountInfoDecoder(r io.Reader) *MountInfoDecoder { + return &MountInfoDecoder{s: bufio.NewScanner(r)} +} + +func (d *MountInfoDecoder) Decode(v **MountInfo) (err error) { + for d.scan() { + } + err = d.Err() + if err == nil { + *v = d.m + } + return +} + +// Entries returns an iterator over mountinfo entries. +func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] { + return func(yield func(*MountInfoEntry) bool) { + for cur := d.m; cur != nil; cur = cur.Next { + if !yield(&cur.MountInfoEntry) { + return + } + } + for d.scan() { + if !yield(&d.current.MountInfoEntry) { + return + } + } + } +} + +func (d *MountInfoDecoder) Err() error { + if err := d.s.Err(); err != nil { + return err + } + return d.parseErr +} + +func (d *MountInfoDecoder) scan() bool { + if d.complete { + return false + } + if !d.s.Scan() { + d.complete = true + return false + } + + m := new(MountInfo) + if err := parseMountInfoLine(d.s.Text(), &m.MountInfoEntry); err != nil { + d.parseErr = err + d.complete = true + return false + } + + if d.current == nil { + d.m = m + d.current = d.m + } else { + d.current.Next = m + d.current = d.current.Next + } + return true +} + +func parseMountInfoLine(s string, ent *MountInfoEntry) error { + // prevent proceeding with misaligned fields due to optional fields + f := strings.Split(s, " ") + if len(f) < 10 { + return ErrMountInfoFields + } + + // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue + // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) + + // (1) id + if id, err := strconv.Atoi(f[0]); err != nil { // 0 + return err + } else { + ent.ID = id + } + + // (2) parent + if parent, err := strconv.Atoi(f[1]); err != nil { // 1 + return err + } else { + ent.Parent = parent + } + + // (3) maj:min + if n, err := fmt.Sscanf(f[2], "%d:%d", &ent.Devno[0], &ent.Devno[1]); err != nil { + return err + } else if n != 2 { + // unreachable + return ErrMountInfoDevno + } + + // (4) mountroot + ent.Root = Unmangle(f[3]) + if ent.Root == "" { + return ErrMountInfoEmpty + } + + // (5) target + ent.Target = Unmangle(f[4]) + if ent.Target == "" { + return ErrMountInfoEmpty + } + + // (6) vfs options (fs-independent) + ent.VfsOptstr = Unmangle(f[5]) + if ent.VfsOptstr == "" { + return ErrMountInfoEmpty + } + + // (7) optional fields, terminated by " - " + i := len(f) - 4 + ent.OptFields = f[6:i] + + // (8) optional fields end marker + if f[i] != "-" { + return ErrMountInfoSep + } + i++ + + // (9) FS type + ent.FsType = Unmangle(f[i]) + if ent.FsType == "" { + return ErrMountInfoEmpty + } + i++ + + // (10) source -- maybe empty string + ent.Source = Unmangle(f[i]) + i++ + + // (11) fs options (fs specific) + ent.FsOptstr = Unmangle(f[i]) + + return nil +} + +func (e *MountInfoEntry) EqualWithIgnore(want *MountInfoEntry, ignore string) bool { + return (e.ID == want.ID || want.ID == -1) && + (e.Parent == want.Parent || want.Parent == -1) && + (e.Devno == want.Devno || (want.Devno[0] == -1 && want.Devno[1] == -1)) && + (e.Root == want.Root || want.Root == ignore) && + (e.Target == want.Target || want.Target == ignore) && + (e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) && + (slices.Equal(e.OptFields, want.OptFields) || (len(want.OptFields) == 1 && want.OptFields[0] == ignore)) && + (e.FsType == want.FsType || want.FsType == ignore) && + (e.Source == want.Source || want.Source == ignore) && + (e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore) +} + +func (e *MountInfoEntry) String() string { + return fmt.Sprintf("%d %d %d:%d %s %s %s %s %s %s %s", + e.ID, e.Parent, e.Devno[0], e.Devno[1], e.Root, e.Target, e.VfsOptstr, + strings.Join(append(e.OptFields, "-"), " "), e.FsType, e.Source, e.FsOptstr) +} diff --git a/container/vfs/mountinfo_test.go b/container/vfs/mountinfo_test.go new file mode 100644 index 00000000..cec7dcde --- /dev/null +++ b/container/vfs/mountinfo_test.go @@ -0,0 +1,404 @@ +package vfs_test + +import ( + "encoding/json" + "errors" + "iter" + "path" + "reflect" + "slices" + "strconv" + "strings" + "syscall" + "testing" + + "git.gensokyo.uk/security/hakurei/container/vfs" +) + +func TestMountInfo(t *testing.T) { + testCases := []mountInfoTest{ + {"count", sampleMountinfoBase + ` +21 20 0:53/ /mnt/test rw,relatime - tmpfs rw +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, + vfs.ErrMountInfoFields, "", nil, nil, nil}, + + {"sep", sampleMountinfoBase + ` +21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, + vfs.ErrMountInfoSep, "", nil, nil, nil}, + + {"id", sampleMountinfoBase + ` +id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, + strconv.ErrSyntax, "", nil, nil, nil}, + + {"parent", sampleMountinfoBase + ` +21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, + strconv.ErrSyntax, "", nil, nil, nil}, + + {"devno", sampleMountinfoBase + ` +21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, + nil, "unexpected EOF", nil, nil, nil}, + + {"maj", sampleMountinfoBase + ` +21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, + nil, "expected integer", nil, nil, nil}, + + {"min", sampleMountinfoBase + ` +21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, + nil, "expected integer", nil, nil, nil}, + + {"mountroot", sampleMountinfoBase + ` +21 20 0:53 /mnt/test rw,relatime - tmpfs rw +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, + vfs.ErrMountInfoEmpty, "", nil, nil, nil}, + + {"target", sampleMountinfoBase + ` +21 20 0:53 / rw,relatime - tmpfs rw +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, + vfs.ErrMountInfoEmpty, "", nil, nil, nil}, + + {"vfs options", sampleMountinfoBase + ` +21 20 0:53 / /mnt/test - tmpfs rw +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, + vfs.ErrMountInfoEmpty, "", nil, nil, nil}, + + {"FS type", sampleMountinfoBase + ` +21 20 0:53 / /mnt/test rw,relatime - rw +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, + vfs.ErrMountInfoEmpty, "", nil, nil, nil}, + + {"base", sampleMountinfoBase, nil, "", []*wantMountInfo{ + m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil), + m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil), + m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil), + m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil), + m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil), + m(20, 1, 8, 4, "/", "/", "ro,noatime,nodiratime,meow", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_RDONLY|syscall.MS_NOATIME|syscall.MS_NODIRATIME, []string{"meow"}), + }, + mn(20, 1, 8, 4, "/", "/", "ro,noatime,nodiratime,meow", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", false, + mn(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", false, nil, + mn(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", false, nil, + mn(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", false, + mn(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", false, nil, + mn(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", false, nil, nil)), + nil))), nil), func(n *vfs.MountInfoNode) []*vfs.MountInfoNode { + return []*vfs.MountInfoNode{ + n, + n.FirstChild, + n.FirstChild.NextSibling, + n.FirstChild.NextSibling.NextSibling, + n.FirstChild.NextSibling.NextSibling.FirstChild, + n.FirstChild.NextSibling.NextSibling.FirstChild.NextSibling, + } + }}, + + {"sample", sampleMountinfo, nil, "", []*wantMountInfo{ + m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil), + m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil), + m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil), + m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil), + m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil), + m(20, 1, 8, 4, "/", "/", "rw,noatime", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_NOATIME, nil), + m(21, 16, 0, 17, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", o(), "tmpfs", "tmpfs", "rw,mode=755", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), + m(22, 21, 0, 18, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), + m(23, 21, 0, 19, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpuset", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), + m(24, 21, 0, 20, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,ns", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), + m(25, 21, 0, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpu", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), + m(26, 21, 0, 22, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpuacct", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), + m(27, 21, 0, 23, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,memory", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), + m(28, 21, 0, 24, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,devices", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), + m(29, 21, 0, 25, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,freezer", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), + m(30, 21, 0, 26, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,net_cls", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), + m(31, 21, 0, 27, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,blkio", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), + m(32, 16, 0, 28, "/", "/sys/kernel/security", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil), + m(33, 17, 0, 29, "/", "/dev/hugepages", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil), + m(34, 16, 0, 30, "/", "/sys/kernel/debug", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil), + m(35, 15, 0, 31, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil), + m(36, 17, 0, 32, "/", "/dev/mqueue", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil), + m(37, 15, 0, 14, "/", "/proc/bus/usb", "rw,relatime", o(), "usbfs", "/proc/bus/usb", "rw", syscall.MS_RELATIME, nil), + m(38, 33, 0, 33, "/", "/dev/hugepages", "rw,relatime", o(), "hugetlbfs", "hugetlbfs", "rw", syscall.MS_RELATIME, nil), + m(39, 36, 0, 12, "/", "/dev/mqueue", "rw,relatime", o(), "mqueue", "mqueue", "rw", syscall.MS_RELATIME, nil), + m(40, 20, 8, 6, "/", "/boot", "rw,noatime", o(), "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered", syscall.MS_NOATIME, nil), + m(41, 20, 253, 0, "/", "/home/kzak", "rw,noatime", o(), "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered", syscall.MS_NOATIME, nil), + m(42, 35, 0, 34, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", o(), "binfmt_misc", "none", "rw", syscall.MS_RELATIME, nil), + m(43, 16, 0, 35, "/", "/sys/fs/fuse/connections", "rw,relatime", o(), "fusectl", "fusectl", "rw", syscall.MS_RELATIME, nil), + m(44, 41, 0, 36, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", o(), "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_RELATIME, nil), + m(45, 20, 0, 37, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", o(), "rpc_pipefs", "sunrpc", "rw", syscall.MS_RELATIME, nil), + m(47, 20, 0, 38, "/", "/mnt/sounds", "rw,relatime", o(), "cifs", "//foo.home/bar/", "rw,unc=\\\\foo.home\\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344", syscall.MS_RELATIME, nil), + m(49, 20, 0, 56, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", o("shared:323"), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME|vfs.MS_NOSYMFOLLOW, nil), + }, nil, nil}, + + {"sample nosrc", sampleMountinfoNoSrc, nil, "", []*wantMountInfo{ + m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil), + m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil), + m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil), + m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil), + m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil), + m(20, 1, 8, 4, "/", "/", "rw,noatime", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_NOATIME, nil), + m(21, 20, 0, 53, "/", "/mnt/test", "rw,relatime", o("shared:212"), "tmpfs", "", "rw", syscall.MS_RELATIME, nil), + }, nil, nil}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Run("decode", func(t *testing.T) { + var got *vfs.MountInfo + d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) + err := d.Decode(&got) + tc.check(t, d, "Decode", + func(yield func(*vfs.MountInfoEntry) bool) { + for cur := got; cur != nil; cur = cur.Next { + if !yield(&cur.MountInfoEntry) { + return + } + } + }, func() error { return err }) + t.Run("reuse", func(t *testing.T) { + tc.check(t, d, "Entries", + d.Entries(), d.Err) + }) + }) + + t.Run("iter", func(t *testing.T) { + d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) + tc.check(t, d, "Entries", + d.Entries(), d.Err) + + t.Run("reuse", func(t *testing.T) { + tc.check(t, d, "Entries", + d.Entries(), d.Err) + }) + }) + + t.Run("yield", func(t *testing.T) { + d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) + v := false + d.Entries()(func(entry *vfs.MountInfoEntry) bool { v = !v; return v }) + d.Entries()(func(entry *vfs.MountInfoEntry) bool { return false }) + + tc.check(t, d, "Entries", + d.Entries(), d.Err) + + t.Run("reuse", func(t *testing.T) { + tc.check(t, d, "Entries", + d.Entries(), d.Err) + }) + }) + }) + } +} + +type mountInfoTest struct { + name string + sample string + wantErr error + wantError string + want []*wantMountInfo + + wantNode *vfs.MountInfoNode + wantCollectF func(n *vfs.MountInfoNode) []*vfs.MountInfoNode +} + +func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName string, + got iter.Seq[*vfs.MountInfoEntry], gotErr func() error) { + i := 0 + for cur := range got { + if i == len(tc.want) { + if funcName != "Decode" && (tc.wantErr != nil || tc.wantError != "") { + continue + } + + t.Errorf("%s: got more than %d entries", funcName, len(tc.want)) + break + } + + if !reflect.DeepEqual(cur, &tc.want[i].MountInfoEntry) { + t.Errorf("%s: entry %d\ngot: %#v\nwant: %#v", + funcName, i, cur, tc.want[i]) + } + + flags, unmatched := cur.Flags() + if flags != tc.want[i].flags { + t.Errorf("Flags(%q): %#x, want %#x", + cur.VfsOptstr, flags, tc.want[i].flags) + } + if !slices.Equal(unmatched, tc.want[i].unmatched) { + t.Errorf("Flags(%q): unmatched = %#q, want %#q", + cur.VfsOptstr, unmatched, tc.want[i].unmatched) + } + + i++ + } + + if i != len(tc.want) { + t.Errorf("%s: got %d entries, want %d", funcName, i, len(tc.want)) + } + + if tc.wantErr == nil && tc.wantError == "" && tc.wantCollectF != nil { + t.Run("unfold", func(t *testing.T) { + n, err := d.Unfold("/") + if err != nil { + t.Errorf("Unfold: error = %v", err) + } else { + t.Run("stop", func(t *testing.T) { + v := false + n.Collective()(func(node *vfs.MountInfoNode) bool { v = !v; return v }) + }) + + if !reflect.DeepEqual(n, tc.wantNode) { + t.Errorf("Unfold: %s, want %s", + mustMarshal(n), mustMarshal(tc.wantNode)) + } + + t.Run("collective", func(t *testing.T) { + wantCollect := tc.wantCollectF(n) + if gotCollect := slices.Collect(n.Collective()); !reflect.DeepEqual(gotCollect, wantCollect) { + t.Errorf("Collective: \ngot %#v\nwant %#v", + gotCollect, wantCollect) + } + }) + } + }) + } else if tc.wantNode != nil || tc.wantCollectF != nil { + panic("invalid test case") + } else if _, err := d.Unfold("/"); !errors.Is(err, tc.wantErr) { + if tc.wantError == "" { + t.Errorf("Unfold: error = %v, wantErr %v", + err, tc.wantErr) + } else if err != nil && err.Error() != tc.wantError { + t.Errorf("Unfold: error = %q, wantError %q", + err, tc.wantError) + } + } + + if err := gotErr(); !errors.Is(err, tc.wantErr) { + if tc.wantError == "" { + t.Errorf("%s: error = %v, wantErr %v", + funcName, err, tc.wantErr) + } else if err != nil && err.Error() != tc.wantError { + t.Errorf("%s: error = %q, wantError %q", + funcName, err, tc.wantError) + } + } +} + +func mustMarshal(v any) string { + p, err := json.Marshal(v) + if err != nil { + panic(err.Error()) + } + return string(p) +} + +type wantMountInfo struct { + vfs.MountInfoEntry + flags uintptr + unmatched []string +} + +func m( + id, parent, maj, min int, root, target, vfsOptstr string, optFields []string, fsType, source, fsOptstr string, + flags uintptr, unmatched []string, +) *wantMountInfo { + return &wantMountInfo{ + vfs.MountInfoEntry{ + ID: id, + Parent: parent, + Devno: vfs.DevT{maj, min}, + Root: root, + Target: target, + VfsOptstr: vfsOptstr, + OptFields: optFields, + FsType: fsType, + Source: source, + FsOptstr: fsOptstr, + }, flags, unmatched, + } +} + +func mn( + id, parent, maj, min int, root, target, vfsOptstr string, optFields []string, fsType, source, fsOptstr string, + covered bool, firstChild, nextSibling *vfs.MountInfoNode, +) *vfs.MountInfoNode { + return &vfs.MountInfoNode{ + MountInfoEntry: &vfs.MountInfoEntry{ + ID: id, + Parent: parent, + Devno: vfs.DevT{maj, min}, + Root: root, + Target: target, + VfsOptstr: vfsOptstr, + OptFields: optFields, + FsType: fsType, + Source: source, + FsOptstr: fsOptstr, + }, + FirstChild: firstChild, + NextSibling: nextSibling, + Clean: path.Clean(target), + Covered: covered, + } +} + +func o(field ...string) []string { + if field == nil { + return []string{} + } + return field +} + +const ( + sampleMountinfoBase = `15 20 0:3 / /proc rw,relatime - proc /proc rw +16 20 0:15 / /sys rw,relatime - sysfs /sys rw +17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755 +18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw +20 1 8:4 / / ro,noatime,nodiratime,meow - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered` + + sampleMountinfo = `15 20 0:3 / /proc rw,relatime - proc /proc rw +16 20 0:15 / /sys rw,relatime - sysfs /sys rw +17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755 +18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw +20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 +22 21 0:18 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +23 21 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset +24 21 0:20 / /sys/fs/cgroup/ns rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,ns +25 21 0:21 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu +26 21 0:22 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct +27 21 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory +28 21 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices +29 21 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer +30 21 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls +31 21 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio +32 16 0:28 / /sys/kernel/security rw,relatime - autofs systemd-1 rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct +33 17 0:29 / /dev/hugepages rw,relatime - autofs systemd-1 rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct +34 16 0:30 / /sys/kernel/debug rw,relatime - autofs systemd-1 rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct +35 15 0:31 / /proc/sys/fs/binfmt_misc rw,relatime - autofs systemd-1 rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct +36 17 0:32 / /dev/mqueue rw,relatime - autofs systemd-1 rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct +37 15 0:14 / /proc/bus/usb rw,relatime - usbfs /proc/bus/usb rw +38 33 0:33 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw +39 36 0:12 / /dev/mqueue rw,relatime - mqueue mqueue rw +40 20 8:6 / /boot rw,noatime - ext3 /dev/sda6 rw,errors=continue,barrier=0,data=ordered +41 20 253:0 / /home/kzak rw,noatime - ext4 /dev/mapper/kzak-home rw,barrier=1,data=ordered +42 35 0:34 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc none rw +43 16 0:35 / /sys/fs/fuse/connections rw,relatime - fusectl fusectl rw +44 41 0:36 / /home/kzak/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=500,group_id=500 +45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw +47 20 0:38 / /mnt/sounds rw,relatime - cifs //foo.home/bar/ rw,unc=\\foo.home\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344 +49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw` + + sampleMountinfoNoSrc = `15 20 0:3 / /proc rw,relatime - proc /proc rw +16 20 0:15 / /sys rw,relatime - sysfs /sys rw +17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755 +18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw +20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered +21 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw` +) diff --git a/container/vfs/unfold.go b/container/vfs/unfold.go new file mode 100644 index 00000000..ca0e37b7 --- /dev/null +++ b/container/vfs/unfold.go @@ -0,0 +1,107 @@ +package vfs + +import ( + "iter" + "path" + "strings" + "syscall" +) + +// MountInfoNode positions a [MountInfoEntry] in its mount hierarchy. +type MountInfoNode struct { + *MountInfoEntry + FirstChild *MountInfoNode `json:"first_child"` + NextSibling *MountInfoNode `json:"next_sibling"` + + Clean string `json:"clean"` + Covered bool `json:"covered"` +} + +// Collective returns an iterator over visible mountinfo nodes. +func (n *MountInfoNode) Collective() iter.Seq[*MountInfoNode] { + return func(yield func(*MountInfoNode) bool) { n.visit(yield) } +} + +func (n *MountInfoNode) visit(yield func(*MountInfoNode) bool) bool { + if !n.Covered && !yield(n) { + return false + } + for cur := n.FirstChild; cur != nil; cur = cur.NextSibling { + if !cur.visit(yield) { + return false + } + } + return true +} + +// Unfold unfolds the mount hierarchy and resolves covered paths. +func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) { + targetClean := path.Clean(target) + + var mountinfoSize int + for range d.Entries() { + mountinfoSize++ + } + if err := d.Err(); err != nil { + return nil, err + } + + mountinfo := make([]*MountInfoNode, mountinfoSize) + // mount ID to index lookup + idIndex := make(map[int]int, mountinfoSize) + // final entry to match target + targetIndex := -1 + { + i := 0 + for ent := range d.Entries() { + mountinfo[i] = &MountInfoNode{Clean: path.Clean(ent.Target), MountInfoEntry: ent} + idIndex[ent.ID] = i + if mountinfo[i].Clean == targetClean { + targetIndex = i + } + + i++ + } + } + + if targetIndex == -1 { + return nil, syscall.ESTALE + } + + for _, cur := range mountinfo { + var parent *MountInfoNode + if p, ok := idIndex[cur.Parent]; !ok { + continue + } else { + parent = mountinfo[p] + } + + if !strings.HasPrefix(cur.Clean, targetClean) { + continue + } + if parent.Clean == cur.Clean { + parent.Covered = true + } + + covered := false + nsp := &parent.FirstChild + for s := parent.FirstChild; s != nil; s = s.NextSibling { + if strings.HasPrefix(cur.Clean, s.Clean) { + covered = true + break + } + + if strings.HasPrefix(s.Clean, cur.Clean) { + *nsp = s.NextSibling + } else { + nsp = &s.NextSibling + } + } + if covered { + continue + } + *nsp = cur + } + + return mountinfo[targetIndex], nil +} diff --git a/container/vfs/unfold_test.go b/container/vfs/unfold_test.go new file mode 100644 index 00000000..c464e0fe --- /dev/null +++ b/container/vfs/unfold_test.go @@ -0,0 +1,93 @@ +package vfs_test + +import ( + "errors" + "reflect" + "slices" + "strings" + "syscall" + "testing" + + "git.gensokyo.uk/security/hakurei/container/vfs" +) + +func TestUnfold(t *testing.T) { + testCases := []struct { + name string + sample string + target string + wantErr error + + want *vfs.MountInfoNode + wantCollectF func(n *vfs.MountInfoNode) []*vfs.MountInfoNode + wantCollectN []string + }{ + { + "no match", + sampleMountinfoBase, + "/mnt", + syscall.ESTALE, nil, nil, nil, + }, + { + "cover", + `33 1 0:33 / / rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755 +37 33 0:32 / /proc rw,nosuid,nodev,noexec,relatime shared:41 - proc proc rw +551 33 0:121 / /mnt rw,relatime shared:666 - tmpfs tmpfs rw +595 551 0:123 / /mnt rw,relatime shared:990 - tmpfs tmpfs rw +611 595 0:142 / /mnt/etc rw,relatime shared:1112 - tmpfs tmpfs rw +625 644 0:142 /passwd /mnt/etc/passwd rw,relatime shared:1112 - tmpfs tmpfs rw +641 625 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755 +644 611 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755 +`, "/mnt", nil, + mn(595, 551, 0, 123, "/", "/mnt", "rw,relatime", o("shared:990"), "tmpfs", "tmpfs", "rw", false, + mn(611, 595, 0, 142, "/", "/mnt/etc", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", false, + mn(644, 611, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", true, + mn(625, 644, 0, 142, "/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", true, + mn(641, 625, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", false, + nil, nil), nil), nil), nil), nil), func(n *vfs.MountInfoNode) []*vfs.MountInfoNode { + return []*vfs.MountInfoNode{n, n.FirstChild, n.FirstChild.FirstChild.FirstChild.FirstChild} + }, []string{"/mnt", "/mnt/etc", "/mnt/etc/passwd"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) + got, err := d.Unfold(tc.target) + + if !errors.Is(err, tc.wantErr) { + t.Errorf("Unfold: error = %v, wantErr %v", + err, tc.wantErr) + } + + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("Unfold:\ngot %s\nwant %s", + mustMarshal(got), mustMarshal(tc.want)) + } + + if err == nil && tc.wantCollectF != nil { + t.Run("collective", func(t *testing.T) { + wantCollect := tc.wantCollectF(got) + gotCollect := slices.Collect(got.Collective()) + if !reflect.DeepEqual(gotCollect, wantCollect) { + t.Errorf("Collective: \ngot %#v\nwant %#v", + gotCollect, wantCollect) + } + t.Run("target", func(t *testing.T) { + gotCollectN := slices.Collect[string](func(yield func(v string) bool) { + for _, cur := range gotCollect { + if !yield(cur.Clean) { + return + } + } + }) + if !reflect.DeepEqual(gotCollectN, tc.wantCollectN) { + t.Errorf("Collective: got %q, want %q", + gotCollectN, tc.wantCollectN) + } + }) + }) + } + }) + } +} diff --git a/container_test.go b/container_test.go deleted file mode 100644 index 4604d025..00000000 --- a/container_test.go +++ /dev/null @@ -1,281 +0,0 @@ -package hakurei_test - -import ( - "bytes" - "context" - "encoding/gob" - "log" - "os" - "os/exec" - "strings" - "syscall" - "testing" - "time" - - "git.gensokyo.uk/security/hakurei" - "git.gensokyo.uk/security/hakurei/hst" - "git.gensokyo.uk/security/hakurei/internal" - "git.gensokyo.uk/security/hakurei/internal/hlog" - "git.gensokyo.uk/security/hakurei/ldd" - "git.gensokyo.uk/security/hakurei/seccomp" - "git.gensokyo.uk/security/hakurei/vfs" -) - -const ( - ignore = "\x00" - ignoreV = -1 -) - -func TestContainer(t *testing.T) { - { - oldVerbose := hlog.Load() - oldOutput := hakurei.GetOutput() - internal.InstallOutput(true) - t.Cleanup(func() { hlog.Store(oldVerbose) }) - t.Cleanup(func() { hakurei.SetOutput(oldOutput) }) - } - - testCases := []struct { - name string - filter bool - session bool - net bool - ops *hakurei.Ops - mnt []*vfs.MountInfoEntry - host string - rules []seccomp.NativeRule - flags seccomp.ExportFlag - presets seccomp.FilterPreset - }{ - {"minimal", true, false, false, - new(hakurei.Ops), nil, "test-minimal", - nil, 0, seccomp.PresetStrict}, - {"allow", true, true, true, - new(hakurei.Ops), nil, "test-minimal", - nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel}, - {"no filter", false, true, true, - new(hakurei.Ops), nil, "test-no-filter", - nil, 0, seccomp.PresetExt}, - {"custom rules", true, true, true, - new(hakurei.Ops), nil, "test-no-filter", - []seccomp.NativeRule{ - {seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}, - }, 0, seccomp.PresetExt}, - {"tmpfs", true, false, false, - new(hakurei.Ops). - Tmpfs(hst.Tmp, 0, 0755), - []*vfs.MountInfoEntry{ - e("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), - }, "test-tmpfs", - nil, 0, seccomp.PresetStrict}, - {"dev", true, true /* go test output is not a tty */, false, - new(hakurei.Ops). - Dev("/dev"). - Mqueue("/dev/mqueue"), - []*vfs.MountInfoEntry{ - e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), - e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), - e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), - e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), - e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), - e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), - e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), - e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), - e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), - }, "", - nil, 0, seccomp.PresetStrict}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) - defer cancel() - - container := hakurei.New(ctx, "/usr/bin/sandbox.test", "-test.v", - "-test.run=TestHelperCheckContainer", "--", "check", tc.host) - container.Uid = 1000 - container.Gid = 100 - container.Hostname = tc.host - container.CommandContext = commandContext - container.Stdout, container.Stderr = os.Stdout, os.Stderr - container.Ops = tc.ops - container.SeccompRules = tc.rules - container.SeccompFlags = tc.flags | seccomp.AllowMultiarch - container.SeccompPresets = tc.presets - container.SeccompDisable = !tc.filter - container.RetainSession = tc.session - container.HostNet = tc.net - if container.Args[5] == "" { - if name, err := os.Hostname(); err != nil { - t.Fatalf("cannot get hostname: %v", err) - } else { - container.Args[5] = name - } - } - - container. - Tmpfs("/tmp", 0, 0755). - Bind(os.Args[0], os.Args[0], 0). - Mkdir("/usr/bin", 0755). - Link(os.Args[0], "/usr/bin/sandbox.test"). - Place("/etc/hostname", []byte(container.Args[5])) - // in case test has cgo enabled - var libPaths []string - if entries, err := ldd.ExecFilter(ctx, - commandContext, - func(v []byte) []byte { - return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1] - }, os.Args[0]); err != nil { - log.Fatalf("ldd: %v", err) - } else { - libPaths = ldd.Path(entries) - } - for _, name := range libPaths { - container.Bind(name, name, 0) - } - // needs /proc to check mountinfo - container.Proc("/proc") - - mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) - mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore)) - mnt = append(mnt, tc.mnt...) - mnt = append(mnt, - e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), - e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore), - e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore), - ) - for _, name := range libPaths { - mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore)) - } - mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw")) - want := new(bytes.Buffer) - if err := gob.NewEncoder(want).Encode(mnt); err != nil { - t.Fatalf("cannot serialise expected mount points: %v", err) - } - container.Stdin = want - - if err := container.Start(); err != nil { - hlog.PrintBaseError(err, "start:") - t.Fatalf("cannot start container: %v", err) - } else if err = container.Serve(); err != nil { - hlog.PrintBaseError(err, "serve:") - t.Errorf("cannot serve setup params: %v", err) - } - if err := container.Wait(); err != nil { - hlog.PrintBaseError(err, "wait:") - t.Fatalf("wait: %v", err) - } - }) - } -} - -func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry { - return &vfs.MountInfoEntry{ - ID: ignoreV, - Parent: ignoreV, - Devno: vfs.DevT{ignoreV, ignoreV}, - Root: root, - Target: target, - VfsOptstr: vfsOptstr, - OptFields: []string{ignore}, - FsType: fsType, - Source: source, - FsOptstr: fsOptstr, - } -} - -func TestContainerString(t *testing.T) { - container := hakurei.New(t.Context(), "ldd", "/usr/bin/env") - container.SeccompFlags |= seccomp.AllowMultiarch - container.SeccompRules = seccomp.Preset( - seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY, - container.SeccompFlags) - container.SeccompPresets = seccomp.PresetStrict - want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf` - if got := container.String(); got != want { - t.Errorf("String: %s, want %s", got, want) - } -} - -func TestHelperInit(t *testing.T) { - if len(os.Args) != 5 || os.Args[4] != "init" { - return - } - hakurei.SetOutput(hlog.Output{}) - hakurei.Init(hlog.Prepare, internal.InstallOutput) -} - -func TestHelperCheckContainer(t *testing.T) { - if len(os.Args) != 6 || os.Args[4] != "check" { - return - } - - t.Run("user", func(t *testing.T) { - if uid := syscall.Getuid(); uid != 1000 { - t.Errorf("Getuid: %d, want 1000", uid) - } - if gid := syscall.Getgid(); gid != 100 { - t.Errorf("Getgid: %d, want 100", gid) - } - }) - t.Run("hostname", func(t *testing.T) { - if name, err := os.Hostname(); err != nil { - t.Fatalf("cannot get hostname: %v", err) - } else if name != os.Args[5] { - t.Errorf("Hostname: %q, want %q", name, os.Args[5]) - } - - if p, err := os.ReadFile("/etc/hostname"); err != nil { - t.Fatalf("%v", err) - } else if string(p) != os.Args[5] { - t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5]) - } - }) - t.Run("mount", func(t *testing.T) { - var mnt []*vfs.MountInfoEntry - if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil { - t.Fatalf("cannot receive expected mount points: %v", err) - } - - var d *vfs.MountInfoDecoder - if f, err := os.Open("/proc/self/mountinfo"); err != nil { - t.Fatalf("cannot open mountinfo: %v", err) - } else { - d = vfs.NewMountInfoDecoder(f) - } - - i := 0 - for cur := range d.Entries() { - if i == len(mnt) { - t.Errorf("got more than %d entries", len(mnt)) - break - } - - // ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags - cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime") - cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime") - mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime") - mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime") - - if !cur.EqualWithIgnore(mnt[i], "\x00") { - t.Errorf("[FAIL] %s", cur) - } else { - t.Logf("[ OK ] %s", cur) - } - - i++ - } - if err := d.Err(); err != nil { - t.Errorf("cannot parse mountinfo: %v", err) - } - - if i != len(mnt) { - t.Errorf("got %d entries, want %d", i, len(mnt)) - } - }) -} - -func commandContext(ctx context.Context) *exec.Cmd { - return exec.CommandContext(ctx, os.Args[0], "-test.v", - "-test.run=TestHelperInit", "--", "init") -} diff --git a/executable.go b/executable.go deleted file mode 100644 index 2214156f..00000000 --- a/executable.go +++ /dev/null @@ -1,26 +0,0 @@ -package hakurei - -import ( - "log" - "os" - "sync" -) - -var ( - executable string - executableOnce sync.Once -) - -func copyExecutable() { - if name, err := os.Executable(); err != nil { - msg.BeforeExit() - log.Fatalf("cannot read executable path: %v", err) - } else { - executable = name - } -} - -func MustExecutable() string { - executableOnce.Do(copyExecutable) - return executable -} diff --git a/executable_test.go b/executable_test.go deleted file mode 100644 index 27b6223a..00000000 --- a/executable_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package hakurei_test - -import ( - "os" - "testing" - - "git.gensokyo.uk/security/hakurei" -) - -func TestExecutable(t *testing.T) { - for i := 0; i < 16; i++ { - if got := hakurei.MustExecutable(); got != os.Args[0] { - t.Errorf("MustExecutable: %q, want %q", - got, os.Args[0]) - } - } -} diff --git a/helper/container.go b/helper/container.go index 861ae4a9..dbff3687 100644 --- a/helper/container.go +++ b/helper/container.go @@ -9,7 +9,7 @@ import ( "slices" "sync" - "git.gensokyo.uk/security/hakurei" + "git.gensokyo.uk/security/hakurei/container" "git.gensokyo.uk/security/hakurei/helper/proc" ) @@ -20,13 +20,13 @@ func New( wt io.WriterTo, stat bool, argF func(argsFd, statFd int) []string, - cmdF func(container *hakurei.Container), + cmdF func(z *container.Container), extraFiles []*os.File, ) Helper { var args []string h := new(helperContainer) h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles) - h.Container = hakurei.New(ctx, name, args...) + h.Container = container.New(ctx, name, args...) h.WaitDelay = WaitDelay if cmdF != nil { cmdF(h.Container) @@ -40,7 +40,7 @@ type helperContainer struct { mu sync.Mutex *helperFiles - *hakurei.Container + *container.Container } func (h *helperContainer) Start() error { diff --git a/helper/container_test.go b/helper/container_test.go index e6e5a219..574b24b2 100644 --- a/helper/container_test.go +++ b/helper/container_test.go @@ -7,7 +7,7 @@ import ( "os/exec" "testing" - "git.gensokyo.uk/security/hakurei" + "git.gensokyo.uk/security/hakurei/container" "git.gensokyo.uk/security/hakurei/helper" "git.gensokyo.uk/security/hakurei/internal" "git.gensokyo.uk/security/hakurei/internal/hlog" @@ -34,15 +34,13 @@ func TestContainer(t *testing.T) { t.Run("implementation compliance", func(t *testing.T) { testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper { - return helper.New(ctx, os.Args[0], argsWt, stat, argF, func(container *hakurei.Container) { - setOutput(&container.Stdout, &container.Stderr) - container.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) { + return helper.New(ctx, os.Args[0], argsWt, stat, argF, func(z *container.Container) { + setOutput(&z.Stdout, &z.Stderr) + z.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) { return exec.CommandContext(ctx, os.Args[0], "-test.v", "-test.run=TestHelperInit", "--", "init") } - container.Bind("/", "/", 0) - container.Proc("/proc") - container.Dev("/dev") + z.Bind("/", "/", 0).Proc("/proc").Dev("/dev") }, nil) }) }) @@ -52,6 +50,6 @@ func TestHelperInit(t *testing.T) { if len(os.Args) != 5 || os.Args[4] != "init" { return } - hakurei.SetOutput(hlog.Output{}) - hakurei.Init(hlog.Prepare, func(bool) { internal.InstallOutput(false) }) + container.SetOutput(hlog.Output{}) + container.Init(hlog.Prepare, func(bool) { internal.InstallOutput(false) }) } diff --git a/hst/container.go b/hst/container.go index 1a5c4ebd..f0e2dec3 100644 --- a/hst/container.go +++ b/hst/container.go @@ -1,7 +1,7 @@ package hst import ( - "git.gensokyo.uk/security/hakurei/seccomp" + "git.gensokyo.uk/security/hakurei/container/seccomp" ) type ( diff --git a/hst/template.go b/hst/template.go index 60cbfa7b..5e1e8cc7 100644 --- a/hst/template.go +++ b/hst/template.go @@ -1,7 +1,7 @@ package hst import ( - "git.gensokyo.uk/security/hakurei/seccomp" + "git.gensokyo.uk/security/hakurei/container/seccomp" "git.gensokyo.uk/security/hakurei/system" "git.gensokyo.uk/security/hakurei/system/dbus" ) diff --git a/init.go b/init.go deleted file mode 100644 index cb7fd844..00000000 --- a/init.go +++ /dev/null @@ -1,364 +0,0 @@ -package hakurei - -import ( - "errors" - "fmt" - "log" - "os" - "os/exec" - "os/signal" - "path" - "runtime" - "strconv" - . "syscall" - "time" - - "git.gensokyo.uk/security/hakurei/seccomp" -) - -const ( - // time to wait for linger processes after death of initial process - residualProcessTimeout = 5 * time.Second - - // intermediate tmpfs mount point - basePath = "/tmp" - - // setup params file descriptor - setupEnv = "HAKUREI_SETUP" -) - -type initParams struct { - Params - - HostUid, HostGid int - // extra files count - Count int - // verbosity pass through - Verbose bool -} - -func Init(prepare func(prefix string), setVerbose func(verbose bool)) { - runtime.LockOSThread() - prepare("init") - - if os.Getpid() != 1 { - log.Fatal("this process must run as pid 1") - } - - var ( - params initParams - closeSetup func() error - setupFile *os.File - offsetSetup int - ) - if f, err := Receive(setupEnv, ¶ms, &setupFile); err != nil { - if errors.Is(err, ErrInvalid) { - log.Fatal("invalid setup descriptor") - } - if errors.Is(err, ErrNotSet) { - log.Fatal("HAKUREI_SETUP not set") - } - - log.Fatalf("cannot decode init setup payload: %v", err) - } else { - if params.Ops == nil { - log.Fatal("invalid setup parameters") - } - if params.ParentPerm == 0 { - params.ParentPerm = 0755 - } - - setVerbose(params.Verbose) - msg.Verbose("received setup parameters") - closeSetup = f - offsetSetup = int(setupFile.Fd() + 1) - } - - // write uid/gid map here so parent does not need to set dumpable - if err := SetDumpable(SUID_DUMP_USER); err != nil { - log.Fatalf("cannot set SUID_DUMP_USER: %s", err) - } - if err := os.WriteFile("/proc/self/uid_map", - append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...), - 0); err != nil { - log.Fatalf("%v", err) - } - if err := os.WriteFile("/proc/self/setgroups", - []byte("deny\n"), - 0); err != nil && !os.IsNotExist(err) { - log.Fatalf("%v", err) - } - if err := os.WriteFile("/proc/self/gid_map", - append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...), - 0); err != nil { - log.Fatalf("%v", err) - } - if err := SetDumpable(SUID_DUMP_DISABLE); err != nil { - log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) - } - - oldmask := Umask(0) - if params.Hostname != "" { - if err := Sethostname([]byte(params.Hostname)); err != nil { - log.Fatalf("cannot set hostname: %v", err) - } - } - - // cache sysctl before pivot_root - LastCap() - - if err := Mount("", "/", "", MS_SILENT|MS_SLAVE|MS_REC, ""); err != nil { - log.Fatalf("cannot make / rslave: %v", err) - } - - for i, op := range *params.Ops { - if op == nil { - log.Fatalf("invalid op %d", i) - } - - if err := op.early(¶ms.Params); err != nil { - msg.PrintBaseErr(err, - fmt.Sprintf("cannot prepare op %d:", i)) - msg.BeforeExit() - os.Exit(1) - } - } - - if err := Mount("rootfs", basePath, "tmpfs", MS_NODEV|MS_NOSUID, ""); err != nil { - log.Fatalf("cannot mount intermediate root: %v", err) - } - if err := os.Chdir(basePath); err != nil { - log.Fatalf("cannot enter base path: %v", err) - } - - if err := os.Mkdir(sysrootDir, 0755); err != nil { - log.Fatalf("%v", err) - } - if err := Mount(sysrootDir, sysrootDir, "", MS_SILENT|MS_MGC_VAL|MS_BIND|MS_REC, ""); err != nil { - log.Fatalf("cannot bind sysroot: %v", err) - } - - if err := os.Mkdir(hostDir, 0755); err != nil { - log.Fatalf("%v", err) - } - // pivot_root uncovers basePath in hostDir - if err := PivotRoot(basePath, hostDir); err != nil { - log.Fatalf("cannot pivot into intermediate root: %v", err) - } - if err := os.Chdir("/"); err != nil { - log.Fatalf("%v", err) - } - - for i, op := range *params.Ops { - // ops already checked during early setup - msg.Verbosef("%s %s", op.prefix(), op) - if err := op.apply(¶ms.Params); err != nil { - msg.PrintBaseErr(err, - fmt.Sprintf("cannot apply op %d:", i)) - msg.BeforeExit() - os.Exit(1) - } - } - - // setup requiring host root complete at this point - if err := Mount(hostDir, hostDir, "", MS_SILENT|MS_REC|MS_PRIVATE, ""); err != nil { - log.Fatalf("cannot make host root rprivate: %v", err) - } - if err := Unmount(hostDir, MNT_DETACH); err != nil { - log.Fatalf("cannot unmount host root: %v", err) - } - - { - var fd int - if err := IgnoringEINTR(func() (err error) { - fd, err = Open("/", O_DIRECTORY|O_RDONLY, 0) - return - }); err != nil { - log.Fatalf("cannot open intermediate root: %v", err) - } - if err := os.Chdir(sysrootPath); err != nil { - log.Fatalf("%v", err) - } - - if err := PivotRoot(".", "."); err != nil { - log.Fatalf("cannot pivot into sysroot: %v", err) - } - if err := Fchdir(fd); err != nil { - log.Fatalf("cannot re-enter intermediate root: %v", err) - } - if err := Unmount(".", MNT_DETACH); err != nil { - log.Fatalf("cannot unmount intemediate root: %v", err) - } - if err := os.Chdir("/"); err != nil { - log.Fatalf("%v", err) - } - - if err := Close(fd); err != nil { - log.Fatalf("cannot close intermediate root: %v", err) - } - } - - if _, _, errno := Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 { - log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", errno) - } - - if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 { - log.Fatalf("cannot clear the ambient capability set: %v", errno) - } - for i := uintptr(0); i <= LastCap(); i++ { - if params.Privileged && i == CAP_SYS_ADMIN { - continue - } - if _, _, errno := Syscall(SYS_PRCTL, PR_CAPBSET_DROP, i, 0); errno != 0 { - log.Fatalf("cannot drop capability from bonding set: %v", errno) - } - } - - var keep [2]uint32 - if params.Privileged { - keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN) - - if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_SYS_ADMIN); errno != 0 { - log.Fatalf("cannot raise CAP_SYS_ADMIN: %v", errno) - } - } - if err := capset( - &capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, - &[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}}, - ); err != nil { - log.Fatalf("cannot capset: %v", err) - } - - if !params.SeccompDisable { - rules := params.SeccompRules - if len(rules) == 0 { // non-empty rules slice always overrides presets - msg.Verbosef("resolving presets %#x", params.SeccompPresets) - rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags) - } - if err := seccomp.Load(rules, params.SeccompFlags); err != nil { - log.Fatalf("cannot load syscall filter: %v", err) - } - msg.Verbosef("%d filter rules loaded", len(rules)) - } else { - msg.Verbose("syscall filter not configured") - } - - extraFiles := make([]*os.File, params.Count) - for i := range extraFiles { - // setup fd is placed before all extra files - extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i)) - } - Umask(oldmask) - - cmd := exec.Command(params.Path) - cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr - cmd.Args = params.Args - cmd.Env = params.Env - cmd.ExtraFiles = extraFiles - cmd.Dir = params.Dir - - if err := cmd.Start(); err != nil { - log.Fatalf("%v", err) - } - msg.Suspend() - - if err := closeSetup(); err != nil { - log.Println("cannot close setup pipe:", err) - // not fatal - } - - type winfo struct { - wpid int - wstatus WaitStatus - } - info := make(chan winfo, 1) - done := make(chan struct{}) - - go func() { - var ( - err error - wpid = -2 - wstatus WaitStatus - ) - - // keep going until no child process is left - for wpid != -1 { - if err != nil { - break - } - - if wpid != -2 { - info <- winfo{wpid, wstatus} - } - - err = EINTR - for errors.Is(err, EINTR) { - wpid, err = Wait4(-1, &wstatus, 0, nil) - } - } - if !errors.Is(err, ECHILD) { - log.Println("unexpected wait4 response:", err) - } - - close(done) - }() - - // handle signals to dump withheld messages - sig := make(chan os.Signal, 2) - signal.Notify(sig, SIGINT, SIGTERM) - - // closed after residualProcessTimeout has elapsed after initial process death - timeout := make(chan struct{}) - - r := 2 - for { - select { - case s := <-sig: - if msg.Resume() { - msg.Verbosef("terminating on %s after process start", s.String()) - } else { - msg.Verbosef("terminating on %s", s.String()) - } - os.Exit(0) - case w := <-info: - if w.wpid == cmd.Process.Pid { - // initial process exited, output is most likely available again - msg.Resume() - - switch { - case w.wstatus.Exited(): - r = w.wstatus.ExitStatus() - msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus()) - case w.wstatus.Signaled(): - r = 128 + int(w.wstatus.Signal()) - msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal()) - default: - r = 255 - msg.Verbosef("initial process exited with status %#x", w.wstatus) - } - - go func() { - time.Sleep(residualProcessTimeout) - close(timeout) - }() - } - case <-done: - msg.BeforeExit() - os.Exit(r) - case <-timeout: - log.Println("timeout exceeded waiting for lingering processes") - msg.BeforeExit() - os.Exit(r) - } - } -} - -// TryArgv0 calls [Init] if the last element of argv0 is "init". -func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) { - if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" { - msg = v - Init(prepare, setVerbose) - msg.BeforeExit() - os.Exit(0) - } -} diff --git a/internal/output.go b/internal/output.go index a15be77d..4e148688 100644 --- a/internal/output.go +++ b/internal/output.go @@ -1,13 +1,13 @@ package internal import ( - "git.gensokyo.uk/security/hakurei" + "git.gensokyo.uk/security/hakurei/container" "git.gensokyo.uk/security/hakurei/internal/hlog" "git.gensokyo.uk/security/hakurei/system" ) func InstallOutput(verbose bool) { hlog.Store(verbose) - hakurei.SetOutput(hlog.Output{}) + container.SetOutput(hlog.Output{}) system.SetOutput(hlog.Output{}) } diff --git a/internal/sys/std.go b/internal/sys/std.go index 098eb3cf..af46117b 100644 --- a/internal/sys/std.go +++ b/internal/sys/std.go @@ -12,7 +12,7 @@ import ( "sync" "syscall" - "git.gensokyo.uk/security/hakurei" + "git.gensokyo.uk/security/hakurei/container" "git.gensokyo.uk/security/hakurei/hst" "git.gensokyo.uk/security/hakurei/internal" "git.gensokyo.uk/security/hakurei/internal/hlog" @@ -36,7 +36,7 @@ func (s *Std) Getgid() int { return os.Getgid() func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) } func (s *Std) TempDir() string { return os.TempDir() } func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) } -func (s *Std) MustExecutable() string { return hakurei.MustExecutable() } +func (s *Std) MustExecutable() string { return container.MustExecutable() } func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) } func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) } func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) } diff --git a/ldd/exec.go b/ldd/exec.go index 9922ba1e..2fa2ec41 100644 --- a/ldd/exec.go +++ b/ldd/exec.go @@ -8,8 +8,8 @@ import ( "os/exec" "time" - "git.gensokyo.uk/security/hakurei" - "git.gensokyo.uk/security/hakurei/seccomp" + "git.gensokyo.uk/security/hakurei/container" + "git.gensokyo.uk/security/hakurei/container/seccomp" ) const lddTimeout = 2 * time.Second @@ -27,24 +27,24 @@ func ExecFilter(ctx context.Context, p string) ([]*Entry, error) { c, cancel := context.WithTimeout(ctx, lddTimeout) defer cancel() - container := hakurei.New(c, "ldd", p) - container.CommandContext = commandContext - container.Hostname = "hakurei-ldd" - container.SeccompFlags |= seccomp.AllowMultiarch - container.SeccompPresets |= seccomp.PresetStrict + z := container.New(c, "ldd", p) + z.CommandContext = commandContext + z.Hostname = "hakurei-ldd" + z.SeccompFlags |= seccomp.AllowMultiarch + z.SeccompPresets |= seccomp.PresetStrict stdout, stderr := new(bytes.Buffer), new(bytes.Buffer) - container.Stdout = stdout - container.Stderr = stderr - container.Bind("/", "/", 0).Proc("/proc").Dev("/dev") + z.Stdout = stdout + z.Stderr = stderr + z.Bind("/", "/", 0).Proc("/proc").Dev("/dev") - if err := container.Start(); err != nil { + if err := z.Start(); err != nil { return nil, err } defer func() { _, _ = io.Copy(os.Stderr, stderr) }() - if err := container.Serve(); err != nil { + if err := z.Serve(); err != nil { return nil, err } - if err := container.Wait(); err != nil { + if err := z.Wait(); err != nil { m := stderr.Bytes() if bytes.Contains(m, append([]byte(p+": "), msgStatic...)) || bytes.Contains(m, msgStaticGlibc) { diff --git a/mount.go b/mount.go deleted file mode 100644 index 44e1d9cc..00000000 --- a/mount.go +++ /dev/null @@ -1,123 +0,0 @@ -package hakurei - -import ( - "errors" - "fmt" - "os" - "path/filepath" - . "syscall" - - "git.gensokyo.uk/security/hakurei/vfs" -) - -func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error { - if eq { - msg.Verbosef("resolved %q flags %#x", target, flags) - } else { - msg.Verbosef("resolved %q on %q flags %#x", source, target, flags) - } - - if err := Mount(source, target, "", MS_SILENT|MS_BIND|flags&MS_REC, ""); err != nil { - return wrapErrSuffix(err, - fmt.Sprintf("cannot mount %q on %q:", source, target)) - } - - var targetFinal string - if v, err := filepath.EvalSymlinks(target); err != nil { - return wrapErrSelf(err) - } else { - targetFinal = v - if targetFinal != target { - msg.Verbosef("target resolves to %q", targetFinal) - } - } - - // final target path according to the kernel through proc - var targetKFinal string - { - var destFd int - if err := IgnoringEINTR(func() (err error) { - destFd, err = Open(targetFinal, O_PATH|O_CLOEXEC, 0) - return - }); err != nil { - return wrapErrSuffix(err, - fmt.Sprintf("cannot open %q:", targetFinal)) - } - if v, err := os.Readlink(p.fd(destFd)); err != nil { - return wrapErrSelf(err) - } else if err = Close(destFd); err != nil { - return wrapErrSuffix(err, - fmt.Sprintf("cannot close %q:", targetFinal)) - } else { - targetKFinal = v - } - } - - mf := MS_NOSUID | flags&MS_NODEV | flags&MS_RDONLY - return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error { - n, err := d.Unfold(targetKFinal) - if err != nil { - if errors.Is(err, ESTALE) { - return msg.WrapErr(err, - fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal)) - } - return wrapErrSuffix(err, - "cannot unfold mount hierarchy:") - } - - if err = remountWithFlags(n, mf); err != nil { - return err - } - if flags&MS_REC == 0 { - return nil - } - - for cur := range n.Collective() { - err = remountWithFlags(cur, mf) - if err != nil && !errors.Is(err, EACCES) { - return err - } - } - - return nil - }) -} - -func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error { - kf, unmatched := n.Flags() - if len(unmatched) != 0 { - msg.Verbosef("unmatched vfs options: %q", unmatched) - } - - if kf&mf != mf { - return wrapErrSuffix( - Mount("none", n.Clean, "", MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, ""), - fmt.Sprintf("cannot remount %q:", n.Clean)) - } - return nil -} - -func mountTmpfs(fsname, name string, size int, perm os.FileMode) error { - target := toSysroot(name) - if err := os.MkdirAll(target, parentPerm(perm)); err != nil { - return wrapErrSelf(err) - } - opt := fmt.Sprintf("mode=%#o", perm) - if size > 0 { - opt += fmt.Sprintf(",size=%d", size) - } - return wrapErrSuffix( - Mount(fsname, target, "tmpfs", MS_NOSUID|MS_NODEV, opt), - fmt.Sprintf("cannot mount tmpfs on %q:", name)) -} - -func parentPerm(perm os.FileMode) os.FileMode { - pperm := 0755 - if perm&0070 == 0 { - pperm &= ^0050 - } - if perm&0007 == 0 { - pperm &= ^0005 - } - return os.FileMode(pperm) -} diff --git a/msg.go b/msg.go deleted file mode 100644 index aa7a272d..00000000 --- a/msg.go +++ /dev/null @@ -1,43 +0,0 @@ -package hakurei - -import ( - "log" - "sync/atomic" -) - -type Msg interface { - IsVerbose() bool - Verbose(v ...any) - Verbosef(format string, v ...any) - WrapErr(err error, a ...any) error - PrintBaseErr(err error, fallback string) - - Suspend() - Resume() bool - - BeforeExit() -} - -type DefaultMsg struct{ inactive atomic.Bool } - -func (msg *DefaultMsg) IsVerbose() bool { return true } -func (msg *DefaultMsg) Verbose(v ...any) { - if !msg.inactive.Load() { - log.Println(v...) - } -} -func (msg *DefaultMsg) Verbosef(format string, v ...any) { - if !msg.inactive.Load() { - log.Printf(format, v...) - } -} - -func (msg *DefaultMsg) WrapErr(err error, a ...any) error { - log.Println(a...) - return err -} -func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) } - -func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) } -func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) } -func (msg *DefaultMsg) BeforeExit() {} diff --git a/ops.go b/ops.go deleted file mode 100644 index 8d5a9e3d..00000000 --- a/ops.go +++ /dev/null @@ -1,482 +0,0 @@ -package hakurei - -import ( - "encoding/gob" - "fmt" - "math" - "os" - "path" - "path/filepath" - "slices" - "strings" - . "syscall" - "unsafe" -) - -type ( - Ops []Op - Op interface { - // early is called in host root. - early(params *Params) error - // apply is called in intermediate root. - apply(params *Params) error - - prefix() string - Is(op Op) bool - fmt.Stringer - } -) - -func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) } - -func init() { gob.Register(new(BindMountOp)) } - -// BindMountOp bind mounts host path Source on container path Target. -type BindMountOp struct { - Source, SourceFinal, Target string - - Flags int -} - -const ( - BindOptional = 1 << iota - BindWritable - BindDevice -) - -func (b *BindMountOp) early(*Params) error { - if !path.IsAbs(b.Source) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", b.Source)) - } - - if v, err := filepath.EvalSymlinks(b.Source); err != nil { - if os.IsNotExist(err) && b.Flags&BindOptional != 0 { - b.SourceFinal = "\x00" - return nil - } - return wrapErrSelf(err) - } else { - b.SourceFinal = v - return nil - } -} - -func (b *BindMountOp) apply(*Params) error { - if b.SourceFinal == "\x00" { - if b.Flags&BindOptional == 0 { - // unreachable - return EBADE - } - return nil - } - - if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) { - return msg.WrapErr(EBADE, "path is not absolute") - } - - source := toHost(b.SourceFinal) - target := toSysroot(b.Target) - - // this perm value emulates bwrap behaviour as it clears bits from 0755 based on - // op->perms which is never set for any bind setup op so always results in 0700 - if fi, err := os.Stat(source); err != nil { - return wrapErrSelf(err) - } else if fi.IsDir() { - if err = os.MkdirAll(target, 0700); err != nil { - return wrapErrSelf(err) - } - } else if err = ensureFile(target, 0444, 0700); err != nil { - return err - } - - var flags uintptr = MS_REC - if b.Flags&BindWritable == 0 { - flags |= MS_RDONLY - } - if b.Flags&BindDevice == 0 { - flags |= MS_NODEV - } - - return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target) -} - -func (b *BindMountOp) Is(op Op) bool { vb, ok := op.(*BindMountOp); return ok && *b == *vb } -func (*BindMountOp) prefix() string { return "mounting" } -func (b *BindMountOp) String() string { - if b.Source == b.Target { - return fmt.Sprintf("%q flags %#x", b.Source, b.Flags) - } - return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable) -} -func (f *Ops) Bind(source, target string, flags int) *Ops { - *f = append(*f, &BindMountOp{source, "", target, flags}) - return f -} - -func init() { gob.Register(new(MountProcOp)) } - -// MountProcOp mounts a private instance of proc. -type MountProcOp string - -func (p MountProcOp) early(*Params) error { return nil } -func (p MountProcOp) apply(params *Params) error { - v := string(p) - - if !path.IsAbs(v) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v)) - } - - target := toSysroot(v) - if err := os.MkdirAll(target, params.ParentPerm); err != nil { - return wrapErrSelf(err) - } - return wrapErrSuffix(Mount("proc", target, "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""), - fmt.Sprintf("cannot mount proc on %q:", v)) -} - -func (p MountProcOp) Is(op Op) bool { vp, ok := op.(MountProcOp); return ok && p == vp } -func (MountProcOp) prefix() string { return "mounting" } -func (p MountProcOp) String() string { return fmt.Sprintf("proc on %q", string(p)) } -func (f *Ops) Proc(dest string) *Ops { - *f = append(*f, MountProcOp(dest)) - return f -} - -func init() { gob.Register(new(MountDevOp)) } - -// MountDevOp mounts part of host dev. -type MountDevOp string - -func (d MountDevOp) early(*Params) error { return nil } -func (d MountDevOp) apply(params *Params) error { - v := string(d) - - if !path.IsAbs(v) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v)) - } - target := toSysroot(v) - - if err := mountTmpfs("devtmpfs", v, 0, params.ParentPerm); err != nil { - return err - } - - for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} { - targetPath := toSysroot(path.Join(v, name)) - if err := ensureFile(targetPath, 0444, params.ParentPerm); err != nil { - return err - } - if err := hostProc.bindMount( - toHost("/dev/"+name), - targetPath, - 0, - true, - ); err != nil { - return err - } - } - for i, name := range []string{"stdin", "stdout", "stderr"} { - if err := os.Symlink( - "/proc/self/fd/"+string(rune(i+'0')), - path.Join(target, name), - ); err != nil { - return wrapErrSelf(err) - } - } - for _, pair := range [][2]string{ - {"/proc/self/fd", "fd"}, - {"/proc/kcore", "core"}, - {"pts/ptmx", "ptmx"}, - } { - if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil { - return wrapErrSelf(err) - } - } - - devPtsPath := path.Join(target, "pts") - for _, name := range []string{path.Join(target, "shm"), devPtsPath} { - if err := os.Mkdir(name, params.ParentPerm); err != nil { - return wrapErrSelf(err) - } - } - - if err := Mount("devpts", devPtsPath, "devpts", MS_NOSUID|MS_NOEXEC, - "newinstance,ptmxmode=0666,mode=620"); err != nil { - return wrapErrSuffix(err, - fmt.Sprintf("cannot mount devpts on %q:", devPtsPath)) - } - - if params.RetainSession { - var buf [8]byte - if _, _, errno := Syscall(SYS_IOCTL, 1, TIOCGWINSZ, uintptr(unsafe.Pointer(&buf[0]))); errno == 0 { - consolePath := toSysroot(path.Join(v, "console")) - if err := ensureFile(consolePath, 0444, params.ParentPerm); err != nil { - return err - } - if name, err := os.Readlink(hostProc.stdout()); err != nil { - return wrapErrSelf(err) - } else if err = hostProc.bindMount( - toHost(name), - consolePath, - 0, - false, - ); err != nil { - return err - } - } - } - - return nil -} - -func (d MountDevOp) Is(op Op) bool { vd, ok := op.(MountDevOp); return ok && d == vd } -func (MountDevOp) prefix() string { return "mounting" } -func (d MountDevOp) String() string { return fmt.Sprintf("dev on %q", string(d)) } -func (f *Ops) Dev(dest string) *Ops { - *f = append(*f, MountDevOp(dest)) - return f -} - -func init() { gob.Register(new(MountMqueueOp)) } - -// MountMqueueOp mounts a private mqueue instance on container Path. -type MountMqueueOp string - -func (m MountMqueueOp) early(*Params) error { return nil } -func (m MountMqueueOp) apply(params *Params) error { - v := string(m) - - if !path.IsAbs(v) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v)) - } - - target := toSysroot(v) - if err := os.MkdirAll(target, params.ParentPerm); err != nil { - return wrapErrSelf(err) - } - return wrapErrSuffix(Mount("mqueue", target, "mqueue", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""), - fmt.Sprintf("cannot mount mqueue on %q:", v)) -} - -func (m MountMqueueOp) Is(op Op) bool { vm, ok := op.(MountMqueueOp); return ok && m == vm } -func (MountMqueueOp) prefix() string { return "mounting" } -func (m MountMqueueOp) String() string { return fmt.Sprintf("mqueue on %q", string(m)) } -func (f *Ops) Mqueue(dest string) *Ops { - *f = append(*f, MountMqueueOp(dest)) - return f -} - -func init() { gob.Register(new(MountTmpfsOp)) } - -// MountTmpfsOp mounts tmpfs on container Path. -type MountTmpfsOp struct { - Path string - Size int - Perm os.FileMode -} - -func (t *MountTmpfsOp) early(*Params) error { return nil } -func (t *MountTmpfsOp) apply(*Params) error { - if !path.IsAbs(t.Path) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) - } - if t.Size < 0 || t.Size > math.MaxUint>>1 { - return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size)) - } - return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm) -} - -func (t *MountTmpfsOp) Is(op Op) bool { vt, ok := op.(*MountTmpfsOp); return ok && *t == *vt } -func (*MountTmpfsOp) prefix() string { return "mounting" } -func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) } -func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops { - *f = append(*f, &MountTmpfsOp{dest, size, perm}) - return f -} - -func init() { gob.Register(new(SymlinkOp)) } - -// SymlinkOp creates a symlink in the container filesystem. -type SymlinkOp [2]string - -func (l *SymlinkOp) early(*Params) error { - if strings.HasPrefix(l[0], "*") { - l[0] = l[0][1:] - if !path.IsAbs(l[0]) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[0])) - } - if name, err := os.Readlink(l[0]); err != nil { - return wrapErrSelf(err) - } else { - l[0] = name - } - } - return nil -} -func (l *SymlinkOp) apply(params *Params) error { - // symlink target is an arbitrary path value, so only validate link name here - if !path.IsAbs(l[1]) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[1])) - } - - target := toSysroot(l[1]) - if err := os.MkdirAll(path.Dir(target), params.ParentPerm); err != nil { - return wrapErrSelf(err) - } - if err := os.Symlink(l[0], target); err != nil { - return wrapErrSelf(err) - } - return nil -} - -func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl } -func (*SymlinkOp) prefix() string { return "creating" } -func (l *SymlinkOp) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) } -func (f *Ops) Link(target, linkName string) *Ops { - *f = append(*f, &SymlinkOp{target, linkName}) - return f -} - -func init() { gob.Register(new(MkdirOp)) } - -// MkdirOp creates a directory in the container filesystem. -type MkdirOp struct { - Path string - Perm os.FileMode -} - -func (m *MkdirOp) early(*Params) error { return nil } -func (m *MkdirOp) apply(*Params) error { - if !path.IsAbs(m.Path) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", m.Path)) - } - - if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil { - return wrapErrSelf(err) - } - return nil -} - -func (m *MkdirOp) Is(op Op) bool { vm, ok := op.(*MkdirOp); return ok && m == vm } -func (*MkdirOp) prefix() string { return "creating" } -func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) } -func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops { - *f = append(*f, &MkdirOp{dest, perm}) - return f -} - -func init() { gob.Register(new(TmpfileOp)) } - -// TmpfileOp places a file in container Path containing Data. -type TmpfileOp struct { - Path string - Data []byte -} - -func (t *TmpfileOp) early(*Params) error { return nil } -func (t *TmpfileOp) apply(params *Params) error { - if !path.IsAbs(t.Path) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) - } - - var tmpPath string - if f, err := os.CreateTemp("/", "tmp.*"); err != nil { - return wrapErrSelf(err) - } else if _, err = f.Write(t.Data); err != nil { - return wrapErrSuffix(err, - "cannot write to intermediate file:") - } else if err = f.Close(); err != nil { - return wrapErrSuffix(err, - "cannot close intermediate file:") - } else { - tmpPath = f.Name() - } - - target := toSysroot(t.Path) - if err := ensureFile(target, 0444, params.ParentPerm); err != nil { - return err - } else if err = hostProc.bindMount( - tmpPath, - target, - MS_RDONLY|MS_NODEV, - false, - ); err != nil { - return err - } else if err = os.Remove(tmpPath); err != nil { - return wrapErrSelf(err) - } - return nil -} - -func (t *TmpfileOp) Is(op Op) bool { - vt, ok := op.(*TmpfileOp) - return ok && t.Path == vt.Path && slices.Equal(t.Data, vt.Data) -} -func (*TmpfileOp) prefix() string { return "placing" } -func (t *TmpfileOp) String() string { - return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data)) -} -func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &TmpfileOp{name, data}); return f } -func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops { - t := &TmpfileOp{Path: name} - *dataP = &t.Data - - *f = append(*f, t) - return f -} - -func init() { gob.Register(new(AutoEtcOp)) } - -// AutoEtcOp expands host /etc into a toplevel symlink mirror with /etc semantics. -// This is not a generic setup op. It is implemented here to reduce ipc overhead. -type AutoEtcOp struct{ Prefix string } - -func (e *AutoEtcOp) early(*Params) error { return nil } -func (e *AutoEtcOp) apply(*Params) error { - const target = sysrootPath + "/etc/" - rel := e.hostRel() + "/" - - if err := os.MkdirAll(target, 0755); err != nil { - return wrapErrSelf(err) - } - if d, err := os.ReadDir(toSysroot(e.hostPath())); err != nil { - return wrapErrSelf(err) - } else { - for _, ent := range d { - n := ent.Name() - switch n { - case ".host": - - case "passwd": - case "group": - - case "mtab": - if err = os.Symlink("/proc/mounts", target+n); err != nil { - return wrapErrSelf(err) - } - - default: - if err = os.Symlink(rel+n, target+n); err != nil { - return wrapErrSelf(err) - } - } - } - } - - return nil -} -func (e *AutoEtcOp) hostPath() string { return "/etc/" + e.hostRel() } -func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix } - -func (e *AutoEtcOp) Is(op Op) bool { - ve, ok := op.(*AutoEtcOp) - return ok && ((e == nil && ve == nil) || (e != nil && ve != nil && *e == *ve)) -} -func (*AutoEtcOp) prefix() string { return "setting up" } -func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) } -func (f *Ops) Etc(host, prefix string) *Ops { - e := &AutoEtcOp{prefix} - f.Mkdir("/etc", 0755) - f.Bind(host, e.hostPath(), 0) - *f = append(*f, e) - return f -} diff --git a/output.go b/output.go deleted file mode 100644 index d64d4712..00000000 --- a/output.go +++ /dev/null @@ -1,26 +0,0 @@ -package hakurei - -var msg Msg = new(DefaultMsg) - -func GetOutput() Msg { return msg } -func SetOutput(v Msg) { - if v == nil { - msg = new(DefaultMsg) - } else { - msg = v - } -} - -func wrapErrSuffix(err error, a ...any) error { - if err == nil { - return nil - } - return msg.WrapErr(err, append(a, err)...) -} - -func wrapErrSelf(err error) error { - if err == nil { - return nil - } - return msg.WrapErr(err, err.Error()) -} diff --git a/params.go b/params.go deleted file mode 100644 index 47441235..00000000 --- a/params.go +++ /dev/null @@ -1,47 +0,0 @@ -package hakurei - -import ( - "encoding/gob" - "errors" - "os" - "strconv" -) - -var ( - ErrNotSet = errors.New("environment variable not set") - ErrInvalid = errors.New("bad file descriptor") -) - -// Setup appends the read end of a pipe for setup params transmission and returns its fd. -func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) { - if r, w, err := os.Pipe(); err != nil { - return -1, nil, err - } else { - fd := 3 + len(*extraFiles) - *extraFiles = append(*extraFiles, r) - return fd, gob.NewEncoder(w), nil - } -} - -// Receive retrieves setup fd from the environment and receives params. -func Receive(key string, e any, v **os.File) (func() error, error) { - var setup *os.File - - if s, ok := os.LookupEnv(key); !ok { - return nil, ErrNotSet - } else { - if fd, err := strconv.Atoi(s); err != nil { - return nil, err - } else { - setup = os.NewFile(uintptr(fd), "setup") - if setup == nil { - return nil, ErrInvalid - } - if v != nil { - *v = setup - } - } - } - - return setup.Close, gob.NewDecoder(setup).Decode(e) -} diff --git a/path.go b/path.go deleted file mode 100644 index 9e777ee3..00000000 --- a/path.go +++ /dev/null @@ -1,94 +0,0 @@ -package hakurei - -import ( - "errors" - "fmt" - "io/fs" - "os" - "path" - "strconv" - "strings" - "syscall" - - "git.gensokyo.uk/security/hakurei/vfs" -) - -const ( - hostPath = "/" + hostDir - hostDir = "host" - sysrootPath = "/" + sysrootDir - sysrootDir = "sysroot" -) - -func toSysroot(name string) string { - name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' }) - return path.Join(sysrootPath, name) -} - -func toHost(name string) string { - name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' }) - return path.Join(hostPath, name) -} - -func createFile(name string, perm, pperm os.FileMode, content []byte) error { - if err := os.MkdirAll(path.Dir(name), pperm); err != nil { - return wrapErrSelf(err) - } - f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm) - if err != nil { - return wrapErrSelf(err) - } - if content != nil { - _, err = f.Write(content) - if err != nil { - err = wrapErrSelf(err) - } - } - return errors.Join(f.Close(), err) -} - -func ensureFile(name string, perm, pperm os.FileMode) error { - fi, err := os.Stat(name) - if err != nil { - if !os.IsNotExist(err) { - return err - } - return createFile(name, perm, pperm, nil) - } - - if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 { - err = msg.WrapErr(syscall.EISDIR, - fmt.Sprintf("path %q is a directory", name)) - } - return err -} - -var hostProc = newProcPats(hostPath) - -func newProcPats(prefix string) *procPaths { - return &procPaths{prefix + "/proc", prefix + "/proc/self"} -} - -type procPaths struct { - prefix string - self string -} - -func (p *procPaths) stdout() string { return p.self + "/fd/1" } -func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) } -func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error { - if r, err := os.Open(p.self + "/mountinfo"); err != nil { - return wrapErrSelf(err) - } else { - d := vfs.NewMountInfoDecoder(r) - err0 := f(d) - if err = r.Close(); err != nil { - return wrapErrSuffix(err, - "cannot close mountinfo:") - } else if err = d.Err(); err != nil { - return wrapErrSuffix(err, - "cannot parse mountinfo:") - } - return err0 - } -} diff --git a/seccomp/libseccomp-helper.c b/seccomp/libseccomp-helper.c deleted file mode 100644 index b09c3eb2..00000000 --- a/seccomp/libseccomp-helper.c +++ /dev/null @@ -1,130 +0,0 @@ -#ifndef _GNU_SOURCE -#define _GNU_SOURCE /* CLONE_NEWUSER */ -#endif - -#include "libseccomp-helper.h" -#include -#include -#include - -#define LEN(arr) (sizeof(arr) / sizeof((arr)[0])) - -int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, - uint32_t multiarch, - struct hakurei_syscall_rule *rules, - size_t rules_sz, hakurei_export_flag flags) { - int i; - int last_allowed_family; - int disallowed; - struct hakurei_syscall_rule *rule; - - int32_t res = 0; /* refer to resPrefix for message */ - - /* Blocklist all but unix, inet, inet6 and netlink */ - struct { - int family; - hakurei_export_flag flags_mask; - } socket_family_allowlist[] = { - /* NOTE: Keep in numerical order */ - {AF_UNSPEC, 0}, - {AF_LOCAL, 0}, - {AF_INET, 0}, - {AF_INET6, 0}, - {AF_NETLINK, 0}, - {AF_CAN, HAKUREI_EXPORT_CAN}, - {AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH}, - }; - - scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); - if (ctx == NULL) { - res = 1; - goto out; - } else - errno = 0; - - /* We only really need to handle arches on multiarch systems. - * If only one arch is supported the default is fine */ - if (arch != 0) { - /* This *adds* the target arch, instead of replacing the - * native one. This is not ideal, because we'd like to only - * allow the target arch, but we can't really disallow the - * native arch at this point, because then bubblewrap - * couldn't continue running. */ - *ret_p = seccomp_arch_add(ctx, arch); - if (*ret_p < 0 && *ret_p != -EEXIST) { - res = 2; - goto out; - } - - if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) { - *ret_p = seccomp_arch_add(ctx, multiarch); - if (*ret_p < 0 && *ret_p != -EEXIST) { - res = 3; - goto out; - } - } - } - - for (i = 0; i < rules_sz; i++) { - rule = &rules[i]; - assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS); - - if (rule->arg) - *ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), - rule->syscall, 1, *rule->arg); - else - *ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), - rule->syscall, 0); - - if (*ret_p == -EFAULT) { - res = 4; - goto out; - } else if (*ret_p < 0) { - res = 5; - goto out; - } - } - - /* Socket filtering doesn't work on e.g. i386, so ignore failures here - * However, we need to user seccomp_rule_add_exact to avoid libseccomp doing - * something else: https://github.com/seccomp/libseccomp/issues/8 */ - last_allowed_family = -1; - for (i = 0; i < LEN(socket_family_allowlist); i++) { - if (socket_family_allowlist[i].flags_mask != 0 && - (socket_family_allowlist[i].flags_mask & flags) != - socket_family_allowlist[i].flags_mask) - continue; - - for (disallowed = last_allowed_family + 1; - disallowed < socket_family_allowlist[i].family; disallowed++) { - /* Blocklist the in-between valid families */ - seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), - SCMP_SYS(socket), 1, - SCMP_A0(SCMP_CMP_EQ, disallowed)); - } - last_allowed_family = socket_family_allowlist[i].family; - } - /* Blocklist the rest */ - seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, - SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1)); - - if (fd < 0) { - *ret_p = seccomp_load(ctx); - if (*ret_p != 0) { - res = 7; - goto out; - } - } else { - *ret_p = seccomp_export_bpf(ctx, fd); - if (*ret_p != 0) { - res = 6; - goto out; - } - } - -out: - if (ctx) - seccomp_release(ctx); - - return res; -} diff --git a/seccomp/libseccomp-helper.h b/seccomp/libseccomp-helper.h deleted file mode 100644 index 330fc99b..00000000 --- a/seccomp/libseccomp-helper.h +++ /dev/null @@ -1,24 +0,0 @@ -#include -#include - -#if (SCMP_VER_MAJOR < 2) || (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \ - (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1) -#error This package requires libseccomp >= v2.5.1 -#endif - -typedef enum { - HAKUREI_EXPORT_MULTIARCH = 1 << 0, - HAKUREI_EXPORT_CAN = 1 << 1, - HAKUREI_EXPORT_BLUETOOTH = 1 << 2, -} hakurei_export_flag; - -struct hakurei_syscall_rule { - int syscall; - int m_errno; - struct scmp_arg_cmp *arg; -}; - -int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, - uint32_t multiarch, - struct hakurei_syscall_rule *rules, - size_t rules_sz, hakurei_export_flag flags); \ No newline at end of file diff --git a/seccomp/libseccomp.go b/seccomp/libseccomp.go deleted file mode 100644 index a43b8f23..00000000 --- a/seccomp/libseccomp.go +++ /dev/null @@ -1,188 +0,0 @@ -package seccomp - -/* -#cgo linux pkg-config: --static libseccomp - -#include -*/ -import "C" -import ( - "errors" - "fmt" - "runtime" - "syscall" - "unsafe" -) - -var ( - ErrInvalidRules = errors.New("invalid native rules slice") -) - -// LibraryError represents a libseccomp error. -type LibraryError struct { - Prefix string - Seccomp syscall.Errno - Errno error -} - -func (e *LibraryError) Error() string { - if e.Seccomp == 0 { - if e.Errno == nil { - panic("invalid libseccomp error") - } - return fmt.Sprintf("%s: %s", e.Prefix, e.Errno) - } - if e.Errno == nil { - return fmt.Sprintf("%s: %s", e.Prefix, e.Seccomp) - } - return fmt.Sprintf("%s: %s (%s)", e.Prefix, e.Seccomp, e.Errno) -} - -func (e *LibraryError) Is(err error) bool { - if e == nil { - return err == nil - } - if ef, ok := err.(*LibraryError); ok { - return *e == *ef - } - return (e.Seccomp != 0 && errors.Is(err, e.Seccomp)) || - (e.Errno != nil && errors.Is(err, e.Errno)) -} - -type ( - ScmpSyscall = C.int - ScmpErrno = C.int -) - -// A NativeRule specifies an arch-specific action taken by seccomp under certain conditions. -type NativeRule struct { - // Syscall is the arch-dependent syscall number to act against. - Syscall ScmpSyscall - // Errno is the errno value to return when the condition is satisfied. - Errno ScmpErrno - // Arg is the optional struct scmp_arg_cmp passed to libseccomp. - Arg *ScmpArgCmp -} - -type ExportFlag = C.hakurei_export_flag - -const ( - // AllowMultiarch allows multiarch/emulation. - AllowMultiarch ExportFlag = C.HAKUREI_EXPORT_MULTIARCH - // AllowCAN allows AF_CAN. - AllowCAN ExportFlag = C.HAKUREI_EXPORT_CAN - // AllowBluetooth allows AF_BLUETOOTH. - AllowBluetooth ExportFlag = C.HAKUREI_EXPORT_BLUETOOTH -) - -var resPrefix = [...]string{ - 0: "", - 1: "seccomp_init failed", - 2: "seccomp_arch_add failed", - 3: "seccomp_arch_add failed (multiarch)", - 4: "internal libseccomp failure", - 5: "seccomp_rule_add failed", - 6: "seccomp_export_bpf failed", - 7: "seccomp_load failed", -} - -// Export streams filter contents to fd, or installs it to the current process if fd < 0. -func Export(fd int, rules []NativeRule, flags ExportFlag) error { - if len(rules) == 0 { - return ErrInvalidRules - } - - var ( - arch C.uint32_t = 0 - multiarch C.uint32_t = 0 - ) - switch runtime.GOARCH { - case "386": - arch = C.SCMP_ARCH_X86 - case "amd64": - arch = C.SCMP_ARCH_X86_64 - multiarch = C.SCMP_ARCH_X86 - case "arm": - arch = C.SCMP_ARCH_ARM - case "arm64": - arch = C.SCMP_ARCH_AARCH64 - multiarch = C.SCMP_ARCH_ARM - } - - var ret C.int - - rulesPinner := new(runtime.Pinner) - for i := range rules { - rule := &rules[i] - rulesPinner.Pin(rule) - if rule.Arg != nil { - rulesPinner.Pin(rule.Arg) - } - } - res, err := C.hakurei_export_filter( - &ret, C.int(fd), - arch, multiarch, - (*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])), - C.size_t(len(rules)), - flags, - ) - rulesPinner.Unpin() - - if prefix := resPrefix[res]; prefix != "" { - return &LibraryError{ - prefix, - -syscall.Errno(ret), - err, - } - } - return err -} - -// ScmpCompare is the equivalent of scmp_compare; -// Comparison operators -type ScmpCompare = C.enum_scmp_compare - -const ( - _SCMP_CMP_MIN = C._SCMP_CMP_MIN - - // not equal - SCMP_CMP_NE = C.SCMP_CMP_NE - // less than - SCMP_CMP_LT = C.SCMP_CMP_LT - // less than or equal - SCMP_CMP_LE = C.SCMP_CMP_LE - // equal - SCMP_CMP_EQ = C.SCMP_CMP_EQ - // greater than or equal - SCMP_CMP_GE = C.SCMP_CMP_GE - // greater than - SCMP_CMP_GT = C.SCMP_CMP_GT - // masked equality - SCMP_CMP_MASKED_EQ = C.SCMP_CMP_MASKED_EQ - - _SCMP_CMP_MAX = C._SCMP_CMP_MAX -) - -// ScmpDatum is the equivalent of scmp_datum_t; -// Argument datum -type ScmpDatum uint64 - -// ScmpArgCmp is the equivalent of struct scmp_arg_cmp; -// Argument / Value comparison definition -type ScmpArgCmp struct { - // argument number, starting at 0 - Arg C.uint - // the comparison op, e.g. SCMP_CMP_* - Op ScmpCompare - - DatumA, DatumB ScmpDatum -} - -// only used for testing -func syscallResolveName(s string) (trap int) { - v := C.CString(s) - trap = int(C.seccomp_syscall_resolve_name(v)) - C.free(unsafe.Pointer(v)) - - return -} diff --git a/seccomp/libseccomp_test.go b/seccomp/libseccomp_test.go deleted file mode 100644 index 441245b6..00000000 --- a/seccomp/libseccomp_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package seccomp_test - -import ( - "crypto/sha512" - "errors" - "io" - "slices" - "syscall" - "testing" - - . "git.gensokyo.uk/security/hakurei/seccomp" -) - -func TestExport(t *testing.T) { - testCases := []struct { - name string - presets FilterPreset - flags ExportFlag - want []byte - wantErr bool - }{ - {"compat", 0, 0, []byte{ - 0x95, 0xec, 0x69, 0xd0, 0x17, 0x73, 0x3e, 0x07, - 0x21, 0x60, 0xe0, 0xda, 0x80, 0xfd, 0xeb, 0xec, - 0xdf, 0x27, 0xae, 0x81, 0x66, 0xf5, 0xe2, 0xa7, - 0x31, 0x27, 0x0c, 0x98, 0xea, 0x2d, 0x29, 0x46, - 0xcb, 0x52, 0x31, 0x02, 0x90, 0x63, 0x66, 0x8a, - 0xf2, 0x15, 0x87, 0x91, 0x55, 0xda, 0x21, 0xac, - 0xa7, 0x9b, 0x07, 0x0e, 0x04, 0xc0, 0xee, 0x9a, - 0xcd, 0xf5, 0x8f, 0x55, 0xcf, 0xa8, 0x15, 0xa5, - }, false}, - {"base", PresetExt, 0, []byte{ - 0xdc, 0x7f, 0x2e, 0x1c, 0x5e, 0x82, 0x9b, 0x79, - 0xeb, 0xb7, 0xef, 0xc7, 0x59, 0x15, 0x0f, 0x54, - 0xa8, 0x3a, 0x75, 0xc8, 0xdf, 0x6f, 0xee, 0x4d, - 0xce, 0x5d, 0xad, 0xc4, 0x73, 0x6c, 0x58, 0x5d, - 0x4d, 0xee, 0xbf, 0xeb, 0x3c, 0x79, 0x69, 0xaf, - 0x3a, 0x07, 0x7e, 0x90, 0xb7, 0x7b, 0xb4, 0x74, - 0x1d, 0xb0, 0x5d, 0x90, 0x99, 0x7c, 0x86, 0x59, - 0xb9, 0x58, 0x91, 0x20, 0x6a, 0xc9, 0x95, 0x2d, - }, false}, - {"everything", PresetExt | - PresetDenyNS | PresetDenyTTY | PresetDenyDevel | - PresetLinux32, AllowMultiarch | AllowCAN | - AllowBluetooth, []byte{ - 0xe9, 0x9d, 0xd3, 0x45, 0xe1, 0x95, 0x41, 0x34, - 0x73, 0xd3, 0xcb, 0xee, 0x07, 0xb4, 0xed, 0x57, - 0xb9, 0x08, 0xbf, 0xa8, 0x9e, 0xa2, 0x07, 0x2f, - 0xe9, 0x34, 0x82, 0x84, 0x7f, 0x50, 0xb5, 0xb7, - 0x58, 0xda, 0x17, 0xe7, 0x4c, 0xa2, 0xbb, 0xc0, - 0x08, 0x13, 0xde, 0x49, 0xa2, 0xb9, 0xbf, 0x83, - 0x4c, 0x02, 0x4e, 0xd4, 0x88, 0x50, 0xbe, 0x69, - 0xb6, 0x8a, 0x9a, 0x4c, 0x5f, 0x53, 0xa9, 0xdb, - }, false}, - {"strict", PresetStrict, 0, []byte{ - 0xe8, 0x80, 0x29, 0x8d, 0xf2, 0xbd, 0x67, 0x51, - 0xd0, 0x04, 0x0f, 0xc2, 0x1b, 0xc0, 0xed, 0x4c, - 0x00, 0xf9, 0x5d, 0xc0, 0xd7, 0xba, 0x50, 0x6c, - 0x24, 0x4d, 0x8b, 0x8c, 0xf6, 0x86, 0x6d, 0xba, - 0x8e, 0xf4, 0xa3, 0x32, 0x96, 0xf2, 0x87, 0xb6, - 0x6c, 0xcc, 0xc1, 0xd7, 0x8e, 0x97, 0x02, 0x65, - 0x97, 0xf8, 0x4c, 0xc7, 0xde, 0xc1, 0x57, 0x3e, - 0x14, 0x89, 0x60, 0xfb, 0xd3, 0x5c, 0xd7, 0x35, - }, false}, - {"strict compat", 0 | - PresetDenyNS | PresetDenyTTY | PresetDenyDevel, 0, []byte{ - 0x39, 0x87, 0x1b, 0x93, 0xff, 0xaf, 0xc8, 0xb9, - 0x79, 0xfc, 0xed, 0xc0, 0xb0, 0xc3, 0x7b, 0x9e, - 0x03, 0x92, 0x2f, 0x5b, 0x02, 0x74, 0x8d, 0xc5, - 0xc3, 0xc1, 0x7c, 0x92, 0x52, 0x7f, 0x6e, 0x02, - 0x2e, 0xde, 0x1f, 0x48, 0xbf, 0xf5, 0x92, 0x46, - 0xea, 0x45, 0x2c, 0x0d, 0x1d, 0xe5, 0x48, 0x27, - 0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd, - 0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa, - }, false}, - {"hakurei default", PresetExt | PresetDenyDevel, 0, []byte{ - 0xc6, 0x98, 0xb0, 0x81, 0xff, 0x95, 0x7a, 0xfe, - 0x17, 0xa6, 0xd9, 0x43, 0x74, 0x53, 0x7d, 0x37, - 0xf2, 0xa6, 0x3f, 0x6f, 0x9d, 0xd7, 0x5d, 0xa7, - 0x54, 0x65, 0x42, 0x40, 0x7a, 0x9e, 0x32, 0x47, - 0x6e, 0xbd, 0xa3, 0x31, 0x2b, 0xa7, 0x78, 0x5d, - 0x7f, 0x61, 0x85, 0x42, 0xbc, 0xfa, 0xf2, 0x7c, - 0xa2, 0x7d, 0xcc, 0x2d, 0xdd, 0xba, 0x85, 0x20, - 0x69, 0xd2, 0x8b, 0xcf, 0xe8, 0xca, 0xd3, 0x9a, - }, false}, - } - - buf := make([]byte, 8) - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - e := New(Preset(tc.presets, tc.flags), tc.flags) - digest := sha512.New() - - if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr { - t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr) - return - } - if err := e.Close(); err != nil { - t.Errorf("Close: error = %v", err) - } - if got := digest.Sum(nil); !slices.Equal(got, tc.want) { - t.Fatalf("Export() hash = %x, want %x", - got, tc.want) - return - } - }) - } - - t.Run("close without use", func(t *testing.T) { - e := New(Preset(0, 0), 0) - if err := e.Close(); !errors.Is(err, syscall.EINVAL) { - t.Errorf("Close: error = %v", err) - return - } - }) - - t.Run("close partial read", func(t *testing.T) { - e := New(Preset(0, 0), 0) - if _, err := e.Read(nil); err != nil { - t.Errorf("Read: error = %v", err) - return - } - // the underlying implementation uses buffered io, so the outcome of this is nondeterministic; - // that is not harmful however, so both outcomes are checked for here - if err := e.Close(); err != nil && - (!errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF)) { - t.Errorf("Close: error = %v", err) - return - } - }) -} - -func BenchmarkExport(b *testing.B) { - buf := make([]byte, 8) - for i := 0; i < b.N; i++ { - e := New( - Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32, - AllowMultiarch|AllowCAN|AllowBluetooth), - AllowMultiarch|AllowCAN|AllowBluetooth) - if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil { - b.Fatalf("cannot export: %v", err) - } - if err := e.Close(); err != nil { - b.Fatalf("cannot close exporter: %v", err) - } - } -} diff --git a/seccomp/mksysnum_linux.pl b/seccomp/mksysnum_linux.pl deleted file mode 100755 index 50b88b62..00000000 --- a/seccomp/mksysnum_linux.pl +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env perl -# Copyright 2009 The Go Authors. All rights reserved. -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file. - -use strict; - -my $command = "mksysnum_linux.pl ". join(' ', @ARGV); - -print < 999){ - # ignore deprecated syscalls that are no longer implemented - # https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/unistd.h?id=refs/heads/master#n716 - return; - } - (my $name_upper = $name) =~ y/a-z/A-Z/; - $num = $num + $offset; - if($num > 302){ # not wired in Go standard library - if($state < 0){ - print " \"$name\": SYS_$name_upper,\n"; - } - else{ - print " SYS_$name_upper = $num;\n"; - } - } - elsif($state < 0){ - print " \"$name\": SYS_$name_upper,\n"; - } - else{ - return; - } -} - -GENERATE: - -my $prev; -open(GCC, "gcc -E -dD $ARGV[0] |") || die "can't run gcc"; -while(){ - if(/^#define __NR_Linux\s+([0-9]+)/){ - # mips/mips64: extract offset - $offset = $1; - } - elsif(/^#define __NR_syscalls\s+/) { - # ignore redefinitions of __NR_syscalls - } - elsif(/^#define __NR_(\w+)\s+([0-9]+)/){ - $prev = $2; - fmt($1, $2); - } - elsif(/^#define __NR3264_(\w+)\s+([0-9]+)/){ - $prev = $2; - fmt($1, $2); - } - elsif(/^#define __NR_(\w+)\s+\(\w+\+\s*([0-9]+)\)/){ - fmt($1, $prev+$2) - } - elsif(/^#define __NR_(\w+)\s+\(__NR_Linux \+ ([0-9]+)/){ - fmt($1, $2); - } -} - -if($state < 0){ - $state = $state + 1; - print "}\n\nconst (\n"; - goto GENERATE; -} - -print ")"; diff --git a/seccomp/presets.go b/seccomp/presets.go deleted file mode 100644 index 4a01d2b0..00000000 --- a/seccomp/presets.go +++ /dev/null @@ -1,229 +0,0 @@ -package seccomp - -/* flatpak commit 4c3bf179e2e4a2a298cd1db1d045adaf3f564532 */ - -import ( - . "syscall" -) - -type FilterPreset int - -const ( - // PresetExt are project-specific extensions. - PresetExt FilterPreset = 1 << iota - // PresetDenyNS denies namespace setup syscalls. - PresetDenyNS - // PresetDenyTTY denies faking input. - PresetDenyTTY - // PresetDenyDevel denies development-related syscalls. - PresetDenyDevel - // PresetLinux32 sets PER_LINUX32. - PresetLinux32 -) - -func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) { - allowedPersonality := PER_LINUX - if presets&PresetLinux32 != 0 { - allowedPersonality = PER_LINUX32 - } - presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality)) - - l := len(presetCommon) - if presets&PresetDenyNS != 0 { - l += len(presetNamespace) - } - if presets&PresetDenyTTY != 0 { - l += len(presetTTY) - } - if presets&PresetDenyDevel != 0 { - l += len(presetDevelFinal) - } - if flags&AllowMultiarch == 0 { - l += len(presetEmu) - } - if presets&PresetExt != 0 { - l += len(presetCommonExt) - if presets&PresetDenyNS != 0 { - l += len(presetNamespaceExt) - } - if flags&AllowMultiarch == 0 { - l += len(presetEmuExt) - } - } - - rules = make([]NativeRule, 0, l) - rules = append(rules, presetCommon...) - if presets&PresetDenyNS != 0 { - rules = append(rules, presetNamespace...) - } - if presets&PresetDenyTTY != 0 { - rules = append(rules, presetTTY...) - } - if presets&PresetDenyDevel != 0 { - rules = append(rules, presetDevelFinal...) - } - if flags&AllowMultiarch == 0 { - rules = append(rules, presetEmu...) - } - if presets&PresetExt != 0 { - rules = append(rules, presetCommonExt...) - if presets&PresetDenyNS != 0 { - rules = append(rules, presetNamespaceExt...) - } - if flags&AllowMultiarch == 0 { - rules = append(rules, presetEmuExt...) - } - } - - return -} - -var ( - presetCommon = []NativeRule{ - /* Block dmesg */ - {ScmpSyscall(SYS_SYSLOG), ScmpErrno(EPERM), nil}, - /* Useless old syscall */ - {ScmpSyscall(SYS_USELIB), ScmpErrno(EPERM), nil}, - /* Don't allow disabling accounting */ - {ScmpSyscall(SYS_ACCT), ScmpErrno(EPERM), nil}, - /* Don't allow reading current quota use */ - {ScmpSyscall(SYS_QUOTACTL), ScmpErrno(EPERM), nil}, - - /* Don't allow access to the kernel keyring */ - {ScmpSyscall(SYS_ADD_KEY), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_KEYCTL), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_REQUEST_KEY), ScmpErrno(EPERM), nil}, - - /* Scary VM/NUMA ops */ - {ScmpSyscall(SYS_MOVE_PAGES), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_MBIND), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_GET_MEMPOLICY), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SET_MEMPOLICY), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_MIGRATE_PAGES), ScmpErrno(EPERM), nil}, - } - - /* hakurei: project-specific extensions */ - presetCommonExt = []NativeRule{ - /* system calls for changing the system clock */ - {ScmpSyscall(SYS_ADJTIMEX), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_CLOCK_ADJTIME), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_CLOCK_ADJTIME64), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_CLOCK_SETTIME), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_CLOCK_SETTIME64), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETTIMEOFDAY), ScmpErrno(EPERM), nil}, - - /* loading and unloading of kernel modules */ - {ScmpSyscall(SYS_DELETE_MODULE), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_FINIT_MODULE), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_INIT_MODULE), ScmpErrno(EPERM), nil}, - - /* system calls for rebooting and reboot preparation */ - {ScmpSyscall(SYS_KEXEC_FILE_LOAD), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_KEXEC_LOAD), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_REBOOT), ScmpErrno(EPERM), nil}, - - /* system calls for enabling/disabling swap devices */ - {ScmpSyscall(SYS_SWAPOFF), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SWAPON), ScmpErrno(EPERM), nil}, - } - - presetNamespace = []NativeRule{ - /* Don't allow subnamespace setups: */ - {ScmpSyscall(SYS_UNSHARE), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETNS), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_MOUNT), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_UMOUNT), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_UMOUNT2), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_PIVOT_ROOT), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_CHROOT), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_CLONE), ScmpErrno(EPERM), - &ScmpArgCmp{cloneArg, SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER}}, - - /* seccomp can't look into clone3()'s struct clone_args to check whether - * the flags are OK, so we have no choice but to block clone3(). - * Return ENOSYS so user-space will fall back to clone(). - * (CVE-2021-41133; see also https://github.com/moby/moby/commit/9f6b562d) - */ - {ScmpSyscall(SYS_CLONE3), ScmpErrno(ENOSYS), nil}, - - /* New mount manipulation APIs can also change our VFS. There's no - * legitimate reason to do these in the sandbox, so block all of them - * rather than thinking about which ones might be dangerous. - * (CVE-2021-41133) */ - {ScmpSyscall(SYS_OPEN_TREE), ScmpErrno(ENOSYS), nil}, - {ScmpSyscall(SYS_MOVE_MOUNT), ScmpErrno(ENOSYS), nil}, - {ScmpSyscall(SYS_FSOPEN), ScmpErrno(ENOSYS), nil}, - {ScmpSyscall(SYS_FSCONFIG), ScmpErrno(ENOSYS), nil}, - {ScmpSyscall(SYS_FSMOUNT), ScmpErrno(ENOSYS), nil}, - {ScmpSyscall(SYS_FSPICK), ScmpErrno(ENOSYS), nil}, - {ScmpSyscall(SYS_MOUNT_SETATTR), ScmpErrno(ENOSYS), nil}, - } - - /* hakurei: project-specific extensions */ - presetNamespaceExt = []NativeRule{ - /* changing file ownership */ - {ScmpSyscall(SYS_CHOWN), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_CHOWN32), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_FCHOWN), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_FCHOWN32), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_FCHOWNAT), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_LCHOWN), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_LCHOWN32), ScmpErrno(EPERM), nil}, - - /* system calls for changing user ID and group ID credentials */ - {ScmpSyscall(SYS_SETGID), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETGID32), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETGROUPS), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETGROUPS32), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETREGID), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETREGID32), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETRESGID), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETRESGID32), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETRESUID), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETRESUID32), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETREUID), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETREUID32), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETUID), ScmpErrno(EPERM), nil}, - {ScmpSyscall(SYS_SETUID32), ScmpErrno(EPERM), nil}, - } - - presetTTY = []NativeRule{ - /* Don't allow faking input to the controlling tty (CVE-2017-5226) */ - {ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM), - &ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCSTI}}, - /* In the unlikely event that the controlling tty is a Linux virtual - * console (/dev/tty2 or similar), copy/paste operations have an effect - * similar to TIOCSTI (CVE-2023-28100) */ - {ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM), - &ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCLINUX}}, - } - - presetEmu = []NativeRule{ - /* modify_ldt is a historic source of interesting information leaks, - * so it's disabled as a hardening measure. - * However, it is required to run old 16-bit applications - * as well as some Wine patches, so it's allowed in multiarch. */ - {ScmpSyscall(SYS_MODIFY_LDT), ScmpErrno(EPERM), nil}, - } - - /* hakurei: project-specific extensions */ - presetEmuExt = []NativeRule{ - {ScmpSyscall(SYS_SUBPAGE_PROT), ScmpErrno(ENOSYS), nil}, - {ScmpSyscall(SYS_SWITCH_ENDIAN), ScmpErrno(ENOSYS), nil}, - {ScmpSyscall(SYS_VM86), ScmpErrno(ENOSYS), nil}, - {ScmpSyscall(SYS_VM86OLD), ScmpErrno(ENOSYS), nil}, - } -) - -func presetDevel(allowedPersonality ScmpDatum) []NativeRule { - return []NativeRule{ - /* Profiling operations; we expect these to be done by tools from outside - * the sandbox. In particular perf has been the source of many CVEs. */ - {ScmpSyscall(SYS_PERF_EVENT_OPEN), ScmpErrno(EPERM), nil}, - /* Don't allow you to switch to bsd emulation or whatnot */ - {ScmpSyscall(SYS_PERSONALITY), ScmpErrno(EPERM), - &ScmpArgCmp{0, SCMP_CMP_NE, allowedPersonality, 0}}, - - {ScmpSyscall(SYS_PTRACE), ScmpErrno(EPERM), nil}, - } -} diff --git a/seccomp/presets_clone_backwards2.go b/seccomp/presets_clone_backwards2.go deleted file mode 100644 index 6a7a636e..00000000 --- a/seccomp/presets_clone_backwards2.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build s390 || s390x - -package seccomp - -/* Architectures with CONFIG_CLONE_BACKWARDS2: the child stack - * and flags arguments are reversed so the flags come second */ -const cloneArg = 1 diff --git a/seccomp/presets_clone_generic.go b/seccomp/presets_clone_generic.go deleted file mode 100644 index 9d20890e..00000000 --- a/seccomp/presets_clone_generic.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build !s390 && !s390x - -package seccomp - -/* Normally the flags come first */ -const cloneArg = 0 diff --git a/seccomp/proc.go b/seccomp/proc.go deleted file mode 100644 index e4ce1853..00000000 --- a/seccomp/proc.go +++ /dev/null @@ -1,78 +0,0 @@ -package seccomp - -import ( - "context" - "errors" - "syscall" - - "git.gensokyo.uk/security/hakurei/helper/proc" -) - -const ( - PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel -) - -// New returns an inactive Encoder instance. -func New(rules []NativeRule, flags ExportFlag) *Encoder { return &Encoder{newExporter(rules, flags)} } - -// Load loads a filter into the kernel. -func Load(rules []NativeRule, flags ExportFlag) error { return Export(-1, rules, flags) } - -/* -An Encoder writes a BPF program to an output stream. - -Methods of Encoder are not safe for concurrent use. - -An Encoder must not be copied after first use. -*/ -type Encoder struct { - *exporter -} - -func (e *Encoder) Read(p []byte) (n int, err error) { - if err = e.prepare(); err != nil { - return - } - return e.r.Read(p) -} - -func (e *Encoder) Close() error { - if e.r == nil { - return syscall.EINVAL - } - - // this hangs if the cgo thread fails to exit - return errors.Join(e.closeWrite(), <-e.exportErr) -} - -// NewFile returns an instance of exporter implementing [proc.File]. -func NewFile(rules []NativeRule, flags ExportFlag) proc.File { - return &File{rules: rules, flags: flags} -} - -// File implements [proc.File] and provides access to the read end of exporter pipe. -type File struct { - rules []NativeRule - flags ExportFlag - proc.BaseFile -} - -func (f *File) ErrCount() int { return 2 } -func (f *File) Fulfill(ctx context.Context, dispatchErr func(error)) error { - e := newExporter(f.rules, f.flags) - if err := e.prepare(); err != nil { - return err - } - f.Set(e.r) - go func() { - select { - case err := <-e.exportErr: - dispatchErr(nil) - dispatchErr(err) - case <-ctx.Done(): - dispatchErr(e.closeWrite()) - dispatchErr(<-e.exportErr) - } - }() - return nil -} diff --git a/seccomp/seccomp.go b/seccomp/seccomp.go deleted file mode 100644 index 664b31c6..00000000 --- a/seccomp/seccomp.go +++ /dev/null @@ -1,60 +0,0 @@ -// Package seccomp provides high level wrappers around libseccomp. -package seccomp - -import ( - "os" - "runtime" - "sync" -) - -type exporter struct { - rules []NativeRule - flags ExportFlag - r, w *os.File - - prepareOnce sync.Once - prepareErr error - closeOnce sync.Once - closeErr error - exportErr <-chan error -} - -func (e *exporter) prepare() error { - e.prepareOnce.Do(func() { - if r, w, err := os.Pipe(); err != nil { - e.prepareErr = err - return - } else { - e.r, e.w = r, w - } - - ec := make(chan error, 1) - go func(fd uintptr) { - ec <- Export(int(fd), e.rules, e.flags) - close(ec) - _ = e.closeWrite() - runtime.KeepAlive(e.w) - }(e.w.Fd()) - e.exportErr = ec - runtime.SetFinalizer(e, (*exporter).closeWrite) - }) - return e.prepareErr -} - -func (e *exporter) closeWrite() error { - e.closeOnce.Do(func() { - if e.w == nil { - panic("closeWrite called on invalid exporter") - } - e.closeErr = e.w.Close() - - // no need for a finalizer anymore - runtime.SetFinalizer(e, nil) - }) - - return e.closeErr -} - -func newExporter(rules []NativeRule, flags ExportFlag) *exporter { - return &exporter{rules: rules, flags: flags} -} diff --git a/seccomp/seccomp_test.go b/seccomp/seccomp_test.go deleted file mode 100644 index 9b43f461..00000000 --- a/seccomp/seccomp_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package seccomp_test - -import ( - "errors" - "runtime" - "syscall" - "testing" - - "git.gensokyo.uk/security/hakurei/seccomp" -) - -func TestLibraryError(t *testing.T) { - testCases := []struct { - name string - sample *seccomp.LibraryError - want string - wantIs bool - compare error - }{ - { - "full", - &seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF}, - "seccomp_export_bpf failed: operation canceled (bad file descriptor)", - true, - &seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF}, - }, - { - "errno only", - &seccomp.LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM}, - "seccomp_init failed: cannot allocate memory", - false, - nil, - }, - { - "seccomp only", - &seccomp.LibraryError{Prefix: "internal libseccomp failure", Seccomp: syscall.EFAULT}, - "internal libseccomp failure: bad address", - true, - syscall.EFAULT, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if errors.Is(tc.sample, tc.compare) != tc.wantIs { - t.Errorf("errors.Is(%#v, %#v) did not return %v", - tc.sample, tc.compare, tc.wantIs) - } - - if got := tc.sample.Error(); got != tc.want { - t.Errorf("Error: %q, want %q", - got, tc.want) - } - }) - } - - t.Run("invalid", func(t *testing.T) { - wantPanic := "invalid libseccomp error" - defer func() { - if r := recover(); r != wantPanic { - t.Errorf("panic: %q, want %q", r, wantPanic) - } - }() - runtime.KeepAlive(new(seccomp.LibraryError).Error()) - }) -} diff --git a/seccomp/syscall.go b/seccomp/syscall.go deleted file mode 100644 index 36a988aa..00000000 --- a/seccomp/syscall.go +++ /dev/null @@ -1,28 +0,0 @@ -package seccomp - -import "iter" - -// Syscalls returns an iterator over all wired syscalls. -func Syscalls() iter.Seq2[string, int] { - return func(yield func(string, int) bool) { - for name, num := range syscallNum { - if !yield(name, num) { - return - } - } - for name, num := range syscallNumExtra { - if !yield(name, num) { - return - } - } - } -} - -// SyscallResolveName resolves a syscall number from its string representation. -func SyscallResolveName(name string) (num int, ok bool) { - if num, ok = syscallNum[name]; ok { - return - } - num, ok = syscallNumExtra[name] - return -} diff --git a/seccomp/syscall_extra_linux_amd64.go b/seccomp/syscall_extra_linux_amd64.go deleted file mode 100644 index 4b83a876..00000000 --- a/seccomp/syscall_extra_linux_amd64.go +++ /dev/null @@ -1,54 +0,0 @@ -package seccomp - -/* -#cgo linux pkg-config: --static libseccomp - -#include -#include -*/ -import "C" - -const ( - PER_LINUX = C.PER_LINUX - PER_LINUX32 = C.PER_LINUX32 -) - -var syscallNumExtra = map[string]int{ - "umount": SYS_UMOUNT, - "subpage_prot": SYS_SUBPAGE_PROT, - "switch_endian": SYS_SWITCH_ENDIAN, - "vm86": SYS_VM86, - "vm86old": SYS_VM86OLD, - "clock_adjtime64": SYS_CLOCK_ADJTIME64, - "clock_settime64": SYS_CLOCK_SETTIME64, - "chown32": SYS_CHOWN32, - "fchown32": SYS_FCHOWN32, - "lchown32": SYS_LCHOWN32, - "setgid32": SYS_SETGID32, - "setgroups32": SYS_SETGROUPS32, - "setregid32": SYS_SETREGID32, - "setresgid32": SYS_SETRESGID32, - "setresuid32": SYS_SETRESUID32, - "setreuid32": SYS_SETREUID32, - "setuid32": SYS_SETUID32, -} - -const ( - SYS_UMOUNT = C.__SNR_umount - SYS_SUBPAGE_PROT = C.__SNR_subpage_prot - SYS_SWITCH_ENDIAN = C.__SNR_switch_endian - SYS_VM86 = C.__SNR_vm86 - SYS_VM86OLD = C.__SNR_vm86old - SYS_CLOCK_ADJTIME64 = C.__SNR_clock_adjtime64 - SYS_CLOCK_SETTIME64 = C.__SNR_clock_settime64 - SYS_CHOWN32 = C.__SNR_chown32 - SYS_FCHOWN32 = C.__SNR_fchown32 - SYS_LCHOWN32 = C.__SNR_lchown32 - SYS_SETGID32 = C.__SNR_setgid32 - SYS_SETGROUPS32 = C.__SNR_setgroups32 - SYS_SETREGID32 = C.__SNR_setregid32 - SYS_SETRESGID32 = C.__SNR_setresgid32 - SYS_SETRESUID32 = C.__SNR_setresuid32 - SYS_SETREUID32 = C.__SNR_setreuid32 - SYS_SETUID32 = C.__SNR_setuid32 -) diff --git a/seccomp/syscall_linux_amd64.go b/seccomp/syscall_linux_amd64.go deleted file mode 100644 index 28dbcb47..00000000 --- a/seccomp/syscall_linux_amd64.go +++ /dev/null @@ -1,459 +0,0 @@ -// mksysnum_linux.pl /usr/include/asm/unistd_64.h -// Code generated by the command above; DO NOT EDIT. - -package seccomp - -import . "syscall" - -var syscallNum = map[string]int{ - "read": SYS_READ, - "write": SYS_WRITE, - "open": SYS_OPEN, - "close": SYS_CLOSE, - "stat": SYS_STAT, - "fstat": SYS_FSTAT, - "lstat": SYS_LSTAT, - "poll": SYS_POLL, - "lseek": SYS_LSEEK, - "mmap": SYS_MMAP, - "mprotect": SYS_MPROTECT, - "munmap": SYS_MUNMAP, - "brk": SYS_BRK, - "rt_sigaction": SYS_RT_SIGACTION, - "rt_sigprocmask": SYS_RT_SIGPROCMASK, - "rt_sigreturn": SYS_RT_SIGRETURN, - "ioctl": SYS_IOCTL, - "pread64": SYS_PREAD64, - "pwrite64": SYS_PWRITE64, - "readv": SYS_READV, - "writev": SYS_WRITEV, - "access": SYS_ACCESS, - "pipe": SYS_PIPE, - "select": SYS_SELECT, - "sched_yield": SYS_SCHED_YIELD, - "mremap": SYS_MREMAP, - "msync": SYS_MSYNC, - "mincore": SYS_MINCORE, - "madvise": SYS_MADVISE, - "shmget": SYS_SHMGET, - "shmat": SYS_SHMAT, - "shmctl": SYS_SHMCTL, - "dup": SYS_DUP, - "dup2": SYS_DUP2, - "pause": SYS_PAUSE, - "nanosleep": SYS_NANOSLEEP, - "getitimer": SYS_GETITIMER, - "alarm": SYS_ALARM, - "setitimer": SYS_SETITIMER, - "getpid": SYS_GETPID, - "sendfile": SYS_SENDFILE, - "socket": SYS_SOCKET, - "connect": SYS_CONNECT, - "accept": SYS_ACCEPT, - "sendto": SYS_SENDTO, - "recvfrom": SYS_RECVFROM, - "sendmsg": SYS_SENDMSG, - "recvmsg": SYS_RECVMSG, - "shutdown": SYS_SHUTDOWN, - "bind": SYS_BIND, - "listen": SYS_LISTEN, - "getsockname": SYS_GETSOCKNAME, - "getpeername": SYS_GETPEERNAME, - "socketpair": SYS_SOCKETPAIR, - "setsockopt": SYS_SETSOCKOPT, - "getsockopt": SYS_GETSOCKOPT, - "clone": SYS_CLONE, - "fork": SYS_FORK, - "vfork": SYS_VFORK, - "execve": SYS_EXECVE, - "exit": SYS_EXIT, - "wait4": SYS_WAIT4, - "kill": SYS_KILL, - "uname": SYS_UNAME, - "semget": SYS_SEMGET, - "semop": SYS_SEMOP, - "semctl": SYS_SEMCTL, - "shmdt": SYS_SHMDT, - "msgget": SYS_MSGGET, - "msgsnd": SYS_MSGSND, - "msgrcv": SYS_MSGRCV, - "msgctl": SYS_MSGCTL, - "fcntl": SYS_FCNTL, - "flock": SYS_FLOCK, - "fsync": SYS_FSYNC, - "fdatasync": SYS_FDATASYNC, - "truncate": SYS_TRUNCATE, - "ftruncate": SYS_FTRUNCATE, - "getdents": SYS_GETDENTS, - "getcwd": SYS_GETCWD, - "chdir": SYS_CHDIR, - "fchdir": SYS_FCHDIR, - "rename": SYS_RENAME, - "mkdir": SYS_MKDIR, - "rmdir": SYS_RMDIR, - "creat": SYS_CREAT, - "link": SYS_LINK, - "unlink": SYS_UNLINK, - "symlink": SYS_SYMLINK, - "readlink": SYS_READLINK, - "chmod": SYS_CHMOD, - "fchmod": SYS_FCHMOD, - "chown": SYS_CHOWN, - "fchown": SYS_FCHOWN, - "lchown": SYS_LCHOWN, - "umask": SYS_UMASK, - "gettimeofday": SYS_GETTIMEOFDAY, - "getrlimit": SYS_GETRLIMIT, - "getrusage": SYS_GETRUSAGE, - "sysinfo": SYS_SYSINFO, - "times": SYS_TIMES, - "ptrace": SYS_PTRACE, - "getuid": SYS_GETUID, - "syslog": SYS_SYSLOG, - "getgid": SYS_GETGID, - "setuid": SYS_SETUID, - "setgid": SYS_SETGID, - "geteuid": SYS_GETEUID, - "getegid": SYS_GETEGID, - "setpgid": SYS_SETPGID, - "getppid": SYS_GETPPID, - "getpgrp": SYS_GETPGRP, - "setsid": SYS_SETSID, - "setreuid": SYS_SETREUID, - "setregid": SYS_SETREGID, - "getgroups": SYS_GETGROUPS, - "setgroups": SYS_SETGROUPS, - "setresuid": SYS_SETRESUID, - "getresuid": SYS_GETRESUID, - "setresgid": SYS_SETRESGID, - "getresgid": SYS_GETRESGID, - "getpgid": SYS_GETPGID, - "setfsuid": SYS_SETFSUID, - "setfsgid": SYS_SETFSGID, - "getsid": SYS_GETSID, - "capget": SYS_CAPGET, - "capset": SYS_CAPSET, - "rt_sigpending": SYS_RT_SIGPENDING, - "rt_sigtimedwait": SYS_RT_SIGTIMEDWAIT, - "rt_sigqueueinfo": SYS_RT_SIGQUEUEINFO, - "rt_sigsuspend": SYS_RT_SIGSUSPEND, - "sigaltstack": SYS_SIGALTSTACK, - "utime": SYS_UTIME, - "mknod": SYS_MKNOD, - "uselib": SYS_USELIB, - "personality": SYS_PERSONALITY, - "ustat": SYS_USTAT, - "statfs": SYS_STATFS, - "fstatfs": SYS_FSTATFS, - "sysfs": SYS_SYSFS, - "getpriority": SYS_GETPRIORITY, - "setpriority": SYS_SETPRIORITY, - "sched_setparam": SYS_SCHED_SETPARAM, - "sched_getparam": SYS_SCHED_GETPARAM, - "sched_setscheduler": SYS_SCHED_SETSCHEDULER, - "sched_getscheduler": SYS_SCHED_GETSCHEDULER, - "sched_get_priority_max": SYS_SCHED_GET_PRIORITY_MAX, - "sched_get_priority_min": SYS_SCHED_GET_PRIORITY_MIN, - "sched_rr_get_interval": SYS_SCHED_RR_GET_INTERVAL, - "mlock": SYS_MLOCK, - "munlock": SYS_MUNLOCK, - "mlockall": SYS_MLOCKALL, - "munlockall": SYS_MUNLOCKALL, - "vhangup": SYS_VHANGUP, - "modify_ldt": SYS_MODIFY_LDT, - "pivot_root": SYS_PIVOT_ROOT, - "_sysctl": SYS__SYSCTL, - "prctl": SYS_PRCTL, - "arch_prctl": SYS_ARCH_PRCTL, - "adjtimex": SYS_ADJTIMEX, - "setrlimit": SYS_SETRLIMIT, - "chroot": SYS_CHROOT, - "sync": SYS_SYNC, - "acct": SYS_ACCT, - "settimeofday": SYS_SETTIMEOFDAY, - "mount": SYS_MOUNT, - "umount2": SYS_UMOUNT2, - "swapon": SYS_SWAPON, - "swapoff": SYS_SWAPOFF, - "reboot": SYS_REBOOT, - "sethostname": SYS_SETHOSTNAME, - "setdomainname": SYS_SETDOMAINNAME, - "iopl": SYS_IOPL, - "ioperm": SYS_IOPERM, - "create_module": SYS_CREATE_MODULE, - "init_module": SYS_INIT_MODULE, - "delete_module": SYS_DELETE_MODULE, - "get_kernel_syms": SYS_GET_KERNEL_SYMS, - "query_module": SYS_QUERY_MODULE, - "quotactl": SYS_QUOTACTL, - "nfsservctl": SYS_NFSSERVCTL, - "getpmsg": SYS_GETPMSG, - "putpmsg": SYS_PUTPMSG, - "afs_syscall": SYS_AFS_SYSCALL, - "tuxcall": SYS_TUXCALL, - "security": SYS_SECURITY, - "gettid": SYS_GETTID, - "readahead": SYS_READAHEAD, - "setxattr": SYS_SETXATTR, - "lsetxattr": SYS_LSETXATTR, - "fsetxattr": SYS_FSETXATTR, - "getxattr": SYS_GETXATTR, - "lgetxattr": SYS_LGETXATTR, - "fgetxattr": SYS_FGETXATTR, - "listxattr": SYS_LISTXATTR, - "llistxattr": SYS_LLISTXATTR, - "flistxattr": SYS_FLISTXATTR, - "removexattr": SYS_REMOVEXATTR, - "lremovexattr": SYS_LREMOVEXATTR, - "fremovexattr": SYS_FREMOVEXATTR, - "tkill": SYS_TKILL, - "time": SYS_TIME, - "futex": SYS_FUTEX, - "sched_setaffinity": SYS_SCHED_SETAFFINITY, - "sched_getaffinity": SYS_SCHED_GETAFFINITY, - "set_thread_area": SYS_SET_THREAD_AREA, - "io_setup": SYS_IO_SETUP, - "io_destroy": SYS_IO_DESTROY, - "io_getevents": SYS_IO_GETEVENTS, - "io_submit": SYS_IO_SUBMIT, - "io_cancel": SYS_IO_CANCEL, - "get_thread_area": SYS_GET_THREAD_AREA, - "lookup_dcookie": SYS_LOOKUP_DCOOKIE, - "epoll_create": SYS_EPOLL_CREATE, - "epoll_ctl_old": SYS_EPOLL_CTL_OLD, - "epoll_wait_old": SYS_EPOLL_WAIT_OLD, - "remap_file_pages": SYS_REMAP_FILE_PAGES, - "getdents64": SYS_GETDENTS64, - "set_tid_address": SYS_SET_TID_ADDRESS, - "restart_syscall": SYS_RESTART_SYSCALL, - "semtimedop": SYS_SEMTIMEDOP, - "fadvise64": SYS_FADVISE64, - "timer_create": SYS_TIMER_CREATE, - "timer_settime": SYS_TIMER_SETTIME, - "timer_gettime": SYS_TIMER_GETTIME, - "timer_getoverrun": SYS_TIMER_GETOVERRUN, - "timer_delete": SYS_TIMER_DELETE, - "clock_settime": SYS_CLOCK_SETTIME, - "clock_gettime": SYS_CLOCK_GETTIME, - "clock_getres": SYS_CLOCK_GETRES, - "clock_nanosleep": SYS_CLOCK_NANOSLEEP, - "exit_group": SYS_EXIT_GROUP, - "epoll_wait": SYS_EPOLL_WAIT, - "epoll_ctl": SYS_EPOLL_CTL, - "tgkill": SYS_TGKILL, - "utimes": SYS_UTIMES, - "vserver": SYS_VSERVER, - "mbind": SYS_MBIND, - "set_mempolicy": SYS_SET_MEMPOLICY, - "get_mempolicy": SYS_GET_MEMPOLICY, - "mq_open": SYS_MQ_OPEN, - "mq_unlink": SYS_MQ_UNLINK, - "mq_timedsend": SYS_MQ_TIMEDSEND, - "mq_timedreceive": SYS_MQ_TIMEDRECEIVE, - "mq_notify": SYS_MQ_NOTIFY, - "mq_getsetattr": SYS_MQ_GETSETATTR, - "kexec_load": SYS_KEXEC_LOAD, - "waitid": SYS_WAITID, - "add_key": SYS_ADD_KEY, - "request_key": SYS_REQUEST_KEY, - "keyctl": SYS_KEYCTL, - "ioprio_set": SYS_IOPRIO_SET, - "ioprio_get": SYS_IOPRIO_GET, - "inotify_init": SYS_INOTIFY_INIT, - "inotify_add_watch": SYS_INOTIFY_ADD_WATCH, - "inotify_rm_watch": SYS_INOTIFY_RM_WATCH, - "migrate_pages": SYS_MIGRATE_PAGES, - "openat": SYS_OPENAT, - "mkdirat": SYS_MKDIRAT, - "mknodat": SYS_MKNODAT, - "fchownat": SYS_FCHOWNAT, - "futimesat": SYS_FUTIMESAT, - "newfstatat": SYS_NEWFSTATAT, - "unlinkat": SYS_UNLINKAT, - "renameat": SYS_RENAMEAT, - "linkat": SYS_LINKAT, - "symlinkat": SYS_SYMLINKAT, - "readlinkat": SYS_READLINKAT, - "fchmodat": SYS_FCHMODAT, - "faccessat": SYS_FACCESSAT, - "pselect6": SYS_PSELECT6, - "ppoll": SYS_PPOLL, - "unshare": SYS_UNSHARE, - "set_robust_list": SYS_SET_ROBUST_LIST, - "get_robust_list": SYS_GET_ROBUST_LIST, - "splice": SYS_SPLICE, - "tee": SYS_TEE, - "sync_file_range": SYS_SYNC_FILE_RANGE, - "vmsplice": SYS_VMSPLICE, - "move_pages": SYS_MOVE_PAGES, - "utimensat": SYS_UTIMENSAT, - "epoll_pwait": SYS_EPOLL_PWAIT, - "signalfd": SYS_SIGNALFD, - "timerfd_create": SYS_TIMERFD_CREATE, - "eventfd": SYS_EVENTFD, - "fallocate": SYS_FALLOCATE, - "timerfd_settime": SYS_TIMERFD_SETTIME, - "timerfd_gettime": SYS_TIMERFD_GETTIME, - "accept4": SYS_ACCEPT4, - "signalfd4": SYS_SIGNALFD4, - "eventfd2": SYS_EVENTFD2, - "epoll_create1": SYS_EPOLL_CREATE1, - "dup3": SYS_DUP3, - "pipe2": SYS_PIPE2, - "inotify_init1": SYS_INOTIFY_INIT1, - "preadv": SYS_PREADV, - "pwritev": SYS_PWRITEV, - "rt_tgsigqueueinfo": SYS_RT_TGSIGQUEUEINFO, - "perf_event_open": SYS_PERF_EVENT_OPEN, - "recvmmsg": SYS_RECVMMSG, - "fanotify_init": SYS_FANOTIFY_INIT, - "fanotify_mark": SYS_FANOTIFY_MARK, - "prlimit64": SYS_PRLIMIT64, - "name_to_handle_at": SYS_NAME_TO_HANDLE_AT, - "open_by_handle_at": SYS_OPEN_BY_HANDLE_AT, - "clock_adjtime": SYS_CLOCK_ADJTIME, - "syncfs": SYS_SYNCFS, - "sendmmsg": SYS_SENDMMSG, - "setns": SYS_SETNS, - "getcpu": SYS_GETCPU, - "process_vm_readv": SYS_PROCESS_VM_READV, - "process_vm_writev": SYS_PROCESS_VM_WRITEV, - "kcmp": SYS_KCMP, - "finit_module": SYS_FINIT_MODULE, - "sched_setattr": SYS_SCHED_SETATTR, - "sched_getattr": SYS_SCHED_GETATTR, - "renameat2": SYS_RENAMEAT2, - "seccomp": SYS_SECCOMP, - "getrandom": SYS_GETRANDOM, - "memfd_create": SYS_MEMFD_CREATE, - "kexec_file_load": SYS_KEXEC_FILE_LOAD, - "bpf": SYS_BPF, - "execveat": SYS_EXECVEAT, - "userfaultfd": SYS_USERFAULTFD, - "membarrier": SYS_MEMBARRIER, - "mlock2": SYS_MLOCK2, - "copy_file_range": SYS_COPY_FILE_RANGE, - "preadv2": SYS_PREADV2, - "pwritev2": SYS_PWRITEV2, - "pkey_mprotect": SYS_PKEY_MPROTECT, - "pkey_alloc": SYS_PKEY_ALLOC, - "pkey_free": SYS_PKEY_FREE, - "statx": SYS_STATX, - "io_pgetevents": SYS_IO_PGETEVENTS, - "rseq": SYS_RSEQ, - "uretprobe": SYS_URETPROBE, - "pidfd_send_signal": SYS_PIDFD_SEND_SIGNAL, - "io_uring_setup": SYS_IO_URING_SETUP, - "io_uring_enter": SYS_IO_URING_ENTER, - "io_uring_register": SYS_IO_URING_REGISTER, - "open_tree": SYS_OPEN_TREE, - "move_mount": SYS_MOVE_MOUNT, - "fsopen": SYS_FSOPEN, - "fsconfig": SYS_FSCONFIG, - "fsmount": SYS_FSMOUNT, - "fspick": SYS_FSPICK, - "pidfd_open": SYS_PIDFD_OPEN, - "clone3": SYS_CLONE3, - "close_range": SYS_CLOSE_RANGE, - "openat2": SYS_OPENAT2, - "pidfd_getfd": SYS_PIDFD_GETFD, - "faccessat2": SYS_FACCESSAT2, - "process_madvise": SYS_PROCESS_MADVISE, - "epoll_pwait2": SYS_EPOLL_PWAIT2, - "mount_setattr": SYS_MOUNT_SETATTR, - "quotactl_fd": SYS_QUOTACTL_FD, - "landlock_create_ruleset": SYS_LANDLOCK_CREATE_RULESET, - "landlock_add_rule": SYS_LANDLOCK_ADD_RULE, - "landlock_restrict_self": SYS_LANDLOCK_RESTRICT_SELF, - "memfd_secret": SYS_MEMFD_SECRET, - "process_mrelease": SYS_PROCESS_MRELEASE, - "futex_waitv": SYS_FUTEX_WAITV, - "set_mempolicy_home_node": SYS_SET_MEMPOLICY_HOME_NODE, - "cachestat": SYS_CACHESTAT, - "fchmodat2": SYS_FCHMODAT2, - "map_shadow_stack": SYS_MAP_SHADOW_STACK, - "futex_wake": SYS_FUTEX_WAKE, - "futex_wait": SYS_FUTEX_WAIT, - "futex_requeue": SYS_FUTEX_REQUEUE, - "statmount": SYS_STATMOUNT, - "listmount": SYS_LISTMOUNT, - "lsm_get_self_attr": SYS_LSM_GET_SELF_ATTR, - "lsm_set_self_attr": SYS_LSM_SET_SELF_ATTR, - "lsm_list_modules": SYS_LSM_LIST_MODULES, - "mseal": SYS_MSEAL, -} - -const ( - SYS_NAME_TO_HANDLE_AT = 303 - SYS_OPEN_BY_HANDLE_AT = 304 - SYS_CLOCK_ADJTIME = 305 - SYS_SYNCFS = 306 - SYS_SENDMMSG = 307 - SYS_SETNS = 308 - SYS_GETCPU = 309 - SYS_PROCESS_VM_READV = 310 - SYS_PROCESS_VM_WRITEV = 311 - SYS_KCMP = 312 - SYS_FINIT_MODULE = 313 - SYS_SCHED_SETATTR = 314 - SYS_SCHED_GETATTR = 315 - SYS_RENAMEAT2 = 316 - SYS_SECCOMP = 317 - SYS_GETRANDOM = 318 - SYS_MEMFD_CREATE = 319 - SYS_KEXEC_FILE_LOAD = 320 - SYS_BPF = 321 - SYS_EXECVEAT = 322 - SYS_USERFAULTFD = 323 - SYS_MEMBARRIER = 324 - SYS_MLOCK2 = 325 - SYS_COPY_FILE_RANGE = 326 - SYS_PREADV2 = 327 - SYS_PWRITEV2 = 328 - SYS_PKEY_MPROTECT = 329 - SYS_PKEY_ALLOC = 330 - SYS_PKEY_FREE = 331 - SYS_STATX = 332 - SYS_IO_PGETEVENTS = 333 - SYS_RSEQ = 334 - SYS_URETPROBE = 335 - SYS_PIDFD_SEND_SIGNAL = 424 - SYS_IO_URING_SETUP = 425 - SYS_IO_URING_ENTER = 426 - SYS_IO_URING_REGISTER = 427 - SYS_OPEN_TREE = 428 - SYS_MOVE_MOUNT = 429 - SYS_FSOPEN = 430 - SYS_FSCONFIG = 431 - SYS_FSMOUNT = 432 - SYS_FSPICK = 433 - SYS_PIDFD_OPEN = 434 - SYS_CLONE3 = 435 - SYS_CLOSE_RANGE = 436 - SYS_OPENAT2 = 437 - SYS_PIDFD_GETFD = 438 - SYS_FACCESSAT2 = 439 - SYS_PROCESS_MADVISE = 440 - SYS_EPOLL_PWAIT2 = 441 - SYS_MOUNT_SETATTR = 442 - SYS_QUOTACTL_FD = 443 - SYS_LANDLOCK_CREATE_RULESET = 444 - SYS_LANDLOCK_ADD_RULE = 445 - SYS_LANDLOCK_RESTRICT_SELF = 446 - SYS_MEMFD_SECRET = 447 - SYS_PROCESS_MRELEASE = 448 - SYS_FUTEX_WAITV = 449 - SYS_SET_MEMPOLICY_HOME_NODE = 450 - SYS_CACHESTAT = 451 - SYS_FCHMODAT2 = 452 - SYS_MAP_SHADOW_STACK = 453 - SYS_FUTEX_WAKE = 454 - SYS_FUTEX_WAIT = 455 - SYS_FUTEX_REQUEUE = 456 - SYS_STATMOUNT = 457 - SYS_LISTMOUNT = 458 - SYS_LSM_GET_SELF_ATTR = 459 - SYS_LSM_SET_SELF_ATTR = 460 - SYS_LSM_LIST_MODULES = 461 - SYS_MSEAL = 462 -) diff --git a/seccomp/syscall_test.go b/seccomp/syscall_test.go deleted file mode 100644 index 933f060b..00000000 --- a/seccomp/syscall_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package seccomp - -import ( - "testing" -) - -func TestSyscallResolveName(t *testing.T) { - for name, want := range Syscalls() { - t.Run(name, func(t *testing.T) { - if got := syscallResolveName(name); got != want { - t.Errorf("syscallResolveName(%q) = %d, want %d", - name, got, want) - } - if got, ok := SyscallResolveName(name); !ok || got != want { - t.Errorf("SyscallResolveName(%q) = %d, want %d", - name, got, want) - } - }) - } -} diff --git a/syscall.go b/syscall.go deleted file mode 100644 index 4b22c23b..00000000 --- a/syscall.go +++ /dev/null @@ -1,81 +0,0 @@ -package hakurei - -import ( - "syscall" - "unsafe" -) - -const ( - O_PATH = 0x200000 - - PR_SET_NO_NEW_PRIVS = 0x26 - - CAP_SYS_ADMIN = 0x15 - CAP_SETPCAP = 0x8 -) - -const ( - SUID_DUMP_DISABLE = iota - SUID_DUMP_USER -) - -func SetDumpable(dumpable uintptr) error { - // linux/sched/coredump.h - if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 { - return errno - } - - return nil -} - -const ( - _LINUX_CAPABILITY_VERSION_3 = 0x20080522 - - PR_CAP_AMBIENT = 0x2f - PR_CAP_AMBIENT_RAISE = 0x2 - PR_CAP_AMBIENT_CLEAR_ALL = 0x4 -) - -type ( - capHeader struct { - version uint32 - pid int32 - } - - capData struct { - effective uint32 - permitted uint32 - inheritable uint32 - } -) - -// See CAP_TO_INDEX in linux/capability.h: -func capToIndex(cap uintptr) uintptr { return cap >> 5 } - -// See CAP_TO_MASK in linux/capability.h: -func capToMask(cap uintptr) uint32 { return 1 << uint(cap&31) } - -func capset(hdrp *capHeader, datap *[2]capData) error { - if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET, - uintptr(unsafe.Pointer(hdrp)), - uintptr(unsafe.Pointer(&datap[0])), 0); errno != 0 { - return errno - } - return nil -} - -// IgnoringEINTR makes a function call and repeats it if it returns an -// EINTR error. This appears to be required even though we install all -// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846. -// Also #20400 and #36644 are issues in which a signal handler is -// installed without setting SA_RESTART. None of these are the common case, -// but there are enough of them that it seems that we can't avoid -// an EINTR loop. -func IgnoringEINTR(fn func() error) error { - for { - err := fn() - if err != syscall.EINTR { - return err - } - } -} diff --git a/sysctl.go b/sysctl.go deleted file mode 100644 index aa74da42..00000000 --- a/sysctl.go +++ /dev/null @@ -1,47 +0,0 @@ -package hakurei - -import ( - "bytes" - "log" - "os" - "strconv" - "sync" -) - -var ( - kernelOverflowuid int - kernelOverflowgid int - kernelCapLastCap int - - sysctlOnce sync.Once -) - -const ( - kernelOverflowuidPath = "/proc/sys/kernel/overflowuid" - kernelOverflowgidPath = "/proc/sys/kernel/overflowgid" - kernelCapLastCapPath = "/proc/sys/kernel/cap_last_cap" -) - -func mustReadSysctl() { - if v, err := os.ReadFile(kernelOverflowuidPath); err != nil { - log.Fatalf("cannot read %q: %v", kernelOverflowuidPath, err) - } else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { - log.Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err) - } - - if v, err := os.ReadFile(kernelOverflowgidPath); err != nil { - log.Fatalf("cannot read %q: %v", kernelOverflowgidPath, err) - } else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { - log.Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err) - } - - if v, err := os.ReadFile(kernelCapLastCapPath); err != nil { - log.Fatalf("cannot read %q: %v", kernelCapLastCapPath, err) - } else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { - log.Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err) - } -} - -func OverflowUid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowuid } -func OverflowGid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowgid } -func LastCap() uintptr { sysctlOnce.Do(mustReadSysctl); return uintptr(kernelCapLastCap) } diff --git a/system/dbus/dbus_test.go b/system/dbus/dbus_test.go index 7b1e1858..6fd23673 100644 --- a/system/dbus/dbus_test.go +++ b/system/dbus/dbus_test.go @@ -13,7 +13,7 @@ import ( "testing" "time" - "git.gensokyo.uk/security/hakurei" + "git.gensokyo.uk/security/hakurei/container" "git.gensokyo.uk/security/hakurei/helper" "git.gensokyo.uk/security/hakurei/internal" "git.gensokyo.uk/security/hakurei/internal/hlog" @@ -134,11 +134,11 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { } p.CmdF = func(v any) { if useSandbox { - container := v.(*hakurei.Container) - if container.Args[0] != dbus.ProxyName { + z := v.(*container.Container) + if z.Args[0] != dbus.ProxyName { panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0])) } - container.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, container.Args[1:]...) + z.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, z.Args[1:]...) } else { cmd := v.(*exec.Cmd) if cmd.Args[0] != dbus.ProxyName { @@ -208,6 +208,6 @@ func TestHelperInit(t *testing.T) { if len(os.Args) != 5 || os.Args[4] != "init" { return } - hakurei.SetOutput(hlog.Output{}) - hakurei.Init(hlog.Prepare, internal.InstallOutput) + container.SetOutput(hlog.Output{}) + container.Init(hlog.Prepare, internal.InstallOutput) } diff --git a/system/dbus/proc.go b/system/dbus/proc.go index e8cccd5f..cb041d01 100644 --- a/system/dbus/proc.go +++ b/system/dbus/proc.go @@ -11,10 +11,10 @@ import ( "strconv" "syscall" - "git.gensokyo.uk/security/hakurei" + "git.gensokyo.uk/security/hakurei/container" + "git.gensokyo.uk/security/hakurei/container/seccomp" "git.gensokyo.uk/security/hakurei/helper" "git.gensokyo.uk/security/hakurei/ldd" - "git.gensokyo.uk/security/hakurei/seccomp" ) // Start starts and configures a D-Bus proxy process. @@ -65,22 +65,22 @@ func (p *Proxy) Start() error { p.helper = helper.New( ctx, toolPath, p.final, true, - argF, func(container *hakurei.Container) { - container.SeccompFlags |= seccomp.AllowMultiarch - container.SeccompPresets |= seccomp.PresetStrict - container.Hostname = "hakurei-dbus" - container.CommandContext = p.CommandContext + argF, func(z *container.Container) { + z.SeccompFlags |= seccomp.AllowMultiarch + z.SeccompPresets |= seccomp.PresetStrict + z.Hostname = "hakurei-dbus" + z.CommandContext = p.CommandContext if p.output != nil { - container.Stdout, container.Stderr = p.output, p.output + z.Stdout, z.Stderr = p.output, p.output } if p.CmdF != nil { - p.CmdF(container) + p.CmdF(z) } // these lib paths are unpredictable, so mount them first so they cannot cover anything for _, name := range libPaths { - container.Bind(name, name, 0) + z.Bind(name, name, 0) } // upstream bus directories @@ -101,7 +101,7 @@ func (p *Proxy) Start() error { slices.Sort(upstreamPaths) upstreamPaths = slices.Compact(upstreamPaths) for _, name := range upstreamPaths { - container.Bind(name, name, 0) + z.Bind(name, name, 0) } // parent directories of bind paths @@ -115,12 +115,12 @@ func (p *Proxy) Start() error { slices.Sort(sockDirPaths) sockDirPaths = slices.Compact(sockDirPaths) for _, name := range sockDirPaths { - container.Bind(name, name, hakurei.BindWritable) + z.Bind(name, name, container.BindWritable) } // xdg-dbus-proxy bin path binPath := path.Dir(toolPath) - container.Bind(binPath, binPath, 0) + z.Bind(binPath, binPath, 0) }, nil) } diff --git a/system/output.go b/system/output.go index ad0559bf..d431310d 100644 --- a/system/output.go +++ b/system/output.go @@ -1,14 +1,14 @@ package system import ( - "git.gensokyo.uk/security/hakurei" + "git.gensokyo.uk/security/hakurei/container" ) -var msg hakurei.Msg = new(hakurei.DefaultMsg) +var msg container.Msg = new(container.DefaultMsg) -func SetOutput(v hakurei.Msg) { +func SetOutput(v container.Msg) { if v == nil { - msg = new(hakurei.DefaultMsg) + msg = new(container.DefaultMsg) } else { msg = v } diff --git a/vfs/mangle.go b/vfs/mangle.go deleted file mode 100644 index 83aba589..00000000 --- a/vfs/mangle.go +++ /dev/null @@ -1,30 +0,0 @@ -package vfs - -import "strings" - -func Unmangle(s string) string { - if !strings.ContainsRune(s, '\\') { - return s - } - - v := make([]byte, len(s)) - var ( - j int - c byte - ) - for i := 0; i < len(s); i++ { - c = s[i] - if c == '\\' && len(s) > i+3 && - (s[i+1] == '0' || s[i+1] == '1') && - (s[i+2] >= '0' && s[i+2] <= '7') && - (s[i+3] >= '0' && s[i+3] <= '7') { - c = ((s[i+1] - '0') << 6) | - ((s[i+2] - '0') << 3) | - (s[i+3] - '0') - i += 3 - } - v[j] = c - j++ - } - return string(v[:j]) -} diff --git a/vfs/mangle_test.go b/vfs/mangle_test.go deleted file mode 100644 index 54af8fbf..00000000 --- a/vfs/mangle_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package vfs_test - -import ( - "testing" - - "git.gensokyo.uk/security/hakurei/vfs" -) - -func TestUnmangle(t *testing.T) { - testCases := []struct { - want string - sample string - }{ - {`\, `, `\134\054\040`}, - {`(10) source -- maybe empty string`, `(10)\040source\040--\040maybe empty string`}, - } - - for _, tc := range testCases { - t.Run(tc.want, func(t *testing.T) { - got := vfs.Unmangle(tc.sample) - if got != tc.want { - t.Errorf("Unmangle: %q, want %q", - got, tc.want) - } - }) - } -} diff --git a/vfs/mountinfo.go b/vfs/mountinfo.go deleted file mode 100644 index bcb3063f..00000000 --- a/vfs/mountinfo.go +++ /dev/null @@ -1,260 +0,0 @@ -// Package vfs provides bindings and iterators over proc_pid_mountinfo(5). -package vfs - -import ( - "bufio" - "errors" - "fmt" - "io" - "iter" - "slices" - "strconv" - "strings" - "syscall" -) - -const ( - MS_NOSYMFOLLOW = 0x100 -) - -var ( - ErrMountInfoFields = errors.New("unexpected field count") - ErrMountInfoEmpty = errors.New("unexpected empty field") - ErrMountInfoDevno = errors.New("bad maj:min field") - ErrMountInfoSep = errors.New("bad optional fields separator") -) - -type ( - // A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream. - MountInfoDecoder struct { - s *bufio.Scanner - m *MountInfo - - current *MountInfo - parseErr error - complete bool - } - - // MountInfo represents the contents of a proc_pid_mountinfo(5) document. - MountInfo struct { - Next *MountInfo - MountInfoEntry - } - - // MountInfoEntry represents a proc_pid_mountinfo(5) entry. - MountInfoEntry struct { - // mount ID: a unique ID for the mount (may be reused after umount(2)). - ID int `json:"id"` - // parent ID: the ID of the parent mount (or of self for the root of this mount namespace's mount tree). - Parent int `json:"parent"` - // major:minor: the value of st_dev for files on this filesystem (see stat(2)). - Devno DevT `json:"devno"` - // root: the pathname of the directory in the filesystem which forms the root of this mount. - Root string `json:"root"` - // mount point: the pathname of the mount point relative to the process's root directory. - Target string `json:"target"` - // mount options: per-mount options (see mount(2)). - VfsOptstr string `json:"vfs_optstr"` - // optional fields: zero or more fields of the form "tag[:value]"; see below. - // separator: the end of the optional fields is marked by a single hyphen. - OptFields []string `json:"opt_fields"` - // filesystem type: the filesystem type in the form "type[.subtype]". - FsType string `json:"fstype"` - // mount source: filesystem-specific information or "none". - Source string `json:"source"` - // super options: per-superblock options (see mount(2)). - FsOptstr string `json:"fs_optstr"` - } - - DevT [2]int -) - -// Flags interprets VfsOptstr and returns the resulting flags and unmatched options. -func (e *MountInfoEntry) Flags() (flags uintptr, unmatched []string) { - for _, s := range strings.Split(e.VfsOptstr, ",") { - switch s { - case "rw": - case "ro": - flags |= syscall.MS_RDONLY - case "nosuid": - flags |= syscall.MS_NOSUID - case "nodev": - flags |= syscall.MS_NODEV - case "noexec": - flags |= syscall.MS_NOEXEC - case "nosymfollow": - flags |= MS_NOSYMFOLLOW - case "noatime": - flags |= syscall.MS_NOATIME - case "nodiratime": - flags |= syscall.MS_NODIRATIME - case "relatime": - flags |= syscall.MS_RELATIME - default: - unmatched = append(unmatched, s) - } - } - return -} - -// NewMountInfoDecoder returns a new decoder that reads from r. -// -// The decoder introduces its own buffering and may read data from r beyond the mountinfo entries requested. -func NewMountInfoDecoder(r io.Reader) *MountInfoDecoder { - return &MountInfoDecoder{s: bufio.NewScanner(r)} -} - -func (d *MountInfoDecoder) Decode(v **MountInfo) (err error) { - for d.scan() { - } - err = d.Err() - if err == nil { - *v = d.m - } - return -} - -// Entries returns an iterator over mountinfo entries. -func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] { - return func(yield func(*MountInfoEntry) bool) { - for cur := d.m; cur != nil; cur = cur.Next { - if !yield(&cur.MountInfoEntry) { - return - } - } - for d.scan() { - if !yield(&d.current.MountInfoEntry) { - return - } - } - } -} - -func (d *MountInfoDecoder) Err() error { - if err := d.s.Err(); err != nil { - return err - } - return d.parseErr -} - -func (d *MountInfoDecoder) scan() bool { - if d.complete { - return false - } - if !d.s.Scan() { - d.complete = true - return false - } - - m := new(MountInfo) - if err := parseMountInfoLine(d.s.Text(), &m.MountInfoEntry); err != nil { - d.parseErr = err - d.complete = true - return false - } - - if d.current == nil { - d.m = m - d.current = d.m - } else { - d.current.Next = m - d.current = d.current.Next - } - return true -} - -func parseMountInfoLine(s string, ent *MountInfoEntry) error { - // prevent proceeding with misaligned fields due to optional fields - f := strings.Split(s, " ") - if len(f) < 10 { - return ErrMountInfoFields - } - - // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue - // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) - - // (1) id - if id, err := strconv.Atoi(f[0]); err != nil { // 0 - return err - } else { - ent.ID = id - } - - // (2) parent - if parent, err := strconv.Atoi(f[1]); err != nil { // 1 - return err - } else { - ent.Parent = parent - } - - // (3) maj:min - if n, err := fmt.Sscanf(f[2], "%d:%d", &ent.Devno[0], &ent.Devno[1]); err != nil { - return err - } else if n != 2 { - // unreachable - return ErrMountInfoDevno - } - - // (4) mountroot - ent.Root = Unmangle(f[3]) - if ent.Root == "" { - return ErrMountInfoEmpty - } - - // (5) target - ent.Target = Unmangle(f[4]) - if ent.Target == "" { - return ErrMountInfoEmpty - } - - // (6) vfs options (fs-independent) - ent.VfsOptstr = Unmangle(f[5]) - if ent.VfsOptstr == "" { - return ErrMountInfoEmpty - } - - // (7) optional fields, terminated by " - " - i := len(f) - 4 - ent.OptFields = f[6:i] - - // (8) optional fields end marker - if f[i] != "-" { - return ErrMountInfoSep - } - i++ - - // (9) FS type - ent.FsType = Unmangle(f[i]) - if ent.FsType == "" { - return ErrMountInfoEmpty - } - i++ - - // (10) source -- maybe empty string - ent.Source = Unmangle(f[i]) - i++ - - // (11) fs options (fs specific) - ent.FsOptstr = Unmangle(f[i]) - - return nil -} - -func (e *MountInfoEntry) EqualWithIgnore(want *MountInfoEntry, ignore string) bool { - return (e.ID == want.ID || want.ID == -1) && - (e.Parent == want.Parent || want.Parent == -1) && - (e.Devno == want.Devno || (want.Devno[0] == -1 && want.Devno[1] == -1)) && - (e.Root == want.Root || want.Root == ignore) && - (e.Target == want.Target || want.Target == ignore) && - (e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) && - (slices.Equal(e.OptFields, want.OptFields) || (len(want.OptFields) == 1 && want.OptFields[0] == ignore)) && - (e.FsType == want.FsType || want.FsType == ignore) && - (e.Source == want.Source || want.Source == ignore) && - (e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore) -} - -func (e *MountInfoEntry) String() string { - return fmt.Sprintf("%d %d %d:%d %s %s %s %s %s %s %s", - e.ID, e.Parent, e.Devno[0], e.Devno[1], e.Root, e.Target, e.VfsOptstr, - strings.Join(append(e.OptFields, "-"), " "), e.FsType, e.Source, e.FsOptstr) -} diff --git a/vfs/mountinfo_test.go b/vfs/mountinfo_test.go deleted file mode 100644 index 255f1c9c..00000000 --- a/vfs/mountinfo_test.go +++ /dev/null @@ -1,404 +0,0 @@ -package vfs_test - -import ( - "encoding/json" - "errors" - "iter" - "path" - "reflect" - "slices" - "strconv" - "strings" - "syscall" - "testing" - - "git.gensokyo.uk/security/hakurei/vfs" -) - -func TestMountInfo(t *testing.T) { - testCases := []mountInfoTest{ - {"count", sampleMountinfoBase + ` -21 20 0:53/ /mnt/test rw,relatime - tmpfs rw -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - vfs.ErrMountInfoFields, "", nil, nil, nil}, - - {"sep", sampleMountinfoBase + ` -21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - vfs.ErrMountInfoSep, "", nil, nil, nil}, - - {"id", sampleMountinfoBase + ` -id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - strconv.ErrSyntax, "", nil, nil, nil}, - - {"parent", sampleMountinfoBase + ` -21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - strconv.ErrSyntax, "", nil, nil, nil}, - - {"devno", sampleMountinfoBase + ` -21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - nil, "unexpected EOF", nil, nil, nil}, - - {"maj", sampleMountinfoBase + ` -21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - nil, "expected integer", nil, nil, nil}, - - {"min", sampleMountinfoBase + ` -21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - nil, "expected integer", nil, nil, nil}, - - {"mountroot", sampleMountinfoBase + ` -21 20 0:53 /mnt/test rw,relatime - tmpfs rw -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - vfs.ErrMountInfoEmpty, "", nil, nil, nil}, - - {"target", sampleMountinfoBase + ` -21 20 0:53 / rw,relatime - tmpfs rw -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - vfs.ErrMountInfoEmpty, "", nil, nil, nil}, - - {"vfs options", sampleMountinfoBase + ` -21 20 0:53 / /mnt/test - tmpfs rw -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - vfs.ErrMountInfoEmpty, "", nil, nil, nil}, - - {"FS type", sampleMountinfoBase + ` -21 20 0:53 / /mnt/test rw,relatime - rw -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - vfs.ErrMountInfoEmpty, "", nil, nil, nil}, - - {"base", sampleMountinfoBase, nil, "", []*wantMountInfo{ - m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil), - m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil), - m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil), - m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil), - m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil), - m(20, 1, 8, 4, "/", "/", "ro,noatime,nodiratime,meow", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_RDONLY|syscall.MS_NOATIME|syscall.MS_NODIRATIME, []string{"meow"}), - }, - mn(20, 1, 8, 4, "/", "/", "ro,noatime,nodiratime,meow", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", false, - mn(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", false, nil, - mn(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", false, nil, - mn(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", false, - mn(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", false, nil, - mn(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", false, nil, nil)), - nil))), nil), func(n *vfs.MountInfoNode) []*vfs.MountInfoNode { - return []*vfs.MountInfoNode{ - n, - n.FirstChild, - n.FirstChild.NextSibling, - n.FirstChild.NextSibling.NextSibling, - n.FirstChild.NextSibling.NextSibling.FirstChild, - n.FirstChild.NextSibling.NextSibling.FirstChild.NextSibling, - } - }}, - - {"sample", sampleMountinfo, nil, "", []*wantMountInfo{ - m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil), - m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil), - m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil), - m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil), - m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil), - m(20, 1, 8, 4, "/", "/", "rw,noatime", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_NOATIME, nil), - m(21, 16, 0, 17, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", o(), "tmpfs", "tmpfs", "rw,mode=755", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), - m(22, 21, 0, 18, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), - m(23, 21, 0, 19, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpuset", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), - m(24, 21, 0, 20, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,ns", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), - m(25, 21, 0, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpu", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), - m(26, 21, 0, 22, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpuacct", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), - m(27, 21, 0, 23, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,memory", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), - m(28, 21, 0, 24, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,devices", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), - m(29, 21, 0, 25, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,freezer", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), - m(30, 21, 0, 26, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,net_cls", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), - m(31, 21, 0, 27, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,blkio", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil), - m(32, 16, 0, 28, "/", "/sys/kernel/security", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil), - m(33, 17, 0, 29, "/", "/dev/hugepages", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil), - m(34, 16, 0, 30, "/", "/sys/kernel/debug", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil), - m(35, 15, 0, 31, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil), - m(36, 17, 0, 32, "/", "/dev/mqueue", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil), - m(37, 15, 0, 14, "/", "/proc/bus/usb", "rw,relatime", o(), "usbfs", "/proc/bus/usb", "rw", syscall.MS_RELATIME, nil), - m(38, 33, 0, 33, "/", "/dev/hugepages", "rw,relatime", o(), "hugetlbfs", "hugetlbfs", "rw", syscall.MS_RELATIME, nil), - m(39, 36, 0, 12, "/", "/dev/mqueue", "rw,relatime", o(), "mqueue", "mqueue", "rw", syscall.MS_RELATIME, nil), - m(40, 20, 8, 6, "/", "/boot", "rw,noatime", o(), "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered", syscall.MS_NOATIME, nil), - m(41, 20, 253, 0, "/", "/home/kzak", "rw,noatime", o(), "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered", syscall.MS_NOATIME, nil), - m(42, 35, 0, 34, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", o(), "binfmt_misc", "none", "rw", syscall.MS_RELATIME, nil), - m(43, 16, 0, 35, "/", "/sys/fs/fuse/connections", "rw,relatime", o(), "fusectl", "fusectl", "rw", syscall.MS_RELATIME, nil), - m(44, 41, 0, 36, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", o(), "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_RELATIME, nil), - m(45, 20, 0, 37, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", o(), "rpc_pipefs", "sunrpc", "rw", syscall.MS_RELATIME, nil), - m(47, 20, 0, 38, "/", "/mnt/sounds", "rw,relatime", o(), "cifs", "//foo.home/bar/", "rw,unc=\\\\foo.home\\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344", syscall.MS_RELATIME, nil), - m(49, 20, 0, 56, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", o("shared:323"), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME|vfs.MS_NOSYMFOLLOW, nil), - }, nil, nil}, - - {"sample nosrc", sampleMountinfoNoSrc, nil, "", []*wantMountInfo{ - m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil), - m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil), - m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil), - m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil), - m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil), - m(20, 1, 8, 4, "/", "/", "rw,noatime", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_NOATIME, nil), - m(21, 20, 0, 53, "/", "/mnt/test", "rw,relatime", o("shared:212"), "tmpfs", "", "rw", syscall.MS_RELATIME, nil), - }, nil, nil}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Run("decode", func(t *testing.T) { - var got *vfs.MountInfo - d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) - err := d.Decode(&got) - tc.check(t, d, "Decode", - func(yield func(*vfs.MountInfoEntry) bool) { - for cur := got; cur != nil; cur = cur.Next { - if !yield(&cur.MountInfoEntry) { - return - } - } - }, func() error { return err }) - t.Run("reuse", func(t *testing.T) { - tc.check(t, d, "Entries", - d.Entries(), d.Err) - }) - }) - - t.Run("iter", func(t *testing.T) { - d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) - tc.check(t, d, "Entries", - d.Entries(), d.Err) - - t.Run("reuse", func(t *testing.T) { - tc.check(t, d, "Entries", - d.Entries(), d.Err) - }) - }) - - t.Run("yield", func(t *testing.T) { - d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) - v := false - d.Entries()(func(entry *vfs.MountInfoEntry) bool { v = !v; return v }) - d.Entries()(func(entry *vfs.MountInfoEntry) bool { return false }) - - tc.check(t, d, "Entries", - d.Entries(), d.Err) - - t.Run("reuse", func(t *testing.T) { - tc.check(t, d, "Entries", - d.Entries(), d.Err) - }) - }) - }) - } -} - -type mountInfoTest struct { - name string - sample string - wantErr error - wantError string - want []*wantMountInfo - - wantNode *vfs.MountInfoNode - wantCollectF func(n *vfs.MountInfoNode) []*vfs.MountInfoNode -} - -func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName string, - got iter.Seq[*vfs.MountInfoEntry], gotErr func() error) { - i := 0 - for cur := range got { - if i == len(tc.want) { - if funcName != "Decode" && (tc.wantErr != nil || tc.wantError != "") { - continue - } - - t.Errorf("%s: got more than %d entries", funcName, len(tc.want)) - break - } - - if !reflect.DeepEqual(cur, &tc.want[i].MountInfoEntry) { - t.Errorf("%s: entry %d\ngot: %#v\nwant: %#v", - funcName, i, cur, tc.want[i]) - } - - flags, unmatched := cur.Flags() - if flags != tc.want[i].flags { - t.Errorf("Flags(%q): %#x, want %#x", - cur.VfsOptstr, flags, tc.want[i].flags) - } - if !slices.Equal(unmatched, tc.want[i].unmatched) { - t.Errorf("Flags(%q): unmatched = %#q, want %#q", - cur.VfsOptstr, unmatched, tc.want[i].unmatched) - } - - i++ - } - - if i != len(tc.want) { - t.Errorf("%s: got %d entries, want %d", funcName, i, len(tc.want)) - } - - if tc.wantErr == nil && tc.wantError == "" && tc.wantCollectF != nil { - t.Run("unfold", func(t *testing.T) { - n, err := d.Unfold("/") - if err != nil { - t.Errorf("Unfold: error = %v", err) - } else { - t.Run("stop", func(t *testing.T) { - v := false - n.Collective()(func(node *vfs.MountInfoNode) bool { v = !v; return v }) - }) - - if !reflect.DeepEqual(n, tc.wantNode) { - t.Errorf("Unfold: %s, want %s", - mustMarshal(n), mustMarshal(tc.wantNode)) - } - - t.Run("collective", func(t *testing.T) { - wantCollect := tc.wantCollectF(n) - if gotCollect := slices.Collect(n.Collective()); !reflect.DeepEqual(gotCollect, wantCollect) { - t.Errorf("Collective: \ngot %#v\nwant %#v", - gotCollect, wantCollect) - } - }) - } - }) - } else if tc.wantNode != nil || tc.wantCollectF != nil { - panic("invalid test case") - } else if _, err := d.Unfold("/"); !errors.Is(err, tc.wantErr) { - if tc.wantError == "" { - t.Errorf("Unfold: error = %v, wantErr %v", - err, tc.wantErr) - } else if err != nil && err.Error() != tc.wantError { - t.Errorf("Unfold: error = %q, wantError %q", - err, tc.wantError) - } - } - - if err := gotErr(); !errors.Is(err, tc.wantErr) { - if tc.wantError == "" { - t.Errorf("%s: error = %v, wantErr %v", - funcName, err, tc.wantErr) - } else if err != nil && err.Error() != tc.wantError { - t.Errorf("%s: error = %q, wantError %q", - funcName, err, tc.wantError) - } - } -} - -func mustMarshal(v any) string { - p, err := json.Marshal(v) - if err != nil { - panic(err.Error()) - } - return string(p) -} - -type wantMountInfo struct { - vfs.MountInfoEntry - flags uintptr - unmatched []string -} - -func m( - id, parent, maj, min int, root, target, vfsOptstr string, optFields []string, fsType, source, fsOptstr string, - flags uintptr, unmatched []string, -) *wantMountInfo { - return &wantMountInfo{ - vfs.MountInfoEntry{ - ID: id, - Parent: parent, - Devno: vfs.DevT{maj, min}, - Root: root, - Target: target, - VfsOptstr: vfsOptstr, - OptFields: optFields, - FsType: fsType, - Source: source, - FsOptstr: fsOptstr, - }, flags, unmatched, - } -} - -func mn( - id, parent, maj, min int, root, target, vfsOptstr string, optFields []string, fsType, source, fsOptstr string, - covered bool, firstChild, nextSibling *vfs.MountInfoNode, -) *vfs.MountInfoNode { - return &vfs.MountInfoNode{ - MountInfoEntry: &vfs.MountInfoEntry{ - ID: id, - Parent: parent, - Devno: vfs.DevT{maj, min}, - Root: root, - Target: target, - VfsOptstr: vfsOptstr, - OptFields: optFields, - FsType: fsType, - Source: source, - FsOptstr: fsOptstr, - }, - FirstChild: firstChild, - NextSibling: nextSibling, - Clean: path.Clean(target), - Covered: covered, - } -} - -func o(field ...string) []string { - if field == nil { - return []string{} - } - return field -} - -const ( - sampleMountinfoBase = `15 20 0:3 / /proc rw,relatime - proc /proc rw -16 20 0:15 / /sys rw,relatime - sysfs /sys rw -17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755 -18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000 -19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw -20 1 8:4 / / ro,noatime,nodiratime,meow - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered` - - sampleMountinfo = `15 20 0:3 / /proc rw,relatime - proc /proc rw -16 20 0:15 / /sys rw,relatime - sysfs /sys rw -17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755 -18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000 -19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw -20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 -22 21 0:18 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd -23 21 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset -24 21 0:20 / /sys/fs/cgroup/ns rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,ns -25 21 0:21 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu -26 21 0:22 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct -27 21 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory -28 21 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices -29 21 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer -30 21 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls -31 21 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio -32 16 0:28 / /sys/kernel/security rw,relatime - autofs systemd-1 rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct -33 17 0:29 / /dev/hugepages rw,relatime - autofs systemd-1 rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct -34 16 0:30 / /sys/kernel/debug rw,relatime - autofs systemd-1 rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct -35 15 0:31 / /proc/sys/fs/binfmt_misc rw,relatime - autofs systemd-1 rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct -36 17 0:32 / /dev/mqueue rw,relatime - autofs systemd-1 rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct -37 15 0:14 / /proc/bus/usb rw,relatime - usbfs /proc/bus/usb rw -38 33 0:33 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw -39 36 0:12 / /dev/mqueue rw,relatime - mqueue mqueue rw -40 20 8:6 / /boot rw,noatime - ext3 /dev/sda6 rw,errors=continue,barrier=0,data=ordered -41 20 253:0 / /home/kzak rw,noatime - ext4 /dev/mapper/kzak-home rw,barrier=1,data=ordered -42 35 0:34 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc none rw -43 16 0:35 / /sys/fs/fuse/connections rw,relatime - fusectl fusectl rw -44 41 0:36 / /home/kzak/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=500,group_id=500 -45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw -47 20 0:38 / /mnt/sounds rw,relatime - cifs //foo.home/bar/ rw,unc=\\foo.home\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344 -49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw` - - sampleMountinfoNoSrc = `15 20 0:3 / /proc rw,relatime - proc /proc rw -16 20 0:15 / /sys rw,relatime - sysfs /sys rw -17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755 -18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000 -19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw -20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered -21 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw` -) diff --git a/vfs/unfold.go b/vfs/unfold.go deleted file mode 100644 index ca0e37b7..00000000 --- a/vfs/unfold.go +++ /dev/null @@ -1,107 +0,0 @@ -package vfs - -import ( - "iter" - "path" - "strings" - "syscall" -) - -// MountInfoNode positions a [MountInfoEntry] in its mount hierarchy. -type MountInfoNode struct { - *MountInfoEntry - FirstChild *MountInfoNode `json:"first_child"` - NextSibling *MountInfoNode `json:"next_sibling"` - - Clean string `json:"clean"` - Covered bool `json:"covered"` -} - -// Collective returns an iterator over visible mountinfo nodes. -func (n *MountInfoNode) Collective() iter.Seq[*MountInfoNode] { - return func(yield func(*MountInfoNode) bool) { n.visit(yield) } -} - -func (n *MountInfoNode) visit(yield func(*MountInfoNode) bool) bool { - if !n.Covered && !yield(n) { - return false - } - for cur := n.FirstChild; cur != nil; cur = cur.NextSibling { - if !cur.visit(yield) { - return false - } - } - return true -} - -// Unfold unfolds the mount hierarchy and resolves covered paths. -func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) { - targetClean := path.Clean(target) - - var mountinfoSize int - for range d.Entries() { - mountinfoSize++ - } - if err := d.Err(); err != nil { - return nil, err - } - - mountinfo := make([]*MountInfoNode, mountinfoSize) - // mount ID to index lookup - idIndex := make(map[int]int, mountinfoSize) - // final entry to match target - targetIndex := -1 - { - i := 0 - for ent := range d.Entries() { - mountinfo[i] = &MountInfoNode{Clean: path.Clean(ent.Target), MountInfoEntry: ent} - idIndex[ent.ID] = i - if mountinfo[i].Clean == targetClean { - targetIndex = i - } - - i++ - } - } - - if targetIndex == -1 { - return nil, syscall.ESTALE - } - - for _, cur := range mountinfo { - var parent *MountInfoNode - if p, ok := idIndex[cur.Parent]; !ok { - continue - } else { - parent = mountinfo[p] - } - - if !strings.HasPrefix(cur.Clean, targetClean) { - continue - } - if parent.Clean == cur.Clean { - parent.Covered = true - } - - covered := false - nsp := &parent.FirstChild - for s := parent.FirstChild; s != nil; s = s.NextSibling { - if strings.HasPrefix(cur.Clean, s.Clean) { - covered = true - break - } - - if strings.HasPrefix(s.Clean, cur.Clean) { - *nsp = s.NextSibling - } else { - nsp = &s.NextSibling - } - } - if covered { - continue - } - *nsp = cur - } - - return mountinfo[targetIndex], nil -} diff --git a/vfs/unfold_test.go b/vfs/unfold_test.go deleted file mode 100644 index 59e52041..00000000 --- a/vfs/unfold_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package vfs_test - -import ( - "errors" - "reflect" - "slices" - "strings" - "syscall" - "testing" - - "git.gensokyo.uk/security/hakurei/vfs" -) - -func TestUnfold(t *testing.T) { - testCases := []struct { - name string - sample string - target string - wantErr error - - want *vfs.MountInfoNode - wantCollectF func(n *vfs.MountInfoNode) []*vfs.MountInfoNode - wantCollectN []string - }{ - { - "no match", - sampleMountinfoBase, - "/mnt", - syscall.ESTALE, nil, nil, nil, - }, - { - "cover", - `33 1 0:33 / / rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755 -37 33 0:32 / /proc rw,nosuid,nodev,noexec,relatime shared:41 - proc proc rw -551 33 0:121 / /mnt rw,relatime shared:666 - tmpfs tmpfs rw -595 551 0:123 / /mnt rw,relatime shared:990 - tmpfs tmpfs rw -611 595 0:142 / /mnt/etc rw,relatime shared:1112 - tmpfs tmpfs rw -625 644 0:142 /passwd /mnt/etc/passwd rw,relatime shared:1112 - tmpfs tmpfs rw -641 625 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755 -644 611 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755 -`, "/mnt", nil, - mn(595, 551, 0, 123, "/", "/mnt", "rw,relatime", o("shared:990"), "tmpfs", "tmpfs", "rw", false, - mn(611, 595, 0, 142, "/", "/mnt/etc", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", false, - mn(644, 611, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", true, - mn(625, 644, 0, 142, "/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", true, - mn(641, 625, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", false, - nil, nil), nil), nil), nil), nil), func(n *vfs.MountInfoNode) []*vfs.MountInfoNode { - return []*vfs.MountInfoNode{n, n.FirstChild, n.FirstChild.FirstChild.FirstChild.FirstChild} - }, []string{"/mnt", "/mnt/etc", "/mnt/etc/passwd"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) - got, err := d.Unfold(tc.target) - - if !errors.Is(err, tc.wantErr) { - t.Errorf("Unfold: error = %v, wantErr %v", - err, tc.wantErr) - } - - if !reflect.DeepEqual(got, tc.want) { - t.Errorf("Unfold:\ngot %s\nwant %s", - mustMarshal(got), mustMarshal(tc.want)) - } - - if err == nil && tc.wantCollectF != nil { - t.Run("collective", func(t *testing.T) { - wantCollect := tc.wantCollectF(got) - gotCollect := slices.Collect(got.Collective()) - if !reflect.DeepEqual(gotCollect, wantCollect) { - t.Errorf("Collective: \ngot %#v\nwant %#v", - gotCollect, wantCollect) - } - t.Run("target", func(t *testing.T) { - gotCollectN := slices.Collect[string](func(yield func(v string) bool) { - for _, cur := range gotCollect { - if !yield(cur.Clean) { - return - } - } - }) - if !reflect.DeepEqual(gotCollectN, tc.wantCollectN) { - t.Errorf("Collective: got %q, want %q", - gotCollectN, tc.wantCollectN) - } - }) - }) - } - }) - } -} -- cgit v1.3.1