Compare commits

...

22 Commits

Author SHA1 Message Date
d2f9a9b83b
treewide: migrate to hakurei.app
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Sandbox (push) Successful in 46s
Test / Hakurei (push) Successful in 2m9s
Test / Sandbox (race detector) (push) Successful in 3m14s
Test / Planterette (push) Successful in 3m41s
Test / Hakurei (race detector) (push) Successful in 3m40s
Test / Flake checks (push) Successful in 1m18s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-03 03:30:39 +09:00
1b5ecd9eaf
container: move out of toplevel
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m52s
Test / Sandbox (race detector) (push) Successful in 3m14s
Test / Planterette (push) Successful in 3m36s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Hakurei (push) Successful in 2m3s
Test / Flake checks (push) Successful in 1m13s
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 <cat@gensokyo.uk>
2025-07-03 02:59:43 +09:00
82561d62b6
system: move system access packages
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m52s
Test / Hakurei (push) Successful in 3m3s
Test / Planterette (push) Successful in 3m38s
Test / Hakurei (race detector) (push) Successful in 4m48s
Test / Sandbox (race detector) (push) Successful in 1m14s
Test / Flake checks (push) Successful in 1m6s
These packages loosely belong in the "system" package and "system" provides high level wrappers for all of them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 21:52:07 +09:00
eec021cc4b
hakurei: move container helpers toplevel
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m1s
Test / Hakurei (push) Successful in 2m52s
Test / Sandbox (race detector) (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m32s
Test / Hakurei (race detector) (push) Successful in 4m27s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 21:31:29 +09:00
a1d98823f8
hakurei: move container toplevel
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m16s
Test / Planterette (push) Successful in 3m32s
Test / Hakurei (race detector) (push) Successful in 4m25s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 21:23:55 +09:00
255b77d91d
cmd/hakurei: move command handlers
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m32s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m6s
The hakurei command is a bit ugly since it's also used for validating the command package. This alleviates some of the ugliness.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 20:59:17 +09:00
f84ec5a3f8
sandbox/wl: track generated files
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m54s
Test / Hakurei (push) Successful in 2m58s
Test / Sandbox (race detector) (push) Successful in 3m16s
Test / Planterette (push) Successful in 3m36s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m9s
This allows the package to be imported.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 20:52:22 +09:00
eb22a8bcc1
cmd/hakurei: move to cmd
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m50s
Test / Hakurei (push) Successful in 3m2s
Test / Sandbox (race detector) (push) Successful in 3m18s
Test / Planterette (push) Successful in 3m36s
Test / Hakurei (race detector) (push) Successful in 4m35s
Test / Flake checks (push) Successful in 1m7s
Having it at the project root never made sense since the "ego" name was deprecated. This change finally addresses it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 20:42:51 +09:00
31aef905fa
sandbox: expose seccomp interface
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m59s
Test / Hakurei (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m11s
Test / Planterette (push) Successful in 3m34s
Test / Hakurei (race detector) (push) Successful in 4m22s
Test / Flake checks (push) Successful in 1m8s
There's no point in artificially limiting and abstracting away these options. The higher level hakurei package is responsible for providing a secure baseline and sane defaults. The sandbox package should present everything to the caller.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 04:47:13 +09:00
a6887f7253
sandbox/seccomp: import dot for syscall
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m55s
Test / Sandbox (race detector) (push) Successful in 3m7s
Test / Planterette (push) Successful in 3m31s
Test / Hakurei (race detector) (push) Successful in 4m19s
Test / Hakurei (push) Successful in 1m57s
Test / Flake checks (push) Successful in 1m11s
This significantly increases readability in some places.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 02:30:35 +09:00
69bd581af7
sandbox/seccomp: append suffix to ops
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m50s
Test / Hakurei (push) Successful in 2m54s
Test / Sandbox (race detector) (push) Successful in 3m9s
Test / Planterette (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m31s
This avoids clashes with stdlib names to allow for . imports.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 01:09:04 +09:00
26b7afc890
sandbox/seccomp: prepare -> export
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m51s
Test / Sandbox (race detector) (push) Successful in 3m3s
Test / Planterette (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m17s
Test / Hakurei (push) Successful in 2m12s
Test / Flake checks (push) Successful in 1m12s
Export makes a lot more sense, and also matches the libseccomp function.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 00:32:48 +09:00
d5532aade0
sandbox/seccomp: native rule slice in helpers
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m33s
Test / Hakurei (race detector) (push) Successful in 4m16s
Test / Flake checks (push) Successful in 1m16s
These helper functions took FilterPreset as input for ease of integration. This moves them to []NativeRule.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 00:22:27 +09:00
0c5409aec7
sandbox/seccomp: native rule type alias
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m57s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m4s
Test / Planterette (push) Successful in 3m39s
Test / Hakurei (race detector) (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m13s
This makes it easier to keep API stable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 00:00:08 +09:00
1a8840bebc
sandbox/seccomp: resolve rules natively
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m45s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m1s
Test / Planterette (push) Successful in 3m31s
Test / Hakurei (race detector) (push) Successful in 4m18s
Test / Flake checks (push) Successful in 1m6s
This enables loading syscall filter policies from external cross-platform config files.

This also removes a significant amount of C code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-01 22:11:32 +09:00
1fb453dffe
sandbox/seccomp: extra constants
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m59s
Test / Hakurei (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 3m1s
Test / Planterette (push) Successful in 3m33s
Test / Hakurei (race detector) (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m7s
These all resolve to pseudo syscall numbers in libseccomp, but are necessary anyway for other platforms.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-01 20:15:42 +09:00
e03d702d08
sandbox/seccomp: implement syscall lookup
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m51s
Test / Hakurei (push) Successful in 2m52s
Test / Sandbox (race detector) (push) Successful in 3m20s
Test / Planterette (push) Successful in 3m40s
Test / Hakurei (race detector) (push) Successful in 4m18s
Test / Flake checks (push) Successful in 1m10s
This uses the Go map and is verified against libseccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-01 00:35:27 +09:00
241dc964a6
sandbox/seccomp: wire extra syscall
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m46s
Test / Hakurei (push) Successful in 2m48s
Test / Sandbox (race detector) (push) Successful in 3m6s
Test / Planterette (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 2m39s
Test / Flake checks (push) Successful in 1m15s
These values are only useful for libseccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-01 00:32:08 +09:00
8ef71e14d5
sandbox/seccomp: emit syscall constants
All checks were successful
Test / Create distribution (push) Successful in 44s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m8s
Test / Sandbox (race detector) (push) Successful in 3m18s
Test / Planterette (push) Successful in 3m55s
Test / Hakurei (race detector) (push) Successful in 4m37s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-30 20:34:33 +09:00
972f4006f0
treewide: switch to hakurei.app
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m0s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m12s
Test / Planterette (push) Successful in 3m35s
Test / Hakurei (race detector) (push) Successful in 4m22s
Test / Flake checks (push) Successful in 1m7s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-26 04:01:02 +09:00
9a8a047908
sandbox/seccomp: syscall name lookup table
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m58s
Test / Hakurei (push) Successful in 2m42s
Test / Sandbox (race detector) (push) Successful in 2m59s
Test / Planterette (push) Successful in 3m31s
Test / Hakurei (race detector) (push) Successful in 4m21s
Test / Flake checks (push) Successful in 1m9s
The script is from Go source of same name. The result is checked against libseccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-26 03:49:07 +09:00
863bf69ad3
treewide: reapply clang-format
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m51s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 2m58s
Test / Planterette (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m15s
Test / Flake checks (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-25 23:43:42 +09:00
146 changed files with 2540 additions and 1295 deletions

2
.gitignore vendored
View File

@ -26,7 +26,7 @@ go.work.sum
.vscode
# go generate
security-context-v1-protocol.*
/cmd/hakurei/LICENSE
# release
/dist/hakurei-*

View File

@ -1,4 +1,4 @@
Copyright (c) 2024 Ophestra Umiker
Copyright (c) 2024-2025 Ophestra
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -7,8 +7,8 @@
</p>
<p align="center">
<a href="https://pkg.go.dev/git.gensokyo.uk/security/hakurei"><img src="https://pkg.go.dev/badge/git.gensokyo.uk/security/hakurei.svg" alt="Go Reference" /></a>
<a href="https://goreportcard.com/report/git.gensokyo.uk/security/hakurei"><img src="https://goreportcard.com/badge/git.gensokyo.uk/security/hakurei" alt="Go Report Card" /></a>
<a href="https://pkg.go.dev/hakurei.app"><img src="https://pkg.go.dev/badge/hakurei.app.svg" alt="Go Reference" /></a>
<a href="https://goreportcard.com/report/hakurei.app"><img src="https://goreportcard.com/badge/hakurei.app" alt="Go Report Card" /></a>
</p>
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.

View File

@ -2,8 +2,6 @@ package main
import (
"context"
_ "embed"
"errors"
"fmt"
"io"
"log"
@ -15,59 +13,23 @@ import (
"syscall"
"time"
"git.gensokyo.uk/security/hakurei/command"
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal"
"git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/app/instance"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"git.gensokyo.uk/security/hakurei/internal/state"
"git.gensokyo.uk/security/hakurei/internal/sys"
"git.gensokyo.uk/security/hakurei/sandbox"
"git.gensokyo.uk/security/hakurei/system"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/app/instance"
"hakurei.app/cmd/hakurei/internal/state"
"hakurei.app/command"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
var (
errSuccess = errors.New("success")
//go:embed LICENSE
license string
)
func init() { hlog.Prepare("hakurei") }
var std sys.State = new(sys.Std)
func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
sandbox.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallFmsg)
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
// not fatal: this program runs as the privileged user
}
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
hlog.BeforeExit()
os.Exit(0)
}
})
log.Fatal("unreachable")
}
func buildCommand(out io.Writer) command.Command {
var (
flagVerbose bool
flagJSON bool
)
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallFmsg(flagVerbose); return nil }).
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")

View File

@ -6,7 +6,7 @@ import (
"flag"
"testing"
"git.gensokyo.uk/security/hakurei/command"
"hakurei.app/command"
)
func TestHelp(t *testing.T) {

View File

@ -5,7 +5,7 @@ import (
"syscall"
"time"
"git.gensokyo.uk/security/hakurei/hst"
"hakurei.app/hst"
)
type App interface {
@ -47,13 +47,3 @@ func (rs *RunState) SetStart() {
now := time.Now().UTC()
rs.Time = &now
}
// Paths contains environment-dependent paths used by hakurei.
type Paths struct {
// path to shared directory (usually `/tmp/hakurei.%d`)
SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value (usually `/run/user/%d`)
RuntimePath string `json:"runtime_path"`
// application runtime directory (usually `/run/user/%d/hakurei`)
RunDirPath string `json:"run_dir_path"`
}

View File

@ -4,7 +4,7 @@ import (
"errors"
"testing"
. "git.gensokyo.uk/security/hakurei/internal/app"
. "hakurei.app/cmd/hakurei/internal/app"
)
func TestParseAppID(t *testing.T) {

View File

@ -8,11 +8,11 @@ import (
"path"
"syscall"
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal/sys"
"git.gensokyo.uk/security/hakurei/sandbox"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/internal/sys"
"hakurei.app/system/dbus"
)
// in practice there should be less than 30 entries added by the runtime;
@ -21,58 +21,61 @@ 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) (*sandbox.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 := &sandbox.Params{
Hostname: s.Hostname,
Seccomp: s.Seccomp,
params := &container.Params{
Hostname: s.Hostname,
SeccompFlags: s.SeccompFlags,
SeccompPresets: s.SeccompPresets,
RetainSession: s.Tty,
HostNet: s.Net,
}
{
ops := make(sandbox.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.Seccomp |= seccomp.FilterMultiarch
params.SeccompFlags |= seccomp.AllowMultiarch
}
if s.Devel {
container.Flags |= sandbox.FAllowDevel
if !s.SeccompCompat {
params.SeccompPresets |= seccomp.PresetExt
}
if s.Userns {
container.Flags |= sandbox.FAllowUserns
if !s.Devel {
params.SeccompPresets |= seccomp.PresetDenyDevel
}
if s.Net {
container.Flags |= sandbox.FAllowNet
if !s.Userns {
params.SeccompPresets |= seccomp.PresetDenyNS
}
if s.Tty {
container.Flags |= sandbox.FAllowTTY
if !s.Tty {
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 = sandbox.OverflowUid()
*gid = sandbox.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", sandbox.BindWritable|sandbox.BindDevice)
params.Bind("/dev", "/dev", container.BindWritable|container.BindDevice)
}
/* retrieve paths and hide them if they're made available in the sandbox;
@ -151,29 +154,29 @@ func NewContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*sandbox
var flags int
if c.Write {
flags |= sandbox.BindWritable
flags |= container.BindWritable
}
if c.Device {
flags |= sandbox.BindDevice | sandbox.BindWritable
flags |= container.BindDevice | container.BindWritable
}
if !c.Must {
flags |= sandbox.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 {

View File

@ -3,8 +3,8 @@ package instance
import (
"syscall"
"git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/app/internal/setuid"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/app/internal/setuid"
)
func PrintRunStateErr(whence int, rs *app.RunState, runErr error) (code int) {

View File

@ -6,9 +6,9 @@ import (
"log"
"syscall"
"git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/app/internal/setuid"
"git.gensokyo.uk/security/hakurei/internal/sys"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/app/internal/setuid"
"hakurei.app/internal/sys"
)
const (

View File

@ -1,6 +1,6 @@
package instance
import "git.gensokyo.uk/security/hakurei/internal/app/internal/setuid"
import "hakurei.app/cmd/hakurei/internal/app/internal/setuid"
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
func ShimMain() { setuid.ShimMain() }

View File

@ -5,10 +5,10 @@ import (
"fmt"
"sync"
"git.gensokyo.uk/security/hakurei/hst"
. "git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"git.gensokyo.uk/security/hakurei/internal/sys"
. "hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
)
func New(ctx context.Context, os sys.State) (App, error) {

View File

@ -1,12 +1,13 @@
package setuid_test
import (
"git.gensokyo.uk/security/hakurei/acl"
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/sandbox"
"git.gensokyo.uk/security/hakurei/system"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/system"
"hakurei.app/system/acl"
"hakurei.app/system/dbus"
)
var testCasesNixos = []sealTestCase{
@ -93,13 +94,12 @@ 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),
&sandbox.Params{
Uid: 1971,
Gid: 100,
Flags: sandbox.FAllowNet | sandbox.FAllowUserns,
Dir: "/var/lib/persist/module/hakurei/0/1",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
&container.Params{
Uid: 1971,
Gid: 100,
Dir: "/var/lib/persist/module/hakurei/0/1",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
@ -114,7 +114,7 @@ var testCasesNixos = []sealTestCase{
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(sandbox.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", sandbox.BindOptional).
Bind("/sys/bus", "/sys/bus", sandbox.BindOptional).
Bind("/sys/class", "/sys/class", sandbox.BindOptional).
Bind("/sys/dev", "/sys/dev", sandbox.BindOptional).
Bind("/sys/devices", "/sys/devices", sandbox.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", sandbox.BindDevice|sandbox.BindWritable|sandbox.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", sandbox.BindWritable).
Bind("/tmp/hakurei.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
Bind("/var/lib/persist/module/hakurei/0/1", "/var/lib/persist/module/hakurei/0/1", sandbox.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).
@ -142,6 +142,8 @@ var testCasesNixos = []sealTestCase{
Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel,
HostNet: true,
},
},
}

View File

@ -3,12 +3,13 @@ package setuid_test
import (
"os"
"git.gensokyo.uk/security/hakurei/acl"
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/sandbox"
"git.gensokyo.uk/security/hakurei/system"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/system"
"hakurei.app/system/acl"
"hakurei.app/system/dbus"
)
var testCasesPd = []sealTestCase{
@ -27,11 +28,10 @@ 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),
&sandbox.Params{
Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
Dir: "/home/chronos",
Path: "/run/current-system/sw/bin/zsh",
Args: []string{"/run/current-system/sw/bin/zsh"},
&container.Params{
Dir: "/home/chronos",
Path: "/run/current-system/sw/bin/zsh",
Args: []string{"/run/current-system/sw/bin/zsh"},
Env: []string{
"HOME=/home/chronos",
"SHELL=/run/current-system/sw/bin/zsh",
@ -41,33 +41,36 @@ var testCasesPd = []sealTestCase{
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(sandbox.Ops).
Ops: new(container.Ops).
Proc("/proc").
Tmpfs(hst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", sandbox.BindWritable).
Bind("/boot", "/boot", sandbox.BindWritable).
Bind("/home", "/home", sandbox.BindWritable).
Bind("/lib", "/lib", sandbox.BindWritable).
Bind("/lib64", "/lib64", sandbox.BindWritable).
Bind("/nix", "/nix", sandbox.BindWritable).
Bind("/root", "/root", sandbox.BindWritable).
Bind("/run", "/run", sandbox.BindWritable).
Bind("/srv", "/srv", sandbox.BindWritable).
Bind("/sys", "/sys", sandbox.BindWritable).
Bind("/usr", "/usr", sandbox.BindWritable).
Bind("/var", "/var", sandbox.BindWritable).
Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.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", sandbox.BindWritable).
Bind("/tmp/hakurei.1971/tmpdir/0", "/tmp", sandbox.BindWritable).
Bind("/home/chronos", "/home/chronos", sandbox.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),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true,
RetainSession: true,
},
},
{
@ -163,11 +166,10 @@ 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),
&sandbox.Params{
Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
Dir: "/home/chronos",
Path: "/run/current-system/sw/bin/zsh",
Args: []string{"zsh", "-c", "exec chromium "},
&container.Params{
Dir: "/home/chronos",
Path: "/run/current-system/sw/bin/zsh",
Args: []string{"zsh", "-c", "exec chromium "},
Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
@ -182,31 +184,31 @@ var testCasesPd = []sealTestCase{
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(sandbox.Ops).
Ops: new(container.Ops).
Proc("/proc").
Tmpfs(hst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", sandbox.BindWritable).
Bind("/boot", "/boot", sandbox.BindWritable).
Bind("/home", "/home", sandbox.BindWritable).
Bind("/lib", "/lib", sandbox.BindWritable).
Bind("/lib64", "/lib64", sandbox.BindWritable).
Bind("/nix", "/nix", sandbox.BindWritable).
Bind("/root", "/root", sandbox.BindWritable).
Bind("/run", "/run", sandbox.BindWritable).
Bind("/srv", "/srv", sandbox.BindWritable).
Bind("/sys", "/sys", sandbox.BindWritable).
Bind("/usr", "/usr", sandbox.BindWritable).
Bind("/var", "/var", sandbox.BindWritable).
Bind("/dev/dri", "/dev/dri", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.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", sandbox.BindWritable).
Bind("/tmp/hakurei.1971/tmpdir/9", "/tmp", sandbox.BindWritable).
Bind("/home/chronos", "/home/chronos", sandbox.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).
@ -215,6 +217,9 @@ var testCasesPd = []sealTestCase{
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true,
RetainSession: true,
},
},
}

View File

@ -7,7 +7,7 @@ import (
"os/user"
"strconv"
"git.gensokyo.uk/security/hakurei/internal/app"
"hakurei.app/hst"
)
// fs methods are not implemented using a real FS
@ -125,8 +125,8 @@ func (s *stubNixOS) Open(name string) (fs.File, error) {
}
}
func (s *stubNixOS) Paths() app.Paths {
return app.Paths{
func (s *stubNixOS) Paths() hst.Paths {
return hst.Paths{
SharePath: "/tmp/hakurei.1971",
RuntimePath: "/run/user/1971",
RunDirPath: "/run/user/1971/hakurei",

View File

@ -7,12 +7,12 @@ import (
"testing"
"time"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/app/internal/setuid"
"git.gensokyo.uk/security/hakurei/internal/sys"
"git.gensokyo.uk/security/hakurei/sandbox"
"git.gensokyo.uk/security/hakurei/system"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/app/internal/setuid"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/sys"
"hakurei.app/system"
)
type sealTestCase struct {
@ -21,7 +21,7 @@ type sealTestCase struct {
config *hst.Config
id app.ID
wantSys *system.I
wantContainer *sandbox.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 *sandbox.Params
gotContainer *container.Params
)
if !t.Run("seal", func(t *testing.T) {
if sa, err := a.Seal(tc.config); err != nil {

View File

@ -4,8 +4,8 @@ import (
"errors"
"log"
. "git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/hlog"
. "hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/internal/hlog"
)
func PrintRunStateErr(rs *RunState, runErr error) (code int) {

View File

@ -1,10 +1,10 @@
package setuid
import (
. "git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/sys"
"git.gensokyo.uk/security/hakurei/sandbox"
"git.gensokyo.uk/security/hakurei/system"
. "hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/container"
"hakurei.app/internal/sys"
"hakurei.app/system"
)
func NewWithID(id ID, os sys.State) App {
@ -14,7 +14,7 @@ func NewWithID(id ID, os sys.State) App {
return a
}
func AppIParams(a App, sa SealedApp) (*system.I, *sandbox.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 {

View File

@ -12,12 +12,12 @@ import (
"syscall"
"time"
"git.gensokyo.uk/security/hakurei/internal"
. "git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"git.gensokyo.uk/security/hakurei/internal/state"
"git.gensokyo.uk/security/hakurei/sandbox"
"git.gensokyo.uk/security/hakurei/system"
. "hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/state"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/system"
)
const shimWaitTimeout = 5 * time.Second
@ -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 := sandbox.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 {

View File

@ -16,17 +16,17 @@ import (
"sync/atomic"
"syscall"
"git.gensokyo.uk/security/hakurei/acl"
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal"
. "git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/app/instance/common"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"git.gensokyo.uk/security/hakurei/internal/sys"
"git.gensokyo.uk/security/hakurei/sandbox"
"git.gensokyo.uk/security/hakurei/sandbox/wl"
"git.gensokyo.uk/security/hakurei/system"
. "hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/app/instance/common"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
"hakurei.app/system"
"hakurei.app/system/acl"
"hakurei.app/system/dbus"
"hakurei.app/system/wayland"
)
const (
@ -80,7 +80,7 @@ type outcome struct {
sys *system.I
ctx context.Context
container *sandbox.Params
container *container.Params
env map[string]string
sync *os.File
@ -97,7 +97,7 @@ type shareHost struct {
runtimeSharePath string
seal *outcome
sc Paths
sc hst.Paths
}
// ensureRuntimeDir must be called if direct access to paths within XDG_RUNTIME_DIR is required
@ -183,7 +183,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
if seal.user.username == "" {
seal.user.username = "chronos"
} else if !posixUsername.MatchString(seal.user.username) ||
len(seal.user.username) >= internal.Sysconf_SC_LOGIN_NAME_MAX() {
len(seal.user.username) >= internal.Sysconf(internal.SC_LOGIN_NAME_MAX) {
return hlog.WrapErr(ErrName,
fmt.Sprintf("invalid user name %q", seal.user.username))
}
@ -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, sandbox.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", sandbox.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, sandbox.BindWritable)
seal.container.Bind(seal.user.data, homeDir, container.BindWritable)
seal.container.Dir = homeDir
seal.env["HOME"] = homeDir
seal.env["USER"] = username
@ -377,23 +377,23 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
if config.Enablements&system.EWayland != 0 {
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath string
if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok {
hlog.Verbose(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName)
socketPath = path.Join(share.sc.RuntimePath, wl.FallbackName)
if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok {
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
socketPath = path.Join(share.sc.RuntimePath, wayland.FallbackName)
} else if !path.IsAbs(name) {
socketPath = path.Join(share.sc.RuntimePath, name)
} else {
socketPath = name
}
innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
seal.env[wl.WaylandDisplay] = wl.FallbackName
innerPath := path.Join(innerRuntimeDir, wayland.FallbackName)
seal.env[wayland.WaylandDisplay] = wayland.FallbackName
if !config.DirectWayland { // set up security-context-v1
appID := config.ID
if appID == "" {
// use instance ID in case app id is not set
appID = "uk.gensokyo.hakurei." + seal.id.String()
appID = "app.hakurei." + seal.id.String()
}
// downstream socket paths
outerPath := path.Join(share.instance(), "wayland")

View File

@ -10,10 +10,10 @@ import (
"syscall"
"time"
"git.gensokyo.uk/security/hakurei/internal"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"git.gensokyo.uk/security/hakurei/sandbox"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
/*
@ -74,7 +74,7 @@ type shimParams struct {
Monitor int
// finalised container params
Container *sandbox.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 := sandbox.SetDumpable(sandbox.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,17 +94,17 @@ func ShimMain() {
params shimParams
closeSetup func() error
)
if f, err := sandbox.Receive(shimEnv, &params, nil); err != nil {
if errors.Is(err, sandbox.ErrInvalid) {
if f, err := container.Receive(shimEnv, &params, nil); err != nil {
if errors.Is(err, container.ErrInvalid) {
log.Fatal("invalid config descriptor")
}
if errors.Is(err, sandbox.ErrNotSet) {
if errors.Is(err, container.ErrNotSet) {
log.Fatal("HAKUREI_SHIM not set")
}
log.Fatalf("cannot receive shim setup params: %v", err)
} else {
internal.InstallFmsg(params.Verbose)
internal.InstallOutput(params.Verbose)
closeSetup = f
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
@ -149,25 +149,28 @@ func ShimMain() {
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() // unreachable
container := sandbox.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:")
}
if err := seccomp.Load(seccomp.PresetCommon); err != nil {
if err := seccomp.Load(
seccomp.Preset(seccomp.PresetStrict, seccomp.AllowMultiarch),
seccomp.AllowMultiarch,
); err != nil {
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) {

View File

@ -3,7 +3,7 @@ package setuid
import (
"strconv"
. "git.gensokyo.uk/security/hakurei/internal/app"
. "hakurei.app/cmd/hakurei/internal/app"
)
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }

View File

@ -13,9 +13,9 @@ import (
"sync"
"syscall"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
// fine-grained locking and access

View File

@ -0,0 +1,9 @@
package state_test
import (
"testing"
"hakurei.app/cmd/hakurei/internal/state"
)
func TestMulti(t *testing.T) { testStore(t, state.NewMulti(t.TempDir())) }

View File

@ -5,8 +5,8 @@ import (
"io"
"time"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/hst"
)
var ErrNoConfig = errors.New("state does not contain config")

View File

@ -10,9 +10,9 @@ import (
"testing"
"time"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/state"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/state"
"hakurei.app/hst"
)
func testStore(t *testing.T, s state.Store) {

51
cmd/hakurei/main.go Normal file
View File

@ -0,0 +1,51 @@
package main
// this works around go:embed '..' limitation
//go:generate cp ../../LICENSE .
import (
_ "embed"
"errors"
"log"
"os"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
)
var (
errSuccess = errors.New("success")
//go:embed LICENSE
license string
)
func init() { hlog.Prepare("hakurei") }
var std sys.State = new(sys.Std)
func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
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
}
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
hlog.BeforeExit()
os.Exit(0)
}
// this catches faulty command handlers that fail to return before this point
})
log.Fatal("unreachable")
}

View File

@ -10,9 +10,9 @@ import (
"strings"
"syscall"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"git.gensokyo.uk/security/hakurei/internal/state"
"hakurei.app/cmd/hakurei/internal/state"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
func tryPath(name string) (config *hst.Config) {

View File

@ -12,10 +12,10 @@ import (
"text/tabwriter"
"time"
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"git.gensokyo.uk/security/hakurei/internal/state"
"hakurei.app/cmd/hakurei/internal/state"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
"hakurei.app/system/dbus"
)
func printShowSystem(output io.Writer, short, flagJSON bool) {
@ -264,7 +264,7 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
as = strconv.Itoa(e.Config.Identity)
id := e.Config.ID
if id == "" {
id = "uk.gensokyo.hakurei." + e.s[:8]
id = "app.hakurei." + e.s[:8]
}
as += " (" + id + ")"
}

View File

@ -5,10 +5,10 @@ import (
"testing"
"time"
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/state"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/state"
"hakurei.app/hst"
"hakurei.app/system/dbus"
)
var (
@ -257,7 +257,8 @@ App
],
"container": {
"hostname": "localhost",
"seccomp": 32,
"seccomp_flags": 1,
"seccomp_presets": 1,
"devel": true,
"userns": true,
"net": true,
@ -382,7 +383,8 @@ App
],
"container": {
"hostname": "localhost",
"seccomp": 32,
"seccomp_flags": 1,
"seccomp_presets": 1,
"devel": true,
"userns": true,
"net": true,
@ -460,8 +462,8 @@ func Test_printPs(t *testing.T) {
{"nil instance", state.Entries{testID: nil}, false, false, " Instance PID Application Uptime\n"},
{"state corruption", state.Entries{app.ID{}: testState}, false, false, " Instance PID Application Uptime\n"},
{"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime
8e2c76b0 256 0 (uk.gensokyo.hakurei.8e2c76b0) 1h2m32s
{"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime
8e2c76b0 256 0 (app.hakurei.8e2c76b0) 1h2m32s
`},
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID Application Uptime
@ -561,7 +563,8 @@ func Test_printPs(t *testing.T) {
],
"container": {
"hostname": "localhost",
"seccomp": 32,
"seccomp_flags": 1,
"seccomp_presets": 1,
"devel": true,
"userns": true,
"net": true,

View File

@ -6,10 +6,10 @@ import (
"os"
"path"
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
"git.gensokyo.uk/security/hakurei/system"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
type appInfo struct {
@ -115,10 +115,10 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
},
}
if app.Multiarch {
config.Container.Seccomp |= seccomp.FilterMultiarch
config.Container.SeccompFlags |= seccomp.AllowMultiarch
}
if app.Bluetooth {
config.Container.Seccomp |= seccomp.FilterBluetooth
config.Container.SeccompFlags |= seccomp.AllowBluetooth
}
return config
}

View File

@ -10,10 +10,10 @@ import (
"path"
"syscall"
"git.gensokyo.uk/security/hakurei/command"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"hakurei.app/command"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
const shellPath = "/run/current-system/sw/bin/bash"
@ -42,7 +42,7 @@ func main() {
flagVerbose bool
flagDropShell bool
)
c := command.New(os.Stderr, log.Printf, "planterette", func([]string) error { internal.InstallFmsg(flagVerbose); return nil }).
c := command.New(os.Stderr, log.Printf, "planterette", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")

View File

@ -8,8 +8,8 @@ import (
"strconv"
"sync/atomic"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
var (

View File

@ -9,9 +9,9 @@ import (
"os"
"os/exec"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
var hakureiPath = internal.MustHakureiPath()

View File

@ -5,9 +5,9 @@ import (
"path"
"strings"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/internal"
)
func withNixDaemon(
@ -43,11 +43,11 @@ func withNixDaemon(
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
Net: net,
Seccomp: seccomp.FilterMultiarch,
Tty: dropShell,
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
Net: net,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []*hst.FilesystemConfig{
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
},
@ -85,9 +85,9 @@ func withCacheDir(
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Seccomp: seccomp.FilterMultiarch,
Tty: dropShell,
Hostname: formatHostname(app.Name) + "-" + action,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []*hst.FilesystemConfig{
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
{Src: workDir, Dst: path.Join(hst.Tmp, "bundle"), Must: true},

View File

@ -3,7 +3,7 @@ package command_test
import (
"testing"
"git.gensokyo.uk/security/hakurei/command"
"hakurei.app/command"
)
func TestBuild(t *testing.T) {

View File

@ -10,7 +10,7 @@ import (
"strings"
"testing"
"git.gensokyo.uk/security/hakurei/command"
"hakurei.app/command"
)
func TestParse(t *testing.T) {

View File

@ -1,5 +1,5 @@
// Package sandbox implements unprivileged Linux container with hardening options useful for creating application sandboxes.
package sandbox
// Package container implements unprivileged Linux containers with built-in support for syscall filtering.
package container
import (
"context"
@ -11,38 +11,12 @@ import (
"os/exec"
"path"
"strconv"
"syscall"
. "syscall"
"time"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
"hakurei.app/container/seccomp"
)
type HardeningFlags uintptr
const (
FSyscallCompat HardeningFlags = 1 << iota
FAllowDevel
FAllowUserns
FAllowTTY
FAllowNet
)
func (flags HardeningFlags) seccomp(opts seccomp.FilterOpts) seccomp.FilterOpts {
if flags&FSyscallCompat == 0 {
opts |= seccomp.FilterExt
}
if flags&FAllowDevel == 0 {
opts |= seccomp.FilterDenyDevel
}
if flags&FAllowUserns == 0 {
opts |= seccomp.FilterDenyNS
}
if flags&FAllowTTY == 0 {
opts |= seccomp.FilterDenyTTY
}
return opts
}
type (
// Container represents a container environment being prepared or run.
// None of [Container] methods are safe for concurrent use.
@ -94,15 +68,23 @@ type (
Hostname string
// Sequential container setup ops.
*Ops
// Extra seccomp options.
Seccomp seccomp.FilterOpts
// 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
Flags HardeningFlags
}
)
@ -117,11 +99,9 @@ func (p *Container) Start() error {
ctx, cancel := context.WithCancel(p.ctx)
p.cancel = cancel
var cloneFlags uintptr = syscall.CLONE_NEWIPC |
syscall.CLONE_NEWUTS |
syscall.CLONE_NEWCGROUP
if p.Flags&FAllowNet == 0 {
cloneFlags |= syscall.CLONE_NEWNET
var cloneFlags uintptr = CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP
if !p.HostNet {
cloneFlags |= CLONE_NEWNET
}
// map to overflow id to work around ownership checks
@ -132,6 +112,10 @@ func (p *Container) Start() error {
p.Gid = OverflowGid()
}
if !p.RetainSession {
p.SeccompPresets |= seccomp.PresetDenyTTY
}
if p.CommandContext != nil {
p.cmd = p.CommandContext(ctx)
} else {
@ -144,17 +128,13 @@ func (p *Container) Start() error {
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(syscall.SIGTERM) }
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(SIGTERM) }
}
p.cmd.Dir = "/"
p.cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: p.Flags&FAllowTTY == 0,
Pdeathsig: syscall.SIGKILL,
Cloneflags: cloneFlags |
syscall.CLONE_NEWUSER |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS,
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},
@ -192,7 +172,7 @@ func (p *Container) Serve() error {
if p.Path != "" && !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(syscall.EINVAL,
return msg.WrapErr(EINVAL,
fmt.Sprintf("invalid executable path %q", p.Path))
}
@ -201,7 +181,7 @@ func (p *Container) Serve() error {
p.Path = os.Getenv("SHELL")
if !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(syscall.EBADE,
return msg.WrapErr(EBADE,
"no command specified and $SHELL is invalid")
}
p.name = path.Base(p.Path)
@ -215,11 +195,16 @@ func (p *Container) Serve() error {
}
}
if p.SeccompRules == nil {
// do not transmit nil
p.SeccompRules = make([]seccomp.NativeRule, 0)
}
err := setup.Encode(
&initParams{
p.Params,
syscall.Getuid(),
syscall.Getgid(),
Getuid(),
Getgid(),
len(p.ExtraFiles),
msg.IsVerbose(),
},
@ -233,8 +218,8 @@ func (p *Container) Serve() error {
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
func (p *Container) String() string {
return fmt.Sprintf("argv: %q, flags: %#x, seccomp: %#x",
p.Args, p.Flags, int(p.Flags.seccomp(p.Seccomp)))
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 {

View File

@ -1,4 +1,4 @@
package sandbox_test
package container_test
import (
"bytes"
@ -12,13 +12,13 @@ import (
"testing"
"time"
"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/sandbox"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
"git.gensokyo.uk/security/hakurei/sandbox/vfs"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/container/vfs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
)
const (
@ -29,30 +29,47 @@ const (
func TestContainer(t *testing.T) {
{
oldVerbose := hlog.Load()
oldOutput := sandbox.GetOutput()
internal.InstallFmsg(true)
oldOutput := container.GetOutput()
internal.InstallOutput(true)
t.Cleanup(func() { hlog.Store(oldVerbose) })
t.Cleanup(func() { sandbox.SetOutput(oldOutput) })
t.Cleanup(func() { container.SetOutput(oldOutput) })
}
testCases := []struct {
name string
flags sandbox.HardeningFlags
ops *sandbox.Ops
mnt []*vfs.MountInfoEntry
host string
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", 0, new(sandbox.Ops), nil, "test-minimal"},
{"allow", sandbox.FAllowUserns | sandbox.FAllowNet | sandbox.FAllowTTY,
new(sandbox.Ops), nil, "test-minimal"},
{"tmpfs", 0,
new(sandbox.Ops).
{"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"},
{"dev", sandbox.FAllowTTY, // go test output is not a tty
new(sandbox.Ops).
}, "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{
@ -65,7 +82,8 @@ func TestContainer(t *testing.T) {
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 {
@ -73,29 +91,34 @@ func TestContainer(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
container := sandbox.New(ctx, "/usr/bin/sandbox.test", "-test.v",
c := container.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.Flags |= tc.flags
container.Stdout, container.Stderr = os.Stdout, os.Stderr
container.Ops = tc.ops
if container.Args[5] == "" {
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 {
container.Args[5] = name
c.Args[5] = name
}
}
container.
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(container.Args[5]))
Place("/etc/hostname", []byte(c.Args[5]))
// in case test has cgo enabled
var libPaths []string
if entries, err := ldd.ExecFilter(ctx,
@ -108,10 +131,10 @@ func TestContainer(t *testing.T) {
libPaths = ldd.Path(entries)
}
for _, name := range libPaths {
container.Bind(name, name, 0)
c.Bind(name, name, 0)
}
// needs /proc to check mountinfo
container.Proc("/proc")
c.Proc("/proc")
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
@ -129,16 +152,16 @@ func TestContainer(t *testing.T) {
if err := gob.NewEncoder(want).Encode(mnt); err != nil {
t.Fatalf("cannot serialise expected mount points: %v", err)
}
container.Stdin = want
c.Stdin = want
if err := container.Start(); err != nil {
if err := c.Start(); err != nil {
hlog.PrintBaseError(err, "start:")
t.Fatalf("cannot start container: %v", err)
} else if err = container.Serve(); err != nil {
} else if err = c.Serve(); err != nil {
hlog.PrintBaseError(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
}
if err := container.Wait(); err != nil {
if err := c.Wait(); err != nil {
hlog.PrintBaseError(err, "wait:")
t.Fatalf("wait: %v", err)
}
@ -162,11 +185,14 @@ func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoE
}
func TestContainerString(t *testing.T) {
container := sandbox.New(t.Context(), "ldd", "/usr/bin/env")
container.Flags |= sandbox.FAllowDevel
container.Seccomp |= seccomp.FilterMultiarch
want := `argv: ["ldd" "/usr/bin/env"], flags: 0x2, seccomp: 0x2e`
if got := container.String(); got != want {
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)
}
}
@ -175,8 +201,8 @@ func TestHelperInit(t *testing.T) {
if len(os.Args) != 5 || os.Args[4] != "init" {
return
}
sandbox.SetOutput(hlog.Output{})
sandbox.Init(hlog.Prepare, internal.InstallFmsg)
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, internal.InstallOutput)
}
func TestHelperCheckContainer(t *testing.T) {

View File

@ -1,4 +1,4 @@
package sandbox
package container
import (
"log"

View File

@ -1,15 +1,15 @@
package sandbox_test
package container_test
import (
"os"
"testing"
"git.gensokyo.uk/security/hakurei/sandbox"
"hakurei.app/container"
)
func TestExecutable(t *testing.T) {
for i := 0; i < 16; i++ {
if got := sandbox.MustExecutable(); got != os.Args[0] {
if got := container.MustExecutable(); got != os.Args[0] {
t.Errorf("MustExecutable: %q, want %q",
got, os.Args[0])
}

View File

@ -1,4 +1,4 @@
package sandbox
package container
import (
"errors"
@ -10,10 +10,10 @@ import (
"path"
"runtime"
"strconv"
"syscall"
. "syscall"
"time"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
"hakurei.app/container/seccomp"
)
const (
@ -97,9 +97,9 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
}
oldmask := syscall.Umask(0)
oldmask := Umask(0)
if params.Hostname != "" {
if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
if err := Sethostname([]byte(params.Hostname)); err != nil {
log.Fatalf("cannot set hostname: %v", err)
}
}
@ -107,9 +107,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
// cache sysctl before pivot_root
LastCap()
if err := syscall.Mount("", "/", "",
syscall.MS_SILENT|syscall.MS_SLAVE|syscall.MS_REC,
""); err != nil {
if err := Mount("", "/", "", MS_SILENT|MS_SLAVE|MS_REC, ""); err != nil {
log.Fatalf("cannot make / rslave: %v", err)
}
@ -126,9 +124,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
}
}
if err := syscall.Mount("rootfs", basePath, "tmpfs",
syscall.MS_NODEV|syscall.MS_NOSUID,
""); err != nil {
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 {
@ -138,9 +134,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if err := os.Mkdir(sysrootDir, 0755); err != nil {
log.Fatalf("%v", err)
}
if err := syscall.Mount(sysrootDir, sysrootDir, "",
syscall.MS_SILENT|syscall.MS_MGC_VAL|syscall.MS_BIND|syscall.MS_REC,
""); err != nil {
if err := Mount(sysrootDir, sysrootDir, "", MS_SILENT|MS_MGC_VAL|MS_BIND|MS_REC, ""); err != nil {
log.Fatalf("cannot bind sysroot: %v", err)
}
@ -148,7 +142,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatalf("%v", err)
}
// pivot_root uncovers basePath in hostDir
if err := syscall.PivotRoot(basePath, hostDir); err != nil {
if err := PivotRoot(basePath, hostDir); err != nil {
log.Fatalf("cannot pivot into intermediate root: %v", err)
}
if err := os.Chdir("/"); err != nil {
@ -167,19 +161,17 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
}
// setup requiring host root complete at this point
if err := syscall.Mount(hostDir, hostDir, "",
syscall.MS_SILENT|syscall.MS_REC|syscall.MS_PRIVATE,
""); err != nil {
if err := Mount(hostDir, hostDir, "", MS_SILENT|MS_REC|MS_PRIVATE, ""); err != nil {
log.Fatalf("cannot make host root rprivate: %v", err)
}
if err := syscall.Unmount(hostDir, syscall.MNT_DETACH); err != nil {
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 = syscall.Open("/", syscall.O_DIRECTORY|syscall.O_RDONLY, 0)
fd, err = Open("/", O_DIRECTORY|O_RDONLY, 0)
return
}); err != nil {
log.Fatalf("cannot open intermediate root: %v", err)
@ -188,36 +180,36 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatalf("%v", err)
}
if err := syscall.PivotRoot(".", "."); err != nil {
if err := PivotRoot(".", "."); err != nil {
log.Fatalf("cannot pivot into sysroot: %v", err)
}
if err := syscall.Fchdir(fd); err != nil {
if err := Fchdir(fd); err != nil {
log.Fatalf("cannot re-enter intermediate root: %v", err)
}
if err := syscall.Unmount(".", syscall.MNT_DETACH); err != nil {
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 := syscall.Close(fd); err != nil {
if err := Close(fd); err != nil {
log.Fatalf("cannot close intermediate root: %v", err)
}
}
if _, _, errno := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 {
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.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 {
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.Syscall(syscall.SYS_PRCTL, syscall.PR_CAPBSET_DROP, i, 0); errno != 0 {
if _, _, errno := Syscall(SYS_PRCTL, PR_CAPBSET_DROP, i, 0); errno != 0 {
log.Fatalf("cannot drop capability from bonding set: %v", errno)
}
}
@ -226,7 +218,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if params.Privileged {
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_SYS_ADMIN); errno != 0 {
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)
}
}
@ -237,8 +229,18 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatalf("cannot capset: %v", err)
}
if err := seccomp.Load(params.Flags.seccomp(params.Seccomp)); err != nil {
log.Fatalf("cannot load syscall filter: %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)
@ -246,7 +248,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
// setup fd is placed before all extra files
extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
}
syscall.Umask(oldmask)
Umask(oldmask)
cmd := exec.Command(params.Path)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
@ -267,7 +269,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
type winfo struct {
wpid int
wstatus syscall.WaitStatus
wstatus WaitStatus
}
info := make(chan winfo, 1)
done := make(chan struct{})
@ -276,7 +278,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
var (
err error
wpid = -2
wstatus syscall.WaitStatus
wstatus WaitStatus
)
// keep going until no child process is left
@ -289,12 +291,12 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
info <- winfo{wpid, wstatus}
}
err = syscall.EINTR
for errors.Is(err, syscall.EINTR) {
wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
err = EINTR
for errors.Is(err, EINTR) {
wpid, err = Wait4(-1, &wstatus, 0, nil)
}
}
if !errors.Is(err, syscall.ECHILD) {
if !errors.Is(err, ECHILD) {
log.Println("unexpected wait4 response:", err)
}
@ -303,7 +305,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
// handle signals to dump withheld messages
sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
signal.Notify(sig, SIGINT, SIGTERM)
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{})

View File

@ -1,13 +1,13 @@
package sandbox
package container
import (
"errors"
"fmt"
"os"
"path/filepath"
"syscall"
. "syscall"
"git.gensokyo.uk/security/hakurei/sandbox/vfs"
"hakurei.app/container/vfs"
)
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
@ -17,8 +17,7 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
}
if err := syscall.Mount(source, target, "",
syscall.MS_SILENT|syscall.MS_BIND|flags&syscall.MS_REC, ""); err != nil {
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))
}
@ -38,7 +37,7 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
{
var destFd int
if err := IgnoringEINTR(func() (err error) {
destFd, err = syscall.Open(targetFinal, O_PATH|syscall.O_CLOEXEC, 0)
destFd, err = Open(targetFinal, O_PATH|O_CLOEXEC, 0)
return
}); err != nil {
return wrapErrSuffix(err,
@ -46,7 +45,7 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
}
if v, err := os.Readlink(p.fd(destFd)); err != nil {
return wrapErrSelf(err)
} else if err = syscall.Close(destFd); err != nil {
} else if err = Close(destFd); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot close %q:", targetFinal))
} else {
@ -54,11 +53,11 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
}
}
mf := syscall.MS_NOSUID | flags&syscall.MS_NODEV | flags&syscall.MS_RDONLY
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, syscall.ESTALE) {
if errors.Is(err, ESTALE) {
return msg.WrapErr(err,
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
}
@ -69,13 +68,13 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
if err = remountWithFlags(n, mf); err != nil {
return err
}
if flags&syscall.MS_REC == 0 {
if flags&MS_REC == 0 {
return nil
}
for cur := range n.Collective() {
err = remountWithFlags(cur, mf)
if err != nil && !errors.Is(err, syscall.EACCES) {
if err != nil && !errors.Is(err, EACCES) {
return err
}
}
@ -91,9 +90,8 @@ func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
}
if kf&mf != mf {
return wrapErrSuffix(syscall.Mount("none", n.Clean, "",
syscall.MS_SILENT|syscall.MS_BIND|syscall.MS_REMOUNT|kf|mf,
""),
return wrapErrSuffix(
Mount("none", n.Clean, "", MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, ""),
fmt.Sprintf("cannot remount %q:", n.Clean))
}
return nil
@ -108,8 +106,8 @@ func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
if size > 0 {
opt += fmt.Sprintf(",size=%d", size)
}
return wrapErrSuffix(syscall.Mount(fsname, target, "tmpfs",
syscall.MS_NOSUID|syscall.MS_NODEV, opt),
return wrapErrSuffix(
Mount(fsname, target, "tmpfs", MS_NOSUID|MS_NODEV, opt),
fmt.Sprintf("cannot mount tmpfs on %q:", name))
}

View File

@ -1,4 +1,4 @@
package sandbox
package container
import (
"log"

View File

@ -1,4 +1,4 @@
package sandbox
package container
import (
"encoding/gob"
@ -9,7 +9,7 @@ import (
"path/filepath"
"slices"
"strings"
"syscall"
. "syscall"
"unsafe"
)
@ -29,10 +29,10 @@ type (
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
func init() { gob.Register(new(BindMount)) }
func init() { gob.Register(new(BindMountOp)) }
// BindMount bind mounts host path Source on container path Target.
type BindMount struct {
// BindMountOp bind mounts host path Source on container path Target.
type BindMountOp struct {
Source, SourceFinal, Target string
Flags int
@ -44,10 +44,9 @@ const (
BindDevice
)
func (b *BindMount) early(*Params) error {
func (b *BindMountOp) early(*Params) error {
if !path.IsAbs(b.Source) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", b.Source))
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", b.Source))
}
if v, err := filepath.EvalSymlinks(b.Source); err != nil {
@ -62,18 +61,17 @@ func (b *BindMount) early(*Params) error {
}
}
func (b *BindMount) apply(*Params) error {
func (b *BindMountOp) apply(*Params) error {
if b.SourceFinal == "\x00" {
if b.Flags&BindOptional == 0 {
// unreachable
return syscall.EBADE
return EBADE
}
return nil
}
if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) {
return msg.WrapErr(syscall.EBADE,
"path is not absolute")
return msg.WrapErr(EBADE, "path is not absolute")
}
source := toHost(b.SourceFinal)
@ -91,73 +89,70 @@ func (b *BindMount) apply(*Params) error {
return err
}
var flags uintptr = syscall.MS_REC
var flags uintptr = MS_REC
if b.Flags&BindWritable == 0 {
flags |= syscall.MS_RDONLY
flags |= MS_RDONLY
}
if b.Flags&BindDevice == 0 {
flags |= syscall.MS_NODEV
flags |= MS_NODEV
}
return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target)
}
func (b *BindMount) Is(op Op) bool { vb, ok := op.(*BindMount); return ok && *b == *vb }
func (*BindMount) prefix() string { return "mounting" }
func (b *BindMount) String() string {
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, &BindMount{source, "", target, flags})
*f = append(*f, &BindMountOp{source, "", target, flags})
return f
}
func init() { gob.Register(new(MountProc)) }
func init() { gob.Register(new(MountProcOp)) }
// MountProc mounts a private instance of proc.
type MountProc string
// MountProcOp mounts a private instance of proc.
type MountProcOp string
func (p MountProc) early(*Params) error { return nil }
func (p MountProc) apply(params *Params) error {
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(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", 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(syscall.Mount("proc", target, "proc",
syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
return wrapErrSuffix(Mount("proc", target, "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""),
fmt.Sprintf("cannot mount proc on %q:", v))
}
func (p MountProc) Is(op Op) bool { vp, ok := op.(MountProc); return ok && p == vp }
func (MountProc) prefix() string { return "mounting" }
func (p MountProc) String() string { return fmt.Sprintf("proc on %q", string(p)) }
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, MountProc(dest))
*f = append(*f, MountProcOp(dest))
return f
}
func init() { gob.Register(new(MountDev)) }
func init() { gob.Register(new(MountDevOp)) }
// MountDev mounts part of host dev.
type MountDev string
// MountDevOp mounts part of host dev.
type MountDevOp string
func (d MountDev) early(*Params) error { return nil }
func (d MountDev) apply(params *Params) error {
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(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", v))
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
}
target := toSysroot(v)
@ -204,19 +199,15 @@ func (d MountDev) apply(params *Params) error {
}
}
if err := syscall.Mount("devpts", devPtsPath, "devpts",
syscall.MS_NOSUID|syscall.MS_NOEXEC,
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.Flags&FAllowTTY != 0 {
if params.RetainSession {
var buf [8]byte
if _, _, errno := syscall.Syscall(
syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ,
uintptr(unsafe.Pointer(&buf[0])),
); errno == 0 {
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
@ -237,86 +228,81 @@ func (d MountDev) apply(params *Params) error {
return nil
}
func (d MountDev) Is(op Op) bool { vd, ok := op.(MountDev); return ok && d == vd }
func (MountDev) prefix() string { return "mounting" }
func (d MountDev) String() string { return fmt.Sprintf("dev on %q", string(d)) }
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, MountDev(dest))
*f = append(*f, MountDevOp(dest))
return f
}
func init() { gob.Register(new(MountMqueue)) }
func init() { gob.Register(new(MountMqueueOp)) }
// MountMqueue mounts a private mqueue instance on container Path.
type MountMqueue string
// MountMqueueOp mounts a private mqueue instance on container Path.
type MountMqueueOp string
func (m MountMqueue) early(*Params) error { return nil }
func (m MountMqueue) apply(params *Params) error {
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(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", 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(syscall.Mount("mqueue", target, "mqueue",
syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
return wrapErrSuffix(Mount("mqueue", target, "mqueue", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""),
fmt.Sprintf("cannot mount mqueue on %q:", v))
}
func (m MountMqueue) Is(op Op) bool { vm, ok := op.(MountMqueue); return ok && m == vm }
func (MountMqueue) prefix() string { return "mounting" }
func (m MountMqueue) String() string { return fmt.Sprintf("mqueue on %q", string(m)) }
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, MountMqueue(dest))
*f = append(*f, MountMqueueOp(dest))
return f
}
func init() { gob.Register(new(MountTmpfs)) }
func init() { gob.Register(new(MountTmpfsOp)) }
// MountTmpfs mounts tmpfs on container Path.
type MountTmpfs struct {
// MountTmpfsOp mounts tmpfs on container Path.
type MountTmpfsOp struct {
Path string
Size int
Perm os.FileMode
}
func (t *MountTmpfs) early(*Params) error { return nil }
func (t *MountTmpfs) apply(*Params) error {
func (t *MountTmpfsOp) early(*Params) error { return nil }
func (t *MountTmpfsOp) apply(*Params) error {
if !path.IsAbs(t.Path) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", 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(syscall.EBADE,
fmt.Sprintf("size %d out of bounds", t.Size))
return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size))
}
return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm)
}
func (t *MountTmpfs) Is(op Op) bool { vt, ok := op.(*MountTmpfs); return ok && *t == *vt }
func (*MountTmpfs) prefix() string { return "mounting" }
func (t *MountTmpfs) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
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, &MountTmpfs{dest, size, perm})
*f = append(*f, &MountTmpfsOp{dest, size, perm})
return f
}
func init() { gob.Register(new(Symlink)) }
func init() { gob.Register(new(SymlinkOp)) }
// Symlink creates a symlink in the container filesystem.
type Symlink [2]string
// SymlinkOp creates a symlink in the container filesystem.
type SymlinkOp [2]string
func (l *Symlink) early(*Params) error {
func (l *SymlinkOp) early(*Params) error {
if strings.HasPrefix(l[0], "*") {
l[0] = l[0][1:]
if !path.IsAbs(l[0]) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", 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)
@ -326,11 +312,10 @@ func (l *Symlink) early(*Params) error {
}
return nil
}
func (l *Symlink) apply(params *Params) error {
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(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", l[1]))
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[1]))
}
target := toSysroot(l[1])
@ -343,27 +328,26 @@ func (l *Symlink) apply(params *Params) error {
return nil
}
func (l *Symlink) Is(op Op) bool { vl, ok := op.(*Symlink); return ok && *l == *vl }
func (*Symlink) prefix() string { return "creating" }
func (l *Symlink) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) }
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, &Symlink{target, linkName})
*f = append(*f, &SymlinkOp{target, linkName})
return f
}
func init() { gob.Register(new(Mkdir)) }
func init() { gob.Register(new(MkdirOp)) }
// Mkdir creates a directory in the container filesystem.
type Mkdir struct {
// MkdirOp creates a directory in the container filesystem.
type MkdirOp struct {
Path string
Perm os.FileMode
}
func (m *Mkdir) early(*Params) error { return nil }
func (m *Mkdir) apply(*Params) error {
func (m *MkdirOp) early(*Params) error { return nil }
func (m *MkdirOp) apply(*Params) error {
if !path.IsAbs(m.Path) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", 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 {
@ -372,27 +356,26 @@ func (m *Mkdir) apply(*Params) error {
return nil
}
func (m *Mkdir) Is(op Op) bool { vm, ok := op.(*Mkdir); return ok && m == vm }
func (*Mkdir) prefix() string { return "creating" }
func (m *Mkdir) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
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, &Mkdir{dest, perm})
*f = append(*f, &MkdirOp{dest, perm})
return f
}
func init() { gob.Register(new(Tmpfile)) }
func init() { gob.Register(new(TmpfileOp)) }
// Tmpfile places a file in container Path containing Data.
type Tmpfile struct {
// TmpfileOp places a file in container Path containing Data.
type TmpfileOp struct {
Path string
Data []byte
}
func (t *Tmpfile) early(*Params) error { return nil }
func (t *Tmpfile) apply(params *Params) error {
func (t *TmpfileOp) early(*Params) error { return nil }
func (t *TmpfileOp) apply(params *Params) error {
if !path.IsAbs(t.Path) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", t.Path))
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path))
}
var tmpPath string
@ -414,7 +397,7 @@ func (t *Tmpfile) apply(params *Params) error {
} else if err = hostProc.bindMount(
tmpPath,
target,
syscall.MS_RDONLY|syscall.MS_NODEV,
MS_RDONLY|MS_NODEV,
false,
); err != nil {
return err
@ -424,31 +407,31 @@ func (t *Tmpfile) apply(params *Params) error {
return nil
}
func (t *Tmpfile) Is(op Op) bool {
vt, ok := op.(*Tmpfile)
func (t *TmpfileOp) Is(op Op) bool {
vt, ok := op.(*TmpfileOp)
return ok && t.Path == vt.Path && slices.Equal(t.Data, vt.Data)
}
func (*Tmpfile) prefix() string { return "placing" }
func (t *Tmpfile) String() string {
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, &Tmpfile{name, data}); return f }
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 := &Tmpfile{Path: name}
t := &TmpfileOp{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}
func init() { gob.Register(new(AutoEtc)) }
func init() { gob.Register(new(AutoEtcOp)) }
// AutoEtc expands host /etc into a toplevel symlink mirror with /etc semantics.
// 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 AutoEtc struct{ Prefix string }
type AutoEtcOp struct{ Prefix string }
func (e *AutoEtc) early(*Params) error { return nil }
func (e *AutoEtc) apply(*Params) error {
func (e *AutoEtcOp) early(*Params) error { return nil }
func (e *AutoEtcOp) apply(*Params) error {
const target = sysrootPath + "/etc/"
rel := e.hostRel() + "/"
@ -481,17 +464,17 @@ func (e *AutoEtc) apply(*Params) error {
return nil
}
func (e *AutoEtc) hostPath() string { return "/etc/" + e.hostRel() }
func (e *AutoEtc) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtcOp) hostPath() string { return "/etc/" + e.hostRel() }
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtc) Is(op Op) bool {
ve, ok := op.(*AutoEtc)
func (e *AutoEtcOp) Is(op Op) bool {
ve, ok := op.(*AutoEtcOp)
return ok && ((e == nil && ve == nil) || (e != nil && ve != nil && *e == *ve))
}
func (*AutoEtc) prefix() string { return "setting up" }
func (e *AutoEtc) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
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 := &AutoEtc{prefix}
e := &AutoEtcOp{prefix}
f.Mkdir("/etc", 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)

View File

@ -1,4 +1,4 @@
package sandbox
package container
var msg Msg = new(DefaultMsg)

View File

@ -1,4 +1,4 @@
package sandbox
package container
import (
"encoding/gob"

View File

@ -1,4 +1,4 @@
package sandbox
package container
import (
"errors"
@ -10,7 +10,7 @@ import (
"strings"
"syscall"
"git.gensokyo.uk/security/hakurei/sandbox/vfs"
"hakurei.app/container/vfs"
)
const (

View File

@ -0,0 +1,130 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* CLONE_NEWUSER */
#endif
#include "libseccomp-helper.h"
#include <assert.h>
#include <errno.h>
#include <sys/socket.h>
#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;
}

View File

@ -0,0 +1,24 @@
#include <seccomp.h>
#include <stdint.h>
#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);

View File

@ -0,0 +1,188 @@
package seccomp
/*
#cgo linux pkg-config: --static libseccomp
#include <libseccomp-helper.h>
*/
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
}

View File

@ -8,17 +8,18 @@ import (
"syscall"
"testing"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
. "hakurei.app/container/seccomp"
)
func TestExport(t *testing.T) {
testCases := []struct {
name string
opts seccomp.FilterOpts
presets FilterPreset
flags ExportFlag
want []byte
wantErr bool
}{
{"compat", 0, []byte{
{"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,
@ -28,7 +29,7 @@ func TestExport(t *testing.T) {
0xa7, 0x9b, 0x07, 0x0e, 0x04, 0xc0, 0xee, 0x9a,
0xcd, 0xf5, 0x8f, 0x55, 0xcf, 0xa8, 0x15, 0xa5,
}, false},
{"base", seccomp.FilterExt, []byte{
{"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,
@ -38,10 +39,10 @@ func TestExport(t *testing.T) {
0x1d, 0xb0, 0x5d, 0x90, 0x99, 0x7c, 0x86, 0x59,
0xb9, 0x58, 0x91, 0x20, 0x6a, 0xc9, 0x95, 0x2d,
}, false},
{"everything", seccomp.FilterExt |
seccomp.FilterDenyNS | seccomp.FilterDenyTTY | seccomp.FilterDenyDevel |
seccomp.FilterMultiarch | seccomp.FilterLinux32 | seccomp.FilterCan |
seccomp.FilterBluetooth, []byte{
{"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,
@ -51,7 +52,7 @@ func TestExport(t *testing.T) {
0x4c, 0x02, 0x4e, 0xd4, 0x88, 0x50, 0xbe, 0x69,
0xb6, 0x8a, 0x9a, 0x4c, 0x5f, 0x53, 0xa9, 0xdb,
}, false},
{"strict", seccomp.PresetStrict, []byte{
{"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,
@ -62,7 +63,7 @@ func TestExport(t *testing.T) {
0x14, 0x89, 0x60, 0xfb, 0xd3, 0x5c, 0xd7, 0x35,
}, false},
{"strict compat", 0 |
seccomp.FilterDenyNS | seccomp.FilterDenyTTY | seccomp.FilterDenyDevel, []byte{
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,
@ -72,7 +73,7 @@ func TestExport(t *testing.T) {
0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd,
0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa,
}, false},
{"hakurei default", seccomp.FilterExt | seccomp.FilterDenyDevel, []byte{
{"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,
@ -87,11 +88,7 @@ func TestExport(t *testing.T) {
buf := make([]byte, 8)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
oldF := seccomp.GetOutput()
seccomp.SetOutput(t.Log)
t.Cleanup(func() { seccomp.SetOutput(oldF) })
e := seccomp.New(tc.opts)
e := New(Preset(tc.presets, tc.flags), tc.flags)
digest := sha512.New()
if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr {
@ -100,7 +97,6 @@ func TestExport(t *testing.T) {
}
if err := e.Close(); err != nil {
t.Errorf("Close: error = %v", err)
return
}
if got := digest.Sum(nil); !slices.Equal(got, tc.want) {
t.Fatalf("Export() hash = %x, want %x",
@ -111,7 +107,7 @@ func TestExport(t *testing.T) {
}
t.Run("close without use", func(t *testing.T) {
e := seccomp.New(0)
e := New(Preset(0, 0), 0)
if err := e.Close(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("Close: error = %v", err)
return
@ -119,7 +115,7 @@ func TestExport(t *testing.T) {
})
t.Run("close partial read", func(t *testing.T) {
e := seccomp.New(0)
e := New(Preset(0, 0), 0)
if _, err := e.Read(nil); err != nil {
t.Errorf("Read: error = %v", err)
return
@ -137,10 +133,10 @@ func TestExport(t *testing.T) {
func BenchmarkExport(b *testing.B) {
buf := make([]byte, 8)
for i := 0; i < b.N; i++ {
e := seccomp.New(seccomp.FilterExt |
seccomp.FilterDenyNS | seccomp.FilterDenyTTY | seccomp.FilterDenyDevel |
seccomp.FilterMultiarch | seccomp.FilterLinux32 | seccomp.FilterCan |
seccomp.FilterBluetooth)
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)
}

View File

@ -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 <<EOF;
// $command
// Code generated by the command above; DO NOT EDIT.
package seccomp
import . "syscall"
var syscallNum = map[string]int{
EOF
my $offset = 0;
my $state = -1;
sub fmt {
my ($name, $num) = @_;
if($num > 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(<GCC>){
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 ")";

View File

@ -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},
}
}

View File

@ -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

View File

@ -0,0 +1,6 @@
//go:build !s390 && !s390x
package seccomp
/* Normally the flags come first */
const cloneArg = 0

View File

@ -5,19 +5,18 @@ import (
"errors"
"syscall"
"git.gensokyo.uk/security/hakurei/helper/proc"
"hakurei.app/helper/proc"
)
const (
PresetStrict = FilterExt | FilterDenyNS | FilterDenyTTY | FilterDenyDevel
PresetCommon = PresetStrict | FilterMultiarch
PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel
)
// New returns an inactive Encoder instance.
func New(opts FilterOpts) *Encoder { return &Encoder{newExporter(opts)} }
func New(rules []NativeRule, flags ExportFlag) *Encoder { return &Encoder{newExporter(rules, flags)} }
// Load loads a filter into the kernel.
func Load(opts FilterOpts) error { return buildFilter(-1, opts) }
func Load(rules []NativeRule, flags ExportFlag) error { return Export(-1, rules, flags) }
/*
An Encoder writes a BPF program to an output stream.
@ -47,17 +46,20 @@ func (e *Encoder) Close() error {
}
// NewFile returns an instance of exporter implementing [proc.File].
func NewFile(opts FilterOpts) proc.File { return &File{opts: opts} }
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 {
opts FilterOpts
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.opts)
e := newExporter(f.rules, f.flags)
if err := e.prepare(); err != nil {
return err
}

View File

@ -1,3 +1,4 @@
// Package seccomp provides high level wrappers around libseccomp.
package seccomp
import (
@ -7,8 +8,9 @@ import (
)
type exporter struct {
opts FilterOpts
r, w *os.File
rules []NativeRule
flags ExportFlag
r, w *os.File
prepareOnce sync.Once
prepareErr error
@ -28,7 +30,7 @@ func (e *exporter) prepare() error {
ec := make(chan error, 1)
go func(fd uintptr) {
ec <- buildFilter(int(fd), e.opts)
ec <- Export(int(fd), e.rules, e.flags)
close(ec)
_ = e.closeWrite()
runtime.KeepAlive(e.w)
@ -53,6 +55,6 @@ func (e *exporter) closeWrite() error {
return e.closeErr
}
func newExporter(opts FilterOpts) *exporter {
return &exporter{opts: opts}
func newExporter(rules []NativeRule, flags ExportFlag) *exporter {
return &exporter{rules: rules, flags: flags}
}

View File

@ -6,7 +6,7 @@ import (
"syscall"
"testing"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
"hakurei.app/container/seccomp"
)
func TestLibraryError(t *testing.T) {

View File

@ -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
}

View File

@ -0,0 +1,54 @@
package seccomp
/*
#cgo linux pkg-config: --static libseccomp
#include <seccomp.h>
#include <sys/personality.h>
*/
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
)

View File

@ -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
)

View File

@ -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)
}
})
}
}

View File

@ -1,4 +1,4 @@
package sandbox
package container
import (
"syscall"

View File

@ -1,4 +1,4 @@
package sandbox
package container
import (
"bytes"

View File

@ -3,7 +3,7 @@ package vfs_test
import (
"testing"
"git.gensokyo.uk/security/hakurei/sandbox/vfs"
"hakurei.app/container/vfs"
)
func TestUnmangle(t *testing.T) {

View File

@ -12,7 +12,7 @@ import (
"syscall"
"testing"
"git.gensokyo.uk/security/hakurei/sandbox/vfs"
"hakurei.app/container/vfs"
)
func TestMountInfo(t *testing.T) {

View File

@ -8,7 +8,7 @@ import (
"syscall"
"testing"
"git.gensokyo.uk/security/hakurei/sandbox/vfs"
"hakurei.app/container/vfs"
)
func TestUnfold(t *testing.T) {

8
dist/release.sh vendored
View File

@ -10,11 +10,11 @@ cp -rv "dist/comp" "${out}"
go generate ./...
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
-X git.gensokyo.uk/security/hakurei/internal.version=${VERSION}
-X git.gensokyo.uk/security/hakurei/internal.hakurei=/usr/bin/hakurei
-X git.gensokyo.uk/security/hakurei/internal.hsu=/usr/bin/hsu
-X hakurei.app/internal.version=${VERSION}
-X hakurei.app/internal.hmain=/usr/bin/hakurei
-X hakurei.app/internal.hsu=/usr/bin/hsu
-X main.hmain=/usr/bin/hakurei" ./...
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
rm -rf "./${out}"
(cd dist && sha512sum "${pname}.tar.gz" > "${pname}.tar.gz.sha512")
(cd dist && sha512sum "${pname}.tar.gz" > "${pname}.tar.gz.sha512")

View File

@ -184,6 +184,18 @@
exec cat ${docText} > options.md
'';
};
generateSyscallTable = pkgs.mkShell {
# this should be made cross-platform via nix
shellHook = "exec ${pkgs.writeShellScript "generate-syscall-table" ''
set -e
${pkgs.perl}/bin/perl \
sandbox/seccomp/mksysnum_linux.pl \
${pkgs.linuxHeaders}/include/asm/unistd_64.h | \
${pkgs.go}/bin/gofmt > \
sandbox/seccomp/syscall_linux_amd64.go
''}";
};
}
);
};

2
go.mod
View File

@ -1,3 +1,3 @@
module git.gensokyo.uk/security/hakurei
module hakurei.app
go 1.24

View File

@ -7,7 +7,7 @@ import (
"syscall"
"testing"
"git.gensokyo.uk/security/hakurei/helper"
"hakurei.app/helper"
)
func TestArgsString(t *testing.T) {

View File

@ -10,7 +10,7 @@ import (
"sync"
"syscall"
"git.gensokyo.uk/security/hakurei/helper/proc"
"hakurei.app/helper/proc"
)
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.

View File

@ -8,7 +8,7 @@ import (
"os/exec"
"testing"
"git.gensokyo.uk/security/hakurei/helper"
"hakurei.app/helper"
)
func TestCmd(t *testing.T) {

View File

@ -9,8 +9,8 @@ import (
"slices"
"sync"
"git.gensokyo.uk/security/hakurei/helper/proc"
"git.gensokyo.uk/security/hakurei/sandbox"
"hakurei.app/container"
"hakurei.app/helper/proc"
)
// New initialises a Helper instance with wt as the null-terminated argument writer.
@ -20,13 +20,13 @@ func New(
wt io.WriterTo,
stat bool,
argF func(argsFd, statFd int) []string,
cmdF func(container *sandbox.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 = sandbox.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
*sandbox.Container
*container.Container
}
func (h *helperContainer) Start() error {

View File

@ -7,10 +7,10 @@ import (
"os/exec"
"testing"
"git.gensokyo.uk/security/hakurei/helper"
"git.gensokyo.uk/security/hakurei/internal"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"git.gensokyo.uk/security/hakurei/sandbox"
"hakurei.app/container"
"hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
func TestContainer(t *testing.T) {
@ -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 *sandbox.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
}
sandbox.SetOutput(hlog.Output{})
sandbox.Init(hlog.Prepare, func(bool) { internal.InstallFmsg(false) })
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, func(bool) { internal.InstallOutput(false) })
}

View File

@ -8,7 +8,7 @@ import (
"os"
"time"
"git.gensokyo.uk/security/hakurei/helper/proc"
"hakurei.app/helper/proc"
)
var WaitDelay = 2 * time.Second

View File

@ -11,7 +11,7 @@ import (
"testing"
"time"
"git.gensokyo.uk/security/hakurei/helper"
"hakurei.app/helper"
)
var (

View File

@ -3,7 +3,7 @@ package helper_test
import (
"testing"
"git.gensokyo.uk/security/hakurei/helper"
"hakurei.app/helper"
)
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }

View File

@ -2,8 +2,8 @@
package hst
import (
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/system"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
const Tmp = "/.hakurei"

View File

@ -1,7 +1,7 @@
package hst
import (
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
"hakurei.app/container/seccomp"
)
type (
@ -11,7 +11,11 @@ type (
Hostname string `json:"hostname,omitempty"`
// extra seccomp flags
Seccomp seccomp.FilterOpts `json:"seccomp"`
SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"`
// extra seccomp presets
SeccompPresets seccomp.FilterPreset `json:"seccomp_presets"`
// disable project-specific filter extensions
SeccompCompat bool `json:"seccomp_compat,omitempty"`
// allow ptrace and friends
Devel bool `json:"devel,omitempty"`
// allow userns creation in container

11
hst/paths.go Normal file
View File

@ -0,0 +1,11 @@
package hst
// Paths contains environment-dependent paths used by hakurei.
type Paths struct {
// path to shared directory (usually `/tmp/hakurei.%d`)
SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value (usually `/run/user/%d`)
RuntimePath string `json:"runtime_path"`
// application runtime directory (usually `/run/user/%d/hakurei`)
RunDirPath string `json:"run_dir_path"`
}

View File

@ -1,9 +1,9 @@
package hst
import (
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
"git.gensokyo.uk/security/hakurei/system"
"hakurei.app/container/seccomp"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
// Template returns a fully populated instance of Config.
@ -57,15 +57,16 @@ func Template() *Config {
Groups: []string{"video", "dialout", "plugdev"},
Container: &ContainerConfig{
Hostname: "localhost",
Devel: true,
Userns: true,
Net: true,
Device: true,
Seccomp: seccomp.FilterMultiarch,
Tty: true,
Multiarch: true,
MapRealUID: true,
Hostname: "localhost",
Devel: true,
Userns: true,
Net: true,
Device: true,
SeccompFlags: seccomp.AllowMultiarch,
SeccompPresets: seccomp.PresetExt,
Tty: true,
Multiarch: true,
MapRealUID: true,
// example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{

View File

@ -4,7 +4,7 @@ import (
"encoding/json"
"testing"
"git.gensokyo.uk/security/hakurei/hst"
"hakurei.app/hst"
)
func TestTemplate(t *testing.T) {
@ -80,7 +80,8 @@ func TestTemplate(t *testing.T) {
],
"container": {
"hostname": "localhost",
"seccomp": 32,
"seccomp_flags": 1,
"seccomp_presets": 1,
"devel": true,
"userns": true,
"net": true,

View File

@ -3,7 +3,7 @@ package internal
import (
"os"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"hakurei.app/internal/hlog"
)
func Exit(code int) { hlog.BeforeExit(); os.Exit(code) }

View File

@ -1,17 +1,13 @@
package internal
import (
"git.gensokyo.uk/security/hakurei/internal/hlog"
"git.gensokyo.uk/security/hakurei/sandbox"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
"git.gensokyo.uk/security/hakurei/system"
"hakurei.app/container"
"hakurei.app/internal/hlog"
"hakurei.app/system"
)
func InstallFmsg(verbose bool) {
func InstallOutput(verbose bool) {
hlog.Store(verbose)
sandbox.SetOutput(hlog.Output{})
container.SetOutput(hlog.Output{})
system.SetOutput(hlog.Output{})
if verbose {
seccomp.SetOutput(hlog.Verbose)
}
}

View File

@ -4,16 +4,16 @@ import (
"log"
"path"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"hakurei.app/internal/hlog"
)
var (
hakurei = compPoison
hsu = compPoison
hmain = compPoison
hsu = compPoison
)
func MustHakureiPath() string {
if name, ok := checkPath(hakurei); ok {
if name, ok := checkPath(hmain); ok {
return name
}
hlog.BeforeExit()

View File

@ -1,11 +0,0 @@
package state_test
import (
"testing"
"git.gensokyo.uk/security/hakurei/internal/state"
)
func TestMulti(t *testing.T) {
testStore(t, state.NewMulti(t.TempDir()))
}

View File

@ -1,3 +1,4 @@
// Package sys wraps OS interaction library functions.
package sys
import (
@ -6,8 +7,8 @@ import (
"path"
"strconv"
"git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
// State provides safe interaction with operating system state.
@ -40,15 +41,15 @@ type State interface {
Println(v ...any)
Printf(format string, v ...any)
// Paths returns a populated [Paths] struct.
Paths() app.Paths
// Paths returns a populated [hst.Paths] struct.
Paths() hst.Paths
// Uid invokes hsu and returns target uid.
// Any errors returned by Uid is already wrapped [fmsg.BaseError].
Uid(aid int) (int, error)
}
// CopyPaths is a generic implementation of [hst.Paths].
func CopyPaths(os State, v *app.Paths) {
func CopyPaths(os State, v *hst.Paths) {
v.SharePath = path.Join(os.TempDir(), "hakurei."+strconv.Itoa(os.Getuid()))
hlog.Verbosef("process share directory at %q", v.SharePath)

View File

@ -12,15 +12,15 @@ import (
"sync"
"syscall"
"git.gensokyo.uk/security/hakurei/internal"
"git.gensokyo.uk/security/hakurei/internal/app"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"git.gensokyo.uk/security/hakurei/sandbox"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
// Std implements System using the standard library.
type Std struct {
paths app.Paths
paths hst.Paths
pathsOnce sync.Once
uidOnce sync.Once
@ -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 sandbox.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) }
@ -48,7 +48,7 @@ func (s *Std) Printf(format string, v ...any) { hlog.Verbosef(form
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) Paths() app.Paths {
func (s *Std) Paths() hst.Paths {
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) })
return s.paths
}

View File

@ -3,4 +3,6 @@ package internal
//#include <unistd.h>
import "C"
func Sysconf_SC_LOGIN_NAME_MAX() int { return int(C.sysconf(C._SC_LOGIN_NAME_MAX)) }
const SC_LOGIN_NAME_MAX = C._SC_LOGIN_NAME_MAX
func Sysconf(name C.int) int { return int(C.sysconf(name)) }

View File

@ -8,7 +8,8 @@ import (
"os/exec"
"time"
"git.gensokyo.uk/security/hakurei/sandbox"
"hakurei.app/container"
"hakurei.app/container/seccomp"
)
const lddTimeout = 2 * time.Second
@ -26,22 +27,24 @@ func ExecFilter(ctx context.Context,
p string) ([]*Entry, error) {
c, cancel := context.WithTimeout(ctx, lddTimeout)
defer cancel()
container := sandbox.New(c, "ldd", p)
container.CommandContext = commandContext
container.Hostname = "hakurei-ldd"
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) {

View File

@ -5,7 +5,7 @@ import (
"reflect"
"testing"
"git.gensokyo.uk/security/hakurei/ldd"
"hakurei.app/ldd"
)
func TestParseError(t *testing.T) {

Some files were not shown because too many files have changed in this diff Show More