Compare commits
185 Commits
v0.3.1
...
wip-bootst
| Author | SHA1 | Date | |
|---|---|---|---|
|
7ad8f15030
|
|||
|
993afde840
|
|||
|
c9cd16fd2a
|
|||
|
e42ea32dbe
|
|||
|
e7982b4ee9
|
|||
|
ef1ebf12d9
|
|||
|
775a9f57c9
|
|||
|
2f8ca83376
|
|||
|
3d720ada92
|
|||
|
2e5362e536
|
|||
|
6d3bd27220
|
|||
|
a27305cb4a
|
|||
|
0e476c5e5b
|
|||
|
54712e0426
|
|||
|
b77c1ecfdb
|
|||
|
dce5839a79
|
|||
|
d597592e1f
|
|||
|
056f5b12d4
|
|||
|
da2bb546ba
|
|||
|
7bfbd59810
|
|||
|
ea815a59e8
|
|||
|
28a8dc67d2
|
|||
|
ec49c63c5f
|
|||
|
5a50bf80ee
|
|||
|
ce06b7b663
|
|||
|
08bdc68f3a
|
|||
|
8cb0b433b2
|
|||
|
767f1844d2
|
|||
|
54610aaddc
|
|||
|
2e80660169
|
|||
|
d0a3c6a2f3
|
|||
|
0c0e3d6fc2
|
|||
|
fae910a1ad
|
|||
|
178c8bc28b
|
|||
|
30dcab0734
|
|||
|
0ea051062b
|
|||
|
b0f2ab6fff
|
|||
|
00a5bdf006
|
|||
|
a27dfdc058
|
|||
|
6d0d9cecd1
|
|||
|
17248d7d61
|
|||
|
41e5628c67
|
|||
|
ffbec828e1
|
|||
|
de0467a65e
|
|||
|
b5999b8814
|
|||
|
ebc67bb8ad
|
|||
|
e60ff660f6
|
|||
|
47db461546
|
|||
|
0a3fe5f907
|
|||
|
b72d502f1c
|
|||
|
f8b3db3f66
|
|||
|
0e2fb1788f
|
|||
|
d8417e2927
|
|||
|
ccc0d98bd7
|
|||
|
a3fd05765e
|
|||
|
c538df7daa
|
|||
|
44e5aa1a36
|
|||
|
cf0e7d8c27
|
|||
|
130add21e5
|
|||
|
5ec4045e24
|
|||
|
be2075f169
|
|||
|
e9fb1d7be5
|
|||
|
dafe9f8efc
|
|||
|
96dd7abd80
|
|||
|
d5fb179012
|
|||
|
462863e290
|
|||
|
2786611b88
|
|||
|
791a1dfa55
|
|||
|
564db6863b
|
|||
|
87781c7658
|
|||
|
0c38fb7b6a
|
|||
|
357cfcddee
|
|||
|
6bf245cf1b
|
|||
|
c8eeb4a4d1
|
|||
|
5785714b64
|
|||
|
422efcf258
|
|||
|
104eeecf65
|
|||
|
bf856f06e5
|
|||
|
1931b54600
|
|||
|
093e30c788
|
|||
|
1b17ccda91
|
|||
|
7c6fc1128b
|
|||
|
8cdd659239
|
|||
|
15c2839a09
|
|||
|
b9b9705b52
|
|||
|
246e04214a
|
|||
|
503bfc6468
|
|||
|
d837628b4c
|
|||
|
3cb58b4b72
|
|||
|
bb1fc4c7bc
|
|||
|
f44923da29
|
|||
|
5e7861bb00
|
|||
|
7cb3308a53
|
|||
|
490093a659
|
|||
|
2b22efcdf1
|
|||
|
8a2f9edcf9
|
|||
|
0d3f332d45
|
|||
|
d5509cc6e5
|
|||
|
0d3ae6cb23
|
|||
|
69b1131d66
|
|||
|
2c0b92771a
|
|||
|
054c91879f
|
|||
|
c34439fc5f
|
|||
|
32fb137bb2
|
|||
|
e7a665e043
|
|||
|
af741f20a0
|
|||
|
39c6716fb0
|
|||
|
7bc73afadd
|
|||
|
647aa9d02f
|
|||
|
91aaabaa1b
|
|||
|
3d4c7cdd9e
|
|||
|
4fd6d6c037
|
|||
|
de3fc7ba38
|
|||
|
5a5c4705dd
|
|||
|
f703aa20a5
|
|||
|
5c12425d48
|
|||
|
cbe86dc4f0
|
|||
|
d08a1081bd
|
|||
|
72a2601d74
|
|||
|
1dab87aaf0
|
|||
|
2bafde99e3
|
|||
|
91efeb101a
|
|||
|
dcb22a61c0
|
|||
|
e028a61fc1
|
|||
|
73987be7d4
|
|||
|
563b5e66fc
|
|||
|
2edcfe1e68
|
|||
|
2698ca00e8
|
|||
|
1d0143386d
|
|||
|
a55c209099
|
|||
|
10ff276da1
|
|||
|
fd4d379b67
|
|||
|
77f5b89a41
|
|||
|
14e33f17e5
|
|||
|
cfeb7818eb
|
|||
|
05391da556
|
|||
|
463f8836e6
|
|||
|
2e465c94da
|
|||
|
26009fd3f7
|
|||
|
2d7b896a8c
|
|||
|
a0eb010aab
|
|||
|
b1b27ac1df
|
|||
|
fc3d78fe01
|
|||
|
591637264a
|
|||
|
e77652bf89
|
|||
|
88d3e46413
|
|||
|
e51e81bb22
|
|||
|
8f4a3bcf9f
|
|||
|
827dc9e1ba
|
|||
|
d92de1c709
|
|||
|
5bcafcf734
|
|||
|
9f7b0c2f46
|
|||
|
3e87187c4c
|
|||
|
b651d95e77
|
|||
|
aab92ce3c1
|
|||
|
a495e09a8f
|
|||
|
3afca2bd5b
|
|||
|
b73a789dfe
|
|||
|
38b5ff0cec
|
|||
|
3c204b9b40
|
|||
|
00771efeb4
|
|||
|
61972d61f6
|
|||
|
fe40af7b7e
|
|||
|
12751932d1
|
|||
|
41b49137a8
|
|||
|
c761e1de4d
|
|||
|
a91920310d
|
|||
|
16e674782a
|
|||
|
47244daefb
|
|||
|
46fa104419
|
|||
|
45953b3d9c
|
|||
|
42759e7a9f
|
|||
|
8e2d2c8246
|
|||
|
299685775a
|
|||
|
b7406cc4c4
|
|||
|
690a0ed0d6
|
|||
|
a9d72a5eb1
|
|||
|
6d14bb814f
|
|||
|
be0e387ab0
|
|||
|
abeb67964f
|
|||
|
bf5d10743f
|
|||
|
4e7aab07d5
|
|||
|
15a66a2b31
|
|||
|
f347d44c22
|
|||
|
b5630f6883
|
2
.clang-format
Normal file
2
.clang-format
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ColumnLimit: 0
|
||||||
|
IndentWidth: 4
|
||||||
@@ -2,7 +2,6 @@ name: Test
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
- push
|
- push
|
||||||
- pull_request
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
hakurei:
|
hakurei:
|
||||||
@@ -73,6 +72,23 @@ jobs:
|
|||||||
path: result/*
|
path: result/*
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
|
sharefs:
|
||||||
|
name: ShareFS
|
||||||
|
runs-on: nix
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run NixOS test
|
||||||
|
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sharefs
|
||||||
|
|
||||||
|
- name: Upload test output
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: "sharefs-vm-output"
|
||||||
|
path: result/*
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
hpkg:
|
hpkg:
|
||||||
name: Hpkg
|
name: Hpkg
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
@@ -97,6 +113,7 @@ jobs:
|
|||||||
- race
|
- race
|
||||||
- sandbox
|
- sandbox
|
||||||
- sandbox-race
|
- sandbox-race
|
||||||
|
- sharefs
|
||||||
- hpkg
|
- hpkg
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -11,21 +11,24 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
_ "unsafe"
|
_ "unsafe" // for go:linkname
|
||||||
|
|
||||||
"hakurei.app/command"
|
"hakurei.app/command"
|
||||||
"hakurei.app/container/check"
|
"hakurei.app/container/check"
|
||||||
"hakurei.app/container/fhs"
|
"hakurei.app/container/fhs"
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/internal"
|
"hakurei.app/internal/dbus"
|
||||||
"hakurei.app/internal/env"
|
"hakurei.app/internal/env"
|
||||||
|
"hakurei.app/internal/info"
|
||||||
"hakurei.app/internal/outcome"
|
"hakurei.app/internal/outcome"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
"hakurei.app/system/dbus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
|
||||||
|
// if it is not nil, or the original value if it is.
|
||||||
|
//
|
||||||
//go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap
|
//go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap
|
||||||
func optionalErrorUnwrap(_ error) error
|
func optionalErrorUnwrap(err error) error
|
||||||
|
|
||||||
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
|
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
|
||||||
var (
|
var (
|
||||||
@@ -88,7 +91,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
|||||||
|
|
||||||
flagPrivateRuntime, flagPrivateTmpdir bool
|
flagPrivateRuntime, flagPrivateTmpdir bool
|
||||||
|
|
||||||
flagWayland, flagX11, flagDBus, flagPulse bool
|
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
|
||||||
)
|
)
|
||||||
|
|
||||||
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error {
|
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error {
|
||||||
@@ -143,8 +146,8 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
|||||||
if flagDBus {
|
if flagDBus {
|
||||||
et |= hst.EDBus
|
et |= hst.EDBus
|
||||||
}
|
}
|
||||||
if flagPulse {
|
if flagPipeWire || flagPulse {
|
||||||
et |= hst.EPulse
|
et |= hst.EPipeWire
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &hst.Config{
|
config := &hst.Config{
|
||||||
@@ -294,8 +297,10 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
|||||||
"Enable direct connection to X11").
|
"Enable direct connection to X11").
|
||||||
Flag(&flagDBus, "dbus", command.BoolFlag(false),
|
Flag(&flagDBus, "dbus", command.BoolFlag(false),
|
||||||
"Enable proxied connection to D-Bus").
|
"Enable proxied connection to D-Bus").
|
||||||
|
Flag(&flagPipeWire, "pipewire", command.BoolFlag(false),
|
||||||
|
"Enable connection to PipeWire via SecurityContext").
|
||||||
Flag(&flagPulse, "pulse", command.BoolFlag(false),
|
Flag(&flagPulse, "pulse", command.BoolFlag(false),
|
||||||
"Enable direct connection to PulseAudio")
|
"Enable PulseAudio compatibility daemon")
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -350,7 +355,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
|||||||
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
|
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Command("version", "Display version information", func(args []string) error { fmt.Println(internal.Version()); return errSuccess })
|
c.Command("version", "Display version information", func(args []string) error { fmt.Println(info.Version()); return errSuccess })
|
||||||
c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
|
c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
|
||||||
c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess })
|
c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess })
|
||||||
c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })
|
c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Commands:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"run", []string{"run", "-h"}, `
|
"run", []string{"run", "-h"}, `
|
||||||
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
|
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS]
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
-X Enable direct connection to X11
|
-X Enable direct connection to X11
|
||||||
@@ -58,12 +58,14 @@ Flags:
|
|||||||
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
|
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
|
||||||
-mpris
|
-mpris
|
||||||
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
||||||
|
-pipewire
|
||||||
|
Enable connection to PipeWire via SecurityContext
|
||||||
-private-runtime
|
-private-runtime
|
||||||
Do not share XDG_RUNTIME_DIR between containers under the same identity
|
Do not share XDG_RUNTIME_DIR between containers under the same identity
|
||||||
-private-tmpdir
|
-private-tmpdir
|
||||||
Do not share TMPDIR between containers under the same identity
|
Do not share TMPDIR between containers under the same identity
|
||||||
-pulse
|
-pulse
|
||||||
Enable direct connection to PulseAudio
|
Enable PulseAudio compatibility daemon
|
||||||
-u string
|
-u string
|
||||||
Passwd user name within sandbox (default "chronos")
|
Passwd user name within sandbox (default "chronos")
|
||||||
-wayland
|
-wayland
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
package main_test
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
_ "unsafe"
|
|
||||||
|
|
||||||
"hakurei.app/container/stub"
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:linkname decodeJSON hakurei.app/cmd/hakurei.decodeJSON
|
|
||||||
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any)
|
|
||||||
|
|
||||||
func TestDecodeJSON(t *testing.T) {
|
func TestDecodeJSON(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -62,9 +57,6 @@ func TestDecodeJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:linkname encodeJSON hakurei.app/cmd/hakurei.encodeJSON
|
|
||||||
func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any)
|
|
||||||
|
|
||||||
func TestEncodeJSON(t *testing.T) {
|
func TestEncodeJSON(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -74,7 +66,7 @@ func TestEncodeJSON(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{"marshaler", errorJSONMarshaler{},
|
{"marshaler", errorJSONMarshaler{},
|
||||||
`cannot encode json for main_test.errorJSONMarshaler: unique error 3735928559 injected by the test suite`},
|
`cannot encode json for main.errorJSONMarshaler: unique error 3735928559 injected by the test suite`},
|
||||||
{"default", func() {},
|
{"default", func() {},
|
||||||
`cannot write json: json: unsupported type: func()`},
|
`cannot write json: json: unsupported type: func()`},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/internal"
|
|
||||||
"hakurei.app/internal/env"
|
|
||||||
"hakurei.app/internal/outcome"
|
"hakurei.app/internal/outcome"
|
||||||
"hakurei.app/internal/store"
|
"hakurei.app/internal/store"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
@@ -23,21 +21,19 @@ import (
|
|||||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||||
t := newPrinter(output)
|
t := newPrinter(output)
|
||||||
defer t.MustFlush()
|
defer t.MustFlush()
|
||||||
|
hi := outcome.Info()
|
||||||
info := &hst.Info{Version: internal.Version(), User: new(outcome.Hsu).MustID(nil)}
|
|
||||||
env.CopyPaths().Copy(&info.Paths, info.User)
|
|
||||||
|
|
||||||
if flagJSON {
|
if flagJSON {
|
||||||
encodeJSON(log.Fatal, output, short, info)
|
encodeJSON(log.Fatal, output, short, hi)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Printf("Version:\t%s\n", info.Version)
|
t.Printf("Version:\t%s (libwayland %s)\n", hi.Version, hi.WaylandVersion)
|
||||||
t.Printf("User:\t%d\n", info.User)
|
t.Printf("User:\t%d\n", hi.User)
|
||||||
t.Printf("TempDir:\t%s\n", info.TempDir)
|
t.Printf("TempDir:\t%s\n", hi.TempDir)
|
||||||
t.Printf("SharePath:\t%s\n", info.SharePath)
|
t.Printf("SharePath:\t%s\n", hi.SharePath)
|
||||||
t.Printf("RuntimePath:\t%s\n", info.RuntimePath)
|
t.Printf("RuntimePath:\t%s\n", hi.RuntimePath)
|
||||||
t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
|
t.Printf("RunDirPath:\t%s\n", hi.RunDirPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// printShowInstance writes a representation of [hst.State] or [hst.Config] to output.
|
// printShowInstance writes a representation of [hst.State] or [hst.Config] to output.
|
||||||
@@ -90,12 +86,6 @@ func printShowInstance(
|
|||||||
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
|
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
|
||||||
}
|
}
|
||||||
if config.Container != nil {
|
if config.Container != nil {
|
||||||
if config.Container.Home != nil {
|
|
||||||
t.Printf(" Home:\t%s\n", config.Container.Home)
|
|
||||||
}
|
|
||||||
if config.Container.Hostname != "" {
|
|
||||||
t.Printf(" Hostname:\t%s\n", config.Container.Hostname)
|
|
||||||
}
|
|
||||||
flags := config.Container.Flags.String()
|
flags := config.Container.Flags.String()
|
||||||
|
|
||||||
// this is included in the upper hst.Config struct but is relevant here
|
// this is included in the upper hst.Config struct but is relevant here
|
||||||
@@ -110,6 +100,12 @@ func printShowInstance(
|
|||||||
}
|
}
|
||||||
t.Printf(" Flags:\t%s\n", flags)
|
t.Printf(" Flags:\t%s\n", flags)
|
||||||
|
|
||||||
|
if config.Container.Home != nil {
|
||||||
|
t.Printf(" Home:\t%s\n", config.Container.Home)
|
||||||
|
}
|
||||||
|
if config.Container.Hostname != "" {
|
||||||
|
t.Printf(" Hostname:\t%s\n", config.Container.Hostname)
|
||||||
|
}
|
||||||
if config.Container.Path != nil {
|
if config.Container.Path != nil {
|
||||||
t.Printf(" Path:\t%s\n", config.Container.Path)
|
t.Printf(" Path:\t%s\n", config.Container.Path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ var (
|
|||||||
PID: 0xbeef,
|
PID: 0xbeef,
|
||||||
ShimPID: 0xcafe,
|
ShimPID: 0xcafe,
|
||||||
Config: &hst.Config{
|
Config: &hst.Config{
|
||||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EPulse),
|
Enablements: hst.NewEnablements(hst.EWayland | hst.EPipeWire),
|
||||||
Identity: 1,
|
Identity: 1,
|
||||||
Container: &hst.ContainerConfig{
|
Container: &hst.ContainerConfig{
|
||||||
Shell: check.MustAbs("/bin/sh"),
|
Shell: check.MustAbs("/bin/sh"),
|
||||||
@@ -62,11 +62,11 @@ func TestPrintShowInstance(t *testing.T) {
|
|||||||
{"nil", nil, nil, false, false, "Error: invalid configuration!\n\n", false},
|
{"nil", nil, nil, false, false, "Error: invalid configuration!\n\n", false},
|
||||||
{"config", nil, hst.Template(), false, false, `App
|
{"config", nil, hst.Template(), false, false, `App
|
||||||
Identity: 9 (org.chromium.Chromium)
|
Identity: 9 (org.chromium.Chromium)
|
||||||
Enablements: wayland, dbus, pulseaudio
|
Enablements: wayland, dbus, pipewire
|
||||||
Groups: video, dialout, plugdev
|
Groups: video, dialout, plugdev
|
||||||
|
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
||||||
Home: /data/data/org.chromium.Chromium
|
Home: /data/data/org.chromium.Chromium
|
||||||
Hostname: localhost
|
Hostname: localhost
|
||||||
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
|
||||||
Path: /run/current-system/sw/bin/chromium
|
Path: /run/current-system/sw/bin/chromium
|
||||||
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
|
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
|
||||||
|
|
||||||
@@ -159,11 +159,11 @@ Session bus
|
|||||||
|
|
||||||
App
|
App
|
||||||
Identity: 9 (org.chromium.Chromium)
|
Identity: 9 (org.chromium.Chromium)
|
||||||
Enablements: wayland, dbus, pulseaudio
|
Enablements: wayland, dbus, pipewire
|
||||||
Groups: video, dialout, plugdev
|
Groups: video, dialout, plugdev
|
||||||
|
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
||||||
Home: /data/data/org.chromium.Chromium
|
Home: /data/data/org.chromium.Chromium
|
||||||
Hostname: localhost
|
Hostname: localhost
|
||||||
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
|
||||||
Path: /run/current-system/sw/bin/chromium
|
Path: /run/current-system/sw/bin/chromium
|
||||||
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
|
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ App
|
|||||||
"enablements": {
|
"enablements": {
|
||||||
"wayland": true,
|
"wayland": true,
|
||||||
"dbus": true,
|
"dbus": true,
|
||||||
"pulse": true
|
"pipewire": true
|
||||||
},
|
},
|
||||||
"session_bus": {
|
"session_bus": {
|
||||||
"see": null,
|
"see": null,
|
||||||
@@ -366,7 +366,7 @@ App
|
|||||||
"enablements": {
|
"enablements": {
|
||||||
"wayland": true,
|
"wayland": true,
|
||||||
"dbus": true,
|
"dbus": true,
|
||||||
"pulse": true
|
"pipewire": true
|
||||||
},
|
},
|
||||||
"session_bus": {
|
"session_bus": {
|
||||||
"see": null,
|
"see": null,
|
||||||
@@ -564,7 +564,7 @@ func TestPrintPs(t *testing.T) {
|
|||||||
"enablements": {
|
"enablements": {
|
||||||
"wayland": true,
|
"wayland": true,
|
||||||
"dbus": true,
|
"dbus": true,
|
||||||
"pulse": true
|
"pipewire": true
|
||||||
},
|
},
|
||||||
"session_bus": {
|
"session_bus": {
|
||||||
"see": null,
|
"see": null,
|
||||||
@@ -715,7 +715,7 @@ func TestPrintPs(t *testing.T) {
|
|||||||
"shim_pid": 51966,
|
"shim_pid": 51966,
|
||||||
"enablements": {
|
"enablements": {
|
||||||
"wayland": true,
|
"wayland": true,
|
||||||
"pulse": true
|
"pipewire": true
|
||||||
},
|
},
|
||||||
"identity": 1,
|
"identity": 1,
|
||||||
"groups": null,
|
"groups": null,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
allow_wayland ? true,
|
allow_wayland ? true,
|
||||||
allow_x11 ? false,
|
allow_x11 ? false,
|
||||||
allow_dbus ? true,
|
allow_dbus ? true,
|
||||||
allow_pulse ? true,
|
allow_audio ? true,
|
||||||
gpu ? allow_wayland || allow_x11,
|
gpu ? allow_wayland || allow_x11,
|
||||||
}:
|
}:
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ let
|
|||||||
wayland = allow_wayland;
|
wayland = allow_wayland;
|
||||||
x11 = allow_x11;
|
x11 = allow_x11;
|
||||||
dbus = allow_dbus;
|
dbus = allow_dbus;
|
||||||
pulse = allow_pulse;
|
pipewire = allow_audio;
|
||||||
};
|
};
|
||||||
|
|
||||||
mesa = if gpu then mesaWrappers else null;
|
mesa = if gpu then mesaWrappers else null;
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/internal"
|
"hakurei.app/internal/info"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
var hakureiPathVal = internal.MustHakureiPath().String()
|
var hakureiPathVal = info.MustHakureiPath().String()
|
||||||
|
|
||||||
func mustRunApp(ctx context.Context, msg message.Msg, config *hst.Config, beforeFail func()) {
|
func mustRunApp(ctx context.Context, msg message.Msg, config *hst.Config, beforeFail func()) {
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
nixosTest,
|
testers,
|
||||||
callPackage,
|
callPackage,
|
||||||
|
|
||||||
system,
|
system,
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
let
|
let
|
||||||
buildPackage = self.buildPackage.${system};
|
buildPackage = self.buildPackage.${system};
|
||||||
in
|
in
|
||||||
nixosTest {
|
testers.nixosTest {
|
||||||
name = "hpkg";
|
name = "hpkg";
|
||||||
nodes.machine = {
|
nodes.machine = {
|
||||||
environment.etc = {
|
environment.etc = {
|
||||||
|
|||||||
@@ -90,13 +90,13 @@ wait_for_window("hakurei@machine-foot")
|
|||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
||||||
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
|
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
|
||||||
collect_state_ui("app_wayland")
|
collect_state_ui("app_wayland")
|
||||||
check_state("foot", {"wayland": True, "dbus": True, "pulse": True})
|
check_state("foot", {"wayland": True, "dbus": True, "pipewire": True})
|
||||||
# Verify acl on XDG_RUNTIME_DIR:
|
# Verify acl on XDG_RUNTIME_DIR:
|
||||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10002"))
|
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002"))
|
||||||
machine.send_chars("exit\n")
|
machine.send_chars("exit\n")
|
||||||
machine.wait_until_fails("pgrep foot")
|
machine.wait_until_fails("pgrep foot")
|
||||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
||||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10002")
|
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002")
|
||||||
|
|
||||||
# Exit Sway and verify process exit status 0:
|
# Exit Sway and verify process exit status 0:
|
||||||
swaymsg("exit", succeed=False)
|
swaymsg("exit", succeed=False)
|
||||||
@@ -107,4 +107,4 @@ print(machine.succeed("find /tmp/hakurei.0 "
|
|||||||
+ "-path '/tmp/hakurei.0/runtime/*/*' -prune -o "
|
+ "-path '/tmp/hakurei.0/runtime/*/*' -prune -o "
|
||||||
+ "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
|
+ "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
|
||||||
+ "-print"))
|
+ "-print"))
|
||||||
print(machine.succeed("find /run/user/1000/hakurei"))
|
print(machine.fail("ls /run/user/1000/hakurei"))
|
||||||
|
|||||||
282
cmd/sharefs/fuse-operations.c
Normal file
282
cmd/sharefs/fuse-operations.c
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
#ifndef _GNU_SOURCE
|
||||||
|
#define _GNU_SOURCE /* O_DIRECT */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* TODO(ophestra): remove after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
|
||||||
|
#include <sys/syscall.h>
|
||||||
|
|
||||||
|
#include "fuse-operations.h"
|
||||||
|
|
||||||
|
/* MUST_TRANSLATE_PATHNAME translates a userspace pathname to a relative pathname;
|
||||||
|
* the resulting address points to a constant string or part of pathname, it is never heap allocated. */
|
||||||
|
#define MUST_TRANSLATE_PATHNAME(pathname) \
|
||||||
|
do { \
|
||||||
|
if (pathname == NULL) \
|
||||||
|
return -EINVAL; \
|
||||||
|
while (*pathname == '/') \
|
||||||
|
pathname++; \
|
||||||
|
if (*pathname == '\0') \
|
||||||
|
pathname = "."; \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
/* GET_CONTEXT_PRIV obtains fuse context and private data for the calling thread. */
|
||||||
|
#define GET_CONTEXT_PRIV(ctx, priv) \
|
||||||
|
do { \
|
||||||
|
ctx = fuse_get_context(); \
|
||||||
|
priv = ctx->private_data; \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
/* impl_getattr modifies a struct stat from the kernel to present to userspace;
|
||||||
|
* impl_getattr returns a negative errno style error code. */
|
||||||
|
static int impl_getattr(struct fuse_context *ctx, struct stat *statbuf) {
|
||||||
|
/* allowlist of permitted types */
|
||||||
|
if (!S_ISDIR(statbuf->st_mode) && !S_ISREG(statbuf->st_mode) && !S_ISLNK(statbuf->st_mode)) {
|
||||||
|
return -ENOTRECOVERABLE; /* returning an errno causes all operations on the file to return EIO */
|
||||||
|
}
|
||||||
|
|
||||||
|
#define OVERRIDE_PERM(v) (statbuf->st_mode & ~0777) | (v & 0777)
|
||||||
|
if (S_ISDIR(statbuf->st_mode))
|
||||||
|
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_DIR);
|
||||||
|
else if (S_ISREG(statbuf->st_mode))
|
||||||
|
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_REG);
|
||||||
|
else
|
||||||
|
statbuf->st_mode = 0; /* should always be symlink in this case */
|
||||||
|
|
||||||
|
statbuf->st_uid = ctx->uid;
|
||||||
|
statbuf->st_gid = SHAREFS_MEDIA_RW_ID;
|
||||||
|
statbuf->st_ctim = statbuf->st_mtim;
|
||||||
|
statbuf->st_nlink = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fuse_operations implementation */
|
||||||
|
|
||||||
|
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi) {
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
(void)fi;
|
||||||
|
|
||||||
|
if (fstatat(priv->dirfd, pathname, statbuf, AT_SYMLINK_NOFOLLOW) == -1)
|
||||||
|
return -errno;
|
||||||
|
return impl_getattr(ctx, statbuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags) {
|
||||||
|
int fd;
|
||||||
|
DIR *dp;
|
||||||
|
struct stat st;
|
||||||
|
int ret = 0;
|
||||||
|
struct dirent *de;
|
||||||
|
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
(void)offset;
|
||||||
|
(void)fi;
|
||||||
|
|
||||||
|
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_DIRECTORY | O_CLOEXEC)) == -1)
|
||||||
|
return -errno;
|
||||||
|
if ((dp = fdopendir(fd)) == NULL) {
|
||||||
|
close(fd);
|
||||||
|
return -errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
errno = 0; /* for the next readdir call */
|
||||||
|
while ((de = readdir(dp)) != NULL) {
|
||||||
|
if (flags & FUSE_READDIR_PLUS) {
|
||||||
|
if (fstatat(dirfd(dp), de->d_name, &st, AT_SYMLINK_NOFOLLOW) == -1) {
|
||||||
|
ret = -errno;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ret = impl_getattr(ctx, &st)) < 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
errno = 0;
|
||||||
|
ret = filler(buf, de->d_name, &st, 0, FUSE_FILL_DIR_PLUS);
|
||||||
|
} else
|
||||||
|
ret = filler(buf, de->d_name, NULL, 0, 0);
|
||||||
|
|
||||||
|
if (ret != 0) {
|
||||||
|
ret = errno != 0 ? -errno : -EIO; /* filler */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
errno = 0; /* for the next readdir call */
|
||||||
|
}
|
||||||
|
if (ret == 0 && errno != 0)
|
||||||
|
ret = -errno; /* readdir */
|
||||||
|
|
||||||
|
closedir(dp);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_mkdir(const char *pathname, mode_t mode) {
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
(void)mode;
|
||||||
|
|
||||||
|
if (mkdirat(priv->dirfd, pathname, SHAREFS_PERM_DIR) == -1)
|
||||||
|
return -errno;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_unlink(const char *pathname) {
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
if (unlinkat(priv->dirfd, pathname, 0) == -1)
|
||||||
|
return -errno;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_rmdir(const char *pathname) {
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
if (unlinkat(priv->dirfd, pathname, AT_REMOVEDIR) == -1)
|
||||||
|
return -errno;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags) {
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(oldpath);
|
||||||
|
MUST_TRANSLATE_PATHNAME(newpath);
|
||||||
|
|
||||||
|
/* TODO(ophestra): replace with wrapper after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
|
||||||
|
if (syscall(__NR_renameat2, priv->dirfd, oldpath, priv->dirfd, newpath, flags) == -1)
|
||||||
|
return -errno;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi) {
|
||||||
|
int fd;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
(void)fi;
|
||||||
|
|
||||||
|
if ((fd = openat(priv->dirfd, pathname, O_WRONLY | O_CLOEXEC)) == -1)
|
||||||
|
return -errno;
|
||||||
|
if ((ret = ftruncate(fd, length)) == -1)
|
||||||
|
ret = -errno;
|
||||||
|
close(fd);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi) {
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
(void)fi;
|
||||||
|
|
||||||
|
if (utimensat(priv->dirfd, pathname, times, AT_SYMLINK_NOFOLLOW) == -1)
|
||||||
|
return -errno;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi) {
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
(void)mode;
|
||||||
|
|
||||||
|
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS, SHAREFS_PERM_REG)) == -1)
|
||||||
|
return -errno;
|
||||||
|
fi->fh = fd;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_open(const char *pathname, struct fuse_file_info *fi) {
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS)) == -1)
|
||||||
|
return -errno;
|
||||||
|
fi->fh = fd;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
(void)pathname;
|
||||||
|
|
||||||
|
if ((ret = pread(fi->fh, buf, count, offset)) == -1)
|
||||||
|
return -errno;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
(void)pathname;
|
||||||
|
|
||||||
|
if ((ret = pwrite(fi->fh, buf, count, offset)) == -1)
|
||||||
|
return -errno;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_statfs(const char *pathname, struct statvfs *statbuf) {
|
||||||
|
int fd;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_CLOEXEC)) == -1)
|
||||||
|
return -errno;
|
||||||
|
if ((ret = fstatvfs(fd, statbuf)) == -1)
|
||||||
|
ret = -errno;
|
||||||
|
close(fd);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_release(const char *pathname, struct fuse_file_info *fi) {
|
||||||
|
(void)pathname;
|
||||||
|
|
||||||
|
return close(fi->fh);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi) {
|
||||||
|
(void)pathname;
|
||||||
|
|
||||||
|
if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1)
|
||||||
|
return -errno;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
34
cmd/sharefs/fuse-operations.h
Normal file
34
cmd/sharefs/fuse-operations.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12)
|
||||||
|
#include <fuse.h>
|
||||||
|
#include <fuse_lowlevel.h> /* for fuse_cmdline_help */
|
||||||
|
|
||||||
|
#if (FUSE_VERSION < FUSE_MAKE_VERSION(3, 12))
|
||||||
|
#error This package requires libfuse >= v3.12
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
|
||||||
|
#define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
|
||||||
|
#define SHAREFS_PERM_REG 0600 /* permission bits for regular files presented to userspace */
|
||||||
|
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
|
||||||
|
|
||||||
|
/* sharefs_private is populated by sharefs_init and contains process-wide context */
|
||||||
|
struct sharefs_private {
|
||||||
|
int dirfd; /* source dirfd opened during sharefs_init */
|
||||||
|
uintptr_t setup; /* cgo handle of opaque setup state */
|
||||||
|
};
|
||||||
|
|
||||||
|
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi);
|
||||||
|
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags);
|
||||||
|
int sharefs_mkdir(const char *pathname, mode_t mode);
|
||||||
|
int sharefs_unlink(const char *pathname);
|
||||||
|
int sharefs_rmdir(const char *pathname);
|
||||||
|
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags);
|
||||||
|
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi);
|
||||||
|
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi);
|
||||||
|
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi);
|
||||||
|
int sharefs_open(const char *pathname, struct fuse_file_info *fi);
|
||||||
|
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
|
||||||
|
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
|
||||||
|
int sharefs_statfs(const char *pathname, struct statvfs *statbuf);
|
||||||
|
int sharefs_release(const char *pathname, struct fuse_file_info *fi);
|
||||||
|
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi);
|
||||||
556
cmd/sharefs/fuse.go
Normal file
556
cmd/sharefs/fuse.go
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo pkg-config: --static fuse3
|
||||||
|
|
||||||
|
#include "fuse-operations.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
extern void *sharefs_init(struct fuse_conn_info *conn, struct fuse_config *cfg);
|
||||||
|
extern void sharefs_destroy(void *private_data);
|
||||||
|
|
||||||
|
typedef void (*closure)();
|
||||||
|
static inline struct fuse_opt _FUSE_OPT_END() { return (struct fuse_opt)FUSE_OPT_END; };
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"runtime/cgo"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/container/check"
|
||||||
|
"hakurei.app/container/std"
|
||||||
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/internal/helper/proc"
|
||||||
|
"hakurei.app/internal/info"
|
||||||
|
"hakurei.app/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// closure represents a C function pointer.
|
||||||
|
closure = C.closure
|
||||||
|
|
||||||
|
// fuseArgs represents the fuse_args structure.
|
||||||
|
fuseArgs = C.struct_fuse_args
|
||||||
|
|
||||||
|
// setupState holds state used for setup. Its cgo handle is included in
|
||||||
|
// sharefs_private and considered opaque to non-setup callbacks.
|
||||||
|
setupState struct {
|
||||||
|
// Whether sharefs_init failed.
|
||||||
|
initFailed bool
|
||||||
|
|
||||||
|
// Whether to create source directory as root.
|
||||||
|
mkdir bool
|
||||||
|
|
||||||
|
// Open file descriptor to fuse.
|
||||||
|
Fuse int
|
||||||
|
|
||||||
|
// Pathname to open for dirfd.
|
||||||
|
Source *check.Absolute
|
||||||
|
// New uid and gid to set by sharefs_init when starting as root.
|
||||||
|
Setuid, Setgid int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { gob.Register(new(setupState)) }
|
||||||
|
|
||||||
|
// destroySetup invalidates the setup [cgo.Handle] in a sharefs_private structure.
|
||||||
|
func destroySetup(private_data unsafe.Pointer) (ok bool) {
|
||||||
|
if private_data == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
priv := (*C.struct_sharefs_private)(private_data)
|
||||||
|
|
||||||
|
if h := cgo.Handle(priv.setup); h != 0 {
|
||||||
|
priv.setup = 0
|
||||||
|
h.Delete()
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//export sharefs_init
|
||||||
|
func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.Pointer {
|
||||||
|
ctx := C.fuse_get_context()
|
||||||
|
priv := (*C.struct_sharefs_private)(ctx.private_data)
|
||||||
|
setup := cgo.Handle(priv.setup).Value().(*setupState)
|
||||||
|
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
log.Println("filesystem daemon must not run as root")
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.use_ino = C.true
|
||||||
|
cfg.direct_io = C.false
|
||||||
|
// getattr is context-dependent
|
||||||
|
cfg.attr_timeout = 0
|
||||||
|
cfg.entry_timeout = 0
|
||||||
|
cfg.negative_timeout = 0
|
||||||
|
|
||||||
|
// all future filesystem operations happen through this dirfd
|
||||||
|
if fd, err := syscall.Open(setup.Source.String(), syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC, 0); err != nil {
|
||||||
|
log.Printf("cannot open %q: %v", setup.Source, err)
|
||||||
|
goto fail
|
||||||
|
} else if err = syscall.Fchdir(fd); err != nil {
|
||||||
|
_ = syscall.Close(fd)
|
||||||
|
log.Printf("cannot enter %q: %s", setup.Source, err)
|
||||||
|
goto fail
|
||||||
|
} else {
|
||||||
|
priv.dirfd = C.int(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.private_data
|
||||||
|
|
||||||
|
fail:
|
||||||
|
setup.initFailed = true
|
||||||
|
C.fuse_exit(ctx.fuse)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//export sharefs_destroy
|
||||||
|
func sharefs_destroy(private_data unsafe.Pointer) {
|
||||||
|
if private_data != nil {
|
||||||
|
destroySetup(private_data)
|
||||||
|
priv := (*C.struct_sharefs_private)(private_data)
|
||||||
|
|
||||||
|
if err := syscall.Close(int(priv.dirfd)); err != nil {
|
||||||
|
log.Printf("cannot close source directory: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// showHelp prints the help message.
|
||||||
|
func showHelp(args *fuseArgs) {
|
||||||
|
executableName := sharefsName
|
||||||
|
if args.argc > 0 {
|
||||||
|
executableName = path.Base(C.GoString(*args.argv))
|
||||||
|
} else if name, err := os.Executable(); err == nil {
|
||||||
|
executableName = path.Base(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
|
||||||
|
|
||||||
|
fmt.Println("Filesystem options:")
|
||||||
|
fmt.Println(" -o source=/data/media source directory to be mounted")
|
||||||
|
fmt.Println(" -o setuid=1023 uid to run as when starting as root")
|
||||||
|
fmt.Println(" -o setgid=1023 gid to run as when starting as root")
|
||||||
|
|
||||||
|
fmt.Println("\nFUSE options:")
|
||||||
|
C.fuse_cmdline_help()
|
||||||
|
C.fuse_lib_help(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOpts parses fuse options via fuse_opt_parse.
|
||||||
|
func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
|
||||||
|
var unsafeOpts struct {
|
||||||
|
// Pathname to writable source directory.
|
||||||
|
source *C.char
|
||||||
|
|
||||||
|
// Whether to create source directory as root.
|
||||||
|
mkdir C.int
|
||||||
|
|
||||||
|
// Decimal string representation of uid to set when running as root.
|
||||||
|
setuid *C.char
|
||||||
|
// Decimal string representation of gid to set when running as root.
|
||||||
|
setgid *C.char
|
||||||
|
|
||||||
|
// Decimal string representation of open file descriptor to read setupState from.
|
||||||
|
// This is an internal detail for containerisation and must not be specified directly.
|
||||||
|
setup *C.char
|
||||||
|
}
|
||||||
|
|
||||||
|
if C.fuse_opt_parse(args, unsafe.Pointer(&unsafeOpts), &[]C.struct_fuse_opt{
|
||||||
|
{templ: C.CString("source=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.source)), value: 0},
|
||||||
|
{templ: C.CString("mkdir"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.mkdir)), value: 1},
|
||||||
|
{templ: C.CString("setuid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setuid)), value: 0},
|
||||||
|
{templ: C.CString("setgid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setgid)), value: 0},
|
||||||
|
|
||||||
|
{templ: C.CString("setup=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setup)), value: 0},
|
||||||
|
|
||||||
|
C._FUSE_OPT_END(),
|
||||||
|
}[0], nil) == -1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsafeOpts.source != nil {
|
||||||
|
defer C.free(unsafe.Pointer(unsafeOpts.source))
|
||||||
|
}
|
||||||
|
if unsafeOpts.setuid != nil {
|
||||||
|
defer C.free(unsafe.Pointer(unsafeOpts.setuid))
|
||||||
|
}
|
||||||
|
if unsafeOpts.setgid != nil {
|
||||||
|
defer C.free(unsafe.Pointer(unsafeOpts.setgid))
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsafeOpts.setup != nil {
|
||||||
|
defer C.free(unsafe.Pointer(unsafeOpts.setup))
|
||||||
|
|
||||||
|
if v, err := strconv.Atoi(C.GoString(unsafeOpts.setup)); err != nil || v < 3 {
|
||||||
|
log.Println("invalid value for option setup")
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
r := os.NewFile(uintptr(v), "setup")
|
||||||
|
defer func() {
|
||||||
|
if err = r.Close(); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err = gob.NewDecoder(r).Decode(setup); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if setup.Fuse < 3 {
|
||||||
|
log.Println("invalid file descriptor", setup.Fuse)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsafeOpts.source == nil {
|
||||||
|
showHelp(args)
|
||||||
|
return false
|
||||||
|
} else if a, err := check.NewAbs(C.GoString(unsafeOpts.source)); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
setup.Source = a
|
||||||
|
}
|
||||||
|
setup.mkdir = unsafeOpts.mkdir != 0
|
||||||
|
|
||||||
|
if unsafeOpts.setuid == nil {
|
||||||
|
setup.Setuid = -1
|
||||||
|
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setuid)); err != nil || v <= 0 {
|
||||||
|
log.Println("invalid value for option setuid")
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
setup.Setuid = v
|
||||||
|
}
|
||||||
|
if unsafeOpts.setgid == nil {
|
||||||
|
setup.Setgid = -1
|
||||||
|
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setgid)); err != nil || v <= 0 {
|
||||||
|
log.Println("invalid value for option setgid")
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
setup.Setgid = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyArgs returns a heap allocated copy of an argument slice in fuse_args representation.
|
||||||
|
func copyArgs(s ...string) fuseArgs {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return fuseArgs{argc: 0, argv: nil, allocated: 0}
|
||||||
|
}
|
||||||
|
args := unsafe.Slice((**C.char)(C.malloc(C.size_t(uintptr(len(s))*unsafe.Sizeof(s[0])))), len(s))
|
||||||
|
for i, arg := range s {
|
||||||
|
args[i] = C.CString(arg)
|
||||||
|
}
|
||||||
|
return fuseArgs{argc: C.int(len(s)), argv: &args[0], allocated: 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
// freeArgs frees the contents of argument list.
|
||||||
|
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }
|
||||||
|
|
||||||
|
// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
|
||||||
|
// The last byte of arg must be 0.
|
||||||
|
func unsafeAddArgument(args *fuseArgs, arg string) {
|
||||||
|
C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func _main(s ...string) (exitCode int) {
|
||||||
|
msg := message.New(log.Default())
|
||||||
|
container.TryArgv0(msg)
|
||||||
|
runtime.LockOSThread()
|
||||||
|
|
||||||
|
// don't mask creation mode, kernel already did that
|
||||||
|
syscall.Umask(0)
|
||||||
|
|
||||||
|
var pinner runtime.Pinner
|
||||||
|
defer pinner.Unpin()
|
||||||
|
|
||||||
|
args := copyArgs(s...)
|
||||||
|
defer freeArgs(&args)
|
||||||
|
|
||||||
|
// this causes the kernel to enforce access control based on
|
||||||
|
// struct stat populated by sharefs_getattr
|
||||||
|
unsafeAddArgument(&args, "-odefault_permissions\x00")
|
||||||
|
|
||||||
|
var priv C.struct_sharefs_private
|
||||||
|
pinner.Pin(&priv)
|
||||||
|
var setup setupState
|
||||||
|
priv.setup = C.uintptr_t(cgo.NewHandle(&setup))
|
||||||
|
defer destroySetup(unsafe.Pointer(&priv))
|
||||||
|
|
||||||
|
var opts C.struct_fuse_cmdline_opts
|
||||||
|
if C.fuse_parse_cmdline(&args, &opts) != 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if opts.mountpoint != nil {
|
||||||
|
defer C.free(unsafe.Pointer(opts.mountpoint))
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.show_version != 0 {
|
||||||
|
fmt.Println("hakurei version", info.Version())
|
||||||
|
fmt.Println("FUSE library version", C.GoString(C.fuse_pkgversion()))
|
||||||
|
C.fuse_lowlevel_version()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.show_help != 0 {
|
||||||
|
showHelp(&args)
|
||||||
|
return 0
|
||||||
|
} else if opts.mountpoint == nil {
|
||||||
|
log.Println("no mountpoint specified")
|
||||||
|
return 2
|
||||||
|
} else {
|
||||||
|
// hack to keep fuse_parse_cmdline happy in the container
|
||||||
|
mountpoint := C.GoString(opts.mountpoint)
|
||||||
|
pathnameArg := -1
|
||||||
|
for i, arg := range s {
|
||||||
|
if arg == mountpoint {
|
||||||
|
pathnameArg = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pathnameArg < 0 {
|
||||||
|
log.Println("mountpoint must be absolute")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
s[pathnameArg] = container.Nonexistent
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parseOpts(&args, &setup, msg.GetLogger()) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
asRoot := os.Geteuid() == 0
|
||||||
|
|
||||||
|
if asRoot {
|
||||||
|
if setup.Setuid <= 0 || setup.Setgid <= 0 {
|
||||||
|
log.Println("setuid and setgid must not be 0")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if setup.Fuse >= 3 {
|
||||||
|
log.Println("filesystem daemon must not run as root")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if setup.mkdir {
|
||||||
|
if err := os.MkdirAll(setup.Source.String(), 0700); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrExist) {
|
||||||
|
log.Println(err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
// skip setup for existing source directory
|
||||||
|
} else if err = os.Chown(setup.Source.String(), setup.Setuid, setup.Setgid); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if setup.Fuse < 3 && (setup.Setuid > 0 || setup.Setgid > 0) {
|
||||||
|
log.Println("setuid and setgid has no effect when not starting as root")
|
||||||
|
return 1
|
||||||
|
} else if setup.mkdir {
|
||||||
|
log.Println("mkdir has no effect when not starting as root")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
op := C.struct_fuse_operations{
|
||||||
|
init: closure(C.sharefs_init),
|
||||||
|
destroy: closure(C.sharefs_destroy),
|
||||||
|
|
||||||
|
// implemented in fuse-helper.c
|
||||||
|
getattr: closure(C.sharefs_getattr),
|
||||||
|
readdir: closure(C.sharefs_readdir),
|
||||||
|
mkdir: closure(C.sharefs_mkdir),
|
||||||
|
unlink: closure(C.sharefs_unlink),
|
||||||
|
rmdir: closure(C.sharefs_rmdir),
|
||||||
|
rename: closure(C.sharefs_rename),
|
||||||
|
truncate: closure(C.sharefs_truncate),
|
||||||
|
utimens: closure(C.sharefs_utimens),
|
||||||
|
create: closure(C.sharefs_create),
|
||||||
|
open: closure(C.sharefs_open),
|
||||||
|
read: closure(C.sharefs_read),
|
||||||
|
write: closure(C.sharefs_write),
|
||||||
|
statfs: closure(C.sharefs_statfs),
|
||||||
|
release: closure(C.sharefs_release),
|
||||||
|
fsync: closure(C.sharefs_fsync),
|
||||||
|
}
|
||||||
|
|
||||||
|
fuse := C.fuse_new_fn(&args, &op, C.size_t(unsafe.Sizeof(op)), unsafe.Pointer(&priv))
|
||||||
|
if fuse == nil {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
defer C.fuse_destroy(fuse)
|
||||||
|
se := C.fuse_get_session(fuse)
|
||||||
|
|
||||||
|
if setup.Fuse < 3 {
|
||||||
|
// unconfined, set up mount point and container
|
||||||
|
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
// unmounted by initial process
|
||||||
|
defer func() {
|
||||||
|
if exitCode == 5 {
|
||||||
|
C.fuse_unmount(fuse)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if asRoot {
|
||||||
|
if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); err != nil {
|
||||||
|
log.Printf("cannot set gid: %v", err)
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
if err := syscall.Setgroups(nil); err != nil {
|
||||||
|
log.Printf("cannot set supplementary groups: %v", err)
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
if err := syscall.Setresuid(setup.Setuid, setup.Setuid, setup.Setuid); err != nil {
|
||||||
|
log.Printf("cannot set uid: %v", err)
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.SwapVerbose(opts.debug != 0)
|
||||||
|
ctx := context.Background()
|
||||||
|
if opts.foreground != 0 {
|
||||||
|
c, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer cancel()
|
||||||
|
ctx = c
|
||||||
|
}
|
||||||
|
z := container.New(ctx, msg)
|
||||||
|
z.AllowOrphan = opts.foreground == 0
|
||||||
|
z.Env = os.Environ()
|
||||||
|
|
||||||
|
// keep fuse_parse_cmdline happy in the container
|
||||||
|
z.Tmpfs(check.MustAbs(container.Nonexistent), 1<<10, 0755)
|
||||||
|
|
||||||
|
if a, err := check.NewAbs(container.MustExecutable(msg)); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 5
|
||||||
|
} else {
|
||||||
|
z.Path = a
|
||||||
|
}
|
||||||
|
z.Args = s
|
||||||
|
z.ForwardCancel = true
|
||||||
|
z.SeccompPresets |= std.PresetStrict
|
||||||
|
z.ParentPerm = 0700
|
||||||
|
z.Bind(setup.Source, setup.Source, std.BindWritable)
|
||||||
|
if !z.AllowOrphan {
|
||||||
|
z.WaitDelay = hst.WaitDelayMax
|
||||||
|
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
}
|
||||||
|
z.Bind(z.Path, z.Path, 0)
|
||||||
|
setup.Fuse = int(proc.ExtraFileSlice(&z.ExtraFiles, os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse")))
|
||||||
|
|
||||||
|
var setupWriter io.WriteCloser
|
||||||
|
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 5
|
||||||
|
} else {
|
||||||
|
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
|
||||||
|
setupWriter = w
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := z.Start(); err != nil {
|
||||||
|
if m, ok := message.GetMessage(err); ok {
|
||||||
|
log.Println(m)
|
||||||
|
} else {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
if err := z.Serve(); err != nil {
|
||||||
|
if m, ok := message.GetMessage(err); ok {
|
||||||
|
log.Println(m)
|
||||||
|
} else {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 5
|
||||||
|
} else if err = setupWriter.Close(); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !z.AllowOrphan {
|
||||||
|
if err := z.Wait(); err != nil {
|
||||||
|
var exitError *exec.ExitError
|
||||||
|
if !errors.As(err, &exitError) || exitError == nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
switch code := exitError.ExitCode(); syscall.Signal(code & 0x7f) {
|
||||||
|
case syscall.SIGINT:
|
||||||
|
case syscall.SIGTERM:
|
||||||
|
|
||||||
|
default:
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else { // confined
|
||||||
|
C.free(unsafe.Pointer(opts.mountpoint))
|
||||||
|
// must be heap allocated
|
||||||
|
opts.mountpoint = C.CString("/dev/fd/" + strconv.Itoa(setup.Fuse))
|
||||||
|
|
||||||
|
if err := os.Chdir("/"); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
defer C.fuse_unmount(fuse)
|
||||||
|
|
||||||
|
if C.fuse_set_signal_handlers(se) != 0 {
|
||||||
|
return 6
|
||||||
|
}
|
||||||
|
defer C.fuse_remove_signal_handlers(se)
|
||||||
|
|
||||||
|
if opts.singlethread != 0 {
|
||||||
|
if C.fuse_loop(fuse) != 0 {
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loopConfig := C.fuse_loop_cfg_create()
|
||||||
|
if loopConfig == nil {
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
defer C.fuse_loop_cfg_destroy(loopConfig)
|
||||||
|
|
||||||
|
C.fuse_loop_cfg_set_clone_fd(loopConfig, C.uint(opts.clone_fd))
|
||||||
|
|
||||||
|
C.fuse_loop_cfg_set_idle_threads(loopConfig, opts.max_idle_threads)
|
||||||
|
C.fuse_loop_cfg_set_max_threads(loopConfig, opts.max_threads)
|
||||||
|
if C.fuse_loop_mt(fuse, loopConfig) != 0 {
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if setup.initFailed {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
113
cmd/sharefs/fuse_test.go
Normal file
113
cmd/sharefs/fuse_test.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/check"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseOpts(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
want setupState
|
||||||
|
wantLog string
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{"zero length", []string{}, setupState{}, "", false},
|
||||||
|
|
||||||
|
{"not absolute", []string{"sharefs",
|
||||||
|
"-o", "source=nonexistent",
|
||||||
|
"-o", "setuid=1023",
|
||||||
|
"-o", "setgid=1023",
|
||||||
|
}, setupState{}, "sharefs: path \"nonexistent\" is not absolute\n", false},
|
||||||
|
|
||||||
|
{"not specified", []string{"sharefs",
|
||||||
|
"-o", "setuid=1023",
|
||||||
|
"-o", "setgid=1023",
|
||||||
|
}, setupState{}, "", false},
|
||||||
|
|
||||||
|
{"invalid setuid", []string{"sharefs",
|
||||||
|
"-o", "source=/proc/nonexistent",
|
||||||
|
"-o", "setuid=ff",
|
||||||
|
"-o", "setgid=1023",
|
||||||
|
}, setupState{
|
||||||
|
Source: check.MustAbs("/proc/nonexistent"),
|
||||||
|
}, "sharefs: invalid value for option setuid\n", false},
|
||||||
|
|
||||||
|
{"invalid setgid", []string{"sharefs",
|
||||||
|
"-o", "source=/proc/nonexistent",
|
||||||
|
"-o", "setuid=1023",
|
||||||
|
"-o", "setgid=ff",
|
||||||
|
}, setupState{
|
||||||
|
Source: check.MustAbs("/proc/nonexistent"),
|
||||||
|
Setuid: 1023,
|
||||||
|
}, "sharefs: invalid value for option setgid\n", false},
|
||||||
|
|
||||||
|
{"simple", []string{"sharefs",
|
||||||
|
"-o", "source=/proc/nonexistent",
|
||||||
|
}, setupState{
|
||||||
|
Source: check.MustAbs("/proc/nonexistent"),
|
||||||
|
Setuid: -1,
|
||||||
|
Setgid: -1,
|
||||||
|
}, "", true},
|
||||||
|
|
||||||
|
{"root", []string{"sharefs",
|
||||||
|
"-o", "source=/proc/nonexistent",
|
||||||
|
"-o", "setuid=1023",
|
||||||
|
"-o", "setgid=1023",
|
||||||
|
}, setupState{
|
||||||
|
Source: check.MustAbs("/proc/nonexistent"),
|
||||||
|
Setuid: 1023,
|
||||||
|
Setgid: 1023,
|
||||||
|
}, "", true},
|
||||||
|
|
||||||
|
{"setuid", []string{"sharefs",
|
||||||
|
"-o", "source=/proc/nonexistent",
|
||||||
|
"-o", "setuid=1023",
|
||||||
|
}, setupState{
|
||||||
|
Source: check.MustAbs("/proc/nonexistent"),
|
||||||
|
Setuid: 1023,
|
||||||
|
Setgid: -1,
|
||||||
|
}, "", true},
|
||||||
|
|
||||||
|
{"setgid", []string{"sharefs",
|
||||||
|
"-o", "source=/proc/nonexistent",
|
||||||
|
"-o", "setgid=1023",
|
||||||
|
}, setupState{
|
||||||
|
Source: check.MustAbs("/proc/nonexistent"),
|
||||||
|
Setuid: -1,
|
||||||
|
Setgid: 1023,
|
||||||
|
}, "", true},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
got setupState
|
||||||
|
buf bytes.Buffer
|
||||||
|
)
|
||||||
|
args := copyArgs(tc.args...)
|
||||||
|
defer freeArgs(&args)
|
||||||
|
unsafeAddArgument(&args, "-odefault_permissions\x00")
|
||||||
|
|
||||||
|
if ok := parseOpts(&args, &got, log.New(&buf, "sharefs: ", 0)); ok != tc.wantOk {
|
||||||
|
t.Errorf("parseOpts: ok = %v, want %v", ok, tc.wantOk)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(&got, &tc.want) {
|
||||||
|
t.Errorf("parseOpts: setup = %#v, want %#v", got, tc.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.String() != tc.wantLog {
|
||||||
|
t.Errorf("parseOpts: log =\n%s\nwant\n%s", buf.String(), tc.wantLog)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
31
cmd/sharefs/main.go
Normal file
31
cmd/sharefs/main.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sharefsName is the prefix used by log.std in the sharefs process.
|
||||||
|
const sharefsName = "sharefs"
|
||||||
|
|
||||||
|
// handleMountArgs returns an alternative, libfuse-compatible args slice for
|
||||||
|
// args passed by mount -t fuse.sharefs [options] sharefs <mountpoint>.
|
||||||
|
//
|
||||||
|
// In this case, args always has a length of 5 with index 0 being what comes
|
||||||
|
// after "fuse." in the filesystem type, 1 is the uninterpreted string passed
|
||||||
|
// to mount (sharefsName is used as the magic string to enable this hack),
|
||||||
|
// 2 is passed through to libfuse as mountpoint, and 3 is always "-o".
|
||||||
|
func handleMountArgs(args []string) []string {
|
||||||
|
if len(args) == 5 && args[1] == sharefsName && args[3] == "-o" {
|
||||||
|
return []string{sharefsName, args[2], "-o", args[4]}
|
||||||
|
}
|
||||||
|
return slices.Clone(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.SetPrefix(sharefsName + ": ")
|
||||||
|
|
||||||
|
os.Exit(_main(handleMountArgs(os.Args)...))
|
||||||
|
}
|
||||||
29
cmd/sharefs/main_test.go
Normal file
29
cmd/sharefs/main_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandleMountArgs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{"nil", nil, nil},
|
||||||
|
{"passthrough", []string{"sharefs", "-V"}, []string{"sharefs", "-V"}},
|
||||||
|
{"replace", []string{"/sbin/sharefs", "sharefs", "/sdcard", "-o", "rw"}, []string{"sharefs", "/sdcard", "-o", "rw"}},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if got := handleMountArgs(tc.args); !slices.Equal(got, tc.want) {
|
||||||
|
t.Errorf("handleMountArgs: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
41
cmd/sharefs/test/configuration.nix
Normal file
41
cmd/sharefs/test/configuration.nix
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
users.users = {
|
||||||
|
alice = {
|
||||||
|
isNormalUser = true;
|
||||||
|
description = "Alice Foobar";
|
||||||
|
password = "foobar";
|
||||||
|
uid = 1000;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
home-manager.users.alice.home.stateVersion = "24.11";
|
||||||
|
|
||||||
|
# Automatically login on tty1 as a normal user:
|
||||||
|
services.getty.autologinUser = "alice";
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
# For benchmarking sharefs:
|
||||||
|
systemPackages = [ pkgs.fsmark ];
|
||||||
|
};
|
||||||
|
|
||||||
|
virtualisation = {
|
||||||
|
diskSize = 6 * 1024;
|
||||||
|
|
||||||
|
qemu.options = [
|
||||||
|
# Increase test performance:
|
||||||
|
"-smp 8"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.hakurei = rec {
|
||||||
|
enable = true;
|
||||||
|
stateDir = "/var/lib/hakurei";
|
||||||
|
sharefs.source = "${stateDir}/sdcard";
|
||||||
|
users.alice = 0;
|
||||||
|
|
||||||
|
extraHomeConfig = {
|
||||||
|
home.stateVersion = "23.05";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
44
cmd/sharefs/test/default.nix
Normal file
44
cmd/sharefs/test/default.nix
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
testers,
|
||||||
|
|
||||||
|
system,
|
||||||
|
self,
|
||||||
|
}:
|
||||||
|
testers.nixosTest {
|
||||||
|
name = "sharefs";
|
||||||
|
nodes.machine =
|
||||||
|
{ options, pkgs, ... }:
|
||||||
|
let
|
||||||
|
fhs =
|
||||||
|
let
|
||||||
|
hakurei = options.environment.hakurei.package.default;
|
||||||
|
in
|
||||||
|
pkgs.buildFHSEnv {
|
||||||
|
pname = "hakurei-fhs";
|
||||||
|
inherit (hakurei) version;
|
||||||
|
targetPkgs = _: hakurei.targetPkgs;
|
||||||
|
extraOutputsToInstall = [ "dev" ];
|
||||||
|
profile = ''
|
||||||
|
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
environment.systemPackages = [
|
||||||
|
# For go tests:
|
||||||
|
(pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" ''
|
||||||
|
cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei"
|
||||||
|
${fhs}/bin/hakurei-fhs -c 'CC="clang -O3 -Werror" go test ./...'
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
./configuration.nix
|
||||||
|
|
||||||
|
self.nixosModules.hakurei
|
||||||
|
self.inputs.home-manager.nixosModules.home-manager
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = builtins.readFile ./test.py;
|
||||||
|
}
|
||||||
60
cmd/sharefs/test/test.py
Normal file
60
cmd/sharefs/test/test.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
start_all()
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
|
# To check sharefs version:
|
||||||
|
print(machine.succeed("sharefs -V"))
|
||||||
|
|
||||||
|
# Make sure sharefs started:
|
||||||
|
machine.wait_for_unit("sdcard.mount")
|
||||||
|
|
||||||
|
machine.succeed("mkdir /mnt")
|
||||||
|
def check_bad_opts_output(opts, want, source="/etc", privileged=False):
|
||||||
|
output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"sharefs -f -o source={source},{opts} /mnt 2>&1")
|
||||||
|
if output != want:
|
||||||
|
raise Exception(f"unexpected output: {output}")
|
||||||
|
|
||||||
|
# Malformed setuid/setgid representation:
|
||||||
|
check_bad_opts_output("setuid=ff", "sharefs: invalid value for option setuid\n")
|
||||||
|
check_bad_opts_output("setgid=ff", "sharefs: invalid value for option setgid\n")
|
||||||
|
|
||||||
|
# Bounds check for setuid/setgid:
|
||||||
|
check_bad_opts_output("setuid=0", "sharefs: invalid value for option setuid\n")
|
||||||
|
check_bad_opts_output("setgid=0", "sharefs: invalid value for option setgid\n")
|
||||||
|
check_bad_opts_output("setuid=-1", "sharefs: invalid value for option setuid\n")
|
||||||
|
check_bad_opts_output("setgid=-1", "sharefs: invalid value for option setgid\n")
|
||||||
|
|
||||||
|
# Non-root setuid/setgid:
|
||||||
|
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||||
|
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||||
|
check_bad_opts_output("setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||||
|
check_bad_opts_output("mkdir", "sharefs: mkdir has no effect when not starting as root\n")
|
||||||
|
|
||||||
|
# Starting as root without setuid/setgid:
|
||||||
|
check_bad_opts_output("allow_other", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||||
|
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||||
|
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||||
|
|
||||||
|
# Make sure nothing actually got mounted:
|
||||||
|
machine.fail("umount /mnt")
|
||||||
|
machine.succeed("rmdir /mnt")
|
||||||
|
|
||||||
|
# Unprivileged mount/unmount:
|
||||||
|
machine.succeed("sudo -u alice -i mkdir /home/alice/{sdcard,persistent}")
|
||||||
|
machine.succeed("sudo -u alice -i sharefs -o source=/home/alice/persistent /home/alice/sdcard")
|
||||||
|
machine.succeed("sudo -u alice -i touch /home/alice/sdcard/check")
|
||||||
|
machine.succeed("sudo -u alice -i umount /home/alice/sdcard")
|
||||||
|
machine.succeed("sudo -u alice -i rm /home/alice/persistent/check")
|
||||||
|
machine.succeed("sudo -u alice -i rmdir /home/alice/{sdcard,persistent}")
|
||||||
|
|
||||||
|
# Benchmark sharefs:
|
||||||
|
machine.succeed("fs_mark -v -d /sdcard/fs_mark -l /tmp/fs_log.txt")
|
||||||
|
machine.copy_from_vm("/tmp/fs_log.txt", "")
|
||||||
|
|
||||||
|
# Check permissions:
|
||||||
|
machine.succeed("sudo -u sharefs touch /var/lib/hakurei/sdcard/fs_mark/.check")
|
||||||
|
machine.succeed("sudo -u sharefs rm /var/lib/hakurei/sdcard/fs_mark/.check")
|
||||||
|
machine.succeed("sudo -u alice rm -rf /sdcard/fs_mark")
|
||||||
|
machine.fail("ls /var/lib/hakurei/sdcard/fs_mark")
|
||||||
|
|
||||||
|
# Run hakurei tests on sharefs:
|
||||||
|
machine.succeed("sudo -u alice -i sharefs-workload-hakurei-tests")
|
||||||
@@ -59,6 +59,7 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (e *AutoEtcOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
|
||||||
func (e *AutoEtcOp) hostPath() *check.Absolute { return fhs.AbsEtc.Append(e.hostRel()) }
|
func (e *AutoEtcOp) hostPath() *check.Absolute { return fhs.AbsEtc.Append(e.hostRel()) }
|
||||||
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
|
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (r *AutoRootOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
|
||||||
func (r *AutoRootOp) Is(op Op) bool {
|
func (r *AutoRootOp) Is(op Op) bool {
|
||||||
vr, ok := op.(*AutoRootOp)
|
vr, ok := op.(*AutoRootOp)
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ func TestIsAutoRootBindable(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
var msg message.Msg
|
var msg message.Msg
|
||||||
if tc.log {
|
if tc.log {
|
||||||
msg = &kstub{nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
|
msg = &kstub{nil, nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
|
||||||
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),
|
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),
|
||||||
}})}
|
}})}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func NewAbs(pathname string) (*Absolute, error) {
|
|||||||
// MustAbs calls [NewAbs] and panics on error.
|
// MustAbs calls [NewAbs] and panics on error.
|
||||||
func MustAbs(pathname string) *Absolute {
|
func MustAbs(pathname string) *Absolute {
|
||||||
if a, err := NewAbs(pathname); err != nil {
|
if a, err := NewAbs(pathname); err != nil {
|
||||||
panic(err.Error())
|
panic(err)
|
||||||
} else {
|
} else {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import (
|
|||||||
. "hakurei.app/container/check"
|
. "hakurei.app/container/check"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// unsafeAbs returns check.Absolute on any string value.
|
||||||
|
//
|
||||||
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
|
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
|
||||||
func unsafeAbs(_ string) *Absolute
|
func unsafeAbs(pathname string) *Absolute
|
||||||
|
|
||||||
func TestAbsoluteError(t *testing.T) {
|
func TestAbsoluteError(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -82,9 +84,9 @@ func TestNewAbs(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
wantPanic := `path "etc" is not absolute`
|
wantPanic := &AbsoluteError{Pathname: "etc"}
|
||||||
|
|
||||||
if r := recover(); r != wantPanic {
|
if r := recover(); !reflect.DeepEqual(r, wantPanic) {
|
||||||
t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
|
t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ type (
|
|||||||
// Container represents a container environment being prepared or run.
|
// Container represents a container environment being prepared or run.
|
||||||
// None of [Container] methods are safe for concurrent use.
|
// None of [Container] methods are safe for concurrent use.
|
||||||
Container struct {
|
Container struct {
|
||||||
|
// Whether the container init should stay alive after its parent terminates.
|
||||||
|
AllowOrphan bool
|
||||||
// Cgroup fd, nil to disable.
|
// Cgroup fd, nil to disable.
|
||||||
Cgroup *int
|
Cgroup *int
|
||||||
// ExtraFiles passed through to initial process in the container,
|
// ExtraFiles passed through to initial process in the container,
|
||||||
@@ -252,8 +254,7 @@ func (p *Container) Start() error {
|
|||||||
}
|
}
|
||||||
p.cmd.Dir = fhs.Root
|
p.cmd.Dir = fhs.Root
|
||||||
p.cmd.SysProcAttr = &SysProcAttr{
|
p.cmd.SysProcAttr = &SysProcAttr{
|
||||||
Setsid: !p.RetainSession,
|
Setsid: !p.RetainSession,
|
||||||
Pdeathsig: SIGKILL,
|
|
||||||
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
|
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
|
||||||
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
|
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
|
||||||
|
|
||||||
@@ -268,6 +269,9 @@ func (p *Container) Start() error {
|
|||||||
|
|
||||||
UseCgroupFD: p.Cgroup != nil,
|
UseCgroupFD: p.Cgroup != nil,
|
||||||
}
|
}
|
||||||
|
if !p.AllowOrphan {
|
||||||
|
p.cmd.SysProcAttr.Pdeathsig = SIGKILL
|
||||||
|
}
|
||||||
if p.cmd.SysProcAttr.UseCgroupFD {
|
if p.cmd.SysProcAttr.UseCgroupFD {
|
||||||
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
|
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"hakurei.app/command"
|
"hakurei.app/command"
|
||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
"hakurei.app/container/check"
|
"hakurei.app/container/check"
|
||||||
|
"hakurei.app/container/fhs"
|
||||||
"hakurei.app/container/seccomp"
|
"hakurei.app/container/seccomp"
|
||||||
"hakurei.app/container/std"
|
"hakurei.app/container/std"
|
||||||
"hakurei.app/container/vfs"
|
"hakurei.app/container/vfs"
|
||||||
@@ -29,6 +30,45 @@ import (
|
|||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Note: this package requires cgo, which is unavailable in the Go playground.
|
||||||
|
func Example() {
|
||||||
|
// Must be called early if the current process starts containers.
|
||||||
|
container.TryArgv0(nil)
|
||||||
|
|
||||||
|
// Configure the container.
|
||||||
|
z := container.New(context.Background(), nil)
|
||||||
|
z.Hostname = "hakurei-example"
|
||||||
|
z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
|
||||||
|
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
|
||||||
|
// Bind / for demonstration.
|
||||||
|
z.Bind(fhs.AbsRoot, fhs.AbsRoot, 0)
|
||||||
|
if name, err := exec.LookPath("hostname"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
z.Path = check.MustAbs(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This completes the first stage of container setup and starts the container init process.
|
||||||
|
// The new process blocks until the Serve method is called.
|
||||||
|
if err := z.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This serves the setup payload to the container init process,
|
||||||
|
// starting the second stage of container setup.
|
||||||
|
if err := z.Serve(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must be called if the Start method succeeds.
|
||||||
|
if err := z.Wait(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output: hakurei-example
|
||||||
|
}
|
||||||
|
|
||||||
func TestStartError(t *testing.T) {
|
func TestStartError(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -722,12 +762,14 @@ func TestMain(m *testing.M) {
|
|||||||
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
|
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
|
||||||
msg := message.New(nil)
|
msg := message.New(nil)
|
||||||
msg.SwapVerbose(testing.Verbose())
|
msg.SwapVerbose(testing.Verbose())
|
||||||
|
executable := check.MustAbs(container.MustExecutable(msg))
|
||||||
|
|
||||||
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
|
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
|
||||||
c.Env = append(c.Env, envDoCheck+"=1")
|
c.Env = append(c.Env, envDoCheck+"=1")
|
||||||
c.Bind(check.MustAbs(os.Args[0]), absHelperInnerPath, 0)
|
c.Bind(executable, absHelperInnerPath, 0)
|
||||||
|
|
||||||
// in case test has cgo enabled
|
// in case test has cgo enabled
|
||||||
if entries, err := ldd.Exec(ctx, msg, os.Args[0]); err != nil {
|
if entries, err := ldd.Resolve(ctx, msg, executable); err != nil {
|
||||||
log.Fatalf("ldd: %v", err)
|
log.Fatalf("ldd: %v", err)
|
||||||
} else {
|
} else {
|
||||||
*libPaths = ldd.Path(entries)
|
*libPaths = ldd.Path(entries)
|
||||||
|
|||||||
@@ -162,7 +162,8 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
wait4signal := make(chan struct{})
|
wait4signal := make(chan struct{})
|
||||||
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
|
lockNotify := make(chan struct{})
|
||||||
|
k := &kstub{wait4signal, lockNotify, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, lockNotify, s} }, tc.want)}
|
||||||
defer stub.HandleExit(t)
|
defer stub.HandleExit(t)
|
||||||
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
|
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
|
||||||
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
|
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
|
||||||
@@ -200,8 +201,8 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
k := &kstub{nil, stub.New(t,
|
k := &kstub{nil, nil, stub.New(t,
|
||||||
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
|
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, nil, s} },
|
||||||
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
|
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
|
||||||
)}
|
)}
|
||||||
state := &setupState{Params: tc.params, Msg: k}
|
state := &setupState{Params: tc.params, Msg: k}
|
||||||
@@ -322,12 +323,19 @@ const (
|
|||||||
|
|
||||||
type kstub struct {
|
type kstub struct {
|
||||||
wait4signal chan struct{}
|
wait4signal chan struct{}
|
||||||
|
lockNotify chan struct{}
|
||||||
*stub.Stub[syscallDispatcher]
|
*stub.Stub[syscallDispatcher]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
|
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
|
||||||
|
|
||||||
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") }
|
func (k *kstub) lockOSThread() {
|
||||||
|
k.Helper()
|
||||||
|
expect := k.Expects("lockOSThread")
|
||||||
|
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
|
||||||
|
<-k.lockNotify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (k *kstub) setPtracer(pid uintptr) error {
|
func (k *kstub) setPtracer(pid uintptr) error {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
@@ -472,6 +480,10 @@ func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
|
|||||||
k.FailNow()
|
k.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
|
||||||
|
defer close(k.lockNotify)
|
||||||
|
}
|
||||||
|
|
||||||
// export channel for external instrumentation
|
// export channel for external instrumentation
|
||||||
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
|
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
|
||||||
chanf(c)
|
chanf(c)
|
||||||
@@ -759,7 +771,8 @@ func (k *kstub) checkMsg(msg message.Msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
|
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
|
||||||
func (k *kstub) IsVerbose() bool { panic("unreachable") }
|
|
||||||
|
func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
|
||||||
|
|
||||||
func (k *kstub) SwapVerbose(verbose bool) bool {
|
func (k *kstub) SwapVerbose(verbose bool) bool {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
|
|||||||
@@ -7,31 +7,36 @@ import (
|
|||||||
|
|
||||||
"hakurei.app/container/check"
|
"hakurei.app/container/check"
|
||||||
"hakurei.app/container/vfs"
|
"hakurei.app/container/vfs"
|
||||||
|
"hakurei.app/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
// messageFromError returns a printable error message for a supported concrete type.
|
// messageFromError returns a printable error message for a supported concrete type.
|
||||||
func messageFromError(err error) (string, bool) {
|
func messageFromError(err error) (m string, ok bool) {
|
||||||
if m, ok := messagePrefixP[MountError]("cannot ", err); ok {
|
if m, ok = messagePrefixP[MountError]("cannot ", err); ok {
|
||||||
return m, ok
|
return
|
||||||
}
|
}
|
||||||
if m, ok := messagePrefixP[os.PathError]("cannot ", err); ok {
|
if m, ok = messagePrefixP[os.PathError]("cannot ", err); ok {
|
||||||
return m, ok
|
return
|
||||||
}
|
}
|
||||||
if m, ok := messagePrefixP[check.AbsoluteError]("", err); ok {
|
if m, ok = messagePrefixP[check.AbsoluteError](zeroString, err); ok {
|
||||||
return m, ok
|
return
|
||||||
}
|
}
|
||||||
if m, ok := messagePrefix[OpRepeatError]("", err); ok {
|
if m, ok = messagePrefix[OpRepeatError](zeroString, err); ok {
|
||||||
return m, ok
|
return
|
||||||
}
|
}
|
||||||
if m, ok := messagePrefix[OpStateError]("", err); ok {
|
if m, ok = messagePrefix[OpStateError](zeroString, err); ok {
|
||||||
return m, ok
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if m, ok := messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
|
if m, ok = messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
|
||||||
return m, ok
|
return
|
||||||
}
|
}
|
||||||
if m, ok := messagePrefix[TmpfsSizeError]("", err); ok {
|
if m, ok = messagePrefix[TmpfsSizeError](zeroString, err); ok {
|
||||||
return m, ok
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if m, ok = message.GetMessage(err); ok {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return zeroString, false
|
return zeroString, false
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import (
|
|||||||
|
|
||||||
/* constants in this file bypass abs check, be extremely careful when changing them! */
|
/* constants in this file bypass abs check, be extremely careful when changing them! */
|
||||||
|
|
||||||
|
// unsafeAbs returns check.Absolute on any string value.
|
||||||
|
//
|
||||||
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
|
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
|
||||||
func unsafeAbs(_ string) *check.Absolute
|
func unsafeAbs(pathname string) *check.Absolute
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// AbsRoot is [Root] as [check.Absolute].
|
// AbsRoot is [Root] as [check.Absolute].
|
||||||
@@ -34,6 +36,8 @@ var (
|
|||||||
|
|
||||||
// AbsDev is [Dev] as [check.Absolute].
|
// AbsDev is [Dev] as [check.Absolute].
|
||||||
AbsDev = unsafeAbs(Dev)
|
AbsDev = unsafeAbs(Dev)
|
||||||
|
// AbsDevShm is [DevShm] as [check.Absolute].
|
||||||
|
AbsDevShm = unsafeAbs(DevShm)
|
||||||
// AbsProc is [Proc] as [check.Absolute].
|
// AbsProc is [Proc] as [check.Absolute].
|
||||||
AbsProc = unsafeAbs(Proc)
|
AbsProc = unsafeAbs(Proc)
|
||||||
// AbsSys is [Sys] as [check.Absolute].
|
// AbsSys is [Sys] as [check.Absolute].
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ const (
|
|||||||
|
|
||||||
// Dev points to the root directory for device nodes.
|
// Dev points to the root directory for device nodes.
|
||||||
Dev = "/dev/"
|
Dev = "/dev/"
|
||||||
|
// DevShm is the place for POSIX shared memory segments, as created via shm_open(3).
|
||||||
|
DevShm = "/dev/shm/"
|
||||||
// Proc points to a virtual kernel file system exposing the process list and other functionality.
|
// Proc points to a virtual kernel file system exposing the process list and other functionality.
|
||||||
Proc = "/proc/"
|
Proc = "/proc/"
|
||||||
// ProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
|
// ProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@@ -9,6 +10,8 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
. "syscall"
|
. "syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,24 +21,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
/* intermediate tmpfs mount point
|
/* intermediateHostPath is the pathname of the intermediate tmpfs mount point.
|
||||||
|
|
||||||
this path might seem like a weird choice, however there are many good reasons to use it:
|
This path might seem like a weird choice, however there are many good reasons to use it:
|
||||||
- the contents of this path is never exposed to the container:
|
- The contents of this path is never exposed to the container:
|
||||||
the tmpfs root established here effectively becomes anonymous after pivot_root
|
The tmpfs root established here effectively becomes anonymous after pivot_root.
|
||||||
- it is safe to assume this path exists and is a directory:
|
- It is safe to assume this path exists and is a directory:
|
||||||
this program will not work correctly without a proper /proc and neither will most others
|
This program will not work correctly without a proper /proc and neither will most others.
|
||||||
- this path belongs to the container init:
|
- This path belongs to the container init:
|
||||||
the container init is not any more privileged or trusted than the rest of the container
|
The container init is not any more privileged or trusted than the rest of the container.
|
||||||
- this path is only accessible by init and root:
|
- This path is only accessible by init and root:
|
||||||
the container init sets SUID_DUMP_DISABLE and terminates if that fails;
|
The container init sets SUID_DUMP_DISABLE and terminates if that fails.
|
||||||
|
|
||||||
it should be noted that none of this should become relevant at any point since the resulting
|
It should be noted that none of this should become relevant at any point since the resulting
|
||||||
intermediate root tmpfs should be effectively anonymous */
|
intermediate root tmpfs should be effectively anonymous. */
|
||||||
intermediateHostPath = fhs.Proc + "self/fd"
|
intermediateHostPath = fhs.Proc + "self/fd"
|
||||||
|
|
||||||
// setup params file descriptor
|
// setupEnv is the name of the environment variable holding the string representation of
|
||||||
|
// the read end file descriptor of the setup params pipe.
|
||||||
setupEnv = "HAKUREI_SETUP"
|
setupEnv = "HAKUREI_SETUP"
|
||||||
|
|
||||||
|
// exitUnexpectedWait4 is the exit code if wait4 returns an unexpected errno.
|
||||||
|
exitUnexpectedWait4 = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -49,6 +56,8 @@ type (
|
|||||||
early(state *setupState, k syscallDispatcher) error
|
early(state *setupState, k syscallDispatcher) error
|
||||||
// apply is called in intermediate root.
|
// apply is called in intermediate root.
|
||||||
apply(state *setupState, k syscallDispatcher) error
|
apply(state *setupState, k syscallDispatcher) error
|
||||||
|
// late is called right before starting the initial process.
|
||||||
|
late(state *setupState, k syscallDispatcher) error
|
||||||
|
|
||||||
// prefix returns a log message prefix, and whether this Op prints no identifying message on its own.
|
// prefix returns a log message prefix, and whether this Op prints no identifying message on its own.
|
||||||
prefix() (string, bool)
|
prefix() (string, bool)
|
||||||
@@ -61,11 +70,29 @@ type (
|
|||||||
// setupState persists context between Ops.
|
// setupState persists context between Ops.
|
||||||
setupState struct {
|
setupState struct {
|
||||||
nonrepeatable uintptr
|
nonrepeatable uintptr
|
||||||
|
|
||||||
|
// Whether early reaping has concluded. Must only be accessed in the wait4 loop.
|
||||||
|
processConcluded bool
|
||||||
|
// Process to syscall.WaitStatus populated in the wait4 loop. Freed after early reaping concludes.
|
||||||
|
process map[int]WaitStatus
|
||||||
|
// Synchronises access to process.
|
||||||
|
processMu sync.RWMutex
|
||||||
|
|
||||||
*Params
|
*Params
|
||||||
|
context.Context
|
||||||
message.Msg
|
message.Msg
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// terminated returns whether the specified pid has been reaped, and its
|
||||||
|
// syscall.WaitStatus if it had. This is only usable by [Op].
|
||||||
|
func (state *setupState) terminated(pid int) (wstatus WaitStatus, ok bool) {
|
||||||
|
state.processMu.RLock()
|
||||||
|
wstatus, ok = state.process[pid]
|
||||||
|
state.processMu.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Grow grows the slice Ops points to using [slices.Grow].
|
// Grow grows the slice Ops points to using [slices.Grow].
|
||||||
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
|
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
|
||||||
|
|
||||||
@@ -180,7 +207,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err))
|
k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
state := &setupState{Params: ¶ms.Params, Msg: msg}
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
state := &setupState{process: make(map[int]WaitStatus), Params: ¶ms.Params, Msg: msg, Context: ctx}
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
/* early is called right before pivot_root into intermediate root;
|
/* early is called right before pivot_root into intermediate root;
|
||||||
this step is mostly for gathering information that would otherwise be difficult to obtain
|
this step is mostly for gathering information that would otherwise be difficult to obtain
|
||||||
@@ -330,6 +359,97 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
}
|
}
|
||||||
k.umask(oldmask)
|
k.umask(oldmask)
|
||||||
|
|
||||||
|
// winfo represents an exited process from wait4.
|
||||||
|
type winfo struct {
|
||||||
|
wpid int
|
||||||
|
wstatus WaitStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// info is closed as the wait4 thread terminates
|
||||||
|
// when there are no longer any processes left to reap
|
||||||
|
info := make(chan winfo, 1)
|
||||||
|
|
||||||
|
// whether initial process has started
|
||||||
|
var initialProcessStarted atomic.Bool
|
||||||
|
|
||||||
|
k.new(func(k syscallDispatcher) {
|
||||||
|
k.lockOSThread()
|
||||||
|
|
||||||
|
wait4:
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
wpid = -2
|
||||||
|
wstatus WaitStatus
|
||||||
|
|
||||||
|
// whether initial process has started
|
||||||
|
started bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// keep going until no child process is left
|
||||||
|
for wpid != -1 {
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if wpid != -2 {
|
||||||
|
if !state.processConcluded {
|
||||||
|
state.processMu.Lock()
|
||||||
|
if state.process == nil {
|
||||||
|
// early reaping has already concluded at this point
|
||||||
|
state.processConcluded = true
|
||||||
|
info <- winfo{wpid, wstatus}
|
||||||
|
} else {
|
||||||
|
// initial process has not yet been created, and the
|
||||||
|
// info channel is not yet being received from
|
||||||
|
state.process[wpid] = wstatus
|
||||||
|
}
|
||||||
|
state.processMu.Unlock()
|
||||||
|
} else {
|
||||||
|
info <- winfo{wpid, wstatus}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !started {
|
||||||
|
started = initialProcessStarted.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = EINTR
|
||||||
|
for errors.Is(err, EINTR) {
|
||||||
|
wpid, err = k.wait4(-1, &wstatus, 0, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, ECHILD) {
|
||||||
|
k.printf(msg, "unexpected wait4 response: %v", err)
|
||||||
|
} else if !started {
|
||||||
|
// initial process has not yet been reached and all daemons
|
||||||
|
// terminated or none were started in the first place
|
||||||
|
time.Sleep(500 * time.Microsecond)
|
||||||
|
goto wait4
|
||||||
|
}
|
||||||
|
|
||||||
|
close(info)
|
||||||
|
})
|
||||||
|
|
||||||
|
// called right before startup of initial process, all state changes to the
|
||||||
|
// current process is prohibited during late
|
||||||
|
for i, op := range *params.Ops {
|
||||||
|
// ops already checked during early setup
|
||||||
|
if err := op.late(state, k); err != nil {
|
||||||
|
if m, ok := messageFromError(err); ok {
|
||||||
|
k.fatal(msg, m)
|
||||||
|
} else if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
k.fatalf(msg, "%s deadline exceeded", op.String())
|
||||||
|
} else {
|
||||||
|
k.fatalf(msg, "cannot complete op at index %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// early reaping has concluded, this must happen before initial process is created
|
||||||
|
state.processMu.Lock()
|
||||||
|
state.process = nil
|
||||||
|
state.processMu.Unlock()
|
||||||
|
|
||||||
if err := closeSetup(); err != nil {
|
if err := closeSetup(); err != nil {
|
||||||
k.fatalf(msg, "cannot close setup pipe: %v", err)
|
k.fatalf(msg, "cannot close setup pipe: %v", err)
|
||||||
}
|
}
|
||||||
@@ -341,50 +461,11 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
cmd.ExtraFiles = extraFiles
|
cmd.ExtraFiles = extraFiles
|
||||||
cmd.Dir = params.Dir.String()
|
cmd.Dir = params.Dir.String()
|
||||||
|
|
||||||
msg.Verbosef("starting initial program %s", params.Path)
|
msg.Verbosef("starting initial process %s", params.Path)
|
||||||
if err := k.start(cmd); err != nil {
|
if err := k.start(cmd); err != nil {
|
||||||
k.fatalf(msg, "%v", err)
|
k.fatalf(msg, "%v", err)
|
||||||
}
|
}
|
||||||
|
initialProcessStarted.Store(true)
|
||||||
type winfo struct {
|
|
||||||
wpid int
|
|
||||||
wstatus WaitStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
// info is closed as the wait4 thread terminates
|
|
||||||
// when there are no longer any processes left to reap
|
|
||||||
info := make(chan winfo, 1)
|
|
||||||
|
|
||||||
k.new(func(k syscallDispatcher) {
|
|
||||||
k.lockOSThread()
|
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
wpid = -2
|
|
||||||
wstatus WaitStatus
|
|
||||||
)
|
|
||||||
|
|
||||||
// keep going until no child process is left
|
|
||||||
for wpid != -1 {
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if wpid != -2 {
|
|
||||||
info <- winfo{wpid, wstatus}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = EINTR
|
|
||||||
for errors.Is(err, EINTR) {
|
|
||||||
wpid, err = k.wait4(-1, &wstatus, 0, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !errors.Is(err, ECHILD) {
|
|
||||||
k.printf(msg, "unexpected wait4 response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
close(info)
|
|
||||||
})
|
|
||||||
|
|
||||||
// handle signals to dump withheld messages
|
// handle signals to dump withheld messages
|
||||||
sig := make(chan os.Signal, 2)
|
sig := make(chan os.Signal, 2)
|
||||||
@@ -394,7 +475,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
// closed after residualProcessTimeout has elapsed after initial process death
|
// closed after residualProcessTimeout has elapsed after initial process death
|
||||||
timeout := make(chan struct{})
|
timeout := make(chan struct{})
|
||||||
|
|
||||||
r := 2
|
r := exitUnexpectedWait4
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case s := <-sig:
|
case s := <-sig:
|
||||||
@@ -426,6 +507,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if w.wpid == cmd.Process.Pid {
|
if w.wpid == cmd.Process.Pid {
|
||||||
|
// cancel Op context early
|
||||||
|
cancel()
|
||||||
|
|
||||||
// start timeout early
|
// start timeout early
|
||||||
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
|
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
|
||||||
|
|
||||||
|
|||||||
@@ -1983,11 +1983,20 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||||
|
call("New", stub.ExpectArgs{}, nil, nil),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(13)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(13)}}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, nil, stub.UniqueError(12)),
|
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, nil, stub.UniqueError(12)),
|
||||||
call("fatalf", stub.ExpectArgs{"%v", []any{stub.UniqueError(12)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"%v", []any{stub.UniqueError(12)}}, nil, nil),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* wait4 */
|
||||||
|
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||||
|
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||||
|
|
||||||
|
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
||||||
|
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
|
||||||
|
}}},
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"lowlastcap signaled cancel forward error", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
|
{"lowlastcap signaled cancel forward error", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
|
||||||
@@ -2062,11 +2071,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||||
|
call("New", stub.ExpectArgs{}, nil, nil),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(10)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(10)}}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||||
call("New", stub.ExpectArgs{}, nil, nil),
|
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
|
||||||
call("verbose", stub.ExpectArgs{[]any{"forwarding context cancellation"}}, nil, nil),
|
call("verbose", stub.ExpectArgs{[]any{"forwarding context cancellation"}}, nil, nil),
|
||||||
// magicWait4Signal as ret causes wait4 stub to unblock
|
// magicWait4Signal as ret causes wait4 stub to unblock
|
||||||
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", os.Interrupt}, magicWait4Signal, stub.UniqueError(9)),
|
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", os.Interrupt}, magicWait4Signal, stub.UniqueError(9)),
|
||||||
@@ -2081,7 +2090,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
|
|
||||||
/* wait4 */
|
/* wait4 */
|
||||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||||
|
|
||||||
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
|
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
|
||||||
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
|
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
|
||||||
@@ -2162,11 +2171,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||||
|
call("New", stub.ExpectArgs{}, nil, nil),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||||
call("New", stub.ExpectArgs{}, nil, nil),
|
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
|
||||||
call("verbosef", stub.ExpectArgs{"got %s, forwarding to initial process", []any{"quit"}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"got %s, forwarding to initial process", []any{"quit"}}, nil, nil),
|
||||||
// magicWait4Signal as ret causes wait4 stub to unblock
|
// magicWait4Signal as ret causes wait4 stub to unblock
|
||||||
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", syscall.SIGQUIT}, magicWait4Signal, stub.UniqueError(0xfe)),
|
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", syscall.SIGQUIT}, magicWait4Signal, stub.UniqueError(0xfe)),
|
||||||
@@ -2181,7 +2190,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
|
|
||||||
/* wait4 */
|
/* wait4 */
|
||||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||||
|
|
||||||
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
|
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
|
||||||
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
|
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
|
||||||
@@ -2262,11 +2271,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||||
|
call("New", stub.ExpectArgs{}, nil, nil),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||||
call("New", stub.ExpectArgs{}, nil, nil),
|
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
|
||||||
call("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil),
|
||||||
call("beforeExit", stub.ExpectArgs{}, nil, nil),
|
call("beforeExit", stub.ExpectArgs{}, nil, nil),
|
||||||
call("exit", stub.ExpectArgs{0}, nil, nil),
|
call("exit", stub.ExpectArgs{0}, nil, nil),
|
||||||
@@ -2274,7 +2283,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
|
|
||||||
/* wait4 */
|
/* wait4 */
|
||||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||||
|
|
||||||
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
||||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
|
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
|
||||||
@@ -2353,11 +2362,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||||
|
call("New", stub.ExpectArgs{}, nil, nil),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(5)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(5)}}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||||
call("New", stub.ExpectArgs{}, nil, nil),
|
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
|
||||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
|
||||||
@@ -2368,7 +2377,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
|
|
||||||
/* wait4 */
|
/* wait4 */
|
||||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||||
|
|
||||||
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil}, 0xbad, nil),
|
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil}, 0xbad, nil),
|
||||||
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
||||||
@@ -2448,11 +2457,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||||
|
call("New", stub.ExpectArgs{}, nil, nil),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(3)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(3)}}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||||
call("New", stub.ExpectArgs{}, nil, nil),
|
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
|
||||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
|
||||||
@@ -2462,7 +2471,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
|
|
||||||
/* wait4 */
|
/* wait4 */
|
||||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||||
|
|
||||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||||
@@ -2586,11 +2595,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||||
|
call("New", stub.ExpectArgs{}, nil, nil),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||||
call("New", stub.ExpectArgs{}, nil, nil),
|
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
|
||||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"initial process exited with code %d", []any{1}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"initial process exited with code %d", []any{1}}, nil, nil),
|
||||||
@@ -2600,7 +2609,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
|
|
||||||
/* wait4 */
|
/* wait4 */
|
||||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||||
|
|
||||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||||
@@ -2728,11 +2737,11 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
call("newFile", stub.ExpectArgs{uintptr(11), "extra file 1"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(11), "extra file 1"}, (*os.File)(nil), nil),
|
||||||
call("newFile", stub.ExpectArgs{uintptr(12), "extra file 2"}, (*os.File)(nil), nil),
|
call("newFile", stub.ExpectArgs{uintptr(12), "extra file 2"}, (*os.File)(nil), nil),
|
||||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||||
|
call("New", stub.ExpectArgs{}, nil, nil),
|
||||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(0)}}, nil, nil),
|
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(0)}}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/bin/zsh")}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/bin/zsh")}}, nil, nil),
|
||||||
call("start", stub.ExpectArgs{"/bin/zsh", []string{"zsh", "-c", "exec vim"}, []string{"DISPLAY=:0"}, "/.hakurei"}, &os.Process{Pid: 0xcafe}, nil),
|
call("start", stub.ExpectArgs{"/bin/zsh", []string{"zsh", "-c", "exec vim"}, []string{"DISPLAY=:0"}, "/.hakurei"}, &os.Process{Pid: 0xcafe}, nil),
|
||||||
call("New", stub.ExpectArgs{}, nil, nil),
|
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
|
||||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||||
@@ -2743,7 +2752,7 @@ func TestInitEntrypoint(t *testing.T) {
|
|||||||
|
|
||||||
/* wait4 */
|
/* wait4 */
|
||||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||||
|
|
||||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
}
|
}
|
||||||
return k.bindMount(state, source, target, flags)
|
return k.bindMount(state, source, target, flags)
|
||||||
}
|
}
|
||||||
|
func (b *BindMountOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
|
||||||
func (b *BindMountOp) Is(op Op) bool {
|
func (b *BindMountOp) Is(op Op) bool {
|
||||||
vb, ok := op.(*BindMountOp)
|
vb, ok := op.(*BindMountOp)
|
||||||
|
|||||||
134
container/initdaemon.go
Normal file
134
container/initdaemon.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hakurei.app/container/check"
|
||||||
|
"hakurei.app/container/fhs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { gob.Register(new(DaemonOp)) }
|
||||||
|
|
||||||
|
const (
|
||||||
|
// daemonTimeout is the duration a [DaemonOp] is allowed to block before the
|
||||||
|
// [DaemonOp.Target] marker becomes available.
|
||||||
|
daemonTimeout = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Daemon appends an [Op] that starts a daemon in the container and blocks until
|
||||||
|
// [DaemonOp.Target] appears.
|
||||||
|
func (f *Ops) Daemon(target, path *check.Absolute, args ...string) *Ops {
|
||||||
|
*f = append(*f, &DaemonOp{target, path, args})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// DaemonOp starts a daemon in the container and blocks until Target appears.
|
||||||
|
type DaemonOp struct {
|
||||||
|
// Pathname indicating readiness of daemon.
|
||||||
|
Target *check.Absolute
|
||||||
|
// Absolute pathname passed to [exec.Cmd].
|
||||||
|
Path *check.Absolute
|
||||||
|
// Arguments (excl. first) passed to [exec.Cmd].
|
||||||
|
Args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// earlyTerminationError is returned by [DaemonOp] when a daemon terminates
|
||||||
|
// before [DaemonOp.Target] appears.
|
||||||
|
type earlyTerminationError struct {
|
||||||
|
// Returned by [DaemonOp.String].
|
||||||
|
op string
|
||||||
|
// Copied from wait4 loop.
|
||||||
|
wstatus syscall.WaitStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *earlyTerminationError) Error() string {
|
||||||
|
res := ""
|
||||||
|
switch {
|
||||||
|
case e.wstatus.Exited():
|
||||||
|
res = "exit status " + strconv.Itoa(e.wstatus.ExitStatus())
|
||||||
|
case e.wstatus.Signaled():
|
||||||
|
res = "signal: " + e.wstatus.Signal().String()
|
||||||
|
case e.wstatus.Stopped():
|
||||||
|
res = "stop signal: " + e.wstatus.StopSignal().String()
|
||||||
|
if e.wstatus.StopSignal() == syscall.SIGTRAP && e.wstatus.TrapCause() != 0 {
|
||||||
|
res += " (trap " + strconv.Itoa(e.wstatus.TrapCause()) + ")"
|
||||||
|
}
|
||||||
|
case e.wstatus.Continued():
|
||||||
|
res = "continued"
|
||||||
|
}
|
||||||
|
if e.wstatus.CoreDump() {
|
||||||
|
res += " (core dumped)"
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *earlyTerminationError) Message() string { return e.op + " " + e.Error() }
|
||||||
|
|
||||||
|
func (d *DaemonOp) Valid() bool { return d != nil && d.Target != nil && d.Path != nil }
|
||||||
|
func (d *DaemonOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
func (d *DaemonOp) apply(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
func (d *DaemonOp) late(state *setupState, k syscallDispatcher) error {
|
||||||
|
cmd := exec.CommandContext(state.Context, d.Path.String(), d.Args...)
|
||||||
|
cmd.Env = state.Env
|
||||||
|
cmd.Dir = fhs.Root
|
||||||
|
if state.IsVerbose() {
|
||||||
|
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||||
|
}
|
||||||
|
// WaitDelay: left unset because lifetime is bound by AdoptWaitDelay on cancellation
|
||||||
|
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
|
||||||
|
|
||||||
|
state.Verbosef("starting %s", d.String())
|
||||||
|
if err := k.start(cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline := time.Now().Add(daemonTimeout)
|
||||||
|
var wstatusErr error
|
||||||
|
|
||||||
|
for {
|
||||||
|
if _, err := k.stat(d.Target.String()); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
_ = k.signal(cmd, os.Kill)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
_ = k.signal(cmd, os.Kill)
|
||||||
|
return context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
if wstatusErr != nil {
|
||||||
|
return wstatusErr
|
||||||
|
}
|
||||||
|
if wstatus, ok := state.terminated(cmd.Process.Pid); ok {
|
||||||
|
// check once again: process could have satisfied Target between stat and the lookup
|
||||||
|
wstatusErr = &earlyTerminationError{d.String(), wstatus}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Microsecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Verbosef("daemon process %d ready", cmd.Process.Pid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DaemonOp) Is(op Op) bool {
|
||||||
|
vd, ok := op.(*DaemonOp)
|
||||||
|
return ok && d.Valid() && vd.Valid() &&
|
||||||
|
d.Target.Is(vd.Target) && d.Path.Is(vd.Path) &&
|
||||||
|
slices.Equal(d.Args, vd.Args)
|
||||||
|
}
|
||||||
|
func (*DaemonOp) prefix() (string, bool) { return zeroString, false }
|
||||||
|
func (d *DaemonOp) String() string { return fmt.Sprintf("daemon providing %q", d.Target) }
|
||||||
127
container/initdaemon_test.go
Normal file
127
container/initdaemon_test.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/check"
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
"hakurei.app/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEarlyTerminationError(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
want string
|
||||||
|
msg string
|
||||||
|
}{
|
||||||
|
{"exited", &earlyTerminationError{
|
||||||
|
`daemon providing "/run/user/1971/pulse/native"`, 127 << 8,
|
||||||
|
}, "exit status 127", `daemon providing "/run/user/1971/pulse/native" exit status 127`},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if got := tc.err.Error(); got != tc.want {
|
||||||
|
t.Errorf("Error: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
if got := tc.err.(message.Error).Message(); got != tc.msg {
|
||||||
|
t.Errorf("Message: %s, want %s", got, tc.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDaemonOp(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
checkSimple(t, "DaemonOp.late", []simpleTestCase{
|
||||||
|
{"success", func(k *kstub) error {
|
||||||
|
state := setupState{Params: &Params{Env: []string{"\x00"}}, Context: t.Context(), Msg: k}
|
||||||
|
return (&DaemonOp{
|
||||||
|
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||||
|
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
Args: []string{"-v"},
|
||||||
|
}).late(&state, k)
|
||||||
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
|
call("isVerbose", stub.ExpectArgs{}, true, nil),
|
||||||
|
call("verbosef", stub.ExpectArgs{"starting %s", []any{`daemon providing "/run/user/1971/pulse/native"`}}, nil, nil),
|
||||||
|
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/pipewire-pulse", []string{"/run/current-system/sw/bin/pipewire-pulse", "-v"}, []string{"\x00"}, "/"}, &os.Process{Pid: 0xcafe}, nil),
|
||||||
|
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
||||||
|
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
||||||
|
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
||||||
|
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), nil),
|
||||||
|
call("verbosef", stub.ExpectArgs{"daemon process %d ready", []any{0xcafe}}, nil, nil),
|
||||||
|
}}, nil},
|
||||||
|
})
|
||||||
|
|
||||||
|
checkOpsValid(t, []opValidTestCase{
|
||||||
|
{"nil", (*DaemonOp)(nil), false},
|
||||||
|
{"zero", new(DaemonOp), false},
|
||||||
|
{"valid", &DaemonOp{
|
||||||
|
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||||
|
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
Args: []string{"-v"},
|
||||||
|
}, true},
|
||||||
|
})
|
||||||
|
|
||||||
|
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||||
|
{"pipewire-pulse", new(Ops).Daemon(
|
||||||
|
check.MustAbs("/run/user/1971/pulse/native"),
|
||||||
|
check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"), "-v",
|
||||||
|
), Ops{
|
||||||
|
&DaemonOp{
|
||||||
|
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||||
|
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
Args: []string{"-v"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
checkOpIs(t, []opIsTestCase{
|
||||||
|
{"zero", new(DaemonOp), new(DaemonOp), false},
|
||||||
|
|
||||||
|
{"args differs", &DaemonOp{
|
||||||
|
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||||
|
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
Args: []string{"-v"},
|
||||||
|
}, &DaemonOp{
|
||||||
|
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||||
|
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
}, false},
|
||||||
|
|
||||||
|
{"path differs", &DaemonOp{
|
||||||
|
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||||
|
Path: check.MustAbs("/run/current-system/sw/bin/pipewire"),
|
||||||
|
}, &DaemonOp{
|
||||||
|
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||||
|
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
}, false},
|
||||||
|
|
||||||
|
{"target differs", &DaemonOp{
|
||||||
|
Target: check.MustAbs("/run/user/65534/pulse/native"),
|
||||||
|
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
}, &DaemonOp{
|
||||||
|
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||||
|
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
}, false},
|
||||||
|
|
||||||
|
{"equals", &DaemonOp{
|
||||||
|
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||||
|
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
}, &DaemonOp{
|
||||||
|
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||||
|
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
}, true},
|
||||||
|
})
|
||||||
|
|
||||||
|
checkOpMeta(t, []opMetaTestCase{
|
||||||
|
{"pipewire-pulse", &DaemonOp{
|
||||||
|
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||||
|
}, zeroString, `daemon providing "/run/user/1971/pulse/native"`},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -126,6 +126,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
}
|
}
|
||||||
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
|
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
|
||||||
}
|
}
|
||||||
|
func (d *MountDevOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
|
||||||
func (d *MountDevOp) Is(op Op) bool {
|
func (d *MountDevOp) Is(op Op) bool {
|
||||||
vd, ok := op.(*MountDevOp)
|
vd, ok := op.(*MountDevOp)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil }
|
|||||||
func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error {
|
func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||||
return k.mkdirAll(toSysroot(m.Path.String()), m.Perm)
|
return k.mkdirAll(toSysroot(m.Path.String()), m.Perm)
|
||||||
}
|
}
|
||||||
|
func (m *MkdirOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
|
||||||
func (m *MkdirOp) Is(op Op) bool {
|
func (m *MkdirOp) Is(op Op) bool {
|
||||||
vm, ok := op.(*MkdirOp)
|
vm, ok := op.(*MkdirOp)
|
||||||
|
|||||||
@@ -205,6 +205,8 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption))
|
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *MountOverlayOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
|
||||||
func (o *MountOverlayOp) Is(op Op) bool {
|
func (o *MountOverlayOp) Is(op Op) bool {
|
||||||
vo, ok := op.(*MountOverlayOp)
|
vo, ok := op.(*MountOverlayOp)
|
||||||
return ok && o.Valid() && vo.Valid() &&
|
return ok && o.Valid() && vo.Valid() &&
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (t *TmpfileOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
|
||||||
func (t *TmpfileOp) Is(op Op) bool {
|
func (t *TmpfileOp) Is(op Op) bool {
|
||||||
vt, ok := op.(*TmpfileOp)
|
vt, ok := op.(*TmpfileOp)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
}
|
}
|
||||||
return k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString)
|
return k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString)
|
||||||
}
|
}
|
||||||
|
func (p *MountProcOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
|
||||||
func (p *MountProcOp) Is(op Op) bool {
|
func (p *MountProcOp) Is(op Op) bool {
|
||||||
vp, ok := op.(*MountProcOp)
|
vp, ok := op.(*MountProcOp)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
|
|||||||
func (r *RemountOp) apply(state *setupState, k syscallDispatcher) error {
|
func (r *RemountOp) apply(state *setupState, k syscallDispatcher) error {
|
||||||
return k.remount(state, toSysroot(r.Target.String()), r.Flags)
|
return k.remount(state, toSysroot(r.Target.String()), r.Flags)
|
||||||
}
|
}
|
||||||
|
func (r *RemountOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
|
||||||
func (r *RemountOp) Is(op Op) bool {
|
func (r *RemountOp) Is(op Op) bool {
|
||||||
vr, ok := op.(*RemountOp)
|
vr, ok := op.(*RemountOp)
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
return k.symlink(l.LinkName, target)
|
return k.symlink(l.LinkName, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *SymlinkOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
|
||||||
func (l *SymlinkOp) Is(op Op) bool {
|
func (l *SymlinkOp) Is(op Op) bool {
|
||||||
vl, ok := op.(*SymlinkOp)
|
vl, ok := op.(*SymlinkOp)
|
||||||
return ok && l.Valid() && vl.Valid() &&
|
return ok && l.Valid() && vl.Valid() &&
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error {
|
|||||||
}
|
}
|
||||||
return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
|
return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
|
||||||
}
|
}
|
||||||
|
func (t *MountTmpfsOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||||
|
|
||||||
func (t *MountTmpfsOp) Is(op Op) bool {
|
func (t *MountTmpfsOp) Is(op Op) bool {
|
||||||
vt, ok := op.(*MountTmpfsOp)
|
vt, ok := op.(*MountTmpfsOp)
|
||||||
|
|||||||
@@ -9,136 +9,130 @@
|
|||||||
|
|
||||||
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
|
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
|
||||||
|
|
||||||
int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
|
int32_t hakurei_scmp_make_filter(
|
||||||
uint32_t arch, uint32_t multiarch,
|
int *ret_p, uintptr_t allocate_p,
|
||||||
struct hakurei_syscall_rule *rules,
|
uint32_t arch, uint32_t multiarch,
|
||||||
size_t rules_sz, hakurei_export_flag flags) {
|
struct hakurei_syscall_rule *rules,
|
||||||
int i;
|
size_t rules_sz, hakurei_export_flag flags) {
|
||||||
int last_allowed_family;
|
int i;
|
||||||
int disallowed;
|
int last_allowed_family;
|
||||||
struct hakurei_syscall_rule *rule;
|
int disallowed;
|
||||||
void *buf;
|
struct hakurei_syscall_rule *rule;
|
||||||
size_t len = 0;
|
void *buf;
|
||||||
|
size_t len = 0;
|
||||||
|
|
||||||
int32_t res = 0; /* refer to resPrefix for message */
|
int32_t res = 0; /* refer to resPrefix for message */
|
||||||
|
|
||||||
/* Blocklist all but unix, inet, inet6 and netlink */
|
/* Blocklist all but unix, inet, inet6 and netlink */
|
||||||
struct {
|
struct {
|
||||||
int family;
|
int family;
|
||||||
hakurei_export_flag flags_mask;
|
hakurei_export_flag flags_mask;
|
||||||
} socket_family_allowlist[] = {
|
} socket_family_allowlist[] = {
|
||||||
/* NOTE: Keep in numerical order */
|
/* NOTE: Keep in numerical order */
|
||||||
{AF_UNSPEC, 0},
|
{AF_UNSPEC, 0},
|
||||||
{AF_LOCAL, 0},
|
{AF_LOCAL, 0},
|
||||||
{AF_INET, 0},
|
{AF_INET, 0},
|
||||||
{AF_INET6, 0},
|
{AF_INET6, 0},
|
||||||
{AF_NETLINK, 0},
|
{AF_NETLINK, 0},
|
||||||
{AF_CAN, HAKUREI_EXPORT_CAN},
|
{AF_CAN, HAKUREI_EXPORT_CAN},
|
||||||
{AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
|
{AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
|
||||||
};
|
};
|
||||||
|
|
||||||
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
|
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
|
||||||
if (ctx == NULL) {
|
if (ctx == NULL) {
|
||||||
res = 1;
|
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;
|
goto out;
|
||||||
}
|
} else
|
||||||
}
|
errno = 0;
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < rules_sz; i++) {
|
/* We only really need to handle arches on multiarch systems.
|
||||||
rule = &rules[i];
|
* If only one arch is supported the default is fine */
|
||||||
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
|
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 (rule->arg)
|
if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) {
|
||||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
|
*ret_p = seccomp_arch_add(ctx, multiarch);
|
||||||
rule->syscall, 1, *rule->arg);
|
if (*ret_p < 0 && *ret_p != -EEXIST) {
|
||||||
else
|
res = 3;
|
||||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
|
goto out;
|
||||||
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 (allocate_p == 0) {
|
|
||||||
*ret_p = seccomp_load(ctx);
|
|
||||||
if (*ret_p != 0) {
|
|
||||||
res = 7;
|
|
||||||
goto out;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
*ret_p = seccomp_export_bpf_mem(ctx, NULL, &len);
|
|
||||||
if (*ret_p != 0) {
|
|
||||||
res = 6;
|
|
||||||
goto out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buf = hakurei_scmp_allocate(allocate_p, len);
|
for (i = 0; i < rules_sz; i++) {
|
||||||
if (buf == NULL) {
|
rule = &rules[i];
|
||||||
res = 4;
|
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
|
||||||
goto out;
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*ret_p = seccomp_export_bpf_mem(ctx, buf, &len);
|
/* Socket filtering doesn't work on e.g. i386, so ignore failures here
|
||||||
if (*ret_p != 0) {
|
* However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
|
||||||
res = 6;
|
* something else: https://github.com/seccomp/libseccomp/issues/8 */
|
||||||
goto out;
|
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 (allocate_p == 0) {
|
||||||
|
*ret_p = seccomp_load(ctx);
|
||||||
|
if (*ret_p != 0) {
|
||||||
|
res = 7;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*ret_p = seccomp_export_bpf_mem(ctx, NULL, &len);
|
||||||
|
if (*ret_p != 0) {
|
||||||
|
res = 6;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = hakurei_scmp_allocate(allocate_p, len);
|
||||||
|
if (buf == NULL) {
|
||||||
|
res = 4;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
*ret_p = seccomp_export_bpf_mem(ctx, buf, &len);
|
||||||
|
if (*ret_p != 0) {
|
||||||
|
res = 6;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
out:
|
out:
|
||||||
if (ctx)
|
if (ctx)
|
||||||
seccomp_release(ctx);
|
seccomp_release(ctx);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
#include <seccomp.h>
|
#include <seccomp.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#if (SCMP_VER_MAJOR < 2) || (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \
|
#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)
|
(SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1)
|
||||||
#error This package requires libseccomp >= v2.5.1
|
#error This package requires libseccomp >= v2.5.1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
HAKUREI_EXPORT_MULTIARCH = 1 << 0,
|
HAKUREI_EXPORT_MULTIARCH = 1 << 0,
|
||||||
HAKUREI_EXPORT_CAN = 1 << 1,
|
HAKUREI_EXPORT_CAN = 1 << 1,
|
||||||
HAKUREI_EXPORT_BLUETOOTH = 1 << 2,
|
HAKUREI_EXPORT_BLUETOOTH = 1 << 2,
|
||||||
} hakurei_export_flag;
|
} hakurei_export_flag;
|
||||||
|
|
||||||
struct hakurei_syscall_rule {
|
struct hakurei_syscall_rule {
|
||||||
int syscall;
|
int syscall;
|
||||||
int m_errno;
|
int m_errno;
|
||||||
struct scmp_arg_cmp *arg;
|
struct scmp_arg_cmp *arg;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern void *hakurei_scmp_allocate(uintptr_t f, size_t len);
|
extern void *hakurei_scmp_allocate(uintptr_t f, size_t len);
|
||||||
int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
|
int32_t hakurei_scmp_make_filter(
|
||||||
uint32_t arch, uint32_t multiarch,
|
int *ret_p, uintptr_t allocate_p,
|
||||||
struct hakurei_syscall_rule *rules,
|
uint32_t arch, uint32_t multiarch,
|
||||||
size_t rules_sz, hakurei_export_flag flags);
|
struct hakurei_syscall_rule *rules,
|
||||||
|
size_t rules_sz, hakurei_export_flag flags);
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"hakurei.app/container/stub"
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Made available here to check panic recovery behaviour.
|
||||||
|
//
|
||||||
//go:linkname handleExitNew hakurei.app/container/stub.handleExitNew
|
//go:linkname handleExitNew hakurei.app/container/stub.handleExitNew
|
||||||
func handleExitNew(_ testing.TB)
|
func handleExitNew(t testing.TB)
|
||||||
|
|
||||||
// overrideTFailNow overrides the Fail and FailNow method.
|
// overrideTFailNow overrides the Fail and FailNow method.
|
||||||
type overrideTFailNow struct {
|
type overrideTFailNow struct {
|
||||||
|
|||||||
3
dist/comp/_hakurei
vendored
3
dist/comp/_hakurei
vendored
@@ -17,7 +17,8 @@ _hakurei_run() {
|
|||||||
'--wayland[Enable connection to Wayland via security-context-v1]' \
|
'--wayland[Enable connection to Wayland via security-context-v1]' \
|
||||||
'-X[Enable direct connection to X11]' \
|
'-X[Enable direct connection to X11]' \
|
||||||
'--dbus[Enable proxied connection to D-Bus]' \
|
'--dbus[Enable proxied connection to D-Bus]' \
|
||||||
'--pulse[Enable direct connection to PulseAudio]' \
|
'--pipewire[Enable connection to PipeWire via SecurityContext]' \
|
||||||
|
'--pulse[Enable PulseAudio compatibility daemon]' \
|
||||||
'--dbus-config[Path to session bus proxy config file]: :_files -g "*.json"' \
|
'--dbus-config[Path to session bus proxy config file]: :_files -g "*.json"' \
|
||||||
'--dbus-system[Path to system bus proxy config file]: :_files -g "*.json"' \
|
'--dbus-system[Path to system bus proxy config file]: :_files -g "*.json"' \
|
||||||
'--mpris[Allow owning MPRIS D-Bus path]' \
|
'--mpris[Allow owning MPRIS D-Bus path]' \
|
||||||
|
|||||||
2
dist/install.sh
vendored
2
dist/install.sh
vendored
@@ -2,7 +2,7 @@
|
|||||||
cd "$(dirname -- "$0")" || exit 1
|
cd "$(dirname -- "$0")" || exit 1
|
||||||
|
|
||||||
install -vDm0755 "bin/hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hakurei"
|
install -vDm0755 "bin/hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hakurei"
|
||||||
install -vDm0755 "bin/hpkg" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hpkg"
|
install -vDm0755 "bin/sharefs" "${HAKUREI_INSTALL_PREFIX}/usr/bin/sharefs"
|
||||||
|
|
||||||
install -vDm4511 "bin/hsu" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hsu"
|
install -vDm4511 "bin/hsu" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hsu"
|
||||||
if [ ! -f "${HAKUREI_INSTALL_PREFIX}/etc/hsurc" ]; then
|
if [ ! -f "${HAKUREI_INSTALL_PREFIX}/etc/hsurc" ]; then
|
||||||
|
|||||||
6
dist/release.sh
vendored
6
dist/release.sh
vendored
@@ -10,9 +10,9 @@ cp -rv "dist/comp" "${out}"
|
|||||||
|
|
||||||
go generate ./...
|
go generate ./...
|
||||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
|
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
|
||||||
-X hakurei.app/internal.buildVersion=${VERSION}
|
-X hakurei.app/internal/info.buildVersion=${VERSION}
|
||||||
-X hakurei.app/internal.hakureiPath=/usr/bin/hakurei
|
-X hakurei.app/internal/info.hakureiPath=/usr/bin/hakurei
|
||||||
-X hakurei.app/internal.hsuPath=/usr/bin/hsu
|
-X hakurei.app/internal/info.hsuPath=/usr/bin/hsu
|
||||||
-X main.hakureiPath=/usr/bin/hakurei" ./...
|
-X main.hakureiPath=/usr/bin/hakurei" ./...
|
||||||
|
|
||||||
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
|
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
|
||||||
|
|||||||
16
flake.lock
generated
16
flake.lock
generated
@@ -7,32 +7,32 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1756679287,
|
"lastModified": 1765384171,
|
||||||
"narHash": "sha256-Xd1vOeY9ccDf5VtVK12yM0FS6qqvfUop8UQlxEB+gTQ=",
|
"narHash": "sha256-FuFtkJrW1Z7u+3lhzPRau69E0CNjADku1mLQQflUORo=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "07fc025fe10487dd80f2ec694f1cd790e752d0e8",
|
"rev": "44777152652bc9eacf8876976fa72cc77ca8b9d8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"ref": "release-25.05",
|
"ref": "release-25.11",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1757020766,
|
"lastModified": 1765311797,
|
||||||
"narHash": "sha256-PLoSjHRa2bUbi1x9HoXgTx2AiuzNXs54c8omhadyvp0=",
|
"narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "fe83bbdde2ccdc2cb9573aa846abe8363f79a97a",
|
"rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-25.05",
|
"ref": "nixos-25.11",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|||||||
27
flake.nix
27
flake.nix
@@ -2,10 +2,10 @@
|
|||||||
description = "hakurei container tool and nixos module";
|
description = "hakurei container tool and nixos module";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||||
|
|
||||||
home-manager = {
|
home-manager = {
|
||||||
url = "github:nix-community/home-manager/release-25.05";
|
url = "github:nix-community/home-manager/release-25.11";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -69,6 +69,8 @@
|
|||||||
withRace = true;
|
withRace = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sharefs = callPackage ./cmd/sharefs/test { inherit system self; };
|
||||||
|
|
||||||
hpkg = callPackage ./cmd/hpkg/test { inherit system self; };
|
hpkg = callPackage ./cmd/hpkg/test { inherit system self; };
|
||||||
|
|
||||||
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
|
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
|
||||||
@@ -114,7 +116,7 @@
|
|||||||
inherit (pkgs)
|
inherit (pkgs)
|
||||||
# passthru.buildInputs
|
# passthru.buildInputs
|
||||||
go
|
go
|
||||||
gcc
|
clang
|
||||||
|
|
||||||
# nativeBuildInputs
|
# nativeBuildInputs
|
||||||
pkg-config
|
pkg-config
|
||||||
@@ -129,9 +131,17 @@
|
|||||||
zstd
|
zstd
|
||||||
gnutar
|
gnutar
|
||||||
coreutils
|
coreutils
|
||||||
|
|
||||||
|
# for check
|
||||||
|
util-linux
|
||||||
|
nettools
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
|
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
|
||||||
|
sharefs = pkgs.linkFarm "sharefs" {
|
||||||
|
"bin/sharefs" = "${hakurei}/libexec/sharefs";
|
||||||
|
"bin/mount.fuse.sharefs" = "${hakurei}/libexec/sharefs";
|
||||||
|
};
|
||||||
|
|
||||||
dist = pkgs.runCommand "${hakurei.name}-dist" { buildInputs = hakurei.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
|
dist = pkgs.runCommand "${hakurei.name}-dist" { buildInputs = hakurei.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
|
||||||
# go requires XDG_CACHE_HOME for the build cache
|
# go requires XDG_CACHE_HOME for the build cache
|
||||||
@@ -144,7 +154,7 @@
|
|||||||
&& chmod -R +w .
|
&& chmod -R +w .
|
||||||
|
|
||||||
export HAKUREI_VERSION="v${hakurei.version}"
|
export HAKUREI_VERSION="v${hakurei.version}"
|
||||||
./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out
|
CC="clang -O3 -Werror" ./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -156,7 +166,10 @@
|
|||||||
pkgs = nixpkgsFor.${system};
|
pkgs = nixpkgsFor.${system};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = pkgs.mkShell { buildInputs = hakurei.targetPkgs; };
|
default = pkgs.mkShell {
|
||||||
|
buildInputs = hakurei.targetPkgs;
|
||||||
|
hardeningDisable = [ "fortify" ];
|
||||||
|
};
|
||||||
withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
|
withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
|
||||||
|
|
||||||
vm =
|
vm =
|
||||||
@@ -181,13 +194,13 @@
|
|||||||
hakurei =
|
hakurei =
|
||||||
let
|
let
|
||||||
# this is used for interactive vm testing during development, where tests might be broken
|
# this is used for interactive vm testing during development, where tests might be broken
|
||||||
package = self.packages.${pkgs.system}.hakurei.override {
|
package = self.packages.${pkgs.stdenv.hostPlatform.system}.hakurei.override {
|
||||||
buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; });
|
buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; });
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit package;
|
inherit package;
|
||||||
hsuPackage = self.packages.${pkgs.system}.hsu.override { hakurei = package; };
|
hsuPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.hsu.override { hakurei = package; };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
73
helper/deprecated.go
Normal file
73
helper/deprecated.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// Package helper exposes the internal/helper package.
|
||||||
|
//
|
||||||
|
// Deprecated: This package will be removed in 0.4.
|
||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
_ "unsafe" // for go:linkname
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/container/check"
|
||||||
|
"hakurei.app/internal/helper"
|
||||||
|
"hakurei.app/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:linkname WaitDelay hakurei.app/internal/helper.WaitDelay
|
||||||
|
var WaitDelay time.Duration
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HakureiHelper is set to 1 when args fd is enabled and 0 otherwise.
|
||||||
|
HakureiHelper = helper.HakureiHelper
|
||||||
|
// HakureiStatus is set to 1 when stat fd is enabled and 0 otherwise.
|
||||||
|
HakureiStatus = helper.HakureiStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
type Helper = helper.Helper
|
||||||
|
|
||||||
|
// NewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
||||||
|
//
|
||||||
|
//go:linkname NewCheckedArgs hakurei.app/internal/helper.NewCheckedArgs
|
||||||
|
func NewCheckedArgs(args ...string) (wt io.WriterTo, err error)
|
||||||
|
|
||||||
|
// MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
||||||
|
// If s contains a NUL byte this function panics instead of returning an error.
|
||||||
|
//
|
||||||
|
//go:linkname MustNewCheckedArgs hakurei.app/internal/helper.MustNewCheckedArgs
|
||||||
|
func MustNewCheckedArgs(args ...string) io.WriterTo
|
||||||
|
|
||||||
|
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
|
||||||
|
// Function argF returns an array of arguments passed directly to the child process.
|
||||||
|
//
|
||||||
|
//go:linkname NewDirect hakurei.app/internal/helper.NewDirect
|
||||||
|
func NewDirect(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
|
cmdF func(cmd *exec.Cmd),
|
||||||
|
extraFiles []*os.File,
|
||||||
|
) Helper
|
||||||
|
|
||||||
|
// New initialises a Helper instance with wt as the null-terminated argument writer.
|
||||||
|
//
|
||||||
|
//go:linkname New hakurei.app/internal/helper.New
|
||||||
|
func New(
|
||||||
|
ctx context.Context,
|
||||||
|
msg message.Msg,
|
||||||
|
pathname *check.Absolute, name string,
|
||||||
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
|
cmdF func(z *container.Container),
|
||||||
|
extraFiles []*os.File,
|
||||||
|
) Helper
|
||||||
|
|
||||||
|
// InternalHelperStub is an internal function but exported because it is cross-package;
|
||||||
|
// it is part of the implementation of the helper stub.
|
||||||
|
func InternalHelperStub() { helper.InternalHelperStub() }
|
||||||
63
helper/proc/deprecated.go
Normal file
63
helper/proc/deprecated.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Deprecated: This package will be removed in 0.4.
|
||||||
|
package proc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
_ "unsafe" // for go:linkname
|
||||||
|
|
||||||
|
"hakurei.app/internal/helper/proc"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:linkname FulfillmentTimeout hakurei.app/internal/helper/proc.FulfillmentTimeout
|
||||||
|
var FulfillmentTimeout time.Duration
|
||||||
|
|
||||||
|
// A File is an extra file with deferred initialisation.
|
||||||
|
type File = proc.File
|
||||||
|
|
||||||
|
// ExtraFilesPre is a linked list storing addresses of [os.File].
|
||||||
|
type ExtraFilesPre = proc.ExtraFilesPre
|
||||||
|
|
||||||
|
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
|
||||||
|
//
|
||||||
|
//go:linkname Fulfill hakurei.app/internal/helper/proc.Fulfill
|
||||||
|
func Fulfill(ctx context.Context,
|
||||||
|
v *[]*os.File, start func() error,
|
||||||
|
files []File, extraFiles *ExtraFilesPre,
|
||||||
|
) (err error)
|
||||||
|
|
||||||
|
// InitFile initialises f as part of the slice extraFiles points to,
|
||||||
|
// and returns its final fd value.
|
||||||
|
//
|
||||||
|
//go:linkname InitFile hakurei.app/internal/helper/proc.InitFile
|
||||||
|
func InitFile(f File, extraFiles *ExtraFilesPre) (fd uintptr)
|
||||||
|
|
||||||
|
// BaseFile implements the Init method of the File interface and provides indirect access to extra file state.
|
||||||
|
type BaseFile = proc.BaseFile
|
||||||
|
|
||||||
|
//go:linkname ExtraFile hakurei.app/internal/helper/proc.ExtraFile
|
||||||
|
func ExtraFile(cmd *exec.Cmd, f *os.File) (fd uintptr)
|
||||||
|
|
||||||
|
//go:linkname ExtraFileSlice hakurei.app/internal/helper/proc.ExtraFileSlice
|
||||||
|
func ExtraFileSlice(extraFiles *[]*os.File, f *os.File) (fd uintptr)
|
||||||
|
|
||||||
|
// NewWriterTo returns a [File] that receives content from wt on fulfillment.
|
||||||
|
//
|
||||||
|
//go:linkname NewWriterTo hakurei.app/internal/helper/proc.NewWriterTo
|
||||||
|
func NewWriterTo(wt io.WriterTo) File
|
||||||
|
|
||||||
|
// NewStat returns a [File] implementing the behaviour
|
||||||
|
// of the receiving end of xdg-dbus-proxy stat fd.
|
||||||
|
//
|
||||||
|
//go:linkname NewStat hakurei.app/internal/helper/proc.NewStat
|
||||||
|
func NewStat(s *io.Closer) File
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:linkname ErrStatFault hakurei.app/internal/helper/proc.ErrStatFault
|
||||||
|
ErrStatFault error
|
||||||
|
//go:linkname ErrStatRead hakurei.app/internal/helper/proc.ErrStatRead
|
||||||
|
ErrStatRead error
|
||||||
|
)
|
||||||
@@ -23,9 +23,54 @@ type Config struct {
|
|||||||
// System D-Bus proxy configuration.
|
// System D-Bus proxy configuration.
|
||||||
// If set to nil, system bus proxy is disabled.
|
// If set to nil, system bus proxy is disabled.
|
||||||
SystemBus *BusConfig `json:"system_bus,omitempty"`
|
SystemBus *BusConfig `json:"system_bus,omitempty"`
|
||||||
|
|
||||||
// Direct access to wayland socket, no attempt is made to attach security-context-v1
|
// Direct access to wayland socket, no attempt is made to attach security-context-v1
|
||||||
// and the bare socket is made available to the container.
|
// and the bare socket is made available to the container.
|
||||||
|
//
|
||||||
|
// This option is unsupported and most likely enables full control over the Wayland
|
||||||
|
// session. Do not set this to true unless you are sure you know what you are doing.
|
||||||
DirectWayland bool `json:"direct_wayland,omitempty"`
|
DirectWayland bool `json:"direct_wayland,omitempty"`
|
||||||
|
// Direct access to the PipeWire socket established via SecurityContext::Create, no
|
||||||
|
// attempt is made to start the pipewire-pulse server.
|
||||||
|
//
|
||||||
|
// The SecurityContext machinery is fatally flawed, it blindly sets read and execute
|
||||||
|
// bits on all objects for clients with the lowest achievable privilege level (by
|
||||||
|
// setting PW_KEY_ACCESS to "restricted"). This enables them to call any method
|
||||||
|
// targeting any object, and since Registry::Destroy checks for the read and execute bit,
|
||||||
|
// allows the destruction of any object other than PW_ID_CORE as well. This behaviour
|
||||||
|
// is implemented separately in media-session and wireplumber, with the wireplumber
|
||||||
|
// implementation in Lua via an embedded Lua vm. In all known setups, wireplumber is
|
||||||
|
// in use, and there is no known way to change its behaviour and set permissions
|
||||||
|
// differently without replacing the Lua script. Also, since PipeWire relies on these
|
||||||
|
// permissions to work, reducing them is not possible.
|
||||||
|
//
|
||||||
|
// Currently, the only other sandboxed use case is flatpak, which is not aware of
|
||||||
|
// PipeWire and blindly exposes the bare PulseAudio socket to the container (behaves
|
||||||
|
// like DirectPulse). This socket is backed by the pipewire-pulse compatibility daemon,
|
||||||
|
// which obtains client pid via the SO_PEERCRED option. The PipeWire daemon, pipewire-pulse
|
||||||
|
// daemon and the session manager daemon then separately performs the /.flatpak-info hack
|
||||||
|
// described in https://git.gensokyo.uk/security/hakurei/issues/21. Under such use case,
|
||||||
|
// since the client has no direct access to PipeWire, insecure parts of the protocol are
|
||||||
|
// obscured by pipewire-pulse simply not implementing them, and thus hiding the flaws
|
||||||
|
// described above.
|
||||||
|
//
|
||||||
|
// Hakurei does not rely on the /.flatpak-info hack. Instead, a socket is sets up via
|
||||||
|
// SecurityContext. A pipewire-pulse server connected through it achieves the same
|
||||||
|
// permissions as flatpak does via the /.flatpak-info hack and is maintained for the
|
||||||
|
// life of the container.
|
||||||
|
//
|
||||||
|
// This option is unsupported and enables a denial-of-service attack as the sandboxed
|
||||||
|
// client is able to destroy any client object and thus disconnecting them from PipeWire,
|
||||||
|
// or destroy the SecurityContext object preventing any further container creation.
|
||||||
|
// Do not set this to true, it is insecure under any configuration.
|
||||||
|
DirectPipeWire bool `json:"direct_pipewire,omitempty"`
|
||||||
|
// Direct access to PulseAudio socket, no attempt is made to establish pipewire-pulse
|
||||||
|
// server via a PipeWire socket with a SecurityContext attached and the bare socket
|
||||||
|
// is made available to the container.
|
||||||
|
//
|
||||||
|
// This option is unsupported and enables arbitrary code execution as the PulseAudio
|
||||||
|
// server. Do not set this to true, it is insecure under any configuration.
|
||||||
|
DirectPulse bool `json:"direct_pulse,omitempty"`
|
||||||
|
|
||||||
// Extra acl updates to perform before setuid.
|
// Extra acl updates to perform before setuid.
|
||||||
ExtraPerms []ExtraPermConfig `json:"extra_perms,omitempty"`
|
ExtraPerms []ExtraPermConfig `json:"extra_perms,omitempty"`
|
||||||
@@ -49,6 +94,9 @@ var (
|
|||||||
|
|
||||||
// ErrEnviron is returned by [Config.Validate] if an environment variable name contains '=' or NUL.
|
// ErrEnviron is returned by [Config.Validate] if an environment variable name contains '=' or NUL.
|
||||||
ErrEnviron = errors.New("invalid environment variable name")
|
ErrEnviron = errors.New("invalid environment variable name")
|
||||||
|
|
||||||
|
// ErrInsecure is returned by [Config.Validate] if the configuration is considered insecure.
|
||||||
|
ErrInsecure = errors.New("configuration is insecure")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
|
// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
|
||||||
@@ -95,6 +143,11 @@ func (config *Config) Validate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
|
||||||
|
return &AppError{Step: "validate configuration", Err: ErrInsecure,
|
||||||
|
Msg: "enablement PulseAudio is insecure and no longer supported"}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ func TestConfigValidate(t *testing.T) {
|
|||||||
Env: map[string]string{"TERM\x00": ""},
|
Env: map[string]string{"TERM\x00": ""},
|
||||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||||
Msg: `invalid environment variable "TERM\x00"`}},
|
Msg: `invalid environment variable "TERM\x00"`}},
|
||||||
|
{"insecure pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), Container: &hst.ContainerConfig{
|
||||||
|
Home: fhs.AbsTmp,
|
||||||
|
Shell: fhs.AbsTmp,
|
||||||
|
Path: fhs.AbsTmp,
|
||||||
|
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||||
|
Msg: "enablement PulseAudio is insecure and no longer supported"}},
|
||||||
{"valid", &hst.Config{Container: &hst.ContainerConfig{
|
{"valid", &hst.Config{Container: &hst.ContainerConfig{
|
||||||
Home: fhs.AbsTmp,
|
Home: fhs.AbsTmp,
|
||||||
Shell: fhs.AbsTmp,
|
Shell: fhs.AbsTmp,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const (
|
|||||||
EX11
|
EX11
|
||||||
// EDBus enables the per-container xdg-dbus-proxy daemon.
|
// EDBus enables the per-container xdg-dbus-proxy daemon.
|
||||||
EDBus
|
EDBus
|
||||||
|
// EPipeWire exposes a pipewire pathname socket via SecurityContext.
|
||||||
|
EPipeWire
|
||||||
// EPulse copies the PulseAudio cookie to [hst.PrivateTmp] and exposes the PulseAudio socket.
|
// EPulse copies the PulseAudio cookie to [hst.PrivateTmp] and exposes the PulseAudio socket.
|
||||||
EPulse
|
EPulse
|
||||||
|
|
||||||
@@ -35,6 +37,8 @@ func (e Enablement) String() string {
|
|||||||
return "x11"
|
return "x11"
|
||||||
case EDBus:
|
case EDBus:
|
||||||
return "dbus"
|
return "dbus"
|
||||||
|
case EPipeWire:
|
||||||
|
return "pipewire"
|
||||||
case EPulse:
|
case EPulse:
|
||||||
return "pulseaudio"
|
return "pulseaudio"
|
||||||
default:
|
default:
|
||||||
@@ -62,10 +66,11 @@ type Enablements Enablement
|
|||||||
|
|
||||||
// enablementsJSON is the [json] representation of [Enablements].
|
// enablementsJSON is the [json] representation of [Enablements].
|
||||||
type enablementsJSON = struct {
|
type enablementsJSON = struct {
|
||||||
Wayland bool `json:"wayland,omitempty"`
|
Wayland bool `json:"wayland,omitempty"`
|
||||||
X11 bool `json:"x11,omitempty"`
|
X11 bool `json:"x11,omitempty"`
|
||||||
DBus bool `json:"dbus,omitempty"`
|
DBus bool `json:"dbus,omitempty"`
|
||||||
Pulse bool `json:"pulse,omitempty"`
|
PipeWire bool `json:"pipewire,omitempty"`
|
||||||
|
Pulse bool `json:"pulse,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap returns the underlying [Enablement].
|
// Unwrap returns the underlying [Enablement].
|
||||||
@@ -81,10 +86,11 @@ func (e *Enablements) MarshalJSON() ([]byte, error) {
|
|||||||
return nil, syscall.EINVAL
|
return nil, syscall.EINVAL
|
||||||
}
|
}
|
||||||
return json.Marshal(&enablementsJSON{
|
return json.Marshal(&enablementsJSON{
|
||||||
Wayland: Enablement(*e)&EWayland != 0,
|
Wayland: Enablement(*e)&EWayland != 0,
|
||||||
X11: Enablement(*e)&EX11 != 0,
|
X11: Enablement(*e)&EX11 != 0,
|
||||||
DBus: Enablement(*e)&EDBus != 0,
|
DBus: Enablement(*e)&EDBus != 0,
|
||||||
Pulse: Enablement(*e)&EPulse != 0,
|
PipeWire: Enablement(*e)&EPipeWire != 0,
|
||||||
|
Pulse: Enablement(*e)&EPulse != 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +114,9 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
|
|||||||
if v.DBus {
|
if v.DBus {
|
||||||
ve |= EDBus
|
ve |= EDBus
|
||||||
}
|
}
|
||||||
|
if v.PipeWire {
|
||||||
|
ve |= EPipeWire
|
||||||
|
}
|
||||||
if v.Pulse {
|
if v.Pulse {
|
||||||
ve |= EPulse
|
ve |= EPulse
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ func TestEnablementString(t *testing.T) {
|
|||||||
{hst.EWayland | hst.EDBus | hst.EPulse, "wayland, dbus, pulseaudio"},
|
{hst.EWayland | hst.EDBus | hst.EPulse, "wayland, dbus, pulseaudio"},
|
||||||
{hst.EX11 | hst.EDBus | hst.EPulse, "x11, dbus, pulseaudio"},
|
{hst.EX11 | hst.EDBus | hst.EPulse, "x11, dbus, pulseaudio"},
|
||||||
{hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse, "wayland, x11, dbus, pulseaudio"},
|
{hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse, "wayland, x11, dbus, pulseaudio"},
|
||||||
|
{hst.EM - 1, "wayland, x11, dbus, pipewire, pulseaudio"},
|
||||||
|
|
||||||
{1 << 5, "e20"},
|
{1 << 5, "e20"},
|
||||||
{1 << 6, "e40"},
|
{1 << 6, "e40"},
|
||||||
@@ -62,8 +63,9 @@ func TestEnablements(t *testing.T) {
|
|||||||
{"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
|
{"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
|
||||||
{"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
|
{"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
|
||||||
{"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
|
{"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
|
||||||
|
{"pipewire", hst.NewEnablements(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
|
||||||
{"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
|
{"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
|
||||||
{"all", hst.NewEnablements(hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse), `{"wayland":true,"x11":true,"dbus":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pulse":true},"magic":3236757504}`},
|
{"all", hst.NewEnablements(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
|||||||
12
hst/fs.go
12
hst/fs.go
@@ -45,6 +45,9 @@ type Ops interface {
|
|||||||
Root(host *check.Absolute, flags int) Ops
|
Root(host *check.Absolute, flags int) Ops
|
||||||
// Etc appends an op that expands host /etc into a toplevel symlink mirror with /etc semantics.
|
// Etc appends an op that expands host /etc into a toplevel symlink mirror with /etc semantics.
|
||||||
Etc(host *check.Absolute, prefix string) Ops
|
Etc(host *check.Absolute, prefix string) Ops
|
||||||
|
|
||||||
|
// Daemon appends an op that starts a daemon in the container and blocks until target appears.
|
||||||
|
Daemon(target, path *check.Absolute, args ...string) Ops
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyState holds the address of [Ops] and any relevant application state.
|
// ApplyState holds the address of [Ops] and any relevant application state.
|
||||||
@@ -124,6 +127,12 @@ func (f *FilesystemConfigJSON) MarshalJSON() ([]byte, error) {
|
|||||||
*FSLink
|
*FSLink
|
||||||
}{fsType{FilesystemLink}, cv}
|
}{fsType{FilesystemLink}, cv}
|
||||||
|
|
||||||
|
case *FSDaemon:
|
||||||
|
v = &struct {
|
||||||
|
fsType
|
||||||
|
*FSDaemon
|
||||||
|
}{fsType{FilesystemDaemon}, cv}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, FSImplError{f.FilesystemConfig}
|
return nil, FSImplError{f.FilesystemConfig}
|
||||||
}
|
}
|
||||||
@@ -152,6 +161,9 @@ func (f *FilesystemConfigJSON) UnmarshalJSON(data []byte) error {
|
|||||||
case FilesystemLink:
|
case FilesystemLink:
|
||||||
*f = FilesystemConfigJSON{new(FSLink)}
|
*f = FilesystemConfigJSON{new(FSLink)}
|
||||||
|
|
||||||
|
case FilesystemDaemon:
|
||||||
|
*f = FilesystemConfigJSON{new(FSDaemon)}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return FSTypeError(t.Type)
|
return FSTypeError(t.Type)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,16 @@ func TestFilesystemConfigJSON(t *testing.T) {
|
|||||||
}, nil,
|
}, nil,
|
||||||
`{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true}`,
|
`{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true}`,
|
||||||
`{"fs":{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true},"magic":3236757504}`},
|
`{"fs":{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true},"magic":3236757504}`},
|
||||||
|
|
||||||
|
{"daemon", hst.FilesystemConfigJSON{
|
||||||
|
FilesystemConfig: &hst.FSDaemon{
|
||||||
|
Target: m("/run/user/1971/pulse/native"),
|
||||||
|
Exec: m("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
Args: []string{"-v"},
|
||||||
|
},
|
||||||
|
}, nil,
|
||||||
|
`{"type":"daemon","dst":"/run/user/1971/pulse/native","path":"/run/current-system/sw/bin/pipewire-pulse","args":["-v"]}`,
|
||||||
|
`{"fs":{"type":"daemon","dst":"/run/user/1971/pulse/native","path":"/run/current-system/sw/bin/pipewire-pulse","args":["-v"]},"magic":3236757504}`},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -345,6 +355,10 @@ func (p opsAdapter) Etc(host *check.Absolute, prefix string) hst.Ops {
|
|||||||
return opsAdapter{p.Ops.Etc(host, prefix)}
|
return opsAdapter{p.Ops.Etc(host, prefix)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p opsAdapter) Daemon(target, path *check.Absolute, args ...string) hst.Ops {
|
||||||
|
return opsAdapter{p.Ops.Daemon(target, path, args...)}
|
||||||
|
}
|
||||||
|
|
||||||
func m(pathname string) *check.Absolute { return check.MustAbs(pathname) }
|
func m(pathname string) *check.Absolute { return check.MustAbs(pathname) }
|
||||||
func ms(pathnames ...string) []*check.Absolute {
|
func ms(pathnames ...string) []*check.Absolute {
|
||||||
as := make([]*check.Absolute, len(pathnames))
|
as := make([]*check.Absolute, len(pathnames))
|
||||||
|
|||||||
48
hst/fsdaemon.go
Normal file
48
hst/fsdaemon.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package hst
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
|
||||||
|
"hakurei.app/container/check"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { gob.Register(new(FSDaemon)) }
|
||||||
|
|
||||||
|
// FilesystemDaemon is the type string of a daemon.
|
||||||
|
const FilesystemDaemon = "daemon"
|
||||||
|
|
||||||
|
// FSDaemon represents a daemon to be started in the container.
|
||||||
|
type FSDaemon struct {
|
||||||
|
// Pathname indicating readiness of daemon.
|
||||||
|
Target *check.Absolute `json:"dst"`
|
||||||
|
// Absolute pathname to daemon executable file.
|
||||||
|
Exec *check.Absolute `json:"path"`
|
||||||
|
// Arguments (excl. first) passed to daemon.
|
||||||
|
Args []string `json:"args"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FSDaemon) Valid() bool { return d != nil && d.Target != nil && d.Exec != nil }
|
||||||
|
|
||||||
|
func (d *FSDaemon) Path() *check.Absolute {
|
||||||
|
if !d.Valid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.Target
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FSDaemon) Host() []*check.Absolute { return nil }
|
||||||
|
|
||||||
|
func (d *FSDaemon) Apply(z *ApplyState) {
|
||||||
|
if !d.Valid() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
z.Daemon(d.Target, d.Exec, d.Args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FSDaemon) String() string {
|
||||||
|
if !d.Valid() {
|
||||||
|
return "<invalid>"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "daemon:" + d.Target.String()
|
||||||
|
}
|
||||||
29
hst/fsdaemon_test.go
Normal file
29
hst/fsdaemon_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package hst_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/hst"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFSDaemon(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
checkFs(t, []fsTestCase{
|
||||||
|
{"nil", (*hst.FSDaemon)(nil), false, nil, nil, nil, "<invalid>"},
|
||||||
|
{"zero", new(hst.FSDaemon), false, nil, nil, nil, "<invalid>"},
|
||||||
|
|
||||||
|
{"pipewire-pulse", &hst.FSDaemon{
|
||||||
|
Target: m("/run/user/1971/pulse/native"),
|
||||||
|
Exec: m("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
Args: []string{"-v"},
|
||||||
|
}, true, container.Ops{
|
||||||
|
&container.DaemonOp{
|
||||||
|
Target: m("/run/user/1971/pulse/native"),
|
||||||
|
Path: m("/run/current-system/sw/bin/pipewire-pulse"),
|
||||||
|
Args: []string{"-v"},
|
||||||
|
},
|
||||||
|
}, m("/run/user/1971/pulse/native"), nil, `daemon:/run/user/1971/pulse/native`},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -54,6 +54,9 @@ type Paths struct {
|
|||||||
|
|
||||||
// Info holds basic system information collected from the implementation.
|
// Info holds basic system information collected from the implementation.
|
||||||
type Info struct {
|
type Info struct {
|
||||||
|
// WaylandVersion is the libwayland value of WAYLAND_VERSION.
|
||||||
|
WaylandVersion string `json:"WAYLAND_VERSION"`
|
||||||
|
|
||||||
// Version is a hardcoded version string.
|
// Version is a hardcoded version string.
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
// User is the userid according to hsu.
|
// User is the userid according to hsu.
|
||||||
@@ -67,7 +70,7 @@ func Template() *Config {
|
|||||||
return &Config{
|
return &Config{
|
||||||
ID: "org.chromium.Chromium",
|
ID: "org.chromium.Chromium",
|
||||||
|
|
||||||
Enablements: NewEnablements(EWayland | EDBus | EPulse),
|
Enablements: NewEnablements(EWayland | EDBus | EPipeWire),
|
||||||
|
|
||||||
SessionBus: &BusConfig{
|
SessionBus: &BusConfig{
|
||||||
See: nil,
|
See: nil,
|
||||||
@@ -89,7 +92,6 @@ func Template() *Config {
|
|||||||
Log: false,
|
Log: false,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
},
|
},
|
||||||
DirectWayland: false,
|
|
||||||
|
|
||||||
ExtraPerms: []ExtraPermConfig{
|
ExtraPerms: []ExtraPermConfig{
|
||||||
{Path: fhs.AbsVarLib.Append("hakurei/u0"), Ensure: true, Execute: true},
|
{Path: fhs.AbsVarLib.Append("hakurei/u0"), Ensure: true, Execute: true},
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ func TestTemplate(t *testing.T) {
|
|||||||
"enablements": {
|
"enablements": {
|
||||||
"wayland": true,
|
"wayland": true,
|
||||||
"dbus": true,
|
"dbus": true,
|
||||||
"pulse": true
|
"pipewire": true
|
||||||
},
|
},
|
||||||
"session_bus": {
|
"session_bus": {
|
||||||
"see": null,
|
"see": null,
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
_ "unsafe"
|
_ "unsafe" // for go:linkname
|
||||||
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Made available here to check time encoding behaviour of [hst.ID].
|
||||||
|
//
|
||||||
//go:linkname newInstanceID hakurei.app/hst.newInstanceID
|
//go:linkname newInstanceID hakurei.app/hst.newInstanceID
|
||||||
func newInstanceID(id *hst.ID, p uint64) error
|
func newInstanceID(id *hst.ID, p uint64) error
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/system/acl"
|
"hakurei.app/internal/acl"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testFileName = "acl.test"
|
const testFileName = "acl.test"
|
||||||
90
internal/acl/libacl-helper.c
Normal file
90
internal/acl/libacl-helper.c
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#include "libacl-helper.h"
|
||||||
|
#include <acl/libacl.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/acl.h>
|
||||||
|
|
||||||
|
int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
|
||||||
|
acl_perm_t *perms, size_t plen) {
|
||||||
|
int ret;
|
||||||
|
bool v;
|
||||||
|
int i;
|
||||||
|
acl_t acl;
|
||||||
|
acl_entry_t entry;
|
||||||
|
acl_tag_t tag_type;
|
||||||
|
void *qualifier_p;
|
||||||
|
acl_permset_t permset;
|
||||||
|
|
||||||
|
ret = -1; /* acl_get_file */
|
||||||
|
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
|
||||||
|
if (acl == NULL)
|
||||||
|
goto out;
|
||||||
|
|
||||||
|
/* prune entries by uid */
|
||||||
|
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
|
||||||
|
i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
|
||||||
|
ret = -2; /* acl_get_tag_type */
|
||||||
|
if (acl_get_tag_type(entry, &tag_type) != 0)
|
||||||
|
goto out;
|
||||||
|
if (tag_type != ACL_USER)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ret = -3; /* acl_get_qualifier */
|
||||||
|
qualifier_p = acl_get_qualifier(entry);
|
||||||
|
if (qualifier_p == NULL)
|
||||||
|
goto out;
|
||||||
|
v = *(uid_t *)qualifier_p == uid;
|
||||||
|
acl_free(qualifier_p);
|
||||||
|
|
||||||
|
if (!v)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ret = -4; /* acl_delete_entry */
|
||||||
|
if (acl_delete_entry(acl, entry) != 0)
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plen == 0)
|
||||||
|
goto set;
|
||||||
|
|
||||||
|
ret = -5; /* acl_create_entry */
|
||||||
|
if (acl_create_entry(&acl, &entry) != 0)
|
||||||
|
goto out;
|
||||||
|
|
||||||
|
ret = -6; /* acl_get_permset */
|
||||||
|
if (acl_get_permset(entry, &permset) != 0)
|
||||||
|
goto out;
|
||||||
|
|
||||||
|
ret = -7; /* acl_add_perm */
|
||||||
|
for (i = 0; i < plen; i++) {
|
||||||
|
if (acl_add_perm(permset, perms[i]) != 0)
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = -8; /* acl_set_tag_type */
|
||||||
|
if (acl_set_tag_type(entry, ACL_USER) != 0)
|
||||||
|
goto out;
|
||||||
|
|
||||||
|
ret = -9; /* acl_set_qualifier */
|
||||||
|
if (acl_set_qualifier(entry, (void *)&uid) != 0)
|
||||||
|
goto out;
|
||||||
|
|
||||||
|
set:
|
||||||
|
ret = -10; /* acl_calc_mask */
|
||||||
|
if (acl_calc_mask(&acl) != 0)
|
||||||
|
goto out;
|
||||||
|
|
||||||
|
ret = -11; /* acl_valid */
|
||||||
|
if (acl_valid(acl) != 0)
|
||||||
|
goto out;
|
||||||
|
|
||||||
|
ret = -12; /* acl_set_file */
|
||||||
|
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
|
||||||
|
ret = 0;
|
||||||
|
|
||||||
|
out:
|
||||||
|
free((void *)path_p);
|
||||||
|
if (acl != NULL)
|
||||||
|
acl_free((void *)acl);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package acl_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/system/acl"
|
"hakurei.app/internal/acl"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPerms(t *testing.T) {
|
func TestPerms(t *testing.T) {
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/system/dbus"
|
"hakurei.app/internal/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
func TestParse(t *testing.T) {
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/system/dbus"
|
"hakurei.app/internal/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigArgs(t *testing.T) {
|
func TestConfigArgs(t *testing.T) {
|
||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/helper"
|
"hakurei.app/internal/dbus"
|
||||||
|
"hakurei.app/internal/helper"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
"hakurei.app/system/dbus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFinalise(t *testing.T) {
|
func TestFinalise(t *testing.T) {
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"hakurei.app/container/check"
|
"hakurei.app/container/check"
|
||||||
"hakurei.app/container/seccomp"
|
"hakurei.app/container/seccomp"
|
||||||
"hakurei.app/container/std"
|
"hakurei.app/container/std"
|
||||||
"hakurei.app/helper"
|
"hakurei.app/internal/helper"
|
||||||
"hakurei.app/ldd"
|
"hakurei.app/ldd"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ func (p *Proxy) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var libPaths []*check.Absolute
|
var libPaths []*check.Absolute
|
||||||
if entries, err := ldd.Exec(ctx, p.msg, toolPath.String()); err != nil {
|
if entries, err := ldd.Resolve(ctx, p.msg, toolPath); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
libPaths = ldd.Path(entries)
|
libPaths = ldd.Path(entries)
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
"hakurei.app/helper"
|
"hakurei.app/internal/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) }
|
func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) }
|
||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"hakurei.app/helper"
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/internal/helper"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/helper"
|
"hakurei.app/internal/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestArgsString(t *testing.T) {
|
func TestArgsString(t *testing.T) {
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"hakurei.app/helper/proc"
|
"hakurei.app/internal/helper/proc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
|
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
"hakurei.app/helper"
|
"hakurei.app/internal/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCmd(t *testing.T) {
|
func TestCmd(t *testing.T) {
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
"hakurei.app/container/check"
|
"hakurei.app/container/check"
|
||||||
"hakurei.app/helper/proc"
|
"hakurei.app/internal/helper/proc"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
"hakurei.app/container/check"
|
"hakurei.app/container/check"
|
||||||
"hakurei.app/container/fhs"
|
"hakurei.app/container/fhs"
|
||||||
"hakurei.app/helper"
|
"hakurei.app/internal/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestContainer(t *testing.T) {
|
func TestContainer(t *testing.T) {
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/helper/proc"
|
"hakurei.app/internal/helper/proc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var WaitDelay = 2 * time.Second
|
var WaitDelay = 2 * time.Second
|
||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/helper"
|
"hakurei.app/internal/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user