4 Commits

Author SHA1 Message Date
cat 7c98cf72a7 internal/rosa/libxslt: fetch source via git
Test / Create distribution (push) Successful in 1m20s
Test / Sandbox (push) Successful in 3m28s
Test / Hakurei (push) Successful in 4m39s
Test / ShareFS (push) Successful in 4m44s
Test / Sandbox (race detector) (push) Successful in 5m57s
Test / Hakurei (race detector) (push) Successful in 7m8s
Test / Flake checks (push) Successful in 1m30s
Eliminates the xz dependency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-04 23:05:10 +09:00
cat adefb59201 internal/rosa/libxml2: fetch source via git
Test / Create distribution (push) Successful in 1m20s
Test / Sandbox (push) Successful in 3m10s
Test / ShareFS (push) Successful in 4m26s
Test / Sandbox (race detector) (push) Successful in 5m44s
Test / Hakurei (race detector) (push) Successful in 6m50s
Test / Hakurei (push) Successful in 3m3s
Test / Flake checks (push) Successful in 1m29s
Eliminates the xz dependency. This also switches to meson to avoid pulling in autotools.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-04 22:58:29 +09:00
cat 2b3991bd9b internal/rosa/gtk: fetch glib source via git
Test / Create distribution (push) Successful in 1m22s
Test / Sandbox (push) Successful in 3m12s
Test / Hakurei (push) Successful in 4m25s
Test / ShareFS (push) Successful in 4m34s
Test / Sandbox (race detector) (push) Successful in 5m43s
Test / Hakurei (race detector) (push) Successful in 6m58s
Test / Flake checks (push) Successful in 1m35s
This eliminates xz dependency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-04 22:47:10 +09:00
cat 1b57bf691b internal/rosa/gnu: gnutls disable arm64 hardware acceleration
Test / Create distribution (push) Successful in 1m22s
Test / Sandbox (push) Successful in 3m22s
Test / Hakurei (push) Successful in 5m9s
Test / ShareFS (push) Successful in 5m9s
Test / Sandbox (race detector) (push) Successful in 6m17s
Test / Hakurei (race detector) (push) Successful in 7m42s
Test / Flake checks (push) Successful in 1m52s
Hardware on arm64 is quite messy, this miscompiles.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-04 22:37:54 +09:00
101 changed files with 688 additions and 13110 deletions
+27 -4
View File
@@ -1,7 +1,27 @@
# produced by tools and text editors # Binaries for programs and plugins
*.qcow2 *.exe
*.exe~
*.dll
*.so
*.dylib
*.pkg
/hakurei
# Test binary, built with `go test -c`
*.test *.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
.idea .idea
.vscode .vscode
@@ -10,5 +30,8 @@
/internal/pkg/testdata/testtool /internal/pkg/testdata/testtool
/internal/rosa/hakurei_current.tar.gz /internal/rosa/hakurei_current.tar.gz
# cmd/dist default destination # release
/dist /dist/hakurei-*
# interactive nixos vm
nixos.qcow2
-6
View File
@@ -1,6 +0,0 @@
#!/bin/sh -e
TOOLCHAIN_VERSION="$(go version)"
cd "$(dirname -- "$0")/"
echo "# Building cmd/dist using ${TOOLCHAIN_VERSION}."
go run -v --tags=dist ./cmd/dist
+23 -22
View File
@@ -2,7 +2,7 @@
package check package check
import ( import (
"encoding" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
@@ -30,16 +30,6 @@ func (e AbsoluteError) Is(target error) bool {
// Absolute holds a pathname checked to be absolute. // Absolute holds a pathname checked to be absolute.
type Absolute struct{ pathname unique.Handle[string] } type Absolute struct{ pathname unique.Handle[string] }
var (
_ encoding.TextAppender = new(Absolute)
_ encoding.TextMarshaler = new(Absolute)
_ encoding.TextUnmarshaler = new(Absolute)
_ encoding.BinaryAppender = new(Absolute)
_ encoding.BinaryMarshaler = new(Absolute)
_ encoding.BinaryUnmarshaler = new(Absolute)
)
// ok returns whether [Absolute] is not the zero value. // ok returns whether [Absolute] is not the zero value.
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) } func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
@@ -94,16 +84,13 @@ func (a *Absolute) Append(elem ...string) *Absolute {
// Dir calls [filepath.Dir] with [Absolute] as its argument. // Dir calls [filepath.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) } func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }
// AppendText appends the checked pathname. // GobEncode returns the checked pathname.
func (a *Absolute) AppendText(data []byte) ([]byte, error) { func (a *Absolute) GobEncode() ([]byte, error) {
return append(data, a.String()...), nil return []byte(a.String()), nil
} }
// MarshalText returns the checked pathname. // GobDecode stores data if it represents an absolute pathname.
func (a *Absolute) MarshalText() ([]byte, error) { return a.AppendText(nil) } func (a *Absolute) GobDecode(data []byte) error {
// UnmarshalText stores data if it represents an absolute pathname.
func (a *Absolute) UnmarshalText(data []byte) error {
pathname := string(data) pathname := string(data)
if !filepath.IsAbs(pathname) { if !filepath.IsAbs(pathname) {
return AbsoluteError(pathname) return AbsoluteError(pathname)
@@ -112,9 +99,23 @@ func (a *Absolute) UnmarshalText(data []byte) error {
return nil return nil
} }
func (a *Absolute) AppendBinary(data []byte) ([]byte, error) { return a.AppendText(data) } // MarshalJSON returns a JSON representation of the checked pathname.
func (a *Absolute) MarshalBinary() ([]byte, error) { return a.MarshalText() } func (a *Absolute) MarshalJSON() ([]byte, error) {
func (a *Absolute) UnmarshalBinary(data []byte) error { return a.UnmarshalText(data) } return json.Marshal(a.String())
}
// UnmarshalJSON stores data if it represents an absolute pathname.
func (a *Absolute) UnmarshalJSON(data []byte) error {
var pathname string
if err := json.Unmarshal(data, &pathname); err != nil {
return err
}
if !filepath.IsAbs(pathname) {
return AbsoluteError(pathname)
}
a.pathname = unique.Make(pathname)
return nil
}
// SortAbs calls [slices.SortFunc] for a slice of [Absolute]. // SortAbs calls [slices.SortFunc] for a slice of [Absolute].
func SortAbs(x []*Absolute) { func SortAbs(x []*Absolute) {
+15 -6
View File
@@ -170,20 +170,20 @@ func TestCodecAbsolute(t *testing.T) {
{"good", MustAbs("/etc"), {"good", MustAbs("/etc"),
nil, nil,
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc", "\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00", ",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00",
`"/etc"`, `{"val":"/etc","magic":3236757504}`}, `"/etc"`, `{"val":"/etc","magic":3236757504}`},
{"not absolute", nil, {"not absolute", nil,
AbsoluteError("etc"), AbsoluteError("etc"),
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc", "\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00", ",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
`"etc"`, `{"val":"etc","magic":3236757504}`}, `"etc"`, `{"val":"etc","magic":3236757504}`},
{"zero", nil, {"zero", nil,
new(AbsoluteError), new(AbsoluteError),
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00", "\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00", ",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
`""`, `{"val":"","magic":3236757504}`}, `""`, `{"val":"","magic":3236757504}`},
} }
@@ -347,6 +347,15 @@ func TestCodecAbsolute(t *testing.T) {
}) })
}) })
} }
t.Run("json passthrough", func(t *testing.T) {
t.Parallel()
wantErr := "invalid character ':' looking for beginning of value"
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
}
})
} }
func TestAbsoluteWrap(t *testing.T) { func TestAbsoluteWrap(t *testing.T) {
-237
View File
@@ -1,237 +0,0 @@
//go:build dist
package main
import (
"archive/tar"
"compress/gzip"
"context"
"crypto/sha512"
_ "embed"
"encoding/hex"
"fmt"
"io"
"io/fs"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
)
// getenv looks up an environment variable, and returns fallback if it is unset.
func getenv(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok {
return v
}
return fallback
}
// mustRun runs a command with the current process's environment and panics
// on error or non-zero exit code.
func mustRun(ctx context.Context, name string, arg ...string) {
cmd := exec.CommandContext(ctx, name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
//go:embed comp/_hakurei
var comp []byte
func main() {
fmt.Println()
log.SetFlags(0)
log.SetPrefix("# ")
version := getenv("HAKUREI_VERSION", "untagged")
prefix := getenv("PREFIX", "/usr")
destdir := getenv("DESTDIR", "dist")
if err := os.MkdirAll(destdir, 0755); err != nil {
log.Fatal(err)
}
s, err := os.MkdirTemp(destdir, ".dist.*")
if err != nil {
log.Fatal(err)
}
defer func() {
var code int
if err = os.RemoveAll(s); err != nil {
code = 1
log.Println(err)
}
if r := recover(); r != nil {
code = 1
log.Println(r)
}
os.Exit(code)
}()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
log.Println("Building hakurei.")
mustRun(ctx, "go", "generate", "./...")
mustRun(
ctx, "go", "build",
"-trimpath",
"-v", "-o", s,
"-ldflags=-s -w "+
"-buildid= -linkmode external -extldflags=-static "+
"-X hakurei.app/internal/info.buildVersion="+version+" "+
"-X hakurei.app/internal/info.hakureiPath="+prefix+"/bin/hakurei "+
"-X hakurei.app/internal/info.hsuPath="+prefix+"/bin/hsu "+
"-X main.hakureiPath="+prefix+"/bin/hakurei",
"./...",
)
fmt.Println()
log.Println("Testing Hakurei.")
mustRun(
ctx, "go", "test",
"-ldflags=-buildid= -linkmode external -extldflags=-static",
"./...",
)
fmt.Println()
log.Println("Creating distribution.")
const suffix = ".tar.gz"
distName := "hakurei-" + version + "-" + runtime.GOARCH
var f *os.File
if f, err = os.OpenFile(
filepath.Join(s, distName+suffix),
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
0644,
); err != nil {
panic(err)
}
defer func() {
if f == nil {
return
}
if err = f.Close(); err != nil {
log.Println(err)
}
}()
h := sha512.New()
gw := gzip.NewWriter(io.MultiWriter(f, h))
tw := tar.NewWriter(gw)
mustWriteHeader := func(name string, size int64, mode os.FileMode) {
header := tar.Header{
Name: filepath.Join(distName, name),
Size: size,
Mode: int64(mode),
Uname: "root",
Gname: "root",
}
if mode&os.ModeDir != 0 {
header.Typeflag = tar.TypeDir
fmt.Printf("%s %s\n", mode, name)
} else {
header.Typeflag = tar.TypeReg
fmt.Printf("%s %s (%d bytes)\n", mode, name, size)
}
if err = tw.WriteHeader(&header); err != nil {
panic(err)
}
}
mustWriteFile := func(name string, data []byte, mode os.FileMode) {
mustWriteHeader(name, int64(len(data)), mode)
if mode&os.ModeDir != 0 {
return
}
if _, err = tw.Write(data); err != nil {
panic(err)
}
}
mustWriteFromPath := func(dst, src string, mode os.FileMode) {
var r *os.File
if r, err = os.Open(src); err != nil {
panic(err)
}
var fi os.FileInfo
if fi, err = r.Stat(); err != nil {
_ = r.Close()
panic(err)
}
if mode == 0 {
mode = fi.Mode()
}
mustWriteHeader(dst, fi.Size(), mode)
if _, err = io.Copy(tw, r); err != nil {
_ = r.Close()
panic(err)
} else if err = r.Close(); err != nil {
panic(err)
}
}
mustWriteFile(".", nil, fs.ModeDir|0755)
mustWriteFile("comp/", nil, os.ModeDir|0755)
mustWriteFile("comp/_hakurei", comp, 0644)
mustWriteFile("install.sh", []byte(`#!/bin/sh -e
cd "$(dirname -- "$0")" || exit 1
install -vDm0755 "bin/hakurei" "${DESTDIR}`+prefix+`/bin/hakurei"
install -vDm0755 "bin/sharefs" "${DESTDIR}`+prefix+`/bin/sharefs"
install -vDm4511 "bin/hsu" "${DESTDIR}`+prefix+`/bin/hsu"
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
fi
install -vDm0644 "comp/_hakurei" "${DESTDIR}`+prefix+`/share/zsh/site-functions/_hakurei"
`), 0755)
mustWriteFromPath("README.md", "README.md", 0)
mustWriteFile("hsurc.default", []byte("1000 0"), 0400)
mustWriteFromPath("bin/hsu", filepath.Join(s, "hsu"), 04511)
for _, name := range []string{
"hakurei",
"sharefs",
} {
mustWriteFromPath(
filepath.Join("bin", name),
filepath.Join(s, name),
0,
)
}
if err = tw.Close(); err != nil {
panic(err)
} else if err = gw.Close(); err != nil {
panic(err)
} else if err = f.Close(); err != nil {
panic(err)
}
f = nil
if err = os.WriteFile(
filepath.Join(destdir, distName+suffix+".sha512"),
append(hex.AppendEncode(nil, h.Sum(nil)), " "+distName+suffix+"\n"...),
0644,
); err != nil {
panic(err)
}
if err = os.Rename(
filepath.Join(s, distName+suffix),
filepath.Join(destdir, distName+suffix),
); err != nil {
panic(err)
}
}
+6 -13
View File
@@ -38,9 +38,8 @@ var errSuccess = errors.New("success")
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 (
flagVerbose bool flagVerbose bool
flagInsecure bool flagJSON bool
flagJSON bool
) )
c := command.New(out, log.Printf, "hakurei", func([]string) error { c := command.New(out, log.Printf, "hakurei", func([]string) error {
msg.SwapVerbose(flagVerbose) msg.SwapVerbose(flagVerbose)
@@ -58,7 +57,6 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
return nil return nil
}). }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity"). Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagInsecure, "insecure", command.BoolFlag(false), "Allow use of insecure compatibility options").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable") Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess }) c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
@@ -77,12 +75,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
config.Container.Args = append(config.Container.Args, args[1:]...) config.Container.Args = append(config.Container.Args, args[1:]...)
} }
var flags int outcome.Main(ctx, msg, config, flagIdentifierFile)
if flagInsecure {
flags |= hst.VAllowInsecure
}
outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
panic("unreachable") panic("unreachable")
}). }).
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1), Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
@@ -152,7 +145,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
} }
} }
var et hst.Enablements var et hst.Enablement
if flagWayland { if flagWayland {
et |= hst.EWayland et |= hst.EWayland
} }
@@ -170,7 +163,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
ID: flagID, ID: flagID,
Identity: flagIdentity, Identity: flagIdentity,
Groups: flagGroups, Groups: flagGroups,
Enablements: &et, Enablements: hst.NewEnablements(et),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
@@ -289,7 +282,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
} }
} }
outcome.Main(ctx, msg, &config, 0, -1) outcome.Main(ctx, msg, &config, -1)
panic("unreachable") panic("unreachable")
}). }).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"), Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
+1 -1
View File
@@ -20,7 +20,7 @@ func TestHelp(t *testing.T) {
}{ }{
{ {
"main", []string{}, ` "main", []string{}, `
Usage: hakurei [-h | --help] [-v] [--insecure] [--json] COMMAND [OPTIONS] Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Commands: Commands:
run Load and start container from configuration file run Load and start container from configuration file
+1 -1
View File
@@ -56,7 +56,7 @@ func printShowInstance(
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
if err := config.Validate(hst.VAllowInsecure); err != nil { if err := config.Validate(); err != nil {
valid = false valid = false
if m, ok := message.GetMessage(err); ok { if m, ok := message.GetMessage(err); ok {
mustPrint(output, "Error: "+m+"!\n\n") mustPrint(output, "Error: "+m+"!\n\n")
+1 -1
View File
@@ -32,7 +32,7 @@ var (
PID: 0xbeef, PID: 0xbeef,
ShimPID: 0xcafe, ShimPID: 0xcafe,
Config: &hst.Config{ Config: &hst.Config{
Enablements: new(hst.EWayland | hst.EPipeWire), 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"),
+1 -13
View File
@@ -73,8 +73,6 @@ func main() {
flagCures int flagCures int
flagBase string flagBase string
flagIdle bool flagIdle bool
flagHostAbstract bool
) )
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) { c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) {
msg.SwapVerbose(!flagQuiet) msg.SwapVerbose(!flagQuiet)
@@ -93,10 +91,7 @@ func main() {
var flags int var flags int
if flagIdle { if flagIdle {
flags |= pkg.CSchedIdle flags &= pkg.CSchedIdle
}
if flagHostAbstract {
flags |= pkg.CHostAbstract
} }
cache, err = pkg.Open(ctx, msg, flags, flagCures, base) cache, err = pkg.Open(ctx, msg, flags, flagCures, base)
@@ -117,13 +112,6 @@ func main() {
&flagIdle, &flagIdle,
"sched-idle", command.BoolFlag(false), "sched-idle", command.BoolFlag(false),
"Set SCHED_IDLE scheduling policy", "Set SCHED_IDLE scheduling policy",
).Flag(
&flagHostAbstract,
"host-abstract", command.BoolFlag(
os.Getenv("MBF_HOST_ABSTRACT") != "",
),
"Do not restrict networked cure containers from connecting to host "+
"abstract UNIX sockets",
) )
{ {
+2 -2
View File
@@ -7,8 +7,8 @@
#endif #endif
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */ #define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
#define SHAREFS_PERM_DIR 0770 /* permission bits for directories presented to userspace */ #define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
#define SHAREFS_PERM_REG 0660 /* permission bits for regular files 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 */ #define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
/* sharefs_private is populated by sharefs_init and contains process-wide context */ /* sharefs_private is populated by sharefs_init and contains process-wide context */
+7 -10
View File
@@ -19,6 +19,7 @@ import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"os/exec" "os/exec"
@@ -469,14 +470,13 @@ func _main(s ...string) (exitCode int) {
os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"), os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"),
)) ))
var setupPipe [2]*os.File var setupWriter io.WriteCloser
if r, w, err := os.Pipe(); err != nil { if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
log.Println(err) log.Println(err)
return 5 return 5
} else { } else {
z.Args = append(z.Args, "-osetup="+strconv.Itoa(3+len(z.ExtraFiles))) z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
z.ExtraFiles = append(z.ExtraFiles, r) setupWriter = w
setupPipe[0], setupPipe[1] = r, w
} }
if err := z.Start(); err != nil { if err := z.Start(); err != nil {
@@ -487,9 +487,6 @@ func _main(s ...string) (exitCode int) {
} }
return 5 return 5
} }
if err := setupPipe[0].Close(); err != nil {
log.Println(err)
}
if err := z.Serve(); err != nil { if err := z.Serve(); err != nil {
if m, ok := message.GetMessage(err); ok { if m, ok := message.GetMessage(err); ok {
log.Println(m) log.Println(m)
@@ -499,10 +496,10 @@ func _main(s ...string) (exitCode int) {
return 5 return 5
} }
if err := gob.NewEncoder(setupPipe[1]).Encode(&setup); err != nil { if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
log.Println(err) log.Println(err)
return 5 return 5
} else if err = setupPipe[1].Close(); err != nil { } else if err = setupWriter.Close(); err != nil {
log.Println(err) log.Println(err)
} }
-122
View File
@@ -1,122 +0,0 @@
//go:build raceattr
// The raceattr program reproduces vfs inode file attribute race.
//
// Even though libfuse high-level API presents the address of a struct stat
// alongside struct fuse_context, file attributes are actually inherent to the
// inode, instead of the specific call from userspace. The kernel implementation
// in fs/fuse/xattr.c appears to make stale data in the inode (set by a previous
// call) impossible or very unlikely to reach userspace via the stat family of
// syscalls. However, when using default_permissions to have the VFS check
// permissions, this race still happens, despite the resulting struct stat being
// correct when overriding the check via capabilities otherwise.
//
// This program reproduces the failure, but because of its continuous nature, it
// is provided independent of the vm integration test suite.
package main
import (
"context"
"flag"
"log"
"os"
"os/signal"
"runtime"
"sync"
"sync/atomic"
"syscall"
)
func newStatAs(
ctx context.Context, cancel context.CancelFunc,
n *atomic.Uint64, ok *atomic.Bool,
uid uint32, pathname string,
continuous bool,
) func() {
return func() {
runtime.LockOSThread()
defer cancel()
if _, _, errno := syscall.Syscall(
syscall.SYS_SETUID, uintptr(uid),
0, 0,
); errno != 0 {
cancel()
log.Printf("cannot set uid to %d: %s", uid, errno)
}
var stat syscall.Stat_t
for {
if ctx.Err() != nil {
return
}
if err := syscall.Lstat(pathname, &stat); err != nil {
// SHAREFS_PERM_DIR not world executable, or
// SHAREFS_PERM_REG not world readable
if !continuous {
cancel()
}
ok.Store(true)
log.Printf("uid %d: %v", uid, err)
} else if stat.Uid != uid {
// appears to be unreachable
if !continuous {
cancel()
}
ok.Store(true)
log.Printf("got uid %d instead of %d", stat.Uid, uid)
}
n.Add(1)
}
}
}
func main() {
log.SetFlags(0)
log.SetPrefix("raceattr: ")
p := flag.String("target", "/sdcard/raceattr", "pathname of test file")
u0 := flag.Int("uid0", 1<<10-1, "first uid")
u1 := flag.Int("uid1", 1<<10-2, "second uid")
count := flag.Int("count", 1, "threads per uid")
continuous := flag.Bool("continuous", false, "keep running even after reproduce")
flag.Parse()
if os.Geteuid() != 0 {
log.Fatal("this program must run as root")
}
ctx, cancel := signal.NotifyContext(
context.Background(),
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGHUP,
)
if err := os.WriteFile(*p, nil, 0); err != nil {
log.Fatal(err)
}
var (
wg sync.WaitGroup
n atomic.Uint64
ok atomic.Bool
)
if *count < 1 {
*count = 1
}
for range *count {
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u0), *p, *continuous))
if *u1 >= 0 {
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u1), *p, *continuous))
}
}
wg.Wait()
if !*continuous && ok.Load() {
log.Printf("reproduced after %d calls", n.Load())
}
}
+26 -46
View File
@@ -21,7 +21,6 @@ import (
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/internal/landlock"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -29,6 +28,9 @@ const (
// CancelSignal is the signal expected by container init on context cancel. // CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal. // A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGUSR2 CancelSignal = SIGUSR2
// Timeout for writing initParams to Container.setup.
initSetupTimeout = 5 * time.Second
) )
type ( type (
@@ -51,7 +53,7 @@ type (
ExtraFiles []*os.File ExtraFiles []*os.File
// Write end of a pipe connected to the init to deliver [Params]. // Write end of a pipe connected to the init to deliver [Params].
setup [2]*os.File setup *os.File
// Cancels the context passed to the underlying cmd. // Cancels the context passed to the underlying cmd.
cancel context.CancelFunc cancel context.CancelFunc
// Closed after Wait returns. Keeps the spawning thread alive. // Closed after Wait returns. Keeps the spawning thread alive.
@@ -285,16 +287,14 @@ func (p *Container) Start() error {
} }
// place setup pipe before user supplied extra files, this is later restored by init // place setup pipe before user supplied extra files, this is later restored by init
if r, w, err := os.Pipe(); err != nil { if fd, f, err := Setup(&p.cmd.ExtraFiles); err != nil {
return &StartError{ return &StartError{
Fatal: true, Fatal: true,
Step: "set up params stream", Step: "set up params stream",
Err: err, Err: err,
} }
} else { } else {
fd := 3 + len(p.cmd.ExtraFiles) p.setup = f
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, r)
p.setup[0], p.setup[1] = r, w
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)} p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
} }
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...) p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
@@ -308,7 +308,7 @@ func (p *Container) Start() error {
done <- func() error { done <- func() error {
// PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes // PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes
// created from the calling thread // created from the calling thread
if err := setNoNewPrivs(); err != nil { if err := SetNoNewPrivs(); err != nil {
return &StartError{ return &StartError{
Fatal: true, Fatal: true,
Step: "prctl(PR_SET_NO_NEW_PRIVS)", Step: "prctl(PR_SET_NO_NEW_PRIVS)",
@@ -318,17 +318,15 @@ func (p *Container) Start() error {
// landlock: depends on per-thread state but acts on a process group // landlock: depends on per-thread state but acts on a process group
{ {
rulesetAttr := &landlock.RulesetAttr{ rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL}
Scoped: landlock.LANDLOCK_SCOPE_SIGNAL,
}
if !p.HostAbstract { if !p.HostAbstract {
rulesetAttr.Scoped |= landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
} }
if abi, err := landlock.GetABI(); err != nil { if abi, err := LandlockGetABI(); err != nil {
if p.HostAbstract || !p.HostNet { if p.HostAbstract {
// landlock can be skipped here as it restricts access // landlock can be skipped here as it restricts access
// to resources already covered by namespaces (pid, net) // to resources already covered by namespaces (pid)
goto landlockOut goto landlockOut
} }
return &StartError{Step: "get landlock ABI", Err: err} return &StartError{Step: "get landlock ABI", Err: err}
@@ -354,7 +352,7 @@ func (p *Container) Start() error {
} }
} else { } else {
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr) p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = landlock.RestrictSelf(rulesetFd, 0); err != nil { if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd) _ = Close(rulesetFd)
return &StartError{ return &StartError{
Fatal: true, Fatal: true,
@@ -430,33 +428,24 @@ func (p *Container) Start() error {
// Serve serves [Container.Params] to the container init. // Serve serves [Container.Params] to the container init.
// //
// Serve must only be called once. // Serve must only be called once.
func (p *Container) Serve() (err error) { func (p *Container) Serve() error {
if p.setup[0] == nil || p.setup[1] == nil { if p.setup == nil {
panic("invalid serve") panic("invalid serve")
} }
done := make(chan struct{}) setup := p.setup
defer func() { p.setup = nil
if closeErr := p.setup[1].Close(); err == nil { if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil {
err = closeErr
}
if err != nil {
p.cancel()
}
close(done)
p.setup[0], p.setup[1] = nil, nil
}()
if err = p.setup[0].Close(); err != nil {
return &StartError{ return &StartError{
Fatal: true, Fatal: true,
Step: "close read end of init pipe", Step: "set init pipe deadline",
Err: err, Err: err,
Passthrough: true, Passthrough: true,
} }
} }
if p.Path == nil { if p.Path == nil {
p.cancel()
return &StartError{ return &StartError{
Step: "invalid executable pathname", Step: "invalid executable pathname",
Err: EINVAL, Err: EINVAL,
@@ -472,27 +461,18 @@ func (p *Container) Serve() (err error) {
p.SeccompRules = make([]std.NativeRule, 0) p.SeccompRules = make([]std.NativeRule, 0)
} }
t := time.Now().UTC() err := gob.NewEncoder(setup).Encode(&initParams{
go func(f *os.File) {
select {
case <-p.ctx.Done():
if cancelErr := f.SetWriteDeadline(t); cancelErr != nil {
p.msg.Verbose(err)
}
case <-done:
p.msg.Verbose("setup payload took", time.Since(t))
return
}
}(p.setup[1])
return gob.NewEncoder(p.setup[1]).Encode(&initParams{
p.Params, p.Params,
Getuid(), Getuid(),
Getgid(), Getgid(),
len(p.ExtraFiles), len(p.ExtraFiles),
p.msg.IsVerbose(), p.msg.IsVerbose(),
}) })
_ = setup.Close()
if err != nil {
p.cancel()
}
return err
} }
// Wait blocks until the container init process to exit and releases any // Wait blocks until the container init process to exit and releases any
+2 -14
View File
@@ -25,9 +25,6 @@ import (
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/info"
"hakurei.app/internal/landlock"
"hakurei.app/internal/params"
"hakurei.app/ldd" "hakurei.app/ldd"
"hakurei.app/message" "hakurei.app/message"
"hakurei.app/vfs" "hakurei.app/vfs"
@@ -86,9 +83,9 @@ func TestStartError(t *testing.T) {
{"params env", &container.StartError{ {"params env", &container.StartError{
Fatal: true, Fatal: true,
Step: "set up params stream", Step: "set up params stream",
Err: params.ErrReceiveEnv, Err: container.ErrReceiveEnv,
}, "set up params stream: environment variable not set", }, "set up params stream: environment variable not set",
params.ErrReceiveEnv, syscall.EBADF, container.ErrReceiveEnv, syscall.EBADF,
"cannot set up params stream: environment variable not set"}, "cannot set up params stream: environment variable not set"},
{"params", &container.StartError{ {"params", &container.StartError{
@@ -456,15 +453,6 @@ func TestContainer(t *testing.T) {
c.SeccompDisable = !tc.filter c.SeccompDisable = !tc.filter
c.RetainSession = tc.session c.RetainSession = tc.session
c.HostNet = tc.net c.HostNet = tc.net
if info.CanDegrade {
if _, err := landlock.GetABI(); err != nil {
if !errors.Is(err, syscall.ENOSYS) {
t.Fatalf("LandlockGetABI: error = %v", err)
}
c.HostAbstract = true
t.Log("Landlock LSM is unavailable, enabling HostAbstract")
}
}
c. c.
Readonly(check.MustAbs(pathReadonly), 0755). Readonly(check.MustAbs(pathReadonly), 0755).
+4 -5
View File
@@ -16,7 +16,6 @@ import (
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/internal/netlink" "hakurei.app/internal/netlink"
"hakurei.app/internal/params"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -57,7 +56,7 @@ type syscallDispatcher interface {
// isatty provides [Isatty]. // isatty provides [Isatty].
isatty(fd int) bool isatty(fd int) bool
// receive provides [Receive]. // receive provides [Receive].
receive(key string, e any, fdp *int) (closeFunc func() error, err error) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// bindMount provides procPaths.bindMount. // bindMount provides procPaths.bindMount.
bindMount(msg message.Msg, source, target string, flags uintptr) error bindMount(msg message.Msg, source, target string, flags uintptr) error
@@ -148,7 +147,7 @@ func (direct) lockOSThread() { runtime.LockOSThread() }
func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) } func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) } func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error { return setNoNewPrivs() } func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) } func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) } func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
@@ -156,8 +155,8 @@ func (direct) capBoundingSetDrop(cap uintptr) error { return capBound
func (direct) capAmbientClearAll() error { return capAmbientClearAll() } func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) } func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
func (direct) isatty(fd int) bool { return ext.Isatty(fd) } func (direct) isatty(fd int) bool { return ext.Isatty(fd) }
func (direct) receive(key string, e any, fdp *int) (func() error, error) { func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return params.Receive(key, e, fdp) return Receive(key, e, fdp)
} }
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error { func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
+3 -10
View File
@@ -390,7 +390,7 @@ func (k *kstub) isatty(fd int) bool {
return expect.Ret.(bool) return expect.Ret.(bool)
} }
func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, err error) { func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
k.Helper() k.Helper()
expect := k.Expects("receive") expect := k.Expects("receive")
@@ -408,17 +408,10 @@ func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, er
} }
return nil return nil
} }
// avoid changing test cases
var fdpComp *uintptr
if fdp != nil {
fdpComp = new(uintptr(*fdp))
}
err = expect.Error( err = expect.Error(
stub.CheckArg(k.Stub, "key", key, 0), stub.CheckArg(k.Stub, "key", key, 0),
stub.CheckArgReflect(k.Stub, "e", e, 1), stub.CheckArgReflect(k.Stub, "e", e, 1),
stub.CheckArgReflect(k.Stub, "fdp", fdpComp, 2)) stub.CheckArgReflect(k.Stub, "fdp", fdp, 2))
// 3 is unused so stores params // 3 is unused so stores params
if expect.Args[3] != nil { if expect.Args[3] != nil {
@@ -433,7 +426,7 @@ func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, er
if expect.Args[4] != nil { if expect.Args[4] != nil {
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 { if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
if fdp != nil { if fdp != nil {
*fdp = int(v) *fdp = v
} }
} }
} }
+37 -36
View File
@@ -19,7 +19,6 @@ import (
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/internal/params"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -148,33 +147,35 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
} }
var ( var (
param initParams params initParams
closeSetup func() error closeSetup func() error
setupFd int setupFd uintptr
offsetSetup int
) )
if f, err := k.receive(setupEnv, &param, &setupFd); err != nil { if f, err := k.receive(setupEnv, &params, &setupFd); err != nil {
if errors.Is(err, EBADF) { if errors.Is(err, EBADF) {
k.fatal(msg, "invalid setup descriptor") k.fatal(msg, "invalid setup descriptor")
} }
if errors.Is(err, params.ErrReceiveEnv) { if errors.Is(err, ErrReceiveEnv) {
k.fatal(msg, setupEnv+" not set") k.fatal(msg, setupEnv+" not set")
} }
k.fatalf(msg, "cannot decode init setup payload: %v", err) k.fatalf(msg, "cannot decode init setup payload: %v", err)
} else { } else {
if param.Ops == nil { if params.Ops == nil {
k.fatal(msg, "invalid setup parameters") k.fatal(msg, "invalid setup parameters")
} }
if param.ParentPerm == 0 { if params.ParentPerm == 0 {
param.ParentPerm = 0755 params.ParentPerm = 0755
} }
msg.SwapVerbose(param.Verbose) msg.SwapVerbose(params.Verbose)
msg.Verbose("received setup parameters") msg.Verbose("received setup parameters")
closeSetup = f closeSetup = f
offsetSetup = int(setupFd + 1)
} }
if !param.HostNet { if !params.HostNet {
ctx, cancel := signal.NotifyContext(context.Background(), CancelSignal, ctx, cancel := signal.NotifyContext(context.Background(), CancelSignal,
os.Interrupt, SIGTERM, SIGQUIT) os.Interrupt, SIGTERM, SIGQUIT)
defer cancel() // for panics defer cancel() // for panics
@@ -187,7 +188,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err) k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
} }
if err := k.writeFile(fhs.Proc+"self/uid_map", if err := k.writeFile(fhs.Proc+"self/uid_map",
append([]byte{}, strconv.Itoa(param.Uid)+" "+strconv.Itoa(param.HostUid)+" 1\n"...), append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
0); err != nil { 0); err != nil {
k.fatalf(msg, "%v", err) k.fatalf(msg, "%v", err)
} }
@@ -197,7 +198,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "%v", err) k.fatalf(msg, "%v", err)
} }
if err := k.writeFile(fhs.Proc+"self/gid_map", if err := k.writeFile(fhs.Proc+"self/gid_map",
append([]byte{}, strconv.Itoa(param.Gid)+" "+strconv.Itoa(param.HostGid)+" 1\n"...), append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
0); err != nil { 0); err != nil {
k.fatalf(msg, "%v", err) k.fatalf(msg, "%v", err)
} }
@@ -206,8 +207,8 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
} }
oldmask := k.umask(0) oldmask := k.umask(0)
if param.Hostname != "" { if params.Hostname != "" {
if err := k.sethostname([]byte(param.Hostname)); err != nil { if err := k.sethostname([]byte(params.Hostname)); err != nil {
k.fatalf(msg, "cannot set hostname: %v", err) k.fatalf(msg, "cannot set hostname: %v", err)
} }
} }
@@ -220,7 +221,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
state := &setupState{process: make(map[int]WaitStatus), Params: &param.Params, Msg: msg, Context: ctx} state := &setupState{process: make(map[int]WaitStatus), Params: &params.Params, Msg: msg, Context: ctx}
defer cancel() defer cancel()
/* early is called right before pivot_root into intermediate root; /* early is called right before pivot_root into intermediate root;
@@ -228,7 +229,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
difficult to obtain via library functions after pivot_root, and difficult to obtain via library functions after pivot_root, and
implementations are expected to avoid changing the state of the mount implementations are expected to avoid changing the state of the mount
namespace */ namespace */
for i, op := range *param.Ops { for i, op := range *params.Ops {
if op == nil || !op.Valid() { if op == nil || !op.Valid() {
k.fatalf(msg, "invalid op at index %d", i) k.fatalf(msg, "invalid op at index %d", i)
} }
@@ -271,7 +272,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
step sets up the container filesystem, and implementations are expected to step sets up the container filesystem, and implementations are expected to
keep the host root and sysroot mount points intact but otherwise can do keep the host root and sysroot mount points intact but otherwise can do
whatever they need to. Calling chdir is allowed but discouraged. */ whatever they need to. Calling chdir is allowed but discouraged. */
for i, op := range *param.Ops { for i, op := range *params.Ops {
// ops already checked during early setup // ops already checked during early setup
if prefix, ok := op.prefix(); ok { if prefix, ok := op.prefix(); ok {
msg.Verbosef("%s %s", prefix, op) msg.Verbosef("%s %s", prefix, op)
@@ -327,7 +328,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot clear the ambient capability set: %v", err) k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
} }
for i := uintptr(0); i <= lastcap; i++ { for i := uintptr(0); i <= lastcap; i++ {
if param.Privileged && i == CAP_SYS_ADMIN { if params.Privileged && i == CAP_SYS_ADMIN {
continue continue
} }
if err := k.capBoundingSetDrop(i); err != nil { if err := k.capBoundingSetDrop(i); err != nil {
@@ -336,7 +337,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
} }
var keep [2]uint32 var keep [2]uint32
if param.Privileged { if params.Privileged {
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN) keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil { if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
@@ -350,13 +351,13 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot capset: %v", err) k.fatalf(msg, "cannot capset: %v", err)
} }
if !param.SeccompDisable { if !params.SeccompDisable {
rules := param.SeccompRules rules := params.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets if len(rules) == 0 { // non-empty rules slice always overrides presets
msg.Verbosef("resolving presets %#x", param.SeccompPresets) msg.Verbosef("resolving presets %#x", params.SeccompPresets)
rules = seccomp.Preset(param.SeccompPresets, param.SeccompFlags) rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
} }
if err := k.seccompLoad(rules, param.SeccompFlags); err != nil { if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
// this also indirectly asserts PR_SET_NO_NEW_PRIVS // this also indirectly asserts PR_SET_NO_NEW_PRIVS
k.fatalf(msg, "cannot load syscall filter: %v", err) k.fatalf(msg, "cannot load syscall filter: %v", err)
} }
@@ -365,10 +366,10 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
msg.Verbose("syscall filter not configured") msg.Verbose("syscall filter not configured")
} }
extraFiles := make([]*os.File, param.Count) extraFiles := make([]*os.File, params.Count)
for i := range extraFiles { for i := range extraFiles {
// setup fd is placed before all extra files // setup fd is placed before all extra files
extraFiles[i] = k.newFile(uintptr(setupFd+1+i), "extra file "+strconv.Itoa(i)) extraFiles[i] = k.newFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
} }
k.umask(oldmask) k.umask(oldmask)
@@ -446,7 +447,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
// called right before startup of initial process, all state changes to the // called right before startup of initial process, all state changes to the
// current process is prohibited during late // current process is prohibited during late
for i, op := range *param.Ops { for i, op := range *params.Ops {
// ops already checked during early setup // ops already checked during early setup
if err := op.late(state, k); err != nil { if err := op.late(state, k); err != nil {
if m, ok := messageFromError(err); ok { if m, ok := messageFromError(err); ok {
@@ -467,14 +468,14 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot close setup pipe: %v", err) k.fatalf(msg, "cannot close setup pipe: %v", err)
} }
cmd := exec.Command(param.Path.String()) cmd := exec.Command(params.Path.String())
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = param.Args cmd.Args = params.Args
cmd.Env = param.Env cmd.Env = params.Env
cmd.ExtraFiles = extraFiles cmd.ExtraFiles = extraFiles
cmd.Dir = param.Dir.String() cmd.Dir = params.Dir.String()
msg.Verbosef("starting initial process %s", param.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)
} }
@@ -492,9 +493,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
for { for {
select { select {
case s := <-sig: case s := <-sig:
if s == CancelSignal && param.ForwardCancel && cmd.Process != nil { if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
msg.Verbose("forwarding context cancellation") msg.Verbose("forwarding context cancellation")
if err := k.signal(cmd, os.Interrupt); err != nil && !errors.Is(err, os.ErrProcessDone) { if err := k.signal(cmd, os.Interrupt); err != nil {
k.printf(msg, "cannot forward cancellation: %v", err) k.printf(msg, "cannot forward cancellation: %v", err)
} }
continue continue
@@ -524,7 +525,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
cancel() cancel()
// start timeout early // start timeout early
go func() { time.Sleep(param.AdoptWaitDelay); close(timeout) }() go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
// close initial process files; this also keeps them alive // close initial process files; this also keeps them alive
for _, f := range extraFiles { for _, f := range extraFiles {
+1 -2
View File
@@ -10,7 +10,6 @@ import (
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/internal/params"
"hakurei.app/internal/stub" "hakurei.app/internal/stub"
) )
@@ -41,7 +40,7 @@ func TestInitEntrypoint(t *testing.T) {
call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil), call("getpid", stub.ExpectArgs{}, 1, nil),
call("setPtracer", stub.ExpectArgs{uintptr(0)}, nil, nil), call("setPtracer", stub.ExpectArgs{uintptr(0)}, nil, nil),
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, params.ErrReceiveEnv), call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, ErrReceiveEnv),
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SETUP not set"}}, nil, nil), call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SETUP not set"}}, nil, nil),
}, },
}, nil}, }, nil},
@@ -1,4 +1,4 @@
package landlock package container
import ( import (
"strings" "strings"
@@ -14,11 +14,11 @@ const (
LANDLOCK_CREATE_RULESET_VERSION = 1 << iota LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
) )
// AccessFS is bitmask of handled filesystem actions. // LandlockAccessFS is bitmask of handled filesystem actions.
type AccessFS uint64 type LandlockAccessFS uint64
const ( const (
LANDLOCK_ACCESS_FS_EXECUTE AccessFS = 1 << iota LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 1 << iota
LANDLOCK_ACCESS_FS_WRITE_FILE LANDLOCK_ACCESS_FS_WRITE_FILE
LANDLOCK_ACCESS_FS_READ_FILE LANDLOCK_ACCESS_FS_READ_FILE
LANDLOCK_ACCESS_FS_READ_DIR LANDLOCK_ACCESS_FS_READ_DIR
@@ -38,8 +38,8 @@ const (
_LANDLOCK_ACCESS_FS_DELIM _LANDLOCK_ACCESS_FS_DELIM
) )
// String returns a space-separated string of [AccessFS] flags. // String returns a space-separated string of [LandlockAccessFS] flags.
func (f AccessFS) String() string { func (f LandlockAccessFS) String() string {
switch f { switch f {
case LANDLOCK_ACCESS_FS_EXECUTE: case LANDLOCK_ACCESS_FS_EXECUTE:
return "execute" return "execute"
@@ -90,8 +90,8 @@ func (f AccessFS) String() string {
return "fs_ioctl_dev" return "fs_ioctl_dev"
default: default:
var c []AccessFS var c []LandlockAccessFS
for i := AccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 { for i := LandlockAccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
if f&i != 0 { if f&i != 0 {
c = append(c, i) c = append(c, i)
} }
@@ -107,18 +107,18 @@ func (f AccessFS) String() string {
} }
} }
// AccessNet is bitmask of handled network actions. // LandlockAccessNet is bitmask of handled network actions.
type AccessNet uint64 type LandlockAccessNet uint64
const ( const (
LANDLOCK_ACCESS_NET_BIND_TCP AccessNet = 1 << iota LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota
LANDLOCK_ACCESS_NET_CONNECT_TCP LANDLOCK_ACCESS_NET_CONNECT_TCP
_LANDLOCK_ACCESS_NET_DELIM _LANDLOCK_ACCESS_NET_DELIM
) )
// String returns a space-separated string of [AccessNet] flags. // String returns a space-separated string of [LandlockAccessNet] flags.
func (f AccessNet) String() string { func (f LandlockAccessNet) String() string {
switch f { switch f {
case LANDLOCK_ACCESS_NET_BIND_TCP: case LANDLOCK_ACCESS_NET_BIND_TCP:
return "bind_tcp" return "bind_tcp"
@@ -127,8 +127,8 @@ func (f AccessNet) String() string {
return "connect_tcp" return "connect_tcp"
default: default:
var c []AccessNet var c []LandlockAccessNet
for i := AccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 { for i := LandlockAccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
if f&i != 0 { if f&i != 0 {
c = append(c, i) c = append(c, i)
} }
@@ -144,18 +144,18 @@ func (f AccessNet) String() string {
} }
} }
// Scope is bitmask of scopes restricting a Landlock domain from accessing outside resources. // LandlockScope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
type Scope uint64 type LandlockScope uint64
const ( const (
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET Scope = 1 << iota LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota
LANDLOCK_SCOPE_SIGNAL LANDLOCK_SCOPE_SIGNAL
_LANDLOCK_SCOPE_DELIM _LANDLOCK_SCOPE_DELIM
) )
// String returns a space-separated string of [Scope] flags. // String returns a space-separated string of [LandlockScope] flags.
func (f Scope) String() string { func (f LandlockScope) String() string {
switch f { switch f {
case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET: case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET:
return "abstract_unix_socket" return "abstract_unix_socket"
@@ -164,8 +164,8 @@ func (f Scope) String() string {
return "signal" return "signal"
default: default:
var c []Scope var c []LandlockScope
for i := Scope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 { for i := LandlockScope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
if f&i != 0 { if f&i != 0 {
c = append(c, i) c = append(c, i)
} }
@@ -184,12 +184,12 @@ func (f Scope) String() string {
// RulesetAttr is equivalent to struct landlock_ruleset_attr. // RulesetAttr is equivalent to struct landlock_ruleset_attr.
type RulesetAttr struct { type RulesetAttr struct {
// Bitmask of handled filesystem actions. // Bitmask of handled filesystem actions.
HandledAccessFS AccessFS HandledAccessFS LandlockAccessFS
// Bitmask of handled network actions. // Bitmask of handled network actions.
HandledAccessNet AccessNet HandledAccessNet LandlockAccessNet
// Bitmask of scopes restricting a Landlock domain from accessing outside // Bitmask of scopes restricting a Landlock domain from accessing outside
// resources (e.g. IPCs). // resources (e.g. IPCs).
Scoped Scope Scoped LandlockScope
} }
// String returns a user-facing description of [RulesetAttr]. // String returns a user-facing description of [RulesetAttr].
@@ -239,13 +239,13 @@ func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
return fd, nil return fd, nil
} }
// GetABI returns the ABI version supported by the kernel. // LandlockGetABI returns the ABI version supported by the kernel.
func GetABI() (int, error) { func LandlockGetABI() (int, error) {
return (*RulesetAttr)(nil).Create(LANDLOCK_CREATE_RULESET_VERSION) return (*RulesetAttr)(nil).Create(LANDLOCK_CREATE_RULESET_VERSION)
} }
// RestrictSelf applies a loaded ruleset to the calling thread. // LandlockRestrictSelf applies a loaded ruleset to the calling thread.
func RestrictSelf(rulesetFd int, flags uintptr) error { func LandlockRestrictSelf(rulesetFd int, flags uintptr) error {
r, _, errno := syscall.Syscall( r, _, errno := syscall.Syscall(
ext.SYS_LANDLOCK_RESTRICT_SELF, ext.SYS_LANDLOCK_RESTRICT_SELF,
uintptr(rulesetFd), uintptr(rulesetFd),
+65
View File
@@ -0,0 +1,65 @@
package container_test
import (
"testing"
"unsafe"
"hakurei.app/container"
)
func TestLandlockString(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
rulesetAttr *container.RulesetAttr
want string
}{
{"nil", nil, "NULL"},
{"zero", new(container.RulesetAttr), "0"},
{"some", &container.RulesetAttr{Scoped: container.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
{"set", &container.RulesetAttr{
HandledAccessFS: container.LANDLOCK_ACCESS_FS_MAKE_SYM | container.LANDLOCK_ACCESS_FS_IOCTL_DEV | container.LANDLOCK_ACCESS_FS_WRITE_FILE,
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP,
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | container.LANDLOCK_SCOPE_SIGNAL,
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
{"all", &container.RulesetAttr{
HandledAccessFS: container.LANDLOCK_ACCESS_FS_EXECUTE |
container.LANDLOCK_ACCESS_FS_WRITE_FILE |
container.LANDLOCK_ACCESS_FS_READ_FILE |
container.LANDLOCK_ACCESS_FS_READ_DIR |
container.LANDLOCK_ACCESS_FS_REMOVE_DIR |
container.LANDLOCK_ACCESS_FS_REMOVE_FILE |
container.LANDLOCK_ACCESS_FS_MAKE_CHAR |
container.LANDLOCK_ACCESS_FS_MAKE_DIR |
container.LANDLOCK_ACCESS_FS_MAKE_REG |
container.LANDLOCK_ACCESS_FS_MAKE_SOCK |
container.LANDLOCK_ACCESS_FS_MAKE_FIFO |
container.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
container.LANDLOCK_ACCESS_FS_MAKE_SYM |
container.LANDLOCK_ACCESS_FS_REFER |
container.LANDLOCK_ACCESS_FS_TRUNCATE |
container.LANDLOCK_ACCESS_FS_IOCTL_DEV,
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP |
container.LANDLOCK_ACCESS_NET_CONNECT_TCP,
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
container.LANDLOCK_SCOPE_SIGNAL,
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.rulesetAttr.String(); got != tc.want {
t.Errorf("String: %s, want %s", got, tc.want)
}
})
}
}
func TestLandlockAttrSize(t *testing.T) {
t.Parallel()
want := 24
if got := unsafe.Sizeof(container.RulesetAttr{}); got != uintptr(want) {
t.Errorf("Sizeof: %d, want %d", got, want)
}
}
+47
View File
@@ -0,0 +1,47 @@
package container
import (
"encoding/gob"
"errors"
"os"
"strconv"
"syscall"
)
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
func Setup(extraFiles *[]*os.File) (int, *os.File, error) {
if r, w, err := os.Pipe(); err != nil {
return -1, nil, err
} else {
fd := 3 + len(*extraFiles)
*extraFiles = append(*extraFiles, r)
return fd, w, nil
}
}
var (
ErrReceiveEnv = errors.New("environment variable not set")
)
// Receive retrieves setup fd from the environment and receives params.
func Receive(key string, e any, fdp *uintptr) (func() error, error) {
var setup *os.File
if s, ok := os.LookupEnv(key); !ok {
return nil, ErrReceiveEnv
} else {
if fd, err := strconv.Atoi(s); err != nil {
return nil, optionalErrorUnwrap(err)
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
return nil, syscall.EDOM
}
if fdp != nil {
*fdp = setup.Fd()
}
}
}
return setup.Close, gob.NewDecoder(setup).Decode(e)
}
@@ -1,4 +1,4 @@
package params_test package container_test
import ( import (
"encoding/gob" "encoding/gob"
@@ -9,7 +9,7 @@ import (
"syscall" "syscall"
"testing" "testing"
"hakurei.app/internal/params" "hakurei.app/container"
) )
func TestSetupReceive(t *testing.T) { func TestSetupReceive(t *testing.T) {
@@ -30,8 +30,8 @@ func TestSetupReceive(t *testing.T) {
}) })
} }
if _, err := params.Receive(key, nil, nil); !errors.Is(err, params.ErrReceiveEnv) { if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrReceiveEnv) {
t.Errorf("Receive: error = %v, want %v", err, params.ErrReceiveEnv) t.Errorf("Receive: error = %v, want %v", err, container.ErrReceiveEnv)
} }
}) })
@@ -39,7 +39,7 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_FORMAT" const key = "TEST_ENV_FORMAT"
t.Setenv(key, "") t.Setenv(key, "")
if _, err := params.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) { if _, err := container.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax) t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax)
} }
}) })
@@ -48,7 +48,7 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_RANGE" const key = "TEST_ENV_RANGE"
t.Setenv(key, "-1") t.Setenv(key, "-1")
if _, err := params.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) { if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM) t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM)
} }
}) })
@@ -60,22 +60,16 @@ func TestSetupReceive(t *testing.T) {
encoderDone := make(chan error, 1) encoderDone := make(chan error, 1)
extraFiles := make([]*os.File, 0, 1) extraFiles := make([]*os.File, 0, 1)
if r, w, err := os.Pipe(); err != nil { deadline, _ := t.Deadline()
if fd, f, err := container.Setup(&extraFiles); err != nil {
t.Fatalf("Setup: error = %v", err) t.Fatalf("Setup: error = %v", err)
} else if fd != 3 {
t.Fatalf("Setup: fd = %d, want 3", fd)
} else { } else {
t.Cleanup(func() { if err = f.SetDeadline(deadline); err != nil {
if err = errors.Join(r.Close(), w.Close()); err != nil { t.Fatal(err.Error())
t.Fatal(err)
}
})
extraFiles = append(extraFiles, r)
if deadline, ok := t.Deadline(); ok {
if err = w.SetDeadline(deadline); err != nil {
t.Fatal(err)
}
} }
go func() { encoderDone <- gob.NewEncoder(w).Encode(payload) }() go func() { encoderDone <- gob.NewEncoder(f).Encode(payload) }()
} }
if len(extraFiles) != 1 { if len(extraFiles) != 1 {
@@ -93,13 +87,13 @@ func TestSetupReceive(t *testing.T) {
var ( var (
gotPayload []uint64 gotPayload []uint64
fdp *int fdp *uintptr
) )
if !useNilFdp { if !useNilFdp {
fdp = new(int) fdp = new(uintptr)
} }
var closeFile func() error var closeFile func() error
if f, err := params.Receive(key, &gotPayload, fdp); err != nil { if f, err := container.Receive(key, &gotPayload, fdp); err != nil {
t.Fatalf("Receive: error = %v", err) t.Fatalf("Receive: error = %v", err)
} else { } else {
closeFile = f closeFile = f
@@ -109,7 +103,7 @@ func TestSetupReceive(t *testing.T) {
} }
} }
if !useNilFdp { if !useNilFdp {
if *fdp != dupFd { if int(*fdp) != dupFd {
t.Errorf("Fd: %d, want %d", *fdp, dupFd) t.Errorf("Fd: %d, want %d", *fdp, dupFd)
} }
} }
+2 -2
View File
@@ -7,8 +7,8 @@ import (
"hakurei.app/ext" "hakurei.app/ext"
) )
// setNoNewPrivs sets the calling thread's no_new_privs attribute. // SetNoNewPrivs sets the calling thread's no_new_privs attribute.
func setNoNewPrivs() error { func SetNoNewPrivs() error {
return ext.Prctl(PR_SET_NO_NEW_PRIVS, 1, 0) return ext.Prctl(PR_SET_NO_NEW_PRIVS, 1, 0)
} }
View File
+1
View File
@@ -0,0 +1 @@
1000 0
Vendored Executable
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
cd "$(dirname -- "$0")" || exit 1
install -vDm0755 "bin/hakurei" "${DESTDIR}/usr/bin/hakurei"
install -vDm0755 "bin/sharefs" "${DESTDIR}/usr/bin/sharefs"
install -vDm4511 "bin/hsu" "${DESTDIR}/usr/bin/hsu"
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
fi
install -vDm0644 "comp/_hakurei" "${DESTDIR}/usr/share/zsh/site-functions/_hakurei"
Vendored Executable
+31
View File
@@ -0,0 +1,31 @@
#!/bin/sh -e
cd "$(dirname -- "$0")/.."
VERSION="${HAKUREI_VERSION:-untagged}"
pname="hakurei-${VERSION}-$(go env GOARCH)"
out="${DESTDIR:-dist}/${pname}"
echo '# Preparing distribution files.'
mkdir -p "${out}"
cp -v "README.md" "dist/hsurc.default" "dist/install.sh" "${out}"
cp -rv "dist/comp" "${out}"
echo
echo '# Building hakurei.'
go generate ./...
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w
-buildid= -linkmode external -extldflags=-static
-X hakurei.app/internal/info.buildVersion=${VERSION}
-X hakurei.app/internal/info.hakureiPath=/usr/bin/hakurei
-X hakurei.app/internal/info.hsuPath=/usr/bin/hsu
-X main.hakureiPath=/usr/bin/hakurei" ./...
echo
echo '# Testing hakurei.'
go test -ldflags='-buildid= -linkmode external -extldflags=-static' ./...
echo
echo '# Creating distribution.'
rm -f "${out}.tar.gz" && tar -C "${out}/.." -vczf "${out}.tar.gz" "${pname}"
rm -rf "${out}"
(cd "${out}/.." && sha512sum "${pname}.tar.gz" > "${pname}.tar.gz.sha512")
echo
+2 -2
View File
@@ -137,10 +137,11 @@
CC="musl-clang -O3 -Werror -Qunused-arguments" \ CC="musl-clang -O3 -Werror -Qunused-arguments" \
GOCACHE="$(mktemp -d)" \ GOCACHE="$(mktemp -d)" \
HAKUREI_TEST_SKIP_ACL=1 \
PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \ PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \
DESTDIR="$out" \ DESTDIR="$out" \
HAKUREI_VERSION="v${hakurei.version}" \ HAKUREI_VERSION="v${hakurei.version}" \
./all.sh ./dist/release.sh
''; '';
} }
); );
@@ -195,7 +196,6 @@
./test/interactive/vm.nix ./test/interactive/vm.nix
./test/interactive/hakurei.nix ./test/interactive/hakurei.nix
./test/interactive/trace.nix ./test/interactive/trace.nix
./test/interactive/raceattr.nix
self.nixosModules.hakurei self.nixosModules.hakurei
home-manager.nixosModules.home-manager home-manager.nixosModules.home-manager
+11 -36
View File
@@ -140,29 +140,21 @@ var (
ErrInsecure = errors.New("configuration is insecure") ErrInsecure = errors.New("configuration is insecure")
) )
const (
// VAllowInsecure allows use of compatibility options considered insecure
// under any configuration, to work around ecosystem-wide flaws.
VAllowInsecure = 1 << iota
)
// Validate checks [Config] and returns [AppError] if an invalid value is encountered. // Validate checks [Config] and returns [AppError] if an invalid value is encountered.
func (config *Config) Validate(flags int) error { func (config *Config) Validate() error {
const step = "validate configuration"
if config == nil { if config == nil {
return &AppError{Step: step, Err: ErrConfigNull, return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "invalid configuration"} Msg: "invalid configuration"}
} }
// this is checked again in hsu // this is checked again in hsu
if config.Identity < IdentityStart || config.Identity > IdentityEnd { if config.Identity < IdentityStart || config.Identity > IdentityEnd {
return &AppError{Step: step, Err: ErrIdentityBounds, return &AppError{Step: "validate configuration", Err: ErrIdentityBounds,
Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"} Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
} }
if config.SchedPolicy < 0 || config.SchedPolicy > ext.SCHED_LAST { if config.SchedPolicy < 0 || config.SchedPolicy > ext.SCHED_LAST {
return &AppError{Step: step, Err: ErrSchedPolicyBounds, return &AppError{Step: "validate configuration", Err: ErrSchedPolicyBounds,
Msg: "scheduling policy " + Msg: "scheduling policy " +
strconv.Itoa(int(config.SchedPolicy)) + strconv.Itoa(int(config.SchedPolicy)) +
" out of range"} " out of range"}
@@ -176,51 +168,34 @@ func (config *Config) Validate(flags int) error {
} }
if config.Container == nil { if config.Container == nil {
return &AppError{Step: step, Err: ErrConfigNull, return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "configuration missing container state"} Msg: "configuration missing container state"}
} }
if config.Container.Home == nil { if config.Container.Home == nil {
return &AppError{Step: step, Err: ErrConfigNull, return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "container configuration missing path to home directory"} Msg: "container configuration missing path to home directory"}
} }
if config.Container.Shell == nil { if config.Container.Shell == nil {
return &AppError{Step: step, Err: ErrConfigNull, return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "container configuration missing path to shell"} Msg: "container configuration missing path to shell"}
} }
if config.Container.Path == nil { if config.Container.Path == nil {
return &AppError{Step: step, Err: ErrConfigNull, return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "container configuration missing path to initial program"} Msg: "container configuration missing path to initial program"}
} }
for key := range config.Container.Env { for key := range config.Container.Env {
if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 { if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 {
return &AppError{Step: step, Err: ErrEnviron, return &AppError{Step: "validate configuration", Err: ErrEnviron,
Msg: "invalid environment variable " + strconv.Quote(key)} Msg: "invalid environment variable " + strconv.Quote(key)}
} }
} }
et := config.Enablements.Unwrap() if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
if !config.DirectPulse && et&EPulse != 0 { return &AppError{Step: "validate configuration", Err: ErrInsecure,
return &AppError{Step: step, Err: ErrInsecure,
Msg: "enablement PulseAudio is insecure and no longer supported"} Msg: "enablement PulseAudio is insecure and no longer supported"}
} }
if flags&VAllowInsecure == 0 {
switch {
case et&EWayland != 0 && config.DirectWayland:
return &AppError{Step: step, Err: ErrInsecure,
Msg: "direct_wayland is insecure and no longer supported"}
case et&EPipeWire != 0 && config.DirectPipeWire:
return &AppError{Step: step, Err: ErrInsecure,
Msg: "direct_pipewire is insecure and no longer supported"}
case et&EPulse != 0 && config.DirectPulse:
return &AppError{Step: step, Err: ErrInsecure,
Msg: "direct_pulse is insecure and no longer supported"}
}
}
return nil return nil
} }
+17 -61
View File
@@ -14,109 +14,65 @@ func TestConfigValidate(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
config *hst.Config config *hst.Config
flags int
wantErr error wantErr error
}{ }{
{"nil", nil, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, {"nil", nil, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "invalid configuration"}}, Msg: "invalid configuration"}},
{"identity lower", &hst.Config{Identity: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
{"identity lower", &hst.Config{Identity: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
Msg: "identity -1 out of range"}}, Msg: "identity -1 out of range"}},
{"identity upper", &hst.Config{Identity: 10000}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds, {"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
Msg: "identity 10000 out of range"}}, Msg: "identity 10000 out of range"}},
{"sched lower", &hst.Config{SchedPolicy: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
{"sched lower", &hst.Config{SchedPolicy: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
Msg: "scheduling policy -1 out of range"}}, Msg: "scheduling policy -1 out of range"}},
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds, {"sched upper", &hst.Config{SchedPolicy: 0xcafe}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
Msg: "scheduling policy 51966 out of range"}}, Msg: "scheduling policy 51966 out of range"}},
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}},
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}}, 0,
&hst.BadInterfaceError{Interface: "", Segment: "session"}}, &hst.BadInterfaceError{Interface: "", Segment: "session"}},
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}}, 0, {"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}},
&hst.BadInterfaceError{Interface: "", Segment: "system"}}, &hst.BadInterfaceError{Interface: "", Segment: "system"}},
{"container", &hst.Config{}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
{"container", &hst.Config{}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "configuration missing container state"}}, Msg: "configuration missing container state"}},
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, {"home", &hst.Config{Container: &hst.ContainerConfig{}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to home directory"}}, Msg: "container configuration missing path to home directory"}},
{"shell", &hst.Config{Container: &hst.ContainerConfig{ {"shell", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, }}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to shell"}}, Msg: "container configuration missing path to shell"}},
{"path", &hst.Config{Container: &hst.ContainerConfig{ {"path", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, }}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to initial program"}}, Msg: "container configuration missing path to initial program"}},
{"env equals", &hst.Config{Container: &hst.ContainerConfig{ {"env equals", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
Env: map[string]string{"TERM=": ""}, Env: map[string]string{"TERM=": ""},
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron, }}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
Msg: `invalid environment variable "TERM="`}}, Msg: `invalid environment variable "TERM="`}},
{"env NUL", &hst.Config{Container: &hst.ContainerConfig{ {"env NUL", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
Env: map[string]string{"TERM\x00": ""}, Env: map[string]string{"TERM\x00": ""},
}}, 0, &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{
{"insecure pulse", &hst.Config{Enablements: new(hst.EPulse), Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure, }}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "enablement PulseAudio is insecure and no longer supported"}}, Msg: "enablement PulseAudio is insecure and no longer supported"}},
{"direct wayland", &hst.Config{Enablements: new(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "direct_wayland is insecure and no longer supported"}},
{"direct wayland allow", &hst.Config{Enablements: new(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, hst.VAllowInsecure, nil},
{"direct pipewire", &hst.Config{Enablements: new(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "direct_pipewire is insecure and no longer supported"}},
{"direct pipewire allow", &hst.Config{Enablements: new(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, hst.VAllowInsecure, nil},
{"direct pulse", &hst.Config{Enablements: new(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "direct_pulse is insecure and no longer supported"}},
{"direct pulse allow", &hst.Config{Enablements: new(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, hst.VAllowInsecure, nil},
{"valid", &hst.Config{Container: &hst.ContainerConfig{ {"valid", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
}}, 0, nil}, }}, nil},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
if err := tc.config.Validate(tc.flags); !reflect.DeepEqual(err, tc.wantErr) { if err := tc.config.Validate(); !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr) t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr)
} }
}) })
+31 -21
View File
@@ -7,12 +7,12 @@ import (
"syscall" "syscall"
) )
// Enablements denotes optional host service to export to the target user. // Enablement represents an optional host service to export to the target user.
type Enablements byte type Enablement byte
const ( const (
// EWayland exposes a Wayland pathname socket via security-context-v1. // EWayland exposes a Wayland pathname socket via security-context-v1.
EWayland Enablements = 1 << iota EWayland Enablement = 1 << iota
// EX11 adds the target user via X11 ChangeHosts and exposes the X11 // EX11 adds the target user via X11 ChangeHosts and exposes the X11
// pathname socket. // pathname socket.
EX11 EX11
@@ -28,8 +28,8 @@ const (
EM EM
) )
// String returns a string representation of the flags set on [Enablements]. // String returns a string representation of the flags set on [Enablement].
func (e Enablements) String() string { func (e Enablement) String() string {
switch e { switch e {
case 0: case 0:
return "(no enablements)" return "(no enablements)"
@@ -47,7 +47,7 @@ func (e Enablements) String() string {
buf := new(strings.Builder) buf := new(strings.Builder)
buf.Grow(32) buf.Grow(32)
for i := Enablements(1); i < EM; i <<= 1 { for i := Enablement(1); i < EM; i <<= 1 {
if e&i != 0 { if e&i != 0 {
buf.WriteString(", " + i.String()) buf.WriteString(", " + i.String())
} }
@@ -60,6 +60,12 @@ func (e Enablements) String() string {
} }
} }
// NewEnablements returns the address of [Enablement] as [Enablements].
func NewEnablements(e Enablement) *Enablements { return (*Enablements)(&e) }
// Enablements is the [json] adapter for [Enablement].
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"`
@@ -69,21 +75,24 @@ type enablementsJSON = struct {
Pulse bool `json:"pulse,omitempty"` Pulse bool `json:"pulse,omitempty"`
} }
// Unwrap returns the value pointed to by e. // Unwrap returns the underlying [Enablement].
func (e *Enablements) Unwrap() Enablements { func (e *Enablements) Unwrap() Enablement {
if e == nil { if e == nil {
return 0 return 0
} }
return *e return Enablement(*e)
} }
func (e Enablements) MarshalJSON() ([]byte, error) { func (e *Enablements) MarshalJSON() ([]byte, error) {
if e == nil {
return nil, syscall.EINVAL
}
return json.Marshal(&enablementsJSON{ return json.Marshal(&enablementsJSON{
Wayland: e&EWayland != 0, Wayland: Enablement(*e)&EWayland != 0,
X11: e&EX11 != 0, X11: Enablement(*e)&EX11 != 0,
DBus: e&EDBus != 0, DBus: Enablement(*e)&EDBus != 0,
PipeWire: e&EPipeWire != 0, PipeWire: Enablement(*e)&EPipeWire != 0,
Pulse: e&EPulse != 0, Pulse: Enablement(*e)&EPulse != 0,
}) })
} }
@@ -97,21 +106,22 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
return err return err
} }
*e = 0 var ve Enablement
if v.Wayland { if v.Wayland {
*e |= EWayland ve |= EWayland
} }
if v.X11 { if v.X11 {
*e |= EX11 ve |= EX11
} }
if v.DBus { if v.DBus {
*e |= EDBus ve |= EDBus
} }
if v.PipeWire { if v.PipeWire {
*e |= EPipeWire ve |= EPipeWire
} }
if v.Pulse { if v.Pulse {
*e |= EPulse ve |= EPulse
} }
*e = Enablements(ve)
return nil return nil
} }
+12 -9
View File
@@ -13,7 +13,7 @@ func TestEnablementString(t *testing.T) {
t.Parallel() t.Parallel()
testCases := []struct { testCases := []struct {
flags hst.Enablements flags hst.Enablement
want string want string
}{ }{
{0, "(no enablements)"}, {0, "(no enablements)"},
@@ -59,13 +59,13 @@ func TestEnablements(t *testing.T) {
sData string sData string
}{ }{
{"nil", nil, "null", `{"value":null,"magic":3236757504}`}, {"nil", nil, "null", `{"value":null,"magic":3236757504}`},
{"zero", new(hst.Enablements(0)), `{}`, `{"value":{},"magic":3236757504}`}, {"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`},
{"wayland", new(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`}, {"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
{"x11", new(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`}, {"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
{"dbus", new(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`}, {"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
{"pipewire", new(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`}, {"pipewire", hst.NewEnablements(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
{"pulse", new(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`}, {"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
{"all", new(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}`}, {"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 {
@@ -137,7 +137,7 @@ func TestEnablements(t *testing.T) {
}) })
t.Run("val", func(t *testing.T) { t.Run("val", func(t *testing.T) {
if got := new(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse { if got := hst.NewEnablements(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
t.Errorf("Unwrap: %v", got) t.Errorf("Unwrap: %v", got)
} }
}) })
@@ -146,6 +146,9 @@ func TestEnablements(t *testing.T) {
t.Run("passthrough", func(t *testing.T) { t.Run("passthrough", func(t *testing.T) {
t.Parallel() t.Parallel()
if _, err := (*hst.Enablements)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("MarshalJSON: error = %v", err)
}
if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) { if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) {
t.Errorf("UnmarshalJSON: error = %v", err) t.Errorf("UnmarshalJSON: error = %v", err)
} }
+1 -3
View File
@@ -56,10 +56,8 @@ type Ops interface {
// ApplyState holds the address of [Ops] and any relevant application state. // ApplyState holds the address of [Ops] and any relevant application state.
type ApplyState struct { type ApplyState struct {
// Prefix for [FSBind] in autoetc [FSBind.Special] condition. // AutoEtcPrefix is the prefix for [FSBind] in autoetc [FSBind.Special] condition.
AutoEtcPrefix string AutoEtcPrefix string
// Whether to skip remounting root.
NoRemountRoot bool
Ops Ops
} }
+2 -6
View File
@@ -5,7 +5,6 @@ import (
"strings" "strings"
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/fhs"
) )
func init() { gob.Register(new(FSOverlay)) } func init() { gob.Register(new(FSOverlay)) }
@@ -70,12 +69,9 @@ func (o *FSOverlay) Apply(z *ApplyState) {
return return
} }
if o.Upper != nil && o.Work != nil { if o.Upper != nil && o.Work != nil { // rw
z.Overlay(o.Target, o.Upper, o.Work, o.Lower...) z.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
if o.Target.Is(fhs.AbsRoot) { } else { // ro
z.NoRemountRoot = true
}
} else {
z.OverlayReadonly(o.Target, o.Lower...) z.OverlayReadonly(o.Target, o.Lower...)
} }
} }
-13
View File
@@ -49,18 +49,5 @@ func TestFSOverlay(t *testing.T) {
Lower: ms("/tmp/.src0", "/tmp/.src1"), Lower: ms("/tmp/.src0", "/tmp/.src1"),
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"), }}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
"*/mnt/src:/tmp/.src0:/tmp/.src1"}, "*/mnt/src:/tmp/.src0:/tmp/.src1"},
{"no remount root", &hst.FSOverlay{
Target: m("/"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
Upper: m("/tmp/upper"),
Work: m("/tmp/work"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
Upper: m("/tmp/upper"),
Work: m("/tmp/work"),
}}, m("/"), ms("/tmp/upper", "/tmp/work", "/tmp/.src0", "/tmp/.src1"),
"w*/:/tmp/upper:/tmp/work:/tmp/.src0:/tmp/.src1"},
}) })
} }
+1 -1
View File
@@ -72,7 +72,7 @@ func Template() *Config {
return &Config{ return &Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Enablements: new(EWayland | EDBus | EPipeWire), Enablements: NewEnablements(EWayland | EDBus | EPipeWire),
SessionBus: &BusConfig{ SessionBus: &BusConfig{
See: nil, See: nil,
+2 -10
View File
@@ -11,11 +11,9 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"strconv" "strconv"
"syscall"
"testing" "testing"
"hakurei.app/internal/acl" "hakurei.app/internal/acl"
"hakurei.app/internal/info"
) )
const testFileName = "acl.test" const testFileName = "acl.test"
@@ -26,14 +24,8 @@ var (
) )
func TestUpdate(t *testing.T) { func TestUpdate(t *testing.T) {
if info.CanDegrade { if os.Getenv("HAKUREI_TEST_SKIP_ACL") == "1" {
name := filepath.Join(t.TempDir(), "check-degrade") t.Skip("acl test skipped")
if err := os.WriteFile(name, nil, 0); err != nil {
t.Fatal(err)
}
if err := acl.Update(name, os.Geteuid()); errors.Is(err, syscall.ENOTSUP) {
t.Skip(err)
}
} }
testFilePath := filepath.Join(t.TempDir(), testFileName) testFilePath := filepath.Join(t.TempDir(), testFileName)
-7
View File
@@ -1,7 +0,0 @@
//go:build !noskip
package info
// CanDegrade is whether tests are allowed to transparently degrade or skip due
// to required system features being denied or unavailable.
const CanDegrade = true
-5
View File
@@ -1,5 +0,0 @@
//go:build noskip
package info
const CanDegrade = false
-90
View File
@@ -1,90 +0,0 @@
package kobject
import (
"errors"
"strconv"
"strings"
"unsafe"
"hakurei.app/internal/uevent"
)
// Event is a [uevent.Message] with known environment variables processed.
type Event struct {
// alloc_uevent_skb: action_string
Action uevent.KobjectAction `json:"action"`
// alloc_uevent_skb: devpath
DevPath string `json:"devpath"`
// Uninterpreted environment variable pairs. An entry missing a separator
// gains the value "\x00".
Env map[string]string `json:"env"`
// SEQNUM value set by the kernel.
Sequence uint64 `json:"seqnum"`
// SYNTH_UUID value set on trigger, nil denotes a non-synthetic event.
Synth *uevent.UUID `json:"synth_uuid,omitempty"`
// SUBSYSTEM value set by the kernel.
Subsystem string `json:"subsystem"`
}
// Populate populates e with the contents of a [uevent.Message].
//
// The ACTION and DEVPATH environment variables are ignored and assumed to be
// consistent with the header.
func (e *Event) Populate(reportErr func(error), m *uevent.Message) {
if reportErr == nil {
reportErr = func(error) {}
}
*e = Event{
Action: m.Action,
DevPath: m.DevPath,
Env: make(map[string]string),
}
for _, s := range m.Env {
k, v, ok := strings.Cut(s, "=")
if !ok {
if _, ok = e.Env[s]; !ok {
e.Env[s] = "\x00"
}
continue
}
switch k {
case "ACTION", "DEVPATH":
continue
case "SEQNUM":
seq, err := strconv.ParseUint(v, 10, 64)
if err != nil {
if _e := errors.Unwrap(err); _e != nil {
err = _e
}
reportErr(err)
e.Env[k] = v
continue
}
e.Sequence = seq
case "SYNTH_UUID":
var uuid uevent.UUID
err := uuid.UnmarshalText(unsafe.Slice(unsafe.StringData(v), len(v)))
if err != nil {
reportErr(err)
e.Env[k] = v
continue
}
e.Synth = &uuid
case "SUBSYSTEM":
e.Subsystem = v
default:
e.Env[k] = v
}
}
}
-92
View File
@@ -1,92 +0,0 @@
package kobject_test
import (
"reflect"
"strconv"
"testing"
"hakurei.app/internal/kobject"
"hakurei.app/internal/uevent"
)
func TestEvent(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
msg uevent.Message
want kobject.Event
errs []error
}{
{"sample coldboot qemu", uevent.Message{
Action: uevent.KOBJ_ADD,
DevPath: "/devices/LNXSYSTM:00/LNXPWRBN:00",
Env: []string{
"ACTION=add",
"DEVPATH=/devices/LNXSYSTM:00/LNXPWRBN:00",
"SUBSYSTEM=acpi",
"SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed",
"MODALIAS=acpi:LNXPWRBN:",
"SEQNUM=777",
}}, kobject.Event{
Action: uevent.KOBJ_ADD,
DevPath: "/devices/LNXSYSTM:00/LNXPWRBN:00",
Env: map[string]string{
"MODALIAS": "acpi:LNXPWRBN:",
},
Sequence: 777,
Synth: &uevent.UUID{
0xfe, 0x4d, 0x7c, 0x9d,
0xb8, 0xc6,
0x4a, 0x70,
0x9e, 0xf1,
0x3d, 0x8a, 0x58, 0xd1, 0x8e, 0xed,
},
Subsystem: "acpi",
}, []error{}},
{"nil reportErr", uevent.Message{Env: []string{
"SEQNUM=\x00",
}}, kobject.Event{Env: map[string]string{
"SEQNUM": "\x00",
}}, nil},
{"bad SEQNUM SYNTH_UUID", uevent.Message{Env: []string{
"SEQNUM=\x00",
"SYNTH_UUID=\x00",
"SUBSYSTEM=\x00",
}}, kobject.Event{Subsystem: "\x00", Env: map[string]string{
"SEQNUM": "\x00",
"SYNTH_UUID": "\x00",
}}, []error{strconv.ErrSyntax, uevent.UUIDSizeError(1)}},
{"bad sep", uevent.Message{Env: []string{
"SYNTH_UUID",
}}, kobject.Event{Env: map[string]string{
"SYNTH_UUID": "\x00",
}}, []error{}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var f func(error)
gotErrs := make([]error, 0)
if tc.errs != nil {
f = func(err error) {
gotErrs = append(gotErrs, err)
}
}
var got kobject.Event
got.Populate(f, &tc.msg)
if !reflect.DeepEqual(&got, &tc.want) {
t.Errorf("Populate: %#v, want %#v", got, tc.want)
}
if tc.errs != nil && !reflect.DeepEqual(gotErrs, tc.errs) {
t.Errorf("Populate: errs = %v, want %v", gotErrs, tc.errs)
}
})
}
}
-65
View File
@@ -1,65 +0,0 @@
package landlock_test
import (
"testing"
"unsafe"
"hakurei.app/internal/landlock"
)
func TestLandlockString(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
rulesetAttr *landlock.RulesetAttr
want string
}{
{"nil", nil, "NULL"},
{"zero", new(landlock.RulesetAttr), "0"},
{"some", &landlock.RulesetAttr{Scoped: landlock.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
{"set", &landlock.RulesetAttr{
HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_MAKE_SYM | landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV | landlock.LANDLOCK_ACCESS_FS_WRITE_FILE,
HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP,
Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | landlock.LANDLOCK_SCOPE_SIGNAL,
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
{"all", &landlock.RulesetAttr{
HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_EXECUTE |
landlock.LANDLOCK_ACCESS_FS_WRITE_FILE |
landlock.LANDLOCK_ACCESS_FS_READ_FILE |
landlock.LANDLOCK_ACCESS_FS_READ_DIR |
landlock.LANDLOCK_ACCESS_FS_REMOVE_DIR |
landlock.LANDLOCK_ACCESS_FS_REMOVE_FILE |
landlock.LANDLOCK_ACCESS_FS_MAKE_CHAR |
landlock.LANDLOCK_ACCESS_FS_MAKE_DIR |
landlock.LANDLOCK_ACCESS_FS_MAKE_REG |
landlock.LANDLOCK_ACCESS_FS_MAKE_SOCK |
landlock.LANDLOCK_ACCESS_FS_MAKE_FIFO |
landlock.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
landlock.LANDLOCK_ACCESS_FS_MAKE_SYM |
landlock.LANDLOCK_ACCESS_FS_REFER |
landlock.LANDLOCK_ACCESS_FS_TRUNCATE |
landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV,
HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP |
landlock.LANDLOCK_ACCESS_NET_CONNECT_TCP,
Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
landlock.LANDLOCK_SCOPE_SIGNAL,
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.rulesetAttr.String(); got != tc.want {
t.Errorf("String: %s, want %s", got, tc.want)
}
})
}
}
func TestLandlockAttrSize(t *testing.T) {
t.Parallel()
want := 24
if got := unsafe.Sizeof(landlock.RulesetAttr{}); got != uintptr(want) {
t.Errorf("Sizeof: %d, want %d", got, want)
}
}
+3 -4
View File
@@ -17,7 +17,6 @@ import (
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/internal/dbus" "hakurei.app/internal/dbus"
"hakurei.app/internal/info" "hakurei.app/internal/info"
"hakurei.app/internal/params"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -85,7 +84,7 @@ type syscallDispatcher interface {
// setDumpable provides [container.SetDumpable]. // setDumpable provides [container.SetDumpable].
setDumpable(dumpable uintptr) error setDumpable(dumpable uintptr) error
// receive provides [container.Receive]. // receive provides [container.Receive].
receive(key string, e any, fdp *int) (closeFunc func() error, err error) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// containerStart provides the Start method of [container.Container]. // containerStart provides the Start method of [container.Container].
containerStart(z *container.Container) error containerStart(z *container.Container) error
@@ -155,8 +154,8 @@ func (direct) prctl(op, arg2, arg3 uintptr) error { return ext.Prctl(op, arg2, a
func (direct) overflowUid(msg message.Msg) int { return container.OverflowUid(msg) } func (direct) overflowUid(msg message.Msg) int { return container.OverflowUid(msg) }
func (direct) overflowGid(msg message.Msg) int { return container.OverflowGid(msg) } func (direct) overflowGid(msg message.Msg) int { return container.OverflowGid(msg) }
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) } func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
func (direct) receive(key string, e any, fdp *int) (func() error, error) { func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return params.Receive(key, e, fdp) return container.Receive(key, e, fdp)
} }
func (direct) containerStart(z *container.Container) error { return z.Start() } func (direct) containerStart(z *container.Container) error { return z.Start() }
+34 -34
View File
@@ -401,12 +401,12 @@ func (k *kstub) setDumpable(dumpable uintptr) error {
stub.CheckArg(k.Stub, "dumpable", dumpable, 0)) stub.CheckArg(k.Stub, "dumpable", dumpable, 0))
} }
func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, err error) { func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
k.Helper() k.Helper()
expect := k.Expects("receive") expect := k.Expects("receive")
reflect.ValueOf(e).Elem().Set(reflect.ValueOf(expect.Args[1])) reflect.ValueOf(e).Elem().Set(reflect.ValueOf(expect.Args[1]))
if expect.Args[2] != nil { if expect.Args[2] != nil {
*fdp = int(expect.Args[2].(uintptr)) *fdp = expect.Args[2].(uintptr)
} }
return func() error { return k.Expects("closeReceive").Err }, expect.Error( return func() error { return k.Expects("closeReceive").Err }, expect.Error(
stub.CheckArg(k.Stub, "key", key, 0)) stub.CheckArg(k.Stub, "key", key, 0))
@@ -690,38 +690,38 @@ func (panicMsgContext) Value(any) any { panic("unreachable") }
// This type is meant to be embedded in partial syscallDispatcher implementations. // This type is meant to be embedded in partial syscallDispatcher implementations.
type panicDispatcher struct{} type panicDispatcher struct{}
func (panicDispatcher) new(func(k syscallDispatcher, msg message.Msg)) { panic("unreachable") } func (panicDispatcher) new(func(k syscallDispatcher, msg message.Msg)) { panic("unreachable") }
func (panicDispatcher) getppid() int { panic("unreachable") } func (panicDispatcher) getppid() int { panic("unreachable") }
func (panicDispatcher) getpid() int { panic("unreachable") } func (panicDispatcher) getpid() int { panic("unreachable") }
func (panicDispatcher) getuid() int { panic("unreachable") } func (panicDispatcher) getuid() int { panic("unreachable") }
func (panicDispatcher) getgid() int { panic("unreachable") } func (panicDispatcher) getgid() int { panic("unreachable") }
func (panicDispatcher) lookupEnv(string) (string, bool) { panic("unreachable") } func (panicDispatcher) lookupEnv(string) (string, bool) { panic("unreachable") }
func (panicDispatcher) pipe() (*os.File, *os.File, error) { panic("unreachable") } func (panicDispatcher) pipe() (*os.File, *os.File, error) { panic("unreachable") }
func (panicDispatcher) stat(string) (os.FileInfo, error) { panic("unreachable") } func (panicDispatcher) stat(string) (os.FileInfo, error) { panic("unreachable") }
func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") } func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") }
func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") } func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
func (panicDispatcher) tempdir() string { panic("unreachable") } func (panicDispatcher) tempdir() string { panic("unreachable") }
func (panicDispatcher) mkdir(string, os.FileMode) error { panic("unreachable") } func (panicDispatcher) mkdir(string, os.FileMode) error { panic("unreachable") }
func (panicDispatcher) removeAll(string) error { panic("unreachable") } func (panicDispatcher) removeAll(string) error { panic("unreachable") }
func (panicDispatcher) exit(int) { panic("unreachable") } func (panicDispatcher) exit(int) { panic("unreachable") }
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") } func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
func (panicDispatcher) prctl(uintptr, uintptr, uintptr) error { panic("unreachable") } func (panicDispatcher) prctl(uintptr, uintptr, uintptr) error { panic("unreachable") }
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") } func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
func (panicDispatcher) lookPath(string) (string, error) { panic("unreachable") } func (panicDispatcher) lookPath(string) (string, error) { panic("unreachable") }
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") } func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") } func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") } func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
func (panicDispatcher) setDumpable(uintptr) error { panic("unreachable") } func (panicDispatcher) setDumpable(uintptr) error { panic("unreachable") }
func (panicDispatcher) receive(string, any, *int) (func() error, error) { panic("unreachable") } func (panicDispatcher) receive(string, any, *uintptr) (func() error, error) { panic("unreachable") }
func (panicDispatcher) containerStart(*container.Container) error { panic("unreachable") } func (panicDispatcher) containerStart(*container.Container) error { panic("unreachable") }
func (panicDispatcher) containerServe(*container.Container) error { panic("unreachable") } func (panicDispatcher) containerServe(*container.Container) error { panic("unreachable") }
func (panicDispatcher) containerWait(*container.Container) error { panic("unreachable") } func (panicDispatcher) containerWait(*container.Container) error { panic("unreachable") }
func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") } func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") }
func (panicDispatcher) dbusAddress() (string, string) { panic("unreachable") } func (panicDispatcher) dbusAddress() (string, string) { panic("unreachable") }
func (panicDispatcher) setupContSignal(int) (io.ReadCloser, func(), error) { panic("unreachable") } func (panicDispatcher) setupContSignal(int) (io.ReadCloser, func(), error) { panic("unreachable") }
func (panicDispatcher) getMsg() message.Msg { panic("unreachable") } func (panicDispatcher) getMsg() message.Msg { panic("unreachable") }
func (panicDispatcher) fatal(...any) { panic("unreachable") } func (panicDispatcher) fatal(...any) { panic("unreachable") }
func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") } func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") }
func (panicDispatcher) notifyContext(context.Context, ...os.Signal) (context.Context, context.CancelFunc) { func (panicDispatcher) notifyContext(context.Context, ...os.Signal) (context.Context, context.CancelFunc) {
panic("unreachable") panic("unreachable")
+2 -9
View File
@@ -32,14 +32,7 @@ type outcome struct {
syscallDispatcher syscallDispatcher
} }
// finalise prepares an outcome for main. func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, config *hst.Config) error {
func (k *outcome) finalise(
ctx context.Context,
msg message.Msg,
id *hst.ID,
config *hst.Config,
flags int,
) error {
if ctx == nil || id == nil { if ctx == nil || id == nil {
// unreachable // unreachable
panic("invalid call to finalise") panic("invalid call to finalise")
@@ -50,7 +43,7 @@ func (k *outcome) finalise(
} }
k.ctx = ctx k.ctx = ctx
if err := config.Validate(flags); err != nil { if err := config.Validate(); err != nil {
return err return err
} }
+1 -1
View File
@@ -194,7 +194,7 @@ type outcomeStateSys struct {
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem. // Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
appId string appId string
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem. // Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
et hst.Enablements et hst.Enablement
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only. // Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
directWayland bool directWayland bool
+11 -15
View File
@@ -13,6 +13,7 @@ import (
"time" "time"
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/container"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/info" "hakurei.app/internal/info"
@@ -297,12 +298,12 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
// accumulate enablements of remaining instances // accumulate enablements of remaining instances
var ( var (
// alive enablement bits // alive enablement bits
rt hst.Enablements rt hst.Enablement
// alive instance count // alive instance count
n int n int
) )
for eh := range entries { for eh := range entries {
var et hst.Enablements var et hst.Enablement
if et, err = eh.Load(nil); err != nil { if et, err = eh.Load(nil); err != nil {
perror(err, "read state header of instance "+eh.ID.String()) perror(err, "read state header of instance "+eh.ID.String())
} else { } else {
@@ -371,18 +372,17 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
// shim runs in the same session as monitor; see shim.go for behaviour // shim runs in the same session as monitor; see shim.go for behaviour
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) } cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
var shimPipe [2]*os.File var shimPipe *os.File
if r, w, err := os.Pipe(); err != nil { if fd, w, err := container.Setup(&cmd.ExtraFiles); err != nil {
return cmd, nil, &hst.AppError{Step: "create shim setup pipe", Err: err} return cmd, nil, &hst.AppError{Step: "create shim setup pipe", Err: err}
} else { } else {
shimPipe = w
cmd.Env = []string{ cmd.Env = []string{
// passed through to shim by hsu // passed through to shim by hsu
shimEnv + "=" + strconv.Itoa(3+len(cmd.ExtraFiles)), shimEnv + "=" + strconv.Itoa(fd),
// interpreted by hsu // interpreted by hsu
"HAKUREI_IDENTITY=" + k.state.identity.String(), "HAKUREI_IDENTITY=" + k.state.identity.String(),
} }
cmd.ExtraFiles = append(cmd.ExtraFiles, r)
shimPipe[0], shimPipe[1] = r, w
} }
if len(k.supp) > 0 { if len(k.supp) > 0 {
@@ -393,16 +393,12 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
msg.Verbosef("setuid helper at %s", hsuPath) msg.Verbosef("setuid helper at %s", hsuPath)
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
_, _ = shimPipe[0].Close(), shimPipe[1].Close()
msg.Resume() msg.Resume()
return cmd, nil, &hst.AppError{Step: "start setuid wrapper", Err: err} return cmd, shimPipe, &hst.AppError{Step: "start setuid wrapper", Err: err}
}
if err := shimPipe[0].Close(); err != nil {
msg.Verbose(err)
} }
*startTime = time.Now().UTC() *startTime = time.Now().UTC()
return cmd, shimPipe[1], nil return cmd, shimPipe, nil
} }
// serveShim serves outcomeState through the shim setup pipe. // serveShim serves outcomeState through the shim setup pipe.
@@ -415,11 +411,11 @@ func serveShim(msg message.Msg, shimPipe *os.File, state *outcomeState) error {
msg.Verbose(err.Error()) msg.Verbose(err.Error())
} }
if err := gob.NewEncoder(shimPipe).Encode(state); err != nil { if err := gob.NewEncoder(shimPipe).Encode(state); err != nil {
_ = shimPipe.Close()
msg.Resume() msg.Resume()
return &hst.AppError{Step: "transmit shim config", Err: err} return &hst.AppError{Step: "transmit shim config", Err: err}
} }
return shimPipe.Close() _ = shimPipe.Close()
return nil
} }
// printMessageError prints the error message according to [message.GetMessage], // printMessageError prints the error message according to [message.GetMessage],
+2 -8
View File
@@ -18,13 +18,7 @@ import (
func IsPollDescriptor(fd uintptr) bool func IsPollDescriptor(fd uintptr) bool
// Main runs an app according to [hst.Config] and terminates. Main does not return. // Main runs an app according to [hst.Config] and terminates. Main does not return.
func Main( func Main(ctx context.Context, msg message.Msg, config *hst.Config, fd int) {
ctx context.Context,
msg message.Msg,
config *hst.Config,
flags int,
fd int,
) {
// avoids runtime internals or standard streams // avoids runtime internals or standard streams
if fd >= 0 { if fd >= 0 {
if IsPollDescriptor(uintptr(fd)) || fd < 3 { if IsPollDescriptor(uintptr(fd)) || fd < 3 {
@@ -40,7 +34,7 @@ func Main(
k := outcome{syscallDispatcher: direct{msg}} k := outcome{syscallDispatcher: direct{msg}}
finaliseTime := time.Now() finaliseTime := time.Now()
if err := k.finalise(ctx, msg, &id, config, flags); err != nil { if err := k.finalise(ctx, msg, &id, config); err != nil {
printMessageError(msg.GetLogger().Fatalln, "cannot seal app:", err) printMessageError(msg.GetLogger().Fatalln, "cannot seal app:", err)
panic("unreachable") panic("unreachable")
} }
+2 -2
View File
@@ -288,7 +288,7 @@ func TestOutcomeRun(t *testing.T) {
}, },
Filter: true, Filter: true,
}, },
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse), Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
@@ -427,7 +427,7 @@ func TestOutcomeRun(t *testing.T) {
DirectPipeWire: true, DirectPipeWire: true,
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse), Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Env: nil, Env: nil,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
+1 -2
View File
@@ -20,7 +20,6 @@ import (
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/params"
"hakurei.app/internal/pipewire" "hakurei.app/internal/pipewire"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -198,7 +197,7 @@ func shimEntrypoint(k syscallDispatcher) {
if errors.Is(err, syscall.EBADF) { if errors.Is(err, syscall.EBADF) {
k.fatal("invalid config descriptor") k.fatal("invalid config descriptor")
} }
if errors.Is(err, params.ErrReceiveEnv) { if errors.Is(err, container.ErrReceiveEnv) {
k.fatal(shimEnv + " not set") k.fatal(shimEnv + " not set")
} }
+1 -2
View File
@@ -16,7 +16,6 @@ import (
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/env" "hakurei.app/internal/env"
"hakurei.app/internal/params"
"hakurei.app/internal/stub" "hakurei.app/internal/stub"
) )
@@ -173,7 +172,7 @@ func TestShimEntrypoint(t *testing.T) {
call("setDumpable", stub.ExpectArgs{uintptr(ext.SUID_DUMP_DISABLE)}, nil, nil), call("setDumpable", stub.ExpectArgs{uintptr(ext.SUID_DUMP_DISABLE)}, nil, nil),
call("getppid", stub.ExpectArgs{}, 0xbad, nil), call("getppid", stub.ExpectArgs{}, 0xbad, nil),
call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil),
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, params.ErrReceiveEnv), call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, container.ErrReceiveEnv),
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SHIM not set"}}, nil, nil), call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SHIM not set"}}, nil, nil),
// deferred // deferred
+1 -3
View File
@@ -290,9 +290,7 @@ func (s *spFilesystemOp) toContainer(state *outcomeStateParams) error {
if state.Container.Flags&hst.FDevice == 0 { if state.Container.Flags&hst.FDevice == 0 {
state.params.Remount(fhs.AbsDev, syscall.MS_RDONLY) state.params.Remount(fhs.AbsDev, syscall.MS_RDONLY)
} }
if !state.as.NoRemountRoot { state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY)
state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY)
}
state.params.Env = make([]string, 0, len(state.env)) state.params.Env = make([]string, 0, len(state.env))
for key, value := range state.env { for key, value := range state.env {
+1 -1
View File
@@ -21,7 +21,7 @@ func TestSpPulseOp(t *testing.T) {
newConfig := func() *hst.Config { newConfig := func() *hst.Config {
config := hst.Template() config := hst.Template()
config.DirectPulse = true config.DirectPulse = true
config.Enablements = new(hst.EPulse) config.Enablements = hst.NewEnablements(hst.EPulse)
return config return config
} }
-42
View File
@@ -1,42 +0,0 @@
// Package params provides helpers for receiving setup payload from parent.
package params
import (
"encoding/gob"
"errors"
"os"
"strconv"
"syscall"
)
// ErrReceiveEnv is returned by [Receive] if setup fd is not present in environment.
var ErrReceiveEnv = errors.New("environment variable not set")
// Receive retrieves setup fd from the environment and receives params.
//
// The file descriptor written to the value pointed to by fdp must not be passed
// to any system calls. It is made available for ordering file descriptor only.
func Receive(key string, v any, fdp *int) (func() error, error) {
var setup *os.File
if s, ok := os.LookupEnv(key); !ok {
return nil, ErrReceiveEnv
} else {
if fd, err := strconv.Atoi(s); err != nil {
if _err := errors.Unwrap(err); _err != nil {
err = _err
}
return nil, err
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
return nil, syscall.EDOM
}
if fdp != nil {
*fdp = fd
}
}
}
return setup.Close, gob.NewDecoder(setup).Decode(v)
}
+2 -5
View File
@@ -397,7 +397,6 @@ const SeccompPresets = std.PresetStrict &
func (a *execArtifact) makeContainer( func (a *execArtifact) makeContainer(
ctx context.Context, ctx context.Context,
msg message.Msg, msg message.Msg,
flags int,
hostNet bool, hostNet bool,
temp, work *check.Absolute, temp, work *check.Absolute,
getArtifact GetArtifactFunc, getArtifact GetArtifactFunc,
@@ -424,9 +423,7 @@ func (a *execArtifact) makeContainer(
z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompFlags |= seccomp.AllowMultiarch
z.ParentPerm = 0700 z.ParentPerm = 0700
z.HostNet = hostNet z.HostNet = hostNet
z.HostAbstract = flags&CHostAbstract != 0
z.Hostname = "cure" z.Hostname = "cure"
z.SetScheduler = flags&CSchedIdle != 0
z.SchedPolicy = ext.SCHED_IDLE z.SchedPolicy = ext.SCHED_IDLE
if z.HostNet { if z.HostNet {
z.Hostname = "cure-net" z.Hostname = "cure-net"
@@ -562,7 +559,6 @@ func (c *Cache) EnterExec(
var z *container.Container var z *container.Container
z, err = e.makeContainer( z, err = e.makeContainer(
ctx, c.msg, ctx, c.msg,
c.flags,
hostNet, hostNet,
temp, work, temp, work,
func(a Artifact) (*check.Absolute, unique.Handle[Checksum]) { func(a Artifact) (*check.Absolute, unique.Handle[Checksum]) {
@@ -602,13 +598,14 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
msg := f.GetMessage() msg := f.GetMessage()
var z *container.Container var z *container.Container
if z, err = a.makeContainer( if z, err = a.makeContainer(
ctx, msg, f.cache.flags, hostNet, ctx, msg, hostNet,
f.GetTempDir(), f.GetWorkDir(), f.GetTempDir(), f.GetWorkDir(),
f.GetArtifact, f.GetArtifact,
f.cache.Ident, f.cache.Ident,
); err != nil { ); err != nil {
return return
} }
z.SetScheduler = f.cache.flags&CSchedIdle != 0
var status io.Writer var status io.Writer
if status, err = f.GetStatusWriter(); err != nil { if status, err = f.GetStatusWriter(); err != nil {
-8
View File
@@ -521,14 +521,6 @@ const (
// was caused by an incorrect checksum accidentally left behind while // was caused by an incorrect checksum accidentally left behind while
// bumping a package. Only enable this if you are really sure you need it. // bumping a package. Only enable this if you are really sure you need it.
CAssumeChecksum CAssumeChecksum
// CHostAbstract disables restriction of sandboxed processes from connecting
// to an abstract UNIX socket created by a host process.
//
// This is considered less secure in some systems, but does not introduce
// impurity due to [KindExecNet] being [KnownChecksum]. This flag exists
// to support kernels without Landlock LSM enabled.
CHostAbstract
) )
// Cache is a support layer that implementations of [Artifact] can use to store // Cache is a support layer that implementations of [Artifact] can use to store
+1 -15
View File
@@ -24,8 +24,6 @@ import (
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/internal/info"
"hakurei.app/internal/landlock"
"hakurei.app/internal/pkg" "hakurei.app/internal/pkg"
"hakurei.app/internal/stub" "hakurei.app/internal/stub"
"hakurei.app/message" "hakurei.app/message"
@@ -291,20 +289,8 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
msg := message.New(log.New(os.Stderr, "cache: ", 0)) msg := message.New(log.New(os.Stderr, "cache: ", 0))
msg.SwapVerbose(testing.Verbose()) msg.SwapVerbose(testing.Verbose())
flags := tc.flags
if info.CanDegrade {
if _, err := landlock.GetABI(); err != nil {
if !errors.Is(err, syscall.ENOSYS) {
t.Fatalf("LandlockGetABI: error = %v", err)
}
flags |= pkg.CHostAbstract
t.Log("Landlock LSM is unavailable, setting CHostAbstract")
}
}
var scrubFunc func() error // scrub after hashing var scrubFunc func() error // scrub after hashing
if c, err := pkg.Open(t.Context(), msg, flags, 1<<4, base); err != nil { if c, err := pkg.Open(t.Context(), msg, tc.flags, 1<<4, base); err != nil {
t.Fatalf("Open: error = %v", err) t.Fatalf("Open: error = %v", err)
} else { } else {
t.Cleanup(c.Close) t.Cleanup(c.Close)
+7 -14
View File
@@ -882,7 +882,7 @@ func (t Toolchain) newGnuTLS() (pkg.Artifact, string) {
) )
var configureExtra []KV var configureExtra []KV
switch runtime.GOARCH { switch runtime.GOOS {
case "arm64": case "arm64":
configureExtra = []KV{ configureExtra = []KV{
{"disable-hardware-acceleration"}, {"disable-hardware-acceleration"},
@@ -1141,22 +1141,15 @@ func init() {
func (t Toolchain) newMPC() (pkg.Artifact, string) { func (t Toolchain) newMPC() (pkg.Artifact, string) {
const ( const (
version = "1.4.0" version = "1.4.0"
checksum = "TbrxLiE3ipQrHz_F3Xzz4zqBAnkMWyjhNwIK6wh9360RZ39xMt8rxfW3LxA9SnvU" checksum = "75Sgr2hcDTltHYgFaHsRGsFgW74i2jqAUS0oXaBdJYKjMj_CvEeJ1zwGbNYjEl1H"
) )
return t.NewPackage("mpc", version, t.NewViaGit( return t.NewPackage("mpc", version, pkg.NewHTTPGet(
"https://gitlab.inria.fr/mpc/mpc.git", nil, "https://ftpmirror.gnu.org/gnu/mpc/mpc-"+version+".tar.xz",
"refs/tags/"+version,
mustDecode(checksum), mustDecode(checksum),
), &PackageAttr{ ), &PackageAttr{
// does not find mpc-impl.h otherwise SourceKind: SourceKindTarXZ,
EnterSource: true, }, (*MakeHelper)(nil),
}, &MakeHelper{ XZ,
InPlace: true,
Generate: "autoreconf -vfi",
},
Automake,
Libtool,
Texinfo,
MPFR, MPFR,
), version ), version
+3 -3
View File
@@ -73,7 +73,7 @@ func (t Toolchain) newGoLatest() (pkg.Artifact, string) {
case "amd64": case "amd64":
bootstrapExtra = append(bootstrapExtra, t.newGoBootstrap()) bootstrapExtra = append(bootstrapExtra, t.newGoBootstrap())
case "arm64", "riscv64": case "arm64":
bootstrapEnv = append(bootstrapEnv, "GOROOT_BOOTSTRAP=/system") bootstrapEnv = append(bootstrapEnv, "GOROOT_BOOTSTRAP=/system")
bootstrapExtra = t.AppendPresets(bootstrapExtra, gcc) bootstrapExtra = t.AppendPresets(bootstrapExtra, gcc)
finalEnv = append(finalEnv, "CGO_ENABLED=0") finalEnv = append(finalEnv, "CGO_ENABLED=0")
@@ -141,8 +141,8 @@ rm \
) )
const ( const (
version = "1.26.2" version = "1.26.1"
checksum = "v-6BE89_1g3xYf-9oIYpJKFXlo3xKHYJj2_VGkaUq8ZVkIVQmLwrto-xGG03OISH" checksum = "DdC5Ea-aCYPUHNObQh_09uWU0vn4e-8Ben850Vq-5OoamDRrXhuYI4YQ_BOFgaT0"
) )
return t.newGo( return t.newGo(
version, version,
File diff suppressed because it is too large Load Diff
-8
View File
@@ -1,8 +0,0 @@
package rosa
import _ "embed"
//go:embed kernel_riscv64.config
var kernelConfig []byte
const kernelName = "bzImage"
+14 -21
View File
@@ -5,20 +5,15 @@ import "hakurei.app/internal/pkg"
func (t Toolchain) newLibmd() (pkg.Artifact, string) { func (t Toolchain) newLibmd() (pkg.Artifact, string) {
const ( const (
version = "1.1.0" version = "1.1.0"
checksum = "9apYqPPZm0j5HQT8sCsVIhnVIqRD7XgN7kPIaTwTqnTuUq5waUAMq4M7ev8CODJ1" checksum = "72w7Na04b9ji6nOe2h-Tz5JeQ6iStDZN3FOG1JNZ9M_jMO8K2FceG6DZv7lYThZJ"
) )
return t.NewPackage("libmd", version, t.NewViaGit( return t.NewPackage("libmd", version, pkg.NewHTTPGet(
"https://git.hadrons.org/git/libmd.git", nil, "https://libbsd.freedesktop.org/releases/libmd-"+version+".tar.xz",
"refs/tags/"+version,
mustDecode(checksum), mustDecode(checksum),
), nil, &MakeHelper{ ), &PackageAttr{
Generate: "echo '" + version + "' > .dist-version && ./autogen", SourceKind: SourceKindTarXZ,
ScriptMakeEarly: ` }, (*MakeHelper)(nil),
install -D /usr/src/libmd/src/helper.c src/helper.c XZ,
`,
},
Automake,
Libtool,
), version ), version
} }
func init() { func init() {
@@ -36,17 +31,15 @@ func init() {
func (t Toolchain) newLibbsd() (pkg.Artifact, string) { func (t Toolchain) newLibbsd() (pkg.Artifact, string) {
const ( const (
version = "0.12.2" version = "0.12.2"
checksum = "NVS0xFLTwSP8JiElEftsZ-e1_C-IgJhHrHE77RwKt5178M7r087waO-zYx2_dfGX" checksum = "MEJ9MuLai32-gSJUrfmlDgGl7rszjdSxgb3ph9AcI5jv70VwlwwXJy1kxdAixm5Y"
) )
return t.NewPackage("libbsd", version, t.NewViaGit( return t.NewPackage("libbsd", version, pkg.NewHTTPGet(
"https://gitlab.freedesktop.org/libbsd/libbsd.git", nil, "https://libbsd.freedesktop.org/releases/libbsd-"+version+".tar.xz",
"refs/tags/"+version,
mustDecode(checksum), mustDecode(checksum),
), nil, &MakeHelper{ ), &PackageAttr{
Generate: "echo '" + version + "' > .dist-version && ./autogen", SourceKind: SourceKindTarXZ,
}, }, (*MakeHelper)(nil),
Automake, XZ,
Libtool,
Libmd, Libmd,
), version ), version
+2 -2
View File
@@ -4,8 +4,8 @@ import "hakurei.app/internal/pkg"
func (t Toolchain) newLibcap() (pkg.Artifact, string) { func (t Toolchain) newLibcap() (pkg.Artifact, string) {
const ( const (
version = "2.78" version = "2.77"
checksum = "wFdUkBhFMD9InPnrBZyegWrlPSAg_9JiTBC-eSFyWWlmbzL2qjh2mKxr9Kx2a8ut" checksum = "2GOTFU4cl2QoS7Dv5wh0c9-hxsQwIzMB9Y_gfAo5xKHqcM13fiHt1RbPkfemzjmB"
) )
return t.NewPackage("libcap", version, pkg.NewHTTPGetTar( return t.NewPackage("libcap", version, pkg.NewHTTPGetTar(
nil, "https://git.kernel.org/pub/scm/libs/libcap/libcap.git/"+ nil, "https://git.kernel.org/pub/scm/libs/libcap/libcap.git/"+
+29 -44
View File
@@ -13,8 +13,8 @@ import (
// llvmAttr holds the attributes that will be applied to a new [pkg.Artifact] // llvmAttr holds the attributes that will be applied to a new [pkg.Artifact]
// containing a LLVM variant. // containing a LLVM variant.
type llvmAttr struct { type llvmAttr struct {
// Enabled projects and runtimes. // Passed through to PackageAttr.Flag.
pr int flags int
// Concatenated with default environment for PackageAttr.Env. // Concatenated with default environment for PackageAttr.Env.
env []string env []string
@@ -24,6 +24,8 @@ type llvmAttr struct {
append []string append []string
// Passed through to PackageAttr.NonStage0. // Passed through to PackageAttr.NonStage0.
nonStage0 []pkg.Artifact nonStage0 []pkg.Artifact
// Passed through to PackageAttr.Paths.
paths []pkg.ExecPath
// Concatenated with default fixup for CMakeHelper.Script. // Concatenated with default fixup for CMakeHelper.Script.
script string script string
@@ -73,18 +75,19 @@ func llvmFlagName(flag int) string {
// newLLVMVariant returns a [pkg.Artifact] containing a LLVM variant. // newLLVMVariant returns a [pkg.Artifact] containing a LLVM variant.
func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact { func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
if attr == nil { if attr == nil {
panic("LLVM attr must be non-nil") panic("LLVM attr must be non-nil")
} }
var projects, runtimes []string var projects, runtimes []string
for i := 1; i < llvmProjectAll; i <<= 1 { for i := 1; i < llvmProjectAll; i <<= 1 {
if attr.pr&i != 0 { if attr.flags&i != 0 {
projects = append(projects, llvmFlagName(i)) projects = append(projects, llvmFlagName(i))
} }
} }
for i := (llvmProjectAll + 1) << 1; i < llvmRuntimeAll; i <<= 1 { for i := (llvmProjectAll + 1) << 1; i < llvmRuntimeAll; i <<= 1 {
if attr.pr&i != 0 { if attr.flags&i != 0 {
runtimes = append(runtimes, llvmFlagName(i)) runtimes = append(runtimes, llvmFlagName(i))
} }
} }
@@ -123,7 +126,7 @@ func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
}...) }...)
} }
if attr.pr&llvmProjectClang != 0 { if attr.flags&llvmProjectClang != 0 {
cache = append(cache, []KV{ cache = append(cache, []KV{
{"CLANG_DEFAULT_LINKER", "lld"}, {"CLANG_DEFAULT_LINKER", "lld"},
{"CLANG_DEFAULT_CXX_STDLIB", "libc++"}, {"CLANG_DEFAULT_CXX_STDLIB", "libc++"},
@@ -131,30 +134,30 @@ func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
{"CLANG_DEFAULT_UNWINDLIB", "libunwind"}, {"CLANG_DEFAULT_UNWINDLIB", "libunwind"},
}...) }...)
} }
if attr.pr&llvmProjectLld != 0 { if attr.flags&llvmProjectLld != 0 {
script += ` script += `
ln -s ld.lld /work/system/bin/ld ln -s ld.lld /work/system/bin/ld
` `
} }
if attr.pr&llvmRuntimeCompilerRT != 0 { if attr.flags&llvmRuntimeCompilerRT != 0 {
if attr.append == nil { if attr.append == nil {
cache = append(cache, []KV{ cache = append(cache, []KV{
{"COMPILER_RT_USE_LLVM_UNWINDER", "ON"}, {"COMPILER_RT_USE_LLVM_UNWINDER", "ON"},
}...) }...)
} }
} }
if attr.pr&llvmRuntimeLibunwind != 0 { if attr.flags&llvmRuntimeLibunwind != 0 {
cache = append(cache, []KV{ cache = append(cache, []KV{
{"LIBUNWIND_USE_COMPILER_RT", "ON"}, {"LIBUNWIND_USE_COMPILER_RT", "ON"},
}...) }...)
} }
if attr.pr&llvmRuntimeLibcxx != 0 { if attr.flags&llvmRuntimeLibcxx != 0 {
cache = append(cache, []KV{ cache = append(cache, []KV{
{"LIBCXX_HAS_MUSL_LIBC", "ON"}, {"LIBCXX_HAS_MUSL_LIBC", "ON"},
{"LIBCXX_USE_COMPILER_RT", "ON"}, {"LIBCXX_USE_COMPILER_RT", "ON"},
}...) }...)
} }
if attr.pr&llvmRuntimeLibcxxABI != 0 { if attr.flags&llvmRuntimeLibcxxABI != 0 {
cache = append(cache, []KV{ cache = append(cache, []KV{
{"LIBCXXABI_USE_COMPILER_RT", "ON"}, {"LIBCXXABI_USE_COMPILER_RT", "ON"},
{"LIBCXXABI_USE_LLVM_UNWINDER", "ON"}, {"LIBCXXABI_USE_LLVM_UNWINDER", "ON"},
@@ -167,22 +170,7 @@ ln -s ld.lld /work/system/bin/ld
mustDecode(llvmChecksum), mustDecode(llvmChecksum),
pkg.TarGzip, pkg.TarGzip,
), &PackageAttr{ ), &PackageAttr{
Patches: slices.Concat(attr.patches, []KV{ Patches: attr.patches,
{"increase-stack-size-unconditional", `diff --git a/llvm/lib/Support/Threading.cpp b/llvm/lib/Support/Threading.cpp
index 9da357a7ebb9..b2931510c1ae 100644
--- a/llvm/lib/Support/Threading.cpp
+++ b/llvm/lib/Support/Threading.cpp
@@ -80,7 +80,7 @@ unsigned llvm::ThreadPoolStrategy::compute_thread_count() const {
// keyword.
#include "llvm/Support/thread.h"
-#if defined(__APPLE__)
+#if defined(__APPLE__) || 1
// Darwin's default stack size for threads except the main one is only 512KB,
// which is not enough for some/many normal LLVM compilations. This implements
// the same interface as std::thread but requests the same stack size as the
`},
}),
NonStage0: attr.nonStage0, NonStage0: attr.nonStage0,
Env: slices.Concat([]string{ Env: slices.Concat([]string{
@@ -190,7 +178,8 @@ index 9da357a7ebb9..b2931510c1ae 100644
"ROSA_LLVM_RUNTIMES=" + strings.Join(runtimes, ";"), "ROSA_LLVM_RUNTIMES=" + strings.Join(runtimes, ";"),
}, attr.env), }, attr.env),
Flag: TExclusive, Paths: attr.paths,
Flag: TExclusive,
}, &CMakeHelper{ }, &CMakeHelper{
Variant: variant, Variant: variant,
@@ -212,19 +201,15 @@ index 9da357a7ebb9..b2931510c1ae 100644
// newLLVM returns LLVM toolchain across multiple [pkg.Artifact]. // newLLVM returns LLVM toolchain across multiple [pkg.Artifact].
func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) { func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) {
target := "'AArch64;RISCV;X86'" var target string
if t.isStage0() { switch runtime.GOARCH {
switch runtime.GOARCH { case "386", "amd64":
case "386", "amd64": target = "X86"
target = "X86" case "arm64":
case "arm64": target = "AArch64"
target = "AArch64"
case "riscv64":
target = "RISCV"
default: default:
panic("unsupported target " + runtime.GOARCH) panic("unsupported target " + runtime.GOARCH)
}
} }
minimalDeps := []KV{ minimalDeps := []KV{
@@ -246,7 +231,7 @@ func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) {
{"CMAKE_CXX_COMPILER_TARGET", ""}, {"CMAKE_CXX_COMPILER_TARGET", ""},
{"COMPILER_RT_BUILD_BUILTINS", "ON"}, {"COMPILER_RT_BUILD_BUILTINS", "ON"},
{"COMPILER_RT_DEFAULT_TARGET_ONLY", "OFF"}, {"COMPILER_RT_DEFAULT_TARGET_ONLY", "ON"},
{"COMPILER_RT_SANITIZERS_TO_BUILD", "asan"}, {"COMPILER_RT_SANITIZERS_TO_BUILD", "asan"},
{"LLVM_ENABLE_PER_TARGET_RUNTIME_DIR", "ON"}, {"LLVM_ENABLE_PER_TARGET_RUNTIME_DIR", "ON"},
@@ -290,7 +275,7 @@ ln -s \
env: stage0ExclConcat(t, []string{}, env: stage0ExclConcat(t, []string{},
"LDFLAGS="+earlyLDFLAGS(false), "LDFLAGS="+earlyLDFLAGS(false),
), ),
pr: llvmRuntimeLibunwind | llvmRuntimeLibcxx | llvmRuntimeLibcxxABI, flags: llvmRuntimeLibunwind | llvmRuntimeLibcxx | llvmRuntimeLibcxxABI,
cmake: slices.Concat([]KV{ cmake: slices.Concat([]KV{
// libc++ not yet available // libc++ not yet available
{"CMAKE_CXX_COMPILER_WORKS", "ON"}, {"CMAKE_CXX_COMPILER_WORKS", "ON"},
@@ -306,7 +291,7 @@ ln -s \
}) })
clang = t.newLLVMVariant("clang", &llvmAttr{ clang = t.newLLVMVariant("clang", &llvmAttr{
pr: llvmProjectClang | llvmProjectLld, flags: llvmProjectClang | llvmProjectLld,
env: stage0ExclConcat(t, []string{}, env: stage0ExclConcat(t, []string{},
"CFLAGS="+earlyCFLAGS, "CFLAGS="+earlyCFLAGS,
"CXXFLAGS="+earlyCXXFLAGS(), "CXXFLAGS="+earlyCXXFLAGS(),
@@ -329,7 +314,7 @@ ln -s clang++ /work/system/bin/c++
ninja check-all ninja check-all
`, `,
patches: []KV{ patches: slices.Concat([]KV{
{"add-rosa-vendor", `diff --git a/llvm/include/llvm/TargetParser/Triple.h b/llvm/include/llvm/TargetParser/Triple.h {"add-rosa-vendor", `diff --git a/llvm/include/llvm/TargetParser/Triple.h b/llvm/include/llvm/TargetParser/Triple.h
index 9c83abeeb3b1..5acfe5836a23 100644 index 9c83abeeb3b1..5acfe5836a23 100644
--- a/llvm/include/llvm/TargetParser/Triple.h --- a/llvm/include/llvm/TargetParser/Triple.h
@@ -501,7 +486,7 @@ index 64324a3f8b01..15ce70b68217 100644
"/System/Library/Frameworks"}; "/System/Library/Frameworks"};
`}, `},
}, }, clangPatches),
}) })
return return
+4
View File
@@ -0,0 +1,4 @@
package rosa
// clangPatches are patches applied to the LLVM source tree for building clang.
var clangPatches []KV
+12
View File
@@ -0,0 +1,12 @@
package rosa
// clangPatches are patches applied to the LLVM source tree for building clang.
var clangPatches []KV
// one version behind, latest fails 5 tests with 2 flaky on arm64
const (
llvmVersionMajor = "21"
llvmVersion = llvmVersionMajor + ".1.8"
llvmChecksum = "8SUpqDkcgwOPsqHVtmf9kXfFeVmjVxl4LMn-qSE1AI_Xoeju-9HaoPNGtidyxyka"
)
+4 -2
View File
@@ -1,9 +1,11 @@
//go:build !arm64
package rosa package rosa
// latest version of LLVM, conditional to temporarily avoid broken new releases // latest version of LLVM, conditional to temporarily avoid broken new releases
const ( const (
llvmVersionMajor = "22" llvmVersionMajor = "22"
llvmVersion = llvmVersionMajor + ".1.3" llvmVersion = llvmVersionMajor + ".1.2"
llvmChecksum = "CUwnpzua_y28HZ9oI0NmcKL2wClsSjFpgY9do5-7cCZJHI5KNF64vfwGvY0TYyR3" llvmChecksum = "FwsmurWDVyYYQlOowowFjekwIGSB5__aKTpW_VGP3eWoZGXvBny-bOn1DuQ1U5xE"
) )
+14 -14
View File
@@ -53,24 +53,24 @@ func init() {
func (t Toolchain) newLibnftnl() (pkg.Artifact, string) { func (t Toolchain) newLibnftnl() (pkg.Artifact, string) {
const ( const (
version = "1.3.1" version = "1.3.1"
checksum = "91ou66K-I17iX6DB6hiQkhhC_v4DFW5iDGzwjVRNbJNEmKqowLZBlh3FY-ZDO0r9" checksum = "A6EFNv2TbOcjcsXX2hQ-pKsF5FvlSh-BNEf9LrgnVH4nDjcv6NbtyHkTriz9kIEu"
) )
return t.NewPackage("libnftnl", version, t.NewViaGit( return t.NewPackage("libnftnl", version, pkg.NewHTTPGet(
"https://git.netfilter.org/libnftnl", nil, "https://www.netfilter.org/projects/libnftnl/files/"+
"refs/tags/libnftnl-"+version, "libnftnl-"+version+".tar.xz",
mustDecode(checksum), mustDecode(checksum),
), &PackageAttr{ ), &PackageAttr{
SourceKind: SourceKindTarXZ,
Env: []string{ Env: []string{
"CFLAGS=-D_GNU_SOURCE", "CFLAGS=-D_GNU_SOURCE",
}, },
}, &MakeHelper{ }, &MakeHelper{
Generate: "./autogen.sh",
Configure: []KV{ Configure: []KV{
{"enable-static"}, {"enable-static"},
}, },
}, },
Automake, XZ,
Libtool,
PkgConfig, PkgConfig,
Libmnl, Libmnl,
@@ -96,13 +96,15 @@ func init() {
func (t Toolchain) newIPTables() (pkg.Artifact, string) { func (t Toolchain) newIPTables() (pkg.Artifact, string) {
const ( const (
version = "1.8.13" version = "1.8.13"
checksum = "TUA-cFIAsiMvtRR-XzQvXzoIhJUOc9J2gQDJCbBRjmgmVfGfPTCf58wL7e-cUKVQ" checksum = "JsNI7dyZHnHLtDkKWAxzAIMZ5t-ff3LkSPqNJsn5VM5Eq2m1bA5NKI-XfMRpQsg6"
) )
return t.NewPackage("iptables", version, t.NewViaGit( return t.NewPackage("iptables", version, pkg.NewHTTPGet(
"https://git.netfilter.org/iptables", nil, "https://www.netfilter.org/projects/iptables/files/"+
"refs/tags/v"+version, "iptables-"+version+".tar.xz",
mustDecode(checksum), mustDecode(checksum),
), &PackageAttr{ ), &PackageAttr{
SourceKind: SourceKindTarXZ,
ScriptEarly: ` ScriptEarly: `
rm \ rm \
extensions/libxt_connlabel.txlate \ extensions/libxt_connlabel.txlate \
@@ -113,7 +115,6 @@ sed -i \
extensions/libebt_snat.txlate extensions/libebt_snat.txlate
`, `,
}, &MakeHelper{ }, &MakeHelper{
Generate: "./autogen.sh",
Configure: []KV{ Configure: []KV{
{"enable-static"}, {"enable-static"},
}, },
@@ -122,8 +123,7 @@ ln -s ../system/bin/bash /bin/
chmod +w /etc/ && ln -s ../usr/src/iptables/etc/ethertypes /etc/ chmod +w /etc/ && ln -s ../usr/src/iptables/etc/ethertypes /etc/
`, `,
}, },
Automake, XZ,
Libtool,
PkgConfig, PkgConfig,
Bash, Bash,
Python, Python,
+2 -2
View File
@@ -4,8 +4,8 @@ import "hakurei.app/internal/pkg"
func (t Toolchain) newOpenSSL() (pkg.Artifact, string) { func (t Toolchain) newOpenSSL() (pkg.Artifact, string) {
const ( const (
version = "3.6.2" version = "3.6.1"
checksum = "jH004dXTiE01Hp0kyShkWXwrSHEksZi4i_3v47D9H9Uz9LQ1aMwF7mrl2Tb4t_XA" checksum = "boMAj2SIVIFXHswZva3qHJuFEpc32rxCCu07wjMPsVe9nn_976BGMmW_5P1zthgg"
) )
return t.NewPackage("openssl", version, pkg.NewHTTPGetTar( return t.NewPackage("openssl", version, pkg.NewHTTPGetTar(
nil, "https://github.com/openssl/openssl/releases/download/"+ nil, "https://github.com/openssl/openssl/releases/download/"+
+2 -2
View File
@@ -9,8 +9,8 @@ import (
func (t Toolchain) newPython() (pkg.Artifact, string) { func (t Toolchain) newPython() (pkg.Artifact, string) {
const ( const (
version = "3.14.4" version = "3.14.3"
checksum = "X0VRAAGOlCVldh4J9tRAE-YrJtDvqfQTJaqxKPXNX6YTPlwpR9GwA5WRIZDO-63s" checksum = "ajEC32WPmn9Jvll0n4gGvlTvhMPUHb2H_j5_h9jf_esHmkZBRfAumDcKY7nTTsCH"
) )
return t.NewPackage("python", version, pkg.NewHTTPGetTar( return t.NewPackage("python", version, pkg.NewHTTPGetTar(
nil, "https://www.python.org/ftp/python/"+version+ nil, "https://www.python.org/ftp/python/"+version+
-2
View File
@@ -56,8 +56,6 @@ func linuxArch() string {
return "x86_64" return "x86_64"
case "arm64": case "arm64":
return "aarch64" return "aarch64"
case "riscv64":
return "riscv64"
default: default:
panic("unsupported target " + runtime.GOARCH) panic("unsupported target " + runtime.GOARCH)
-2
View File
@@ -67,8 +67,6 @@ func NewStage0() pkg.Artifact {
seed = "tqM1Li15BJ-uFG8zU-XjgFxoN_kuzh1VxrSDVUVa0vGmo-NeWapSftH739sY8EAg" seed = "tqM1Li15BJ-uFG8zU-XjgFxoN_kuzh1VxrSDVUVa0vGmo-NeWapSftH739sY8EAg"
case "arm64": case "arm64":
seed = "CJj3ZSnRyLmFHlWIQtTPQD9oikOZY4cD_mI3v_-LIYc2hhg-cq_CZFBLzQBAkFIn" seed = "CJj3ZSnRyLmFHlWIQtTPQD9oikOZY4cD_mI3v_-LIYc2hhg-cq_CZFBLzQBAkFIn"
case "riscv64":
seed = "FcszJjcVWdKAnn-bt8qmUn5GUUTjv_xQjXOWkUpOplRkG3Ckob3StUoAi5KQ5-QF"
default: default:
panic("unsupported target " + runtime.GOARCH) panic("unsupported target " + runtime.GOARCH)
+2 -2
View File
@@ -8,8 +8,8 @@ import (
func (t Toolchain) newTamaGo() (pkg.Artifact, string) { func (t Toolchain) newTamaGo() (pkg.Artifact, string) {
const ( const (
version = "1.26.2" version = "1.26.1"
checksum = "5xlhWq2NGhYCjt0y73QkydJ386lxg6-HkiO84ne6ByQSJBDat7-HSVzNA6jy7Laz" checksum = "fimZnklQcYWGsTQU8KepLn-yCYaTfNdMI9DCg6NJVQv-3gOJnUEO9mqRCMAHnEXZ"
) )
return t.New("tamago-go"+version, 0, t.AppendPresets(nil, return t.New("tamago-go"+version, 0, t.AppendPresets(nil,
Bash, Bash,
+6 -9
View File
@@ -24,7 +24,7 @@ func entryEncode(w io.Writer, s *hst.State) error {
} }
// entryDecodeHeader calls entryReadHeader, returning [hst.AppError] for a non-nil error. // entryDecodeHeader calls entryReadHeader, returning [hst.AppError] for a non-nil error.
func entryDecodeHeader(r io.Reader) (hst.Enablements, error) { func entryDecodeHeader(r io.Reader) (hst.Enablement, error) {
if et, err := entryReadHeader(r); err != nil { if et, err := entryReadHeader(r); err != nil {
return 0, &hst.AppError{Step: "decode state header", Err: err} return 0, &hst.AppError{Step: "decode state header", Err: err}
} else { } else {
@@ -32,23 +32,20 @@ func entryDecodeHeader(r io.Reader) (hst.Enablements, error) {
} }
} }
// entryDecode decodes [hst.State] from [io.Reader] and stores the result in the // entryDecode decodes [hst.State] from [io.Reader] and stores the result in the value pointed to by p.
// value pointed to by p. entryDecode validates the embedded [hst.Config] value. // entryDecode validates the embedded [hst.Config] value.
// //
// A non-nil error returned by entryDecode is of type [hst.AppError]. // A non-nil error returned by entryDecode is of type [hst.AppError].
func entryDecode(r io.Reader, p *hst.State) (hst.Enablements, error) { func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error) {
if et, err := entryDecodeHeader(r); err != nil { if et, err := entryDecodeHeader(r); err != nil {
return et, err return et, err
} else if err = gob.NewDecoder(r).Decode(&p); err != nil { } else if err = gob.NewDecoder(r).Decode(&p); err != nil {
return et, &hst.AppError{Step: "decode state body", Err: err} return et, &hst.AppError{Step: "decode state body", Err: err}
} else if err = p.Config.Validate(hst.VAllowInsecure); err != nil { } else if err = p.Config.Validate(); err != nil {
return et, err return et, err
} else if p.Enablements.Unwrap() != et { } else if p.Enablements.Unwrap() != et {
return et, &hst.AppError{Step: "validate state enablement", Err: os.ErrInvalid, return et, &hst.AppError{Step: "validate state enablement", Err: os.ErrInvalid,
Msg: fmt.Sprintf( Msg: fmt.Sprintf("state entry %s has unexpected enablement byte %#x, %#x", p.ID.String(), byte(p.Enablements.Unwrap()), byte(et))}
"state entry %s has unexpected enablement byte %#x, %#x",
p.ID.String(), byte(p.Enablements.Unwrap()), byte(et),
)}
} else { } else {
return et, nil return et, nil
} }
+12 -15
View File
@@ -14,25 +14,22 @@ import (
const ( const (
// entryHeaderMagic are magic bytes at the beginning of the state entry file. // entryHeaderMagic are magic bytes at the beginning of the state entry file.
entryHeaderMagic = "\x00\xff\xca\xfe" entryHeaderMagic = "\x00\xff\xca\xfe"
// entryHeaderRevision follows entryHeaderMagic and is incremented for // entryHeaderRevision follows entryHeaderMagic and is incremented for revisions of the format.
// revisions of the format.
entryHeaderRevision = "\x00\x00" entryHeaderRevision = "\x00\x00"
// entryHeaderSize is the fixed size of the header in bytes, including the // entryHeaderSize is the fixed size of the header in bytes, including the enablement byte and its complement.
// enablement byte and its complement.
entryHeaderSize = len(entryHeaderMagic+entryHeaderRevision) + 2 entryHeaderSize = len(entryHeaderMagic+entryHeaderRevision) + 2
) )
// entryHeaderEncode encodes a state entry header for a [hst.Enablements] byte. // entryHeaderEncode encodes a state entry header for a [hst.Enablement] byte.
func entryHeaderEncode(et hst.Enablements) *[entryHeaderSize]byte { func entryHeaderEncode(et hst.Enablement) *[entryHeaderSize]byte {
data := [entryHeaderSize]byte([]byte( data := [entryHeaderSize]byte([]byte(
entryHeaderMagic + entryHeaderRevision + string([]hst.Enablements{et, ^et}), entryHeaderMagic + entryHeaderRevision + string([]hst.Enablement{et, ^et}),
)) ))
return &data return &data
} }
// entryHeaderDecode validates a state entry header and returns the // entryHeaderDecode validates a state entry header and returns the [hst.Enablement] byte.
// [hst.Enablements] byte. func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablement, error) {
func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablements, error) {
if magic := data[:len(entryHeaderMagic)]; string(magic) != entryHeaderMagic { if magic := data[:len(entryHeaderMagic)]; string(magic) != entryHeaderMagic {
return 0, errors.New("invalid header " + hex.EncodeToString(magic)) return 0, errors.New("invalid header " + hex.EncodeToString(magic))
} }
@@ -44,7 +41,7 @@ func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablements, error) {
if et != ^data[len(entryHeaderMagic+entryHeaderRevision)+1] { if et != ^data[len(entryHeaderMagic+entryHeaderRevision)+1] {
return 0, errors.New("header enablement value is inconsistent") return 0, errors.New("header enablement value is inconsistent")
} }
return hst.Enablements(et), nil return hst.Enablement(et), nil
} }
// EntrySizeError is returned for a file too small to hold a state entry header. // EntrySizeError is returned for a file too small to hold a state entry header.
@@ -71,8 +68,8 @@ func entryCheckFile(fi os.FileInfo) error {
return nil return nil
} }
// entryReadHeader reads [hst.Enablements] from an [io.Reader]. // entryReadHeader reads [hst.Enablement] from an [io.Reader].
func entryReadHeader(r io.Reader) (hst.Enablements, error) { func entryReadHeader(r io.Reader) (hst.Enablement, error) {
var data [entryHeaderSize]byte var data [entryHeaderSize]byte
if n, err := r.Read(data[:]); err != nil { if n, err := r.Read(data[:]); err != nil {
return 0, err return 0, err
@@ -82,8 +79,8 @@ func entryReadHeader(r io.Reader) (hst.Enablements, error) {
return entryHeaderDecode(&data) return entryHeaderDecode(&data)
} }
// entryWriteHeader writes [hst.Enablements] header to an [io.Writer]. // entryWriteHeader writes [hst.Enablement] header to an [io.Writer].
func entryWriteHeader(w io.Writer, et hst.Enablements) error { func entryWriteHeader(w io.Writer, et hst.Enablement) error {
_, err := w.Write(entryHeaderEncode(et)[:]) _, err := w.Write(entryHeaderEncode(et)[:])
return err return err
} }
+1 -1
View File
@@ -20,7 +20,7 @@ func TestEntryHeader(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
data [entryHeaderSize]byte data [entryHeaderSize]byte
et hst.Enablements et hst.Enablement
err error err error
}{ }{
{"complement mismatch", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00, {"complement mismatch", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
+14 -27
View File
@@ -14,7 +14,6 @@ import (
) )
// EntryHandle is a handle on a state entry retrieved from a [Handle]. // EntryHandle is a handle on a state entry retrieved from a [Handle].
//
// Must only be used while its parent [Handle.Lock] is held. // Must only be used while its parent [Handle.Lock] is held.
type EntryHandle struct { type EntryHandle struct {
// Error returned while decoding pathname. // Error returned while decoding pathname.
@@ -28,7 +27,6 @@ type EntryHandle struct {
} }
// open opens the underlying state entry file. // open opens the underlying state entry file.
//
// A non-nil error returned by open is of type [hst.AppError]. // A non-nil error returned by open is of type [hst.AppError].
func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) { func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
if eh.DecodeErr != nil { if eh.DecodeErr != nil {
@@ -43,7 +41,6 @@ func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
} }
// Destroy removes the underlying state entry. // Destroy removes the underlying state entry.
//
// A non-nil error returned by Destroy is of type [hst.AppError]. // A non-nil error returned by Destroy is of type [hst.AppError].
func (eh *EntryHandle) Destroy() error { func (eh *EntryHandle) Destroy() error {
// destroy does not go through open // destroy does not go through open
@@ -58,10 +55,8 @@ func (eh *EntryHandle) Destroy() error {
} }
// save encodes [hst.State] and writes it to the underlying file. // save encodes [hst.State] and writes it to the underlying file.
//
// An error is returned if a file already exists with the same identifier. // An error is returned if a file already exists with the same identifier.
// save does not validate the embedded [hst.Config]. // save does not validate the embedded [hst.Config].
//
// A non-nil error returned by save is of type [hst.AppError]. // A non-nil error returned by save is of type [hst.AppError].
func (eh *EntryHandle) save(state *hst.State) error { func (eh *EntryHandle) save(state *hst.State) error {
f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
@@ -76,19 +71,17 @@ func (eh *EntryHandle) save(state *hst.State) error {
return err return err
} }
// Load loads and validates the state entry header, and returns the // Load loads and validates the state entry header, and returns the [hst.Enablement] byte.
// [hst.Enablements] byte. For a non-nil v, the full state payload is decoded // for a non-nil v, the full state payload is decoded and stored in the value pointed to by v.
// and stored in the value pointed to by v. // Load validates the embedded [hst.Config] value.
// // A non-nil error returned by Load is of type [hst.AppError].
// Load validates the embedded [hst.Config] value. A non-nil error returned by func (eh *EntryHandle) Load(v *hst.State) (hst.Enablement, error) {
// Load is of type [hst.AppError].
func (eh *EntryHandle) Load(v *hst.State) (hst.Enablements, error) {
f, err := eh.open(os.O_RDONLY, 0) f, err := eh.open(os.O_RDONLY, 0)
if err != nil { if err != nil {
return 0, err return 0, err
} }
var et hst.Enablements var et hst.Enablement
if v != nil { if v != nil {
et, err = entryDecode(f, v) et, err = entryDecode(f, v)
if err == nil && v.ID != eh.ID { if err == nil && v.ID != eh.ID {
@@ -106,7 +99,6 @@ func (eh *EntryHandle) Load(v *hst.State) (hst.Enablements, error) {
} }
// Handle is a handle on a [Store] segment. // Handle is a handle on a [Store] segment.
//
// Initialised by [Store.Handle]. // Initialised by [Store.Handle].
type Handle struct { type Handle struct {
// Identity of instances tracked by this segment. // Identity of instances tracked by this segment.
@@ -121,9 +113,8 @@ type Handle struct {
mu sync.Mutex mu sync.Mutex
} }
// Lock attempts to acquire a lock on [Handle]. If successful, Lock returns a // Lock attempts to acquire a lock on [Handle].
// non-nil unlock function. // If successful, Lock returns a non-nil unlock function.
//
// A non-nil error returned by Lock is of type [hst.AppError]. // A non-nil error returned by Lock is of type [hst.AppError].
func (h *Handle) Lock() (unlock func(), err error) { func (h *Handle) Lock() (unlock func(), err error) {
if unlock, err = h.fileMu.Lock(); err != nil { if unlock, err = h.fileMu.Lock(); err != nil {
@@ -132,24 +123,20 @@ func (h *Handle) Lock() (unlock func(), err error) {
return return
} }
// Save attempts to save [hst.State] as a segment entry, and returns its // Save attempts to save [hst.State] as a segment entry, and returns its [EntryHandle].
// [EntryHandle]. Must be called while holding [Handle.Lock]. // Must be called while holding [Handle.Lock].
//
// An error is returned if an entry already exists with the same identifier. // An error is returned if an entry already exists with the same identifier.
// Save does not validate the embedded [hst.Config]. // Save does not validate the embedded [hst.Config].
//
// A non-nil error returned by Save is of type [hst.AppError]. // A non-nil error returned by Save is of type [hst.AppError].
func (h *Handle) Save(state *hst.State) (*EntryHandle, error) { func (h *Handle) Save(state *hst.State) (*EntryHandle, error) {
eh := EntryHandle{nil, h.Path.Append(state.ID.String()), state.ID} eh := EntryHandle{nil, h.Path.Append(state.ID.String()), state.ID}
return &eh, eh.save(state) return &eh, eh.save(state)
} }
// Entries returns an iterator over all [EntryHandle] held in this segment. Must // Entries returns an iterator over all [EntryHandle] held in this segment.
// be called while holding [Handle.Lock]. // Must be called while holding [Handle.Lock].
// // A non-nil error attached to a [EntryHandle] indicates a malformed identifier and is of type [hst.AppError].
// A non-nil error attached to a [EntryHandle] indicates a malformed identifier // A non-nil error returned by Entries is of type [hst.AppError].
// and is of type [hst.AppError]. A non-nil error returned by Entries is of type
// [hst.AppError].
func (h *Handle) Entries() (iter.Seq[*EntryHandle], int, error) { func (h *Handle) Entries() (iter.Seq[*EntryHandle], int, error) {
// for error reporting // for error reporting
const step = "read store segment entries" const step = "read store segment entries"
+1 -1
View File
@@ -21,7 +21,7 @@ import (
// Made available here for direct validation of state entry files. // Made available here for direct validation of state entry files.
// //
//go:linkname entryDecode hakurei.app/internal/store.entryDecode //go:linkname entryDecode hakurei.app/internal/store.entryDecode
func entryDecode(r io.Reader, p *hst.State) (hst.Enablements, error) func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error)
// Made available here for direct access to known segment handles. // Made available here for direct access to known segment handles.
// //
+4 -5
View File
@@ -17,21 +17,20 @@ func (sys *I) UpdatePerm(path *check.Absolute, perms ...acl.Perm) *I {
return sys return sys
} }
// UpdatePermType maintains [acl.Perms] on a file until its [hst.Enablements] is // UpdatePermType maintains [acl.Perms] on a file until its [Enablement] is no longer satisfied.
// no longer satisfied. func (sys *I) UpdatePermType(et hst.Enablement, path *check.Absolute, perms ...acl.Perm) *I {
func (sys *I) UpdatePermType(et hst.Enablements, path *check.Absolute, perms ...acl.Perm) *I {
sys.ops = append(sys.ops, &aclUpdateOp{et, path.String(), perms}) sys.ops = append(sys.ops, &aclUpdateOp{et, path.String(), perms})
return sys return sys
} }
// aclUpdateOp implements [I.UpdatePermType]. // aclUpdateOp implements [I.UpdatePermType].
type aclUpdateOp struct { type aclUpdateOp struct {
et hst.Enablements et hst.Enablement
path string path string
perms acl.Perms perms acl.Perms
} }
func (a *aclUpdateOp) Type() hst.Enablements { return a.et } func (a *aclUpdateOp) Type() hst.Enablement { return a.et }
func (a *aclUpdateOp) apply(sys *I) error { func (a *aclUpdateOp) apply(sys *I) error {
sys.msg.Verbose("applying ACL", a) sys.msg.Verbose("applying ACL", a)
+2 -4
View File
@@ -31,9 +31,7 @@ func (sys *I) MustProxyDBus(
} }
} }
// ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via // ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via [dbus] and terminates it on revert.
// [dbus] and terminates it on revert.
//
// This [Op] is always [Process] scoped. // This [Op] is always [Process] scoped.
func (sys *I) ProxyDBus( func (sys *I) ProxyDBus(
session, system *hst.BusConfig, session, system *hst.BusConfig,
@@ -86,7 +84,7 @@ type dbusProxyOp struct {
system bool system bool
} }
func (d *dbusProxyOp) Type() hst.Enablements { return Process } func (d *dbusProxyOp) Type() hst.Enablement { return Process }
func (d *dbusProxyOp) apply(sys *I) error { func (d *dbusProxyOp) apply(sys *I) error {
sys.msg.Verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0]) sys.msg.Verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0])
+3 -7
View File
@@ -21,16 +21,12 @@ type osFile interface {
fs.File fs.File
} }
// syscallDispatcher provides methods that make state-dependent system calls as // syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
// part of their behaviour.
//
// syscallDispatcher is embedded in [I], so all methods must be unexported. // syscallDispatcher is embedded in [I], so all methods must be unexported.
type syscallDispatcher interface { type syscallDispatcher interface {
// new starts a goroutine with a new instance of syscallDispatcher. // new starts a goroutine with a new instance of syscallDispatcher.
// // A syscallDispatcher must never be used in any goroutine other than the one owning it,
// A syscallDispatcher must never be used in any goroutine other than the // just synchronising access is not enough, as this is for test instrumentation.
// one owning it, just synchronising access is not enough, as this is for
// test instrumentation.
new(f func(k syscallDispatcher)) new(f func(k syscallDispatcher))
// stat provides os.Stat. // stat provides os.Stat.
+2 -2
View File
@@ -27,7 +27,7 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
type opBehaviourTestCase struct { type opBehaviourTestCase struct {
name string name string
uid int uid int
ec hst.Enablements ec hst.Enablement
op Op op Op
apply []stub.Call apply []stub.Call
@@ -158,7 +158,7 @@ type opMetaTestCase struct {
name string name string
op Op op Op
wantType hst.Enablements wantType hst.Enablement
wantPath string wantPath string
wantString string wantString string
} }
+4 -4
View File
@@ -12,19 +12,19 @@ func (sys *I) Link(oldname, newname *check.Absolute) *I {
return sys.LinkFileType(Process, oldname, newname) return sys.LinkFileType(Process, oldname, newname)
} }
// LinkFileType maintains a hardlink until its [hst.Enablements] is no longer satisfied. // LinkFileType maintains a hardlink until its [Enablement] is no longer satisfied.
func (sys *I) LinkFileType(et hst.Enablements, oldname, newname *check.Absolute) *I { func (sys *I) LinkFileType(et hst.Enablement, oldname, newname *check.Absolute) *I {
sys.ops = append(sys.ops, &hardlinkOp{et, newname.String(), oldname.String()}) sys.ops = append(sys.ops, &hardlinkOp{et, newname.String(), oldname.String()})
return sys return sys
} }
// hardlinkOp implements [I.LinkFileType]. // hardlinkOp implements [I.LinkFileType].
type hardlinkOp struct { type hardlinkOp struct {
et hst.Enablements et hst.Enablement
dst, src string dst, src string
} }
func (l *hardlinkOp) Type() hst.Enablements { return l.et } func (l *hardlinkOp) Type() hst.Enablement { return l.et }
func (l *hardlinkOp) apply(sys *I) error { func (l *hardlinkOp) apply(sys *I) error {
sys.msg.Verbose("linking", l) sys.msg.Verbose("linking", l)
+4 -5
View File
@@ -15,22 +15,21 @@ func (sys *I) Ensure(name *check.Absolute, perm os.FileMode) *I {
return sys return sys
} }
// Ephemeral ensures the existence of a directory until its [hst.Enablements] is // Ephemeral ensures the existence of a directory until its [Enablement] is no longer satisfied.
// no longer satisfied. func (sys *I) Ephemeral(et hst.Enablement, name *check.Absolute, perm os.FileMode) *I {
func (sys *I) Ephemeral(et hst.Enablements, name *check.Absolute, perm os.FileMode) *I {
sys.ops = append(sys.ops, &mkdirOp{et, name.String(), perm, true}) sys.ops = append(sys.ops, &mkdirOp{et, name.String(), perm, true})
return sys return sys
} }
// mkdirOp implements [I.Ensure] and [I.Ephemeral]. // mkdirOp implements [I.Ensure] and [I.Ephemeral].
type mkdirOp struct { type mkdirOp struct {
et hst.Enablements et hst.Enablement
path string path string
perm os.FileMode perm os.FileMode
ephemeral bool ephemeral bool
} }
func (m *mkdirOp) Type() hst.Enablements { return m.et } func (m *mkdirOp) Type() hst.Enablement { return m.et }
func (m *mkdirOp) apply(sys *I) error { func (m *mkdirOp) apply(sys *I) error {
sys.msg.Verbose("ensuring directory", m) sys.msg.Verbose("ensuring directory", m)
+3 -4
View File
@@ -12,9 +12,8 @@ import (
) )
// PipeWire maintains a pipewire socket with SecurityContext attached via [pipewire]. // PipeWire maintains a pipewire socket with SecurityContext attached via [pipewire].
// // The socket stops accepting connections once the pipe referred to by sync is closed.
// The socket stops accepting connections once the pipe referred to by sync is // The socket is pathname only and is destroyed on revert.
// closed. The socket is pathname only and is destroyed on revert.
func (sys *I) PipeWire(dst *check.Absolute, appID, instanceID string) *I { func (sys *I) PipeWire(dst *check.Absolute, appID, instanceID string) *I {
sys.ops = append(sys.ops, &pipewireOp{nil, dst, appID, instanceID}) sys.ops = append(sys.ops, &pipewireOp{nil, dst, appID, instanceID})
return sys return sys
@@ -28,7 +27,7 @@ type pipewireOp struct {
appID, instanceID string appID, instanceID string
} }
func (p *pipewireOp) Type() hst.Enablements { return Process } func (p *pipewireOp) Type() hst.Enablement { return Process }
func (p *pipewireOp) apply(sys *I) (err error) { func (p *pipewireOp) apply(sys *I) (err error) {
var ctx *pipewire.Context var ctx *pipewire.Context
+7 -9
View File
@@ -20,21 +20,21 @@ const (
) )
// Criteria specifies types of Op to revert. // Criteria specifies types of Op to revert.
type Criteria hst.Enablements type Criteria hst.Enablement
func (ec *Criteria) hasType(t hst.Enablements) bool { func (ec *Criteria) hasType(t hst.Enablement) bool {
// nil criteria: revert everything except User // nil criteria: revert everything except User
if ec == nil { if ec == nil {
return t != User return t != User
} }
return hst.Enablements(*ec)&t != 0 return hst.Enablement(*ec)&t != 0
} }
// Op is a reversible system operation. // Op is a reversible system operation.
type Op interface { type Op interface {
// Type returns [Op]'s enablement type, for matching a revert criteria. // Type returns [Op]'s enablement type, for matching a revert criteria.
Type() hst.Enablements Type() hst.Enablement
apply(sys *I) error apply(sys *I) error
revert(sys *I, ec *Criteria) error revert(sys *I, ec *Criteria) error
@@ -44,8 +44,8 @@ type Op interface {
String() string String() string
} }
// TypeString extends [hst.Enablements] to support [User] and [Process]. // TypeString extends [hst.Enablement.String] to support [User] and [Process].
func TypeString(e hst.Enablements) string { func TypeString(e hst.Enablement) string {
switch e { switch e {
case User: case User:
return "user" return "user"
@@ -110,9 +110,7 @@ func (sys *I) Equal(target *I) bool {
return true return true
} }
// Commit applies all [Op] held by [I] and reverts all successful [Op] on first // Commit applies all [Op] held by [I] and reverts all successful [Op] on first error encountered.
// error encountered.
//
// Commit must not be called more than once. // Commit must not be called more than once.
func (sys *I) Commit() error { func (sys *I) Commit() error {
if sys.committed { if sys.committed {
+3 -3
View File
@@ -20,7 +20,7 @@ func TestCriteria(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
ec, t hst.Enablements ec, t hst.Enablement
want bool want bool
}{ }{
{"nil", 0xff, hst.EWayland, true}, {"nil", 0xff, hst.EWayland, true},
@@ -47,7 +47,7 @@ func TestTypeString(t *testing.T) {
t.Parallel() t.Parallel()
testCases := []struct { testCases := []struct {
e hst.Enablements e hst.Enablement
want string want string
}{ }{
{hst.EWayland, hst.EWayland.String()}, {hst.EWayland, hst.EWayland.String()},
@@ -190,7 +190,7 @@ func TestCommitRevert(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
f func(sys *I) f func(sys *I)
ec hst.Enablements ec hst.Enablement
commit []stub.Call commit []stub.Call
wantErrCommit error wantErrCommit error
+3 -4
View File
@@ -11,9 +11,8 @@ import (
) )
// Wayland maintains a wayland socket with security-context-v1 attached via [wayland]. // Wayland maintains a wayland socket with security-context-v1 attached via [wayland].
// // The socket stops accepting connections once the pipe referred to by sync is closed.
// The socket stops accepting connections once the pipe referred to by sync is // The socket is pathname only and is destroyed on revert.
// closed. The socket is pathname only and is destroyed on revert.
func (sys *I) Wayland(dst, src *check.Absolute, appID, instanceID string) *I { func (sys *I) Wayland(dst, src *check.Absolute, appID, instanceID string) *I {
sys.ops = append(sys.ops, &waylandOp{nil, sys.ops = append(sys.ops, &waylandOp{nil,
dst, src, appID, instanceID}) dst, src, appID, instanceID})
@@ -27,7 +26,7 @@ type waylandOp struct {
appID, instanceID string appID, instanceID string
} }
func (w *waylandOp) Type() hst.Enablements { return Process } func (w *waylandOp) Type() hst.Enablement { return Process }
func (w *waylandOp) apply(sys *I) (err error) { func (w *waylandOp) apply(sys *I) (err error) {
if w.ctx, err = sys.waylandNew(w.src, w.dst, w.appID, w.instanceID); err != nil { if w.ctx, err = sys.waylandNew(w.src, w.dst, w.appID, w.instanceID); err != nil {
+2 -3
View File
@@ -5,8 +5,7 @@ import (
"hakurei.app/internal/xcb" "hakurei.app/internal/xcb"
) )
// ChangeHosts inserts the target user into X11 hosts and deletes it once its // ChangeHosts inserts the target user into X11 hosts and deletes it once its [Enablement] is no longer satisfied.
// [hst.Enablements] is no longer satisfied.
func (sys *I) ChangeHosts(username string) *I { func (sys *I) ChangeHosts(username string) *I {
sys.ops = append(sys.ops, xhostOp(username)) sys.ops = append(sys.ops, xhostOp(username))
return sys return sys
@@ -15,7 +14,7 @@ func (sys *I) ChangeHosts(username string) *I {
// xhostOp implements [I.ChangeHosts]. // xhostOp implements [I.ChangeHosts].
type xhostOp string type xhostOp string
func (x xhostOp) Type() hst.Enablements { return hst.EX11 } func (x xhostOp) Type() hst.Enablement { return hst.EX11 }
func (x xhostOp) apply(sys *I) error { func (x xhostOp) apply(sys *I) error {
sys.msg.Verbosef("inserting entry %s to X11", x) sys.msg.Verbosef("inserting entry %s to X11", x)
+1 -8
View File
@@ -7,7 +7,6 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"slices"
) )
// synthAdd is prepared bytes written to uevent to cause a synthetic add event // synthAdd is prepared bytes written to uevent to cause a synthetic add event
@@ -27,7 +26,6 @@ var synthAdd = []byte(KOBJ_ADD.String())
func Coldboot( func Coldboot(
ctx context.Context, ctx context.Context,
pathname string, pathname string,
uuid *UUID,
visited chan<- string, visited chan<- string,
handleWalkErr func(error) error, handleWalkErr func(error) error,
) error { ) error {
@@ -41,11 +39,6 @@ func Coldboot(
} }
} }
add := synthAdd
if uuid != nil {
add = slices.Concat(add, []byte{' '}, []byte(uuid.String()))
}
return filepath.WalkDir(filepath.Join(pathname, "devices"), func( return filepath.WalkDir(filepath.Join(pathname, "devices"), func(
path string, path string,
d fs.DirEntry, d fs.DirEntry,
@@ -61,7 +54,7 @@ func Coldboot(
if d.IsDir() || d.Name() != "uevent" { if d.IsDir() || d.Name() != "uevent" {
return nil return nil
} }
if err = os.WriteFile(path, add, 0); err != nil { if err = os.WriteFile(path, synthAdd, 0); err != nil {
return handleWalkErr(err) return handleWalkErr(err)
} }
+2 -2
View File
@@ -59,7 +59,7 @@ func TestColdboot(t *testing.T) {
} }
}) })
err := uevent.Coldboot(t.Context(), d, nil, visited, func(err error) error { err := uevent.Coldboot(t.Context(), d, visited, func(err error) error {
t.Errorf("handleWalkErr: %v", err) t.Errorf("handleWalkErr: %v", err)
return err return err
}) })
@@ -219,7 +219,7 @@ func TestColdbootError(t *testing.T) {
} }
} }
if err := uevent.Coldboot(ctx, d, new(uevent.UUID), visited, handleWalkErr); !reflect.DeepEqual(err, wantErr) { if err := uevent.Coldboot(ctx, d, visited, handleWalkErr); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Coldboot: error = %v, want %v", err, wantErr) t.Errorf("Coldboot: error = %v, want %v", err, wantErr)
} }
}) })
+1 -117
View File
@@ -5,14 +5,10 @@ package uevent
import ( import (
"context" "context"
"encoding"
"encoding/hex"
"errors" "errors"
"fmt"
"strconv" "strconv"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"unsafe"
"hakurei.app/internal/netlink" "hakurei.app/internal/netlink"
) )
@@ -125,117 +121,6 @@ func (c *Conn) receiveEvent(ctx context.Context) (*Message, error) {
return &msg, err return &msg, err
} }
// UUID represents the value of SYNTH_UUID.
//
// This is not a generic UUID implementation. Do not attempt to use it for
// anything other than passing and interpreting the SYNTH_UUID environment
// variable of a uevent.
type UUID [16]byte
const (
// SizeUUID is the fixed size of string representation of [UUID] according
// to Documentation/ABI/testing/sysfs-uevent.
SizeUUID = 4 + len(UUID{})*2
// UUIDSep is the separator byte of [UUID].
UUIDSep = '-'
)
var (
_ encoding.TextAppender = new(UUID)
_ encoding.TextMarshaler = new(UUID)
_ encoding.TextUnmarshaler = new(UUID)
)
// String formats uuid according to Documentation/ABI/testing/sysfs-uevent.
func (uuid *UUID) String() string {
s := make([]byte, 0, SizeUUID)
s = hex.AppendEncode(s, uuid[:4])
s = append(s, UUIDSep)
s = hex.AppendEncode(s, uuid[4:6])
s = append(s, UUIDSep)
s = hex.AppendEncode(s, uuid[6:8])
s = append(s, UUIDSep)
s = hex.AppendEncode(s, uuid[8:10])
s = append(s, UUIDSep)
s = hex.AppendEncode(s, uuid[10:16])
return unsafe.String(unsafe.SliceData(s), len(s))
}
func (uuid *UUID) AppendText(data []byte) ([]byte, error) {
return append(data, uuid.String()...), nil
}
func (uuid *UUID) MarshalText() ([]byte, error) {
return uuid.AppendText(nil)
}
var (
// ErrAutoUUID is returned parsing a SYNTH_UUID generated by the kernel for
// a synthetic event without a UUID passed in.
ErrAutoUUID = errors.New("UUID is not passed in")
)
// UUIDSizeError describes an incorrectly sized string representation of [UUID].
type UUIDSizeError int
func (e UUIDSizeError) Error() string {
return "got " + strconv.Itoa(int(e)) + " bytes " +
"instead of " + strconv.Itoa(SizeUUID)
}
// UUIDSeparatorError is an invalid separator in a malformed string
// representation of [UUID].
type UUIDSeparatorError byte
func (e UUIDSeparatorError) Error() string {
return fmt.Sprintf("invalid UUID separator: %#U", rune(e))
}
// UnmarshalText parses data according to Documentation/ABI/testing/sysfs-uevent.
func (uuid *UUID) UnmarshalText(data []byte) (err error) {
if len(data) == 1 && data[0] == '0' {
return ErrAutoUUID
}
if len(data) != SizeUUID {
return UUIDSizeError(len(data))
}
if _, err = hex.Decode(uuid[:], data[:8]); err != nil {
return
}
if data[8] != UUIDSep {
return UUIDSeparatorError(data[8])
}
data = data[9:]
if _, err = hex.Decode(uuid[4:], data[:4]); err != nil {
return
}
if data[4] != UUIDSep {
return UUIDSeparatorError(data[4])
}
data = data[5:]
if _, err = hex.Decode(uuid[6:], data[:4]); err != nil {
return
}
if data[4] != UUIDSep {
return UUIDSeparatorError(data[4])
}
data = data[5:]
if _, err = hex.Decode(uuid[8:], data[:4]); err != nil {
return
}
if data[4] != UUIDSep {
return UUIDSeparatorError(data[4])
}
data = data[5:]
_, err = hex.Decode(uuid[10:], data)
return
}
// Consume continuously receives and parses events from the kernel and handles // Consume continuously receives and parses events from the kernel and handles
// [Recoverable] and [NeedsColdboot] errors via caller-supplied functions, // [Recoverable] and [NeedsColdboot] errors via caller-supplied functions,
// entering coldboot when required. // entering coldboot when required.
@@ -260,7 +145,6 @@ func (uuid *UUID) UnmarshalText(data []byte) (err error) {
func (c *Conn) Consume( func (c *Conn) Consume(
ctx context.Context, ctx context.Context,
sysfs string, sysfs string,
uuid *UUID,
events chan<- *Message, events chan<- *Message,
coldboot bool, coldboot bool,
@@ -309,7 +193,7 @@ coldboot:
ctxColdboot, cancelColdboot := context.WithCancel(ctx) ctxColdboot, cancelColdboot := context.WithCancel(ctx)
var coldbootErr error var coldbootErr error
go func() { go func() {
coldbootErr = Coldboot(ctxColdboot, sysfs, uuid, visited, handleWalkErr) coldbootErr = Coldboot(ctxColdboot, sysfs, visited, handleWalkErr)
close(visited) close(visited)
}() }()
for pathname := range visited { for pathname := range visited {
+1 -57
View File
@@ -3,10 +3,8 @@ package uevent_test
import ( import (
"context" "context"
"encoding" "encoding"
"encoding/hex"
"os" "os"
"reflect" "reflect"
"strings"
"sync" "sync"
"syscall" "syscall"
"testing" "testing"
@@ -25,12 +23,6 @@ func adeT[V any, S interface {
*V *V
}](t *testing.T, name string, v V, want string, wantErr, wantErrE error) { }](t *testing.T, name string, v V, want string, wantErr, wantErrE error) {
t.Helper() t.Helper()
noEncode := strings.HasSuffix(name, "\x00")
if noEncode {
name = name[:len(name)-1]
}
f := func(t *testing.T) { f := func(t *testing.T) {
if name != "" { if name != "" {
t.Parallel() t.Parallel()
@@ -54,10 +46,6 @@ func adeT[V any, S interface {
} }
}) })
if noEncode {
return
}
t.Run("encode", func(t *testing.T) { t.Run("encode", func(t *testing.T) {
t.Parallel() t.Parallel()
t.Helper() t.Helper()
@@ -126,45 +114,6 @@ func adeB[V any, S interface {
} }
} }
func TestUUID(t *testing.T) {
t.Parallel()
adeT(t, "sample", uevent.UUID{
0xfe, 0x4d, 0x7c, 0x9d,
0xb8, 0xc6,
0x4a, 0x70,
0x9e, 0xf1,
0x3d, 0x8a, 0x58, 0xd1, 0x8e, 0xed,
}, "fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed", nil, nil)
adeT(t, "auto\x00", uevent.UUID{}, "0", uevent.ErrAutoUUID, nil)
adeT(t, "short\x00", uevent.UUID{}, "1", uevent.UUIDSizeError(1), nil)
adeT(t, "bad0\x00", uevent.UUID{}, "fe4d7c9\x00-b8c6-4a70-9ef1-3d8a58d18eed",
hex.InvalidByteError(0), nil)
adeT(t, "sep0\x00", uevent.UUID{}, "fe4d7c9d\x00b8c6-4a70-9ef1-3d8a58d18eed",
uevent.UUIDSeparatorError(0), nil)
adeT(t, "bad1\x00", uevent.UUID{}, "fe4d7c9d-b8c\x00-4a70-9ef1-3d8a58d18eed",
hex.InvalidByteError(0), nil)
adeT(t, "sep1\x00", uevent.UUID{}, "fe4d7c9d-b8c6\x004a70-9ef1-3d8a58d18eed",
uevent.UUIDSeparatorError(0), nil)
adeT(t, "bad2\x00", uevent.UUID{}, "fe4d7c9d-b8c6-4a7\x00-9ef1-3d8a58d18eed",
hex.InvalidByteError(0), nil)
adeT(t, "sep2\x00", uevent.UUID{}, "fe4d7c9d-b8c6-4a70\x009ef1-3d8a58d18eed",
uevent.UUIDSeparatorError(0), nil)
adeT(t, "bad3\x00", uevent.UUID{}, "fe4d7c9d-b8c6-4a70-9ef\x00-3d8a58d18eed",
hex.InvalidByteError(0), nil)
adeT(t, "sep3\x00", uevent.UUID{}, "fe4d7c9d-b8c6-4a70-9ef1\x003d8a58d18eed",
uevent.UUIDSeparatorError(0), nil)
adeT(t, "bad4\x00", uevent.UUID{}, "fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18ee\x00",
hex.InvalidByteError(0), nil)
}
func TestDialConsume(t *testing.T) { func TestDialConsume(t *testing.T) {
t.Parallel() t.Parallel()
@@ -208,7 +157,7 @@ func TestDialConsume(t *testing.T) {
defer cancel() defer cancel()
consume := func(c *uevent.Conn, ctx context.Context) error { consume := func(c *uevent.Conn, ctx context.Context) error {
return c.Consume(ctx, fhs.Sys, nil, events, false, func(path string) { return c.Consume(ctx, fhs.Sys, events, false, func(path string) {
t.Log("coldboot visited", path) t.Log("coldboot visited", path)
}, func(err error) bool { }, func(err error) bool {
t.Log(err) t.Log(err)
@@ -295,11 +244,6 @@ func TestErrors(t *testing.T) {
{"BadPortError", &uevent.BadPortError{ {"BadPortError", &uevent.BadPortError{
Pid: 1, Pid: 1,
}, "unexpected message from port id 1 on NETLINK_KOBJECT_UEVENT"}, }, "unexpected message from port id 1 on NETLINK_KOBJECT_UEVENT"},
{"UUIDSizeError", uevent.UUIDSizeError(0xbad),
"got 2989 bytes instead of 36"},
{"UUIDSeparatorError", uevent.UUIDSeparatorError(0xfd),
"invalid UUID separator: U+00FD 'ý'"},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
+5 -1
View File
@@ -7,6 +7,7 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"time"
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/container" "hakurei.app/container"
@@ -26,6 +27,9 @@ const (
// lddName is the file name of ldd(1) passed to exec.LookPath. // lddName is the file name of ldd(1) passed to exec.LookPath.
lddName = "ldd" lddName = "ldd"
// lddTimeout is the maximum duration ldd(1) is allowed to ran for before it
// is terminated.
lddTimeout = 15 * time.Second
) )
// Resolve runs ldd(1) in a strict sandbox and connects its stdout to a [Decoder]. // Resolve runs ldd(1) in a strict sandbox and connects its stdout to a [Decoder].
@@ -48,7 +52,7 @@ func Resolve(
} }
} }
c, cancel := context.WithCancel(ctx) c, cancel := context.WithTimeout(ctx, lddTimeout)
defer cancel() defer cancel()
var toolPath *check.Absolute var toolPath *check.Absolute
+1 -1
View File
@@ -265,7 +265,7 @@ in
''; '';
in in
pkgs.writeShellScriptBin app.name '' pkgs.writeShellScriptBin app.name ''
exec hakurei${if app.verbose then " -v" else ""}${if app.insecureWayland then " --insecure" else ""} run ${checkedConfig "hakurei-app-${app.name}.json" conf} $@ exec hakurei${if app.verbose then " -v" else ""} run ${checkedConfig "hakurei-app-${app.name}.json" conf} $@
'' ''
) )
] ]
+2 -39
View File
@@ -35,7 +35,7 @@ package
*Default:* *Default:*
` <derivation hakurei-static-x86_64-unknown-linux-musl-0.4.0> ` ` <derivation hakurei-static-x86_64-unknown-linux-musl-0.3.6> `
@@ -542,43 +542,6 @@ null or string
## environment\.hakurei\.apps\.\<name>\.schedPolicy
Scheduling policy to set for the container\.
The zero value retains the current scheduling policy\.
*Type:*
null or one of “fifo”, “rr”, “batch”, “idle”, “deadline”, “ext”
*Default:*
` null `
## environment\.hakurei\.apps\.\<name>\.schedPriority
Scheduling priority to set for the container\.
*Type:*
null or integer between 1 and 99 (both inclusive)
*Default:*
` null `
## environment\.hakurei\.apps\.\<name>\.script ## environment\.hakurei\.apps\.\<name>\.script
@@ -842,7 +805,7 @@ package
*Default:* *Default:*
` <derivation hakurei-hsu-0.4.0> ` ` <derivation hakurei-hsu-0.3.6> `
+5 -2
View File
@@ -30,7 +30,7 @@
buildGo126Module rec { buildGo126Module rec {
pname = "hakurei"; pname = "hakurei";
version = "0.4.0"; version = "0.3.7";
srcFiltered = builtins.path { srcFiltered = builtins.path {
name = "${pname}-src"; name = "${pname}-src";
@@ -82,6 +82,9 @@ buildGo126Module rec {
env = { env = {
# use clang instead of gcc # use clang instead of gcc
CC = "clang -O3 -Werror"; CC = "clang -O3 -Werror";
# nix build environment does not allow acls
HAKUREI_TEST_SKIP_ACL = 1;
}; };
buildInputs = [ buildInputs = [
@@ -113,7 +116,7 @@ buildGo126Module rec {
]; ];
in in
'' ''
install -D --target-directory=$out/share/zsh/site-functions cmd/dist/comp/* install -D --target-directory=$out/share/zsh/site-functions dist/comp/*
mkdir "$out/libexec" mkdir "$out/libexec"
mv "$out"/bin/* "$out/libexec/" mv "$out"/bin/* "$out/libexec/"
+1 -1
View File
@@ -44,7 +44,7 @@ testers.nixosTest {
cd ${self.packages.${system}.hakurei.src} cd ${self.packages.${system}.hakurei.src}
${fhs}/bin/hakurei-fhs -c \ ${fhs}/bin/hakurei-fhs -c \
'CC="clang -O3 -Werror" go test --tags=noskip ${if withRace then "-race" else "-count 16"} ./...' \ 'CC="clang -O3 -Werror" go test ${if withRace then "-race" else "-count 16"} ./...' \
&> /tmp/hakurei-test.log && \ &> /tmp/hakurei-test.log && \
touch /tmp/hakurei-test-ok touch /tmp/hakurei-test-ok
touch /tmp/hakurei-test-done touch /tmp/hakurei-test-done
+1 -4
View File
@@ -8,10 +8,7 @@
description = "Alice Foobar"; description = "Alice Foobar";
password = "foobar"; password = "foobar";
uid = 1000; uid = 1000;
extraGroups = [ extraGroups = [ "wheel" ];
"wheel"
"sharefs"
];
}; };
untrusted = { untrusted = {
isNormalUser = true; isNormalUser = true;

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