forked from rosa/hakurei
Compare commits
35 Commits
33a0e6c01b
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
0b1009786f
|
|||
|
b390640376
|
|||
|
ad2c9f36cd
|
|||
|
67db3fbb8d
|
|||
|
560cb626a1
|
|||
|
c33a6a5b7e
|
|||
|
952082bd9b
|
|||
|
24a9b24823
|
|||
|
c2e61e7987
|
|||
|
86787b3bc5
|
|||
|
cdfcfe6ce0
|
|||
|
68a2f0c240
|
|||
|
7319c7adf9
|
|||
|
e9c890cbb2
|
|||
|
6f924336fc
|
|||
|
bd88f10524
|
|||
|
e34e3b917e
|
|||
|
b0ba165107
|
|||
|
351d6c5a35
|
|||
|
f23f73701c
|
|||
|
876917229a
|
|||
|
0558032c2d
|
|||
|
c61cdc505f
|
|||
|
062edb3487
|
|||
|
e4355279a1
|
|||
|
289fdebead
|
|||
|
9c9e190db9
|
|||
|
d7d42c69a1
|
|||
|
c758e762bd
|
|||
|
10f8b1c221
|
|||
|
6907700d67
|
|||
|
0243f3ffbd
|
|||
|
cd0beeaf8e
|
|||
|
a69273ab2a
|
|||
|
4cd0f57e48
|
31
.gitignore
vendored
31
.gitignore
vendored
@@ -1,27 +1,7 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.pkg
|
||||
/hakurei
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
# produced by tools and text editors
|
||||
*.qcow2
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
@@ -30,8 +10,5 @@ go.work.sum
|
||||
/internal/pkg/testdata/testtool
|
||||
/internal/rosa/hakurei_current.tar.gz
|
||||
|
||||
# release
|
||||
/dist/hakurei-*
|
||||
|
||||
# interactive nixos vm
|
||||
nixos.qcow2
|
||||
# cmd/dist default destination
|
||||
/dist
|
||||
|
||||
6
all.sh
Executable file
6
all.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
TOOLCHAIN_VERSION="$(go version)"
|
||||
cd "$(dirname -- "$0")/"
|
||||
echo "# Building cmd/dist using ${TOOLCHAIN_VERSION}."
|
||||
go run -v --tags=dist ./cmd/dist
|
||||
@@ -2,7 +2,7 @@
|
||||
package check
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
@@ -30,6 +30,16 @@ func (e AbsoluteError) Is(target error) bool {
|
||||
// Absolute holds a pathname checked to be absolute.
|
||||
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.
|
||||
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
|
||||
|
||||
@@ -84,13 +94,16 @@ func (a *Absolute) Append(elem ...string) *Absolute {
|
||||
// Dir calls [filepath.Dir] with [Absolute] as its argument.
|
||||
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }
|
||||
|
||||
// GobEncode returns the checked pathname.
|
||||
func (a *Absolute) GobEncode() ([]byte, error) {
|
||||
return []byte(a.String()), nil
|
||||
// AppendText appends the checked pathname.
|
||||
func (a *Absolute) AppendText(data []byte) ([]byte, error) {
|
||||
return append(data, a.String()...), nil
|
||||
}
|
||||
|
||||
// GobDecode stores data if it represents an absolute pathname.
|
||||
func (a *Absolute) GobDecode(data []byte) error {
|
||||
// MarshalText returns the checked pathname.
|
||||
func (a *Absolute) MarshalText() ([]byte, error) { return a.AppendText(nil) }
|
||||
|
||||
// UnmarshalText stores data if it represents an absolute pathname.
|
||||
func (a *Absolute) UnmarshalText(data []byte) error {
|
||||
pathname := string(data)
|
||||
if !filepath.IsAbs(pathname) {
|
||||
return AbsoluteError(pathname)
|
||||
@@ -99,23 +112,9 @@ func (a *Absolute) GobDecode(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON returns a JSON representation of the checked pathname.
|
||||
func (a *Absolute) MarshalJSON() ([]byte, error) {
|
||||
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
|
||||
}
|
||||
func (a *Absolute) AppendBinary(data []byte) ([]byte, error) { return a.AppendText(data) }
|
||||
func (a *Absolute) MarshalBinary() ([]byte, error) { return a.MarshalText() }
|
||||
func (a *Absolute) UnmarshalBinary(data []byte) error { return a.UnmarshalText(data) }
|
||||
|
||||
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
|
||||
func SortAbs(x []*Absolute) {
|
||||
|
||||
@@ -170,20 +170,20 @@ func TestCodecAbsolute(t *testing.T) {
|
||||
|
||||
{"good", MustAbs("/etc"),
|
||||
nil,
|
||||
"\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\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00",
|
||||
"\t\x7f\x06\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",
|
||||
|
||||
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
|
||||
{"not absolute", nil,
|
||||
AbsoluteError("etc"),
|
||||
"\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\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
"\t\x7f\x06\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",
|
||||
|
||||
`"etc"`, `{"val":"etc","magic":3236757504}`},
|
||||
{"zero", nil,
|
||||
new(AbsoluteError),
|
||||
"\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\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
"\t\x7f\x06\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",
|
||||
`""`, `{"val":"","magic":3236757504}`},
|
||||
}
|
||||
|
||||
@@ -347,15 +347,6 @@ 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) {
|
||||
|
||||
237
cmd/dist/main.go
vendored
Normal file
237
cmd/dist/main.go
vendored
Normal file
@@ -0,0 +1,237 @@
|
||||
//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)
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,9 @@ var errSuccess = errors.New("success")
|
||||
|
||||
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagJSON bool
|
||||
flagVerbose bool
|
||||
flagInsecure bool
|
||||
flagJSON bool
|
||||
)
|
||||
c := command.New(out, log.Printf, "hakurei", func([]string) error {
|
||||
msg.SwapVerbose(flagVerbose)
|
||||
@@ -57,6 +58,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
return nil
|
||||
}).
|
||||
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")
|
||||
|
||||
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
|
||||
@@ -75,7 +77,12 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
config.Container.Args = append(config.Container.Args, args[1:]...)
|
||||
}
|
||||
|
||||
outcome.Main(ctx, msg, config, flagIdentifierFile)
|
||||
var flags int
|
||||
if flagInsecure {
|
||||
flags |= hst.VAllowInsecure
|
||||
}
|
||||
|
||||
outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
|
||||
@@ -145,7 +152,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
}
|
||||
}
|
||||
|
||||
var et hst.Enablement
|
||||
var et hst.Enablements
|
||||
if flagWayland {
|
||||
et |= hst.EWayland
|
||||
}
|
||||
@@ -163,7 +170,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
ID: flagID,
|
||||
Identity: flagIdentity,
|
||||
Groups: flagGroups,
|
||||
Enablements: hst.NewEnablements(et),
|
||||
Enablements: &et,
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
@@ -282,7 +289,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
}
|
||||
}
|
||||
|
||||
outcome.Main(ctx, msg, &config, -1)
|
||||
outcome.Main(ctx, msg, &config, 0, -1)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestHelp(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"main", []string{}, `
|
||||
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
||||
Usage: hakurei [-h | --help] [-v] [--insecure] [--json] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
run Load and start container from configuration file
|
||||
|
||||
@@ -56,7 +56,7 @@ func printShowInstance(
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
if err := config.Validate(hst.VAllowInsecure); err != nil {
|
||||
valid = false
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
mustPrint(output, "Error: "+m+"!\n\n")
|
||||
|
||||
@@ -32,7 +32,7 @@ var (
|
||||
PID: 0xbeef,
|
||||
ShimPID: 0xcafe,
|
||||
Config: &hst.Config{
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EPipeWire),
|
||||
Enablements: new(hst.EWayland | hst.EPipeWire),
|
||||
Identity: 1,
|
||||
Container: &hst.ContainerConfig{
|
||||
Shell: check.MustAbs("/bin/sh"),
|
||||
|
||||
@@ -73,6 +73,8 @@ func main() {
|
||||
flagCures int
|
||||
flagBase string
|
||||
flagIdle bool
|
||||
|
||||
flagHostAbstract bool
|
||||
)
|
||||
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) {
|
||||
msg.SwapVerbose(!flagQuiet)
|
||||
@@ -91,7 +93,10 @@ func main() {
|
||||
|
||||
var flags int
|
||||
if flagIdle {
|
||||
flags &= pkg.CSchedIdle
|
||||
flags |= pkg.CSchedIdle
|
||||
}
|
||||
if flagHostAbstract {
|
||||
flags |= pkg.CHostAbstract
|
||||
}
|
||||
cache, err = pkg.Open(ctx, msg, flags, flagCures, base)
|
||||
|
||||
@@ -112,6 +117,13 @@ func main() {
|
||||
&flagIdle,
|
||||
"sched-idle", command.BoolFlag(false),
|
||||
"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",
|
||||
)
|
||||
|
||||
{
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
#endif
|
||||
|
||||
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
|
||||
#define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
|
||||
#define SHAREFS_PERM_REG 0600 /* permission bits for regular files presented to userspace */
|
||||
#define SHAREFS_PERM_DIR 0770 /* permission bits for directories presented to userspace */
|
||||
#define SHAREFS_PERM_REG 0660 /* permission bits for regular files presented to userspace */
|
||||
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
|
||||
|
||||
/* sharefs_private is populated by sharefs_init and contains process-wide context */
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -470,13 +469,14 @@ func _main(s ...string) (exitCode int) {
|
||||
os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"),
|
||||
))
|
||||
|
||||
var setupWriter io.WriteCloser
|
||||
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
|
||||
var setupPipe [2]*os.File
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else {
|
||||
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
|
||||
setupWriter = w
|
||||
z.Args = append(z.Args, "-osetup="+strconv.Itoa(3+len(z.ExtraFiles)))
|
||||
z.ExtraFiles = append(z.ExtraFiles, r)
|
||||
setupPipe[0], setupPipe[1] = r, w
|
||||
}
|
||||
|
||||
if err := z.Start(); err != nil {
|
||||
@@ -487,6 +487,9 @@ func _main(s ...string) (exitCode int) {
|
||||
}
|
||||
return 5
|
||||
}
|
||||
if err := setupPipe[0].Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if err := z.Serve(); err != nil {
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
log.Println(m)
|
||||
@@ -496,10 +499,10 @@ func _main(s ...string) (exitCode int) {
|
||||
return 5
|
||||
}
|
||||
|
||||
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
|
||||
if err := gob.NewEncoder(setupPipe[1]).Encode(&setup); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else if err = setupWriter.Close(); err != nil {
|
||||
} else if err = setupPipe[1].Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
|
||||
122
cmd/sharefs/test/raceattr.go
Normal file
122
cmd/sharefs/test/raceattr.go
Normal file
@@ -0,0 +1,122 @@
|
||||
//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())
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/internal/landlock"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -28,9 +29,6 @@ const (
|
||||
// CancelSignal is the signal expected by container init on context cancel.
|
||||
// A custom [Container.Cancel] function must eventually deliver this signal.
|
||||
CancelSignal = SIGUSR2
|
||||
|
||||
// Timeout for writing initParams to Container.setup.
|
||||
initSetupTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -53,7 +51,7 @@ type (
|
||||
ExtraFiles []*os.File
|
||||
|
||||
// Write end of a pipe connected to the init to deliver [Params].
|
||||
setup *os.File
|
||||
setup [2]*os.File
|
||||
// Cancels the context passed to the underlying cmd.
|
||||
cancel context.CancelFunc
|
||||
// Closed after Wait returns. Keeps the spawning thread alive.
|
||||
@@ -287,14 +285,16 @@ func (p *Container) Start() error {
|
||||
}
|
||||
|
||||
// place setup pipe before user supplied extra files, this is later restored by init
|
||||
if fd, f, err := Setup(&p.cmd.ExtraFiles); err != nil {
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "set up params stream",
|
||||
Err: err,
|
||||
}
|
||||
} else {
|
||||
p.setup = f
|
||||
fd := 3 + len(p.cmd.ExtraFiles)
|
||||
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.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
|
||||
@@ -308,7 +308,7 @@ func (p *Container) Start() error {
|
||||
done <- func() error {
|
||||
// PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes
|
||||
// created from the calling thread
|
||||
if err := SetNoNewPrivs(); err != nil {
|
||||
if err := setNoNewPrivs(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
|
||||
@@ -318,15 +318,17 @@ func (p *Container) Start() error {
|
||||
|
||||
// landlock: depends on per-thread state but acts on a process group
|
||||
{
|
||||
rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL}
|
||||
rulesetAttr := &landlock.RulesetAttr{
|
||||
Scoped: landlock.LANDLOCK_SCOPE_SIGNAL,
|
||||
}
|
||||
if !p.HostAbstract {
|
||||
rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
||||
rulesetAttr.Scoped |= landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
||||
}
|
||||
|
||||
if abi, err := LandlockGetABI(); err != nil {
|
||||
if p.HostAbstract {
|
||||
if abi, err := landlock.GetABI(); err != nil {
|
||||
if p.HostAbstract || !p.HostNet {
|
||||
// landlock can be skipped here as it restricts access
|
||||
// to resources already covered by namespaces (pid)
|
||||
// to resources already covered by namespaces (pid, net)
|
||||
goto landlockOut
|
||||
}
|
||||
return &StartError{Step: "get landlock ABI", Err: err}
|
||||
@@ -352,7 +354,7 @@ func (p *Container) Start() error {
|
||||
}
|
||||
} else {
|
||||
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
|
||||
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
|
||||
if err = landlock.RestrictSelf(rulesetFd, 0); err != nil {
|
||||
_ = Close(rulesetFd)
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
@@ -428,24 +430,33 @@ func (p *Container) Start() error {
|
||||
// Serve serves [Container.Params] to the container init.
|
||||
//
|
||||
// Serve must only be called once.
|
||||
func (p *Container) Serve() error {
|
||||
if p.setup == nil {
|
||||
func (p *Container) Serve() (err error) {
|
||||
if p.setup[0] == nil || p.setup[1] == nil {
|
||||
panic("invalid serve")
|
||||
}
|
||||
|
||||
setup := p.setup
|
||||
p.setup = nil
|
||||
if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil {
|
||||
done := make(chan struct{})
|
||||
defer func() {
|
||||
if closeErr := p.setup[1].Close(); 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{
|
||||
Fatal: true,
|
||||
Step: "set init pipe deadline",
|
||||
Step: "close read end of init pipe",
|
||||
Err: err,
|
||||
Passthrough: true,
|
||||
}
|
||||
}
|
||||
|
||||
if p.Path == nil {
|
||||
p.cancel()
|
||||
return &StartError{
|
||||
Step: "invalid executable pathname",
|
||||
Err: EINVAL,
|
||||
@@ -461,18 +472,27 @@ func (p *Container) Serve() error {
|
||||
p.SeccompRules = make([]std.NativeRule, 0)
|
||||
}
|
||||
|
||||
err := gob.NewEncoder(setup).Encode(&initParams{
|
||||
t := time.Now().UTC()
|
||||
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,
|
||||
Getuid(),
|
||||
Getgid(),
|
||||
len(p.ExtraFiles),
|
||||
p.msg.IsVerbose(),
|
||||
})
|
||||
_ = setup.Close()
|
||||
if err != nil {
|
||||
p.cancel()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait blocks until the container init process to exit and releases any
|
||||
|
||||
@@ -25,6 +25,9 @@ import (
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/landlock"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/ldd"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/vfs"
|
||||
@@ -83,9 +86,9 @@ func TestStartError(t *testing.T) {
|
||||
{"params env", &container.StartError{
|
||||
Fatal: true,
|
||||
Step: "set up params stream",
|
||||
Err: container.ErrReceiveEnv,
|
||||
Err: params.ErrReceiveEnv,
|
||||
}, "set up params stream: environment variable not set",
|
||||
container.ErrReceiveEnv, syscall.EBADF,
|
||||
params.ErrReceiveEnv, syscall.EBADF,
|
||||
"cannot set up params stream: environment variable not set"},
|
||||
|
||||
{"params", &container.StartError{
|
||||
@@ -453,6 +456,15 @@ func TestContainer(t *testing.T) {
|
||||
c.SeccompDisable = !tc.filter
|
||||
c.RetainSession = tc.session
|
||||
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.
|
||||
Readonly(check.MustAbs(pathReadonly), 0755).
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/internal/netlink"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -56,7 +57,7 @@ type syscallDispatcher interface {
|
||||
// isatty provides [Isatty].
|
||||
isatty(fd int) bool
|
||||
// receive provides [Receive].
|
||||
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
||||
receive(key string, e any, fdp *int) (closeFunc func() error, err error)
|
||||
|
||||
// bindMount provides procPaths.bindMount.
|
||||
bindMount(msg message.Msg, source, target string, flags uintptr) error
|
||||
@@ -147,7 +148,7 @@ func (direct) lockOSThread() { runtime.LockOSThread() }
|
||||
|
||||
func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) }
|
||||
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) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
|
||||
@@ -155,8 +156,8 @@ func (direct) capBoundingSetDrop(cap uintptr) error { return capBound
|
||||
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
|
||||
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
|
||||
func (direct) isatty(fd int) bool { return ext.Isatty(fd) }
|
||||
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
return Receive(key, e, fdp)
|
||||
func (direct) receive(key string, e any, fdp *int) (func() error, error) {
|
||||
return params.Receive(key, e, fdp)
|
||||
}
|
||||
|
||||
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
|
||||
|
||||
@@ -390,7 +390,7 @@ func (k *kstub) isatty(fd int) bool {
|
||||
return expect.Ret.(bool)
|
||||
}
|
||||
|
||||
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
||||
func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, err error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("receive")
|
||||
|
||||
@@ -408,10 +408,17 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// avoid changing test cases
|
||||
var fdpComp *uintptr
|
||||
if fdp != nil {
|
||||
fdpComp = new(uintptr(*fdp))
|
||||
}
|
||||
|
||||
err = expect.Error(
|
||||
stub.CheckArg(k.Stub, "key", key, 0),
|
||||
stub.CheckArgReflect(k.Stub, "e", e, 1),
|
||||
stub.CheckArgReflect(k.Stub, "fdp", fdp, 2))
|
||||
stub.CheckArgReflect(k.Stub, "fdp", fdpComp, 2))
|
||||
|
||||
// 3 is unused so stores params
|
||||
if expect.Args[3] != nil {
|
||||
@@ -426,7 +433,7 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
|
||||
if expect.Args[4] != nil {
|
||||
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
|
||||
if fdp != nil {
|
||||
*fdp = v
|
||||
*fdp = int(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -147,35 +148,33 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
var (
|
||||
params initParams
|
||||
closeSetup func() error
|
||||
setupFd uintptr
|
||||
offsetSetup int
|
||||
param initParams
|
||||
closeSetup func() error
|
||||
setupFd int
|
||||
)
|
||||
if f, err := k.receive(setupEnv, ¶ms, &setupFd); err != nil {
|
||||
if f, err := k.receive(setupEnv, ¶m, &setupFd); err != nil {
|
||||
if errors.Is(err, EBADF) {
|
||||
k.fatal(msg, "invalid setup descriptor")
|
||||
}
|
||||
if errors.Is(err, ErrReceiveEnv) {
|
||||
if errors.Is(err, params.ErrReceiveEnv) {
|
||||
k.fatal(msg, setupEnv+" not set")
|
||||
}
|
||||
|
||||
k.fatalf(msg, "cannot decode init setup payload: %v", err)
|
||||
} else {
|
||||
if params.Ops == nil {
|
||||
if param.Ops == nil {
|
||||
k.fatal(msg, "invalid setup parameters")
|
||||
}
|
||||
if params.ParentPerm == 0 {
|
||||
params.ParentPerm = 0755
|
||||
if param.ParentPerm == 0 {
|
||||
param.ParentPerm = 0755
|
||||
}
|
||||
|
||||
msg.SwapVerbose(params.Verbose)
|
||||
msg.SwapVerbose(param.Verbose)
|
||||
msg.Verbose("received setup parameters")
|
||||
closeSetup = f
|
||||
offsetSetup = int(setupFd + 1)
|
||||
}
|
||||
|
||||
if !params.HostNet {
|
||||
if !param.HostNet {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), CancelSignal,
|
||||
os.Interrupt, SIGTERM, SIGQUIT)
|
||||
defer cancel() // for panics
|
||||
@@ -188,7 +187,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
|
||||
}
|
||||
if err := k.writeFile(fhs.Proc+"self/uid_map",
|
||||
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
|
||||
append([]byte{}, strconv.Itoa(param.Uid)+" "+strconv.Itoa(param.HostUid)+" 1\n"...),
|
||||
0); err != nil {
|
||||
k.fatalf(msg, "%v", err)
|
||||
}
|
||||
@@ -198,7 +197,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "%v", err)
|
||||
}
|
||||
if err := k.writeFile(fhs.Proc+"self/gid_map",
|
||||
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
|
||||
append([]byte{}, strconv.Itoa(param.Gid)+" "+strconv.Itoa(param.HostGid)+" 1\n"...),
|
||||
0); err != nil {
|
||||
k.fatalf(msg, "%v", err)
|
||||
}
|
||||
@@ -207,8 +206,8 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
oldmask := k.umask(0)
|
||||
if params.Hostname != "" {
|
||||
if err := k.sethostname([]byte(params.Hostname)); err != nil {
|
||||
if param.Hostname != "" {
|
||||
if err := k.sethostname([]byte(param.Hostname)); err != nil {
|
||||
k.fatalf(msg, "cannot set hostname: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -221,7 +220,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
state := &setupState{process: make(map[int]WaitStatus), Params: ¶ms.Params, Msg: msg, Context: ctx}
|
||||
state := &setupState{process: make(map[int]WaitStatus), Params: ¶m.Params, Msg: msg, Context: ctx}
|
||||
defer cancel()
|
||||
|
||||
/* early is called right before pivot_root into intermediate root;
|
||||
@@ -229,7 +228,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
difficult to obtain via library functions after pivot_root, and
|
||||
implementations are expected to avoid changing the state of the mount
|
||||
namespace */
|
||||
for i, op := range *params.Ops {
|
||||
for i, op := range *param.Ops {
|
||||
if op == nil || !op.Valid() {
|
||||
k.fatalf(msg, "invalid op at index %d", i)
|
||||
}
|
||||
@@ -272,7 +271,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
step sets up the container filesystem, and implementations are expected to
|
||||
keep the host root and sysroot mount points intact but otherwise can do
|
||||
whatever they need to. Calling chdir is allowed but discouraged. */
|
||||
for i, op := range *params.Ops {
|
||||
for i, op := range *param.Ops {
|
||||
// ops already checked during early setup
|
||||
if prefix, ok := op.prefix(); ok {
|
||||
msg.Verbosef("%s %s", prefix, op)
|
||||
@@ -328,7 +327,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
|
||||
}
|
||||
for i := uintptr(0); i <= lastcap; i++ {
|
||||
if params.Privileged && i == CAP_SYS_ADMIN {
|
||||
if param.Privileged && i == CAP_SYS_ADMIN {
|
||||
continue
|
||||
}
|
||||
if err := k.capBoundingSetDrop(i); err != nil {
|
||||
@@ -337,7 +336,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
var keep [2]uint32
|
||||
if params.Privileged {
|
||||
if param.Privileged {
|
||||
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
|
||||
|
||||
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
|
||||
@@ -351,13 +350,13 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "cannot capset: %v", err)
|
||||
}
|
||||
|
||||
if !params.SeccompDisable {
|
||||
rules := params.SeccompRules
|
||||
if !param.SeccompDisable {
|
||||
rules := param.SeccompRules
|
||||
if len(rules) == 0 { // non-empty rules slice always overrides presets
|
||||
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
|
||||
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
|
||||
msg.Verbosef("resolving presets %#x", param.SeccompPresets)
|
||||
rules = seccomp.Preset(param.SeccompPresets, param.SeccompFlags)
|
||||
}
|
||||
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
|
||||
if err := k.seccompLoad(rules, param.SeccompFlags); err != nil {
|
||||
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
|
||||
k.fatalf(msg, "cannot load syscall filter: %v", err)
|
||||
}
|
||||
@@ -366,10 +365,10 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
msg.Verbose("syscall filter not configured")
|
||||
}
|
||||
|
||||
extraFiles := make([]*os.File, params.Count)
|
||||
extraFiles := make([]*os.File, param.Count)
|
||||
for i := range extraFiles {
|
||||
// setup fd is placed before all extra files
|
||||
extraFiles[i] = k.newFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
|
||||
extraFiles[i] = k.newFile(uintptr(setupFd+1+i), "extra file "+strconv.Itoa(i))
|
||||
}
|
||||
k.umask(oldmask)
|
||||
|
||||
@@ -447,7 +446,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
|
||||
// called right before startup of initial process, all state changes to the
|
||||
// current process is prohibited during late
|
||||
for i, op := range *params.Ops {
|
||||
for i, op := range *param.Ops {
|
||||
// ops already checked during early setup
|
||||
if err := op.late(state, k); err != nil {
|
||||
if m, ok := messageFromError(err); ok {
|
||||
@@ -468,14 +467,14 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "cannot close setup pipe: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(params.Path.String())
|
||||
cmd := exec.Command(param.Path.String())
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.Args = params.Args
|
||||
cmd.Env = params.Env
|
||||
cmd.Args = param.Args
|
||||
cmd.Env = param.Env
|
||||
cmd.ExtraFiles = extraFiles
|
||||
cmd.Dir = params.Dir.String()
|
||||
cmd.Dir = param.Dir.String()
|
||||
|
||||
msg.Verbosef("starting initial process %s", params.Path)
|
||||
msg.Verbosef("starting initial process %s", param.Path)
|
||||
if err := k.start(cmd); err != nil {
|
||||
k.fatalf(msg, "%v", err)
|
||||
}
|
||||
@@ -493,7 +492,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
for {
|
||||
select {
|
||||
case s := <-sig:
|
||||
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
|
||||
if s == CancelSignal && param.ForwardCancel && cmd.Process != nil {
|
||||
msg.Verbose("forwarding context cancellation")
|
||||
if err := k.signal(cmd, os.Interrupt); err != nil && !errors.Is(err, os.ErrProcessDone) {
|
||||
k.printf(msg, "cannot forward cancellation: %v", err)
|
||||
@@ -525,7 +524,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
cancel()
|
||||
|
||||
// start timeout early
|
||||
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
|
||||
go func() { time.Sleep(param.AdoptWaitDelay); close(timeout) }()
|
||||
|
||||
// close initial process files; this also keeps them alive
|
||||
for _, f := range extraFiles {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/internal/stub"
|
||||
)
|
||||
|
||||
@@ -40,7 +41,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("getpid", stub.ExpectArgs{}, 1, nil),
|
||||
call("setPtracer", stub.ExpectArgs{uintptr(0)}, nil, nil),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, ErrReceiveEnv),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, params.ErrReceiveEnv),
|
||||
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SETUP not set"}}, nil, nil),
|
||||
},
|
||||
}, nil},
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"hakurei.app/ext"
|
||||
)
|
||||
|
||||
// SetNoNewPrivs sets the calling thread's no_new_privs attribute.
|
||||
func SetNoNewPrivs() error {
|
||||
// setNoNewPrivs sets the calling thread's no_new_privs attribute.
|
||||
func setNoNewPrivs() error {
|
||||
return ext.Prctl(PR_SET_NO_NEW_PRIVS, 1, 0)
|
||||
}
|
||||
|
||||
|
||||
1
dist/hsurc.default
vendored
1
dist/hsurc.default
vendored
@@ -1 +0,0 @@
|
||||
1000 0
|
||||
12
dist/install.sh
vendored
12
dist/install.sh
vendored
@@ -1,12 +0,0 @@
|
||||
#!/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"
|
||||
37
dist/release.sh
vendored
37
dist/release.sh
vendored
@@ -1,37 +0,0 @@
|
||||
#!/bin/sh -e
|
||||
cd "$(dirname -- "$0")/.."
|
||||
VERSION="${HAKUREI_VERSION:-untagged}"
|
||||
prefix="${PREFIX:-/usr}"
|
||||
pname="hakurei-${VERSION}-$(go env GOARCH)"
|
||||
out="${DESTDIR:-dist}/${pname}"
|
||||
|
||||
destroy_workdir() {
|
||||
rm -rf "${out}"
|
||||
}
|
||||
trap destroy_workdir EXIT
|
||||
|
||||
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=${prefix}/bin/hakurei
|
||||
-X hakurei.app/internal/info.hsuPath=${prefix}/bin/hsu
|
||||
-X main.hakureiPath=${prefix}/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
|
||||
@@ -137,11 +137,10 @@
|
||||
|
||||
CC="musl-clang -O3 -Werror -Qunused-arguments" \
|
||||
GOCACHE="$(mktemp -d)" \
|
||||
HAKUREI_TEST_SKIP_ACL=1 \
|
||||
PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \
|
||||
DESTDIR="$out" \
|
||||
HAKUREI_VERSION="v${hakurei.version}" \
|
||||
./dist/release.sh
|
||||
./all.sh
|
||||
'';
|
||||
}
|
||||
);
|
||||
@@ -196,6 +195,7 @@
|
||||
./test/interactive/vm.nix
|
||||
./test/interactive/hakurei.nix
|
||||
./test/interactive/trace.nix
|
||||
./test/interactive/raceattr.nix
|
||||
|
||||
self.nixosModules.hakurei
|
||||
home-manager.nixosModules.home-manager
|
||||
|
||||
@@ -140,21 +140,29 @@ var (
|
||||
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.
|
||||
func (config *Config) Validate() error {
|
||||
func (config *Config) Validate(flags int) error {
|
||||
const step = "validate configuration"
|
||||
|
||||
if config == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "invalid configuration"}
|
||||
}
|
||||
|
||||
// this is checked again in hsu
|
||||
if config.Identity < IdentityStart || config.Identity > IdentityEnd {
|
||||
return &AppError{Step: "validate configuration", Err: ErrIdentityBounds,
|
||||
return &AppError{Step: step, Err: ErrIdentityBounds,
|
||||
Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
|
||||
}
|
||||
|
||||
if config.SchedPolicy < 0 || config.SchedPolicy > ext.SCHED_LAST {
|
||||
return &AppError{Step: "validate configuration", Err: ErrSchedPolicyBounds,
|
||||
return &AppError{Step: step, Err: ErrSchedPolicyBounds,
|
||||
Msg: "scheduling policy " +
|
||||
strconv.Itoa(int(config.SchedPolicy)) +
|
||||
" out of range"}
|
||||
@@ -168,34 +176,51 @@ func (config *Config) Validate() error {
|
||||
}
|
||||
|
||||
if config.Container == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "configuration missing container state"}
|
||||
}
|
||||
if config.Container.Home == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "container configuration missing path to home directory"}
|
||||
}
|
||||
if config.Container.Shell == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "container configuration missing path to shell"}
|
||||
}
|
||||
if config.Container.Path == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "container configuration missing path to initial program"}
|
||||
}
|
||||
|
||||
for key := range config.Container.Env {
|
||||
if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 {
|
||||
return &AppError{Step: "validate configuration", Err: ErrEnviron,
|
||||
return &AppError{Step: step, Err: ErrEnviron,
|
||||
Msg: "invalid environment variable " + strconv.Quote(key)}
|
||||
}
|
||||
}
|
||||
|
||||
if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
|
||||
return &AppError{Step: "validate configuration", Err: ErrInsecure,
|
||||
et := config.Enablements.Unwrap()
|
||||
if !config.DirectPulse && et&EPulse != 0 {
|
||||
return &AppError{Step: step, Err: ErrInsecure,
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -14,65 +14,109 @@ func TestConfigValidate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config *hst.Config
|
||||
flags int
|
||||
wantErr error
|
||||
}{
|
||||
{"nil", nil, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
{"nil", nil, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
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"}},
|
||||
{"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
||||
{"identity upper", &hst.Config{Identity: 10000}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
||||
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"}},
|
||||
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
||||
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
||||
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"}},
|
||||
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}},
|
||||
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}}, 0,
|
||||
&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"}},
|
||||
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "container configuration missing path to home directory"}},
|
||||
{"shell", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "container configuration missing path to shell"}},
|
||||
{"path", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "container configuration missing path to initial program"}},
|
||||
|
||||
{"env equals", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
Env: map[string]string{"TERM=": ""},
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
Msg: `invalid environment variable "TERM="`}},
|
||||
{"env NUL", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
Env: map[string]string{"TERM\x00": ""},
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
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,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
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{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, nil},
|
||||
}}, 0, nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := tc.config.Validate(); !reflect.DeepEqual(err, tc.wantErr) {
|
||||
if err := tc.config.Validate(tc.flags); !reflect.DeepEqual(err, tc.wantErr) {
|
||||
t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Enablement represents an optional host service to export to the target user.
|
||||
type Enablement byte
|
||||
// Enablements denotes optional host service to export to the target user.
|
||||
type Enablements byte
|
||||
|
||||
const (
|
||||
// EWayland exposes a Wayland pathname socket via security-context-v1.
|
||||
EWayland Enablement = 1 << iota
|
||||
EWayland Enablements = 1 << iota
|
||||
// EX11 adds the target user via X11 ChangeHosts and exposes the X11
|
||||
// pathname socket.
|
||||
EX11
|
||||
@@ -28,8 +28,8 @@ const (
|
||||
EM
|
||||
)
|
||||
|
||||
// String returns a string representation of the flags set on [Enablement].
|
||||
func (e Enablement) String() string {
|
||||
// String returns a string representation of the flags set on [Enablements].
|
||||
func (e Enablements) String() string {
|
||||
switch e {
|
||||
case 0:
|
||||
return "(no enablements)"
|
||||
@@ -47,7 +47,7 @@ func (e Enablement) String() string {
|
||||
buf := new(strings.Builder)
|
||||
buf.Grow(32)
|
||||
|
||||
for i := Enablement(1); i < EM; i <<= 1 {
|
||||
for i := Enablements(1); i < EM; i <<= 1 {
|
||||
if e&i != 0 {
|
||||
buf.WriteString(", " + i.String())
|
||||
}
|
||||
@@ -60,12 +60,6 @@ func (e Enablement) 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].
|
||||
type enablementsJSON = struct {
|
||||
Wayland bool `json:"wayland,omitempty"`
|
||||
@@ -75,24 +69,21 @@ type enablementsJSON = struct {
|
||||
Pulse bool `json:"pulse,omitempty"`
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying [Enablement].
|
||||
func (e *Enablements) Unwrap() Enablement {
|
||||
// Unwrap returns the value pointed to by e.
|
||||
func (e *Enablements) Unwrap() Enablements {
|
||||
if e == nil {
|
||||
return 0
|
||||
}
|
||||
return Enablement(*e)
|
||||
return *e
|
||||
}
|
||||
|
||||
func (e *Enablements) MarshalJSON() ([]byte, error) {
|
||||
if e == nil {
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
func (e Enablements) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&enablementsJSON{
|
||||
Wayland: Enablement(*e)&EWayland != 0,
|
||||
X11: Enablement(*e)&EX11 != 0,
|
||||
DBus: Enablement(*e)&EDBus != 0,
|
||||
PipeWire: Enablement(*e)&EPipeWire != 0,
|
||||
Pulse: Enablement(*e)&EPulse != 0,
|
||||
Wayland: e&EWayland != 0,
|
||||
X11: e&EX11 != 0,
|
||||
DBus: e&EDBus != 0,
|
||||
PipeWire: e&EPipeWire != 0,
|
||||
Pulse: e&EPulse != 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,22 +97,21 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var ve Enablement
|
||||
*e = 0
|
||||
if v.Wayland {
|
||||
ve |= EWayland
|
||||
*e |= EWayland
|
||||
}
|
||||
if v.X11 {
|
||||
ve |= EX11
|
||||
*e |= EX11
|
||||
}
|
||||
if v.DBus {
|
||||
ve |= EDBus
|
||||
*e |= EDBus
|
||||
}
|
||||
if v.PipeWire {
|
||||
ve |= EPipeWire
|
||||
*e |= EPipeWire
|
||||
}
|
||||
if v.Pulse {
|
||||
ve |= EPulse
|
||||
*e |= EPulse
|
||||
}
|
||||
*e = Enablements(ve)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ func TestEnablementString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
flags hst.Enablement
|
||||
flags hst.Enablements
|
||||
want string
|
||||
}{
|
||||
{0, "(no enablements)"},
|
||||
@@ -59,13 +59,13 @@ func TestEnablements(t *testing.T) {
|
||||
sData string
|
||||
}{
|
||||
{"nil", nil, "null", `{"value":null,"magic":3236757504}`},
|
||||
{"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`},
|
||||
{"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
|
||||
{"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
|
||||
{"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
|
||||
{"pipewire", hst.NewEnablements(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
|
||||
{"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
|
||||
{"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}`},
|
||||
{"zero", new(hst.Enablements(0)), `{}`, `{"value":{},"magic":3236757504}`},
|
||||
{"wayland", new(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
|
||||
{"x11", new(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
|
||||
{"dbus", new(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
|
||||
{"pipewire", new(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
|
||||
{"pulse", new(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}`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -137,7 +137,7 @@ func TestEnablements(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("val", func(t *testing.T) {
|
||||
if got := hst.NewEnablements(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
|
||||
if got := new(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
|
||||
t.Errorf("Unwrap: %v", got)
|
||||
}
|
||||
})
|
||||
@@ -146,9 +146,6 @@ func TestEnablements(t *testing.T) {
|
||||
t.Run("passthrough", func(t *testing.T) {
|
||||
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) {
|
||||
t.Errorf("UnmarshalJSON: error = %v", err)
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func Template() *Config {
|
||||
return &Config{
|
||||
ID: "org.chromium.Chromium",
|
||||
|
||||
Enablements: NewEnablements(EWayland | EDBus | EPipeWire),
|
||||
Enablements: new(EWayland | EDBus | EPipeWire),
|
||||
|
||||
SessionBus: &BusConfig{
|
||||
See: nil,
|
||||
|
||||
@@ -11,9 +11,11 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/info"
|
||||
)
|
||||
|
||||
const testFileName = "acl.test"
|
||||
@@ -24,8 +26,14 @@ var (
|
||||
)
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
if os.Getenv("HAKUREI_TEST_SKIP_ACL") == "1" {
|
||||
t.Skip("acl test skipped")
|
||||
if info.CanDegrade {
|
||||
name := filepath.Join(t.TempDir(), "check-degrade")
|
||||
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)
|
||||
|
||||
7
internal/info/optional_skip.go
Normal file
7
internal/info/optional_skip.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//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
internal/info/optional_strict.go
Normal file
5
internal/info/optional_strict.go
Normal file
@@ -0,0 +1,5 @@
|
||||
//go:build noskip
|
||||
|
||||
package info
|
||||
|
||||
const CanDegrade = false
|
||||
90
internal/kobject/event.go
Normal file
90
internal/kobject/event.go
Normal file
@@ -0,0 +1,90 @@
|
||||
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
internal/kobject/event_test.go
Normal file
92
internal/kobject/event_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package container
|
||||
package landlock
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -14,11 +14,11 @@ const (
|
||||
LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
|
||||
)
|
||||
|
||||
// LandlockAccessFS is bitmask of handled filesystem actions.
|
||||
type LandlockAccessFS uint64
|
||||
// AccessFS is bitmask of handled filesystem actions.
|
||||
type AccessFS uint64
|
||||
|
||||
const (
|
||||
LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 1 << iota
|
||||
LANDLOCK_ACCESS_FS_EXECUTE AccessFS = 1 << iota
|
||||
LANDLOCK_ACCESS_FS_WRITE_FILE
|
||||
LANDLOCK_ACCESS_FS_READ_FILE
|
||||
LANDLOCK_ACCESS_FS_READ_DIR
|
||||
@@ -38,8 +38,8 @@ const (
|
||||
_LANDLOCK_ACCESS_FS_DELIM
|
||||
)
|
||||
|
||||
// String returns a space-separated string of [LandlockAccessFS] flags.
|
||||
func (f LandlockAccessFS) String() string {
|
||||
// String returns a space-separated string of [AccessFS] flags.
|
||||
func (f AccessFS) String() string {
|
||||
switch f {
|
||||
case LANDLOCK_ACCESS_FS_EXECUTE:
|
||||
return "execute"
|
||||
@@ -90,8 +90,8 @@ func (f LandlockAccessFS) String() string {
|
||||
return "fs_ioctl_dev"
|
||||
|
||||
default:
|
||||
var c []LandlockAccessFS
|
||||
for i := LandlockAccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
|
||||
var c []AccessFS
|
||||
for i := AccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
|
||||
if f&i != 0 {
|
||||
c = append(c, i)
|
||||
}
|
||||
@@ -107,18 +107,18 @@ func (f LandlockAccessFS) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// LandlockAccessNet is bitmask of handled network actions.
|
||||
type LandlockAccessNet uint64
|
||||
// AccessNet is bitmask of handled network actions.
|
||||
type AccessNet uint64
|
||||
|
||||
const (
|
||||
LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota
|
||||
LANDLOCK_ACCESS_NET_BIND_TCP AccessNet = 1 << iota
|
||||
LANDLOCK_ACCESS_NET_CONNECT_TCP
|
||||
|
||||
_LANDLOCK_ACCESS_NET_DELIM
|
||||
)
|
||||
|
||||
// String returns a space-separated string of [LandlockAccessNet] flags.
|
||||
func (f LandlockAccessNet) String() string {
|
||||
// String returns a space-separated string of [AccessNet] flags.
|
||||
func (f AccessNet) String() string {
|
||||
switch f {
|
||||
case LANDLOCK_ACCESS_NET_BIND_TCP:
|
||||
return "bind_tcp"
|
||||
@@ -127,8 +127,8 @@ func (f LandlockAccessNet) String() string {
|
||||
return "connect_tcp"
|
||||
|
||||
default:
|
||||
var c []LandlockAccessNet
|
||||
for i := LandlockAccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
|
||||
var c []AccessNet
|
||||
for i := AccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
|
||||
if f&i != 0 {
|
||||
c = append(c, i)
|
||||
}
|
||||
@@ -144,18 +144,18 @@ func (f LandlockAccessNet) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// LandlockScope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
|
||||
type LandlockScope uint64
|
||||
// Scope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
|
||||
type Scope uint64
|
||||
|
||||
const (
|
||||
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota
|
||||
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET Scope = 1 << iota
|
||||
LANDLOCK_SCOPE_SIGNAL
|
||||
|
||||
_LANDLOCK_SCOPE_DELIM
|
||||
)
|
||||
|
||||
// String returns a space-separated string of [LandlockScope] flags.
|
||||
func (f LandlockScope) String() string {
|
||||
// String returns a space-separated string of [Scope] flags.
|
||||
func (f Scope) String() string {
|
||||
switch f {
|
||||
case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET:
|
||||
return "abstract_unix_socket"
|
||||
@@ -164,8 +164,8 @@ func (f LandlockScope) String() string {
|
||||
return "signal"
|
||||
|
||||
default:
|
||||
var c []LandlockScope
|
||||
for i := LandlockScope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
|
||||
var c []Scope
|
||||
for i := Scope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
|
||||
if f&i != 0 {
|
||||
c = append(c, i)
|
||||
}
|
||||
@@ -184,12 +184,12 @@ func (f LandlockScope) String() string {
|
||||
// RulesetAttr is equivalent to struct landlock_ruleset_attr.
|
||||
type RulesetAttr struct {
|
||||
// Bitmask of handled filesystem actions.
|
||||
HandledAccessFS LandlockAccessFS
|
||||
HandledAccessFS AccessFS
|
||||
// Bitmask of handled network actions.
|
||||
HandledAccessNet LandlockAccessNet
|
||||
HandledAccessNet AccessNet
|
||||
// Bitmask of scopes restricting a Landlock domain from accessing outside
|
||||
// resources (e.g. IPCs).
|
||||
Scoped LandlockScope
|
||||
Scoped Scope
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// LandlockGetABI returns the ABI version supported by the kernel.
|
||||
func LandlockGetABI() (int, error) {
|
||||
// GetABI returns the ABI version supported by the kernel.
|
||||
func GetABI() (int, error) {
|
||||
return (*RulesetAttr)(nil).Create(LANDLOCK_CREATE_RULESET_VERSION)
|
||||
}
|
||||
|
||||
// LandlockRestrictSelf applies a loaded ruleset to the calling thread.
|
||||
func LandlockRestrictSelf(rulesetFd int, flags uintptr) error {
|
||||
// RestrictSelf applies a loaded ruleset to the calling thread.
|
||||
func RestrictSelf(rulesetFd int, flags uintptr) error {
|
||||
r, _, errno := syscall.Syscall(
|
||||
ext.SYS_LANDLOCK_RESTRICT_SELF,
|
||||
uintptr(rulesetFd),
|
||||
65
internal/landlock/landlock_test.go
Normal file
65
internal/landlock/landlock_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package landlock_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/internal/landlock"
|
||||
)
|
||||
|
||||
func TestLandlockString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
rulesetAttr *landlock.RulesetAttr
|
||||
want string
|
||||
}{
|
||||
{"nil", nil, "NULL"},
|
||||
{"zero", new(landlock.RulesetAttr), "0"},
|
||||
{"some", &landlock.RulesetAttr{Scoped: landlock.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
|
||||
{"set", &landlock.RulesetAttr{
|
||||
HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_MAKE_SYM | landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV | landlock.LANDLOCK_ACCESS_FS_WRITE_FILE,
|
||||
HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP,
|
||||
Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | landlock.LANDLOCK_SCOPE_SIGNAL,
|
||||
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
|
||||
{"all", &landlock.RulesetAttr{
|
||||
HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_EXECUTE |
|
||||
landlock.LANDLOCK_ACCESS_FS_WRITE_FILE |
|
||||
landlock.LANDLOCK_ACCESS_FS_READ_FILE |
|
||||
landlock.LANDLOCK_ACCESS_FS_READ_DIR |
|
||||
landlock.LANDLOCK_ACCESS_FS_REMOVE_DIR |
|
||||
landlock.LANDLOCK_ACCESS_FS_REMOVE_FILE |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_CHAR |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_DIR |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_REG |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_SOCK |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_FIFO |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_SYM |
|
||||
landlock.LANDLOCK_ACCESS_FS_REFER |
|
||||
landlock.LANDLOCK_ACCESS_FS_TRUNCATE |
|
||||
landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV,
|
||||
HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP |
|
||||
landlock.LANDLOCK_ACCESS_NET_CONNECT_TCP,
|
||||
Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
|
||||
landlock.LANDLOCK_SCOPE_SIGNAL,
|
||||
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := tc.rulesetAttr.String(); got != tc.want {
|
||||
t.Errorf("String: %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLandlockAttrSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := 24
|
||||
if got := unsafe.Sizeof(landlock.RulesetAttr{}); got != uintptr(want) {
|
||||
t.Errorf("Sizeof: %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -84,7 +85,7 @@ type syscallDispatcher interface {
|
||||
// setDumpable provides [container.SetDumpable].
|
||||
setDumpable(dumpable uintptr) error
|
||||
// receive provides [container.Receive].
|
||||
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
||||
receive(key string, e any, fdp *int) (closeFunc func() error, err error)
|
||||
|
||||
// containerStart provides the Start method of [container.Container].
|
||||
containerStart(z *container.Container) error
|
||||
@@ -154,8 +155,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) overflowGid(msg message.Msg) int { return container.OverflowGid(msg) }
|
||||
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
|
||||
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
return container.Receive(key, e, fdp)
|
||||
func (direct) receive(key string, e any, fdp *int) (func() error, error) {
|
||||
return params.Receive(key, e, fdp)
|
||||
}
|
||||
|
||||
func (direct) containerStart(z *container.Container) error { return z.Start() }
|
||||
|
||||
@@ -401,12 +401,12 @@ func (k *kstub) setDumpable(dumpable uintptr) error {
|
||||
stub.CheckArg(k.Stub, "dumpable", dumpable, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
||||
func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, err error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("receive")
|
||||
reflect.ValueOf(e).Elem().Set(reflect.ValueOf(expect.Args[1]))
|
||||
if expect.Args[2] != nil {
|
||||
*fdp = expect.Args[2].(uintptr)
|
||||
*fdp = int(expect.Args[2].(uintptr))
|
||||
}
|
||||
return func() error { return k.Expects("closeReceive").Err }, expect.Error(
|
||||
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.
|
||||
type panicDispatcher struct{}
|
||||
|
||||
func (panicDispatcher) new(func(k syscallDispatcher, msg message.Msg)) { panic("unreachable") }
|
||||
func (panicDispatcher) getppid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getpid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getuid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getgid() int { panic("unreachable") }
|
||||
func (panicDispatcher) lookupEnv(string) (string, bool) { panic("unreachable") }
|
||||
func (panicDispatcher) pipe() (*os.File, *os.File, error) { panic("unreachable") }
|
||||
func (panicDispatcher) stat(string) (os.FileInfo, error) { panic("unreachable") }
|
||||
func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") }
|
||||
func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
|
||||
func (panicDispatcher) tempdir() string { panic("unreachable") }
|
||||
func (panicDispatcher) mkdir(string, os.FileMode) error { panic("unreachable") }
|
||||
func (panicDispatcher) removeAll(string) error { panic("unreachable") }
|
||||
func (panicDispatcher) exit(int) { panic("unreachable") }
|
||||
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) prctl(uintptr, uintptr, uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) lookPath(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
|
||||
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) setDumpable(uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) receive(string, any, *uintptr) (func() error, error) { panic("unreachable") }
|
||||
func (panicDispatcher) containerStart(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) containerServe(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) containerWait(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") }
|
||||
func (panicDispatcher) dbusAddress() (string, string) { panic("unreachable") }
|
||||
func (panicDispatcher) setupContSignal(int) (io.ReadCloser, func(), error) { panic("unreachable") }
|
||||
func (panicDispatcher) getMsg() message.Msg { panic("unreachable") }
|
||||
func (panicDispatcher) fatal(...any) { panic("unreachable") }
|
||||
func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") }
|
||||
func (panicDispatcher) new(func(k syscallDispatcher, msg message.Msg)) { panic("unreachable") }
|
||||
func (panicDispatcher) getppid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getpid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getuid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getgid() int { panic("unreachable") }
|
||||
func (panicDispatcher) lookupEnv(string) (string, bool) { panic("unreachable") }
|
||||
func (panicDispatcher) pipe() (*os.File, *os.File, error) { panic("unreachable") }
|
||||
func (panicDispatcher) stat(string) (os.FileInfo, error) { panic("unreachable") }
|
||||
func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") }
|
||||
func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
|
||||
func (panicDispatcher) tempdir() string { panic("unreachable") }
|
||||
func (panicDispatcher) mkdir(string, os.FileMode) error { panic("unreachable") }
|
||||
func (panicDispatcher) removeAll(string) error { panic("unreachable") }
|
||||
func (panicDispatcher) exit(int) { panic("unreachable") }
|
||||
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) prctl(uintptr, uintptr, uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) lookPath(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
|
||||
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) setDumpable(uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) receive(string, any, *int) (func() error, error) { panic("unreachable") }
|
||||
func (panicDispatcher) containerStart(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) containerServe(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) containerWait(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") }
|
||||
func (panicDispatcher) dbusAddress() (string, string) { panic("unreachable") }
|
||||
func (panicDispatcher) setupContSignal(int) (io.ReadCloser, func(), error) { panic("unreachable") }
|
||||
func (panicDispatcher) getMsg() message.Msg { panic("unreachable") }
|
||||
func (panicDispatcher) fatal(...any) { panic("unreachable") }
|
||||
func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") }
|
||||
|
||||
func (panicDispatcher) notifyContext(context.Context, ...os.Signal) (context.Context, context.CancelFunc) {
|
||||
panic("unreachable")
|
||||
|
||||
@@ -32,7 +32,14 @@ type outcome struct {
|
||||
syscallDispatcher
|
||||
}
|
||||
|
||||
func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, config *hst.Config) error {
|
||||
// finalise prepares an outcome for main.
|
||||
func (k *outcome) finalise(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
id *hst.ID,
|
||||
config *hst.Config,
|
||||
flags int,
|
||||
) error {
|
||||
if ctx == nil || id == nil {
|
||||
// unreachable
|
||||
panic("invalid call to finalise")
|
||||
@@ -43,7 +50,7 @@ func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, con
|
||||
}
|
||||
k.ctx = ctx
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
if err := config.Validate(flags); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ type outcomeStateSys struct {
|
||||
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
|
||||
appId string
|
||||
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
|
||||
et hst.Enablement
|
||||
et hst.Enablements
|
||||
|
||||
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
|
||||
directWayland bool
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/info"
|
||||
@@ -298,12 +297,12 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
|
||||
// accumulate enablements of remaining instances
|
||||
var (
|
||||
// alive enablement bits
|
||||
rt hst.Enablement
|
||||
rt hst.Enablements
|
||||
// alive instance count
|
||||
n int
|
||||
)
|
||||
for eh := range entries {
|
||||
var et hst.Enablement
|
||||
var et hst.Enablements
|
||||
if et, err = eh.Load(nil); err != nil {
|
||||
perror(err, "read state header of instance "+eh.ID.String())
|
||||
} else {
|
||||
@@ -372,17 +371,18 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
|
||||
// shim runs in the same session as monitor; see shim.go for behaviour
|
||||
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
|
||||
|
||||
var shimPipe *os.File
|
||||
if fd, w, err := container.Setup(&cmd.ExtraFiles); err != nil {
|
||||
var shimPipe [2]*os.File
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
return cmd, nil, &hst.AppError{Step: "create shim setup pipe", Err: err}
|
||||
} else {
|
||||
shimPipe = w
|
||||
cmd.Env = []string{
|
||||
// passed through to shim by hsu
|
||||
shimEnv + "=" + strconv.Itoa(fd),
|
||||
shimEnv + "=" + strconv.Itoa(3+len(cmd.ExtraFiles)),
|
||||
// interpreted by hsu
|
||||
"HAKUREI_IDENTITY=" + k.state.identity.String(),
|
||||
}
|
||||
cmd.ExtraFiles = append(cmd.ExtraFiles, r)
|
||||
shimPipe[0], shimPipe[1] = r, w
|
||||
}
|
||||
|
||||
if len(k.supp) > 0 {
|
||||
@@ -393,12 +393,16 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
|
||||
|
||||
msg.Verbosef("setuid helper at %s", hsuPath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
_, _ = shimPipe[0].Close(), shimPipe[1].Close()
|
||||
msg.Resume()
|
||||
return cmd, shimPipe, &hst.AppError{Step: "start setuid wrapper", Err: err}
|
||||
return cmd, nil, &hst.AppError{Step: "start setuid wrapper", Err: err}
|
||||
}
|
||||
if err := shimPipe[0].Close(); err != nil {
|
||||
msg.Verbose(err)
|
||||
}
|
||||
|
||||
*startTime = time.Now().UTC()
|
||||
return cmd, shimPipe, nil
|
||||
return cmd, shimPipe[1], nil
|
||||
}
|
||||
|
||||
// serveShim serves outcomeState through the shim setup pipe.
|
||||
@@ -411,11 +415,11 @@ func serveShim(msg message.Msg, shimPipe *os.File, state *outcomeState) error {
|
||||
msg.Verbose(err.Error())
|
||||
}
|
||||
if err := gob.NewEncoder(shimPipe).Encode(state); err != nil {
|
||||
_ = shimPipe.Close()
|
||||
msg.Resume()
|
||||
return &hst.AppError{Step: "transmit shim config", Err: err}
|
||||
}
|
||||
_ = shimPipe.Close()
|
||||
return nil
|
||||
return shimPipe.Close()
|
||||
}
|
||||
|
||||
// printMessageError prints the error message according to [message.GetMessage],
|
||||
|
||||
@@ -18,7 +18,13 @@ import (
|
||||
func IsPollDescriptor(fd uintptr) bool
|
||||
|
||||
// Main runs an app according to [hst.Config] and terminates. Main does not return.
|
||||
func Main(ctx context.Context, msg message.Msg, config *hst.Config, fd int) {
|
||||
func Main(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
config *hst.Config,
|
||||
flags int,
|
||||
fd int,
|
||||
) {
|
||||
// avoids runtime internals or standard streams
|
||||
if fd >= 0 {
|
||||
if IsPollDescriptor(uintptr(fd)) || fd < 3 {
|
||||
@@ -34,7 +40,7 @@ func Main(ctx context.Context, msg message.Msg, config *hst.Config, fd int) {
|
||||
k := outcome{syscallDispatcher: direct{msg}}
|
||||
|
||||
finaliseTime := time.Now()
|
||||
if err := k.finalise(ctx, msg, &id, config); err != nil {
|
||||
if err := k.finalise(ctx, msg, &id, config, flags); err != nil {
|
||||
printMessageError(msg.GetLogger().Fatalln, "cannot seal app:", err)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
@@ -427,7 +427,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
DirectPipeWire: true,
|
||||
|
||||
ID: "org.chromium.Chromium",
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Container: &hst.ContainerConfig{
|
||||
Env: nil,
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/internal/pipewire"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
@@ -197,7 +198,7 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
if errors.Is(err, syscall.EBADF) {
|
||||
k.fatal("invalid config descriptor")
|
||||
}
|
||||
if errors.Is(err, container.ErrReceiveEnv) {
|
||||
if errors.Is(err, params.ErrReceiveEnv) {
|
||||
k.fatal(shimEnv + " not set")
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/env"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/internal/stub"
|
||||
)
|
||||
|
||||
@@ -172,7 +173,7 @@ func TestShimEntrypoint(t *testing.T) {
|
||||
call("setDumpable", stub.ExpectArgs{uintptr(ext.SUID_DUMP_DISABLE)}, nil, nil),
|
||||
call("getppid", stub.ExpectArgs{}, 0xbad, nil),
|
||||
call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, container.ErrReceiveEnv),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, params.ErrReceiveEnv),
|
||||
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SHIM not set"}}, nil, nil),
|
||||
|
||||
// deferred
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
newConfig := func() *hst.Config {
|
||||
config := hst.Template()
|
||||
config.DirectPulse = true
|
||||
config.Enablements = hst.NewEnablements(hst.EPulse)
|
||||
config.Enablements = new(hst.EPulse)
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
42
internal/params/params.go
Normal file
42
internal/params/params.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package container_test
|
||||
package params_test
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal/params"
|
||||
)
|
||||
|
||||
func TestSetupReceive(t *testing.T) {
|
||||
@@ -30,8 +30,8 @@ func TestSetupReceive(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrReceiveEnv) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, container.ErrReceiveEnv)
|
||||
if _, err := params.Receive(key, nil, nil); !errors.Is(err, params.ErrReceiveEnv) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, params.ErrReceiveEnv)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestSetupReceive(t *testing.T) {
|
||||
const key = "TEST_ENV_FORMAT"
|
||||
t.Setenv(key, "")
|
||||
|
||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
|
||||
if _, err := params.Receive(key, nil, nil); !errors.Is(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"
|
||||
t.Setenv(key, "-1")
|
||||
|
||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
|
||||
if _, err := params.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM)
|
||||
}
|
||||
})
|
||||
@@ -60,16 +60,22 @@ func TestSetupReceive(t *testing.T) {
|
||||
|
||||
encoderDone := make(chan error, 1)
|
||||
extraFiles := make([]*os.File, 0, 1)
|
||||
deadline, _ := t.Deadline()
|
||||
if fd, f, err := container.Setup(&extraFiles); err != nil {
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
t.Fatalf("Setup: error = %v", err)
|
||||
} else if fd != 3 {
|
||||
t.Fatalf("Setup: fd = %d, want 3", fd)
|
||||
} else {
|
||||
if err = f.SetDeadline(deadline); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
t.Cleanup(func() {
|
||||
if err = errors.Join(r.Close(), w.Close()); err != nil {
|
||||
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(f).Encode(payload) }()
|
||||
go func() { encoderDone <- gob.NewEncoder(w).Encode(payload) }()
|
||||
}
|
||||
|
||||
if len(extraFiles) != 1 {
|
||||
@@ -87,13 +93,13 @@ func TestSetupReceive(t *testing.T) {
|
||||
|
||||
var (
|
||||
gotPayload []uint64
|
||||
fdp *uintptr
|
||||
fdp *int
|
||||
)
|
||||
if !useNilFdp {
|
||||
fdp = new(uintptr)
|
||||
fdp = new(int)
|
||||
}
|
||||
var closeFile func() error
|
||||
if f, err := container.Receive(key, &gotPayload, fdp); err != nil {
|
||||
if f, err := params.Receive(key, &gotPayload, fdp); err != nil {
|
||||
t.Fatalf("Receive: error = %v", err)
|
||||
} else {
|
||||
closeFile = f
|
||||
@@ -103,7 +109,7 @@ func TestSetupReceive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if !useNilFdp {
|
||||
if int(*fdp) != dupFd {
|
||||
if *fdp != dupFd {
|
||||
t.Errorf("Fd: %d, want %d", *fdp, dupFd)
|
||||
}
|
||||
}
|
||||
@@ -397,6 +397,7 @@ const SeccompPresets = std.PresetStrict &
|
||||
func (a *execArtifact) makeContainer(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
flags int,
|
||||
hostNet bool,
|
||||
temp, work *check.Absolute,
|
||||
getArtifact GetArtifactFunc,
|
||||
@@ -423,7 +424,9 @@ func (a *execArtifact) makeContainer(
|
||||
z.SeccompFlags |= seccomp.AllowMultiarch
|
||||
z.ParentPerm = 0700
|
||||
z.HostNet = hostNet
|
||||
z.HostAbstract = flags&CHostAbstract != 0
|
||||
z.Hostname = "cure"
|
||||
z.SetScheduler = flags&CSchedIdle != 0
|
||||
z.SchedPolicy = ext.SCHED_IDLE
|
||||
if z.HostNet {
|
||||
z.Hostname = "cure-net"
|
||||
@@ -559,6 +562,7 @@ func (c *Cache) EnterExec(
|
||||
var z *container.Container
|
||||
z, err = e.makeContainer(
|
||||
ctx, c.msg,
|
||||
c.flags,
|
||||
hostNet,
|
||||
temp, work,
|
||||
func(a Artifact) (*check.Absolute, unique.Handle[Checksum]) {
|
||||
@@ -598,14 +602,13 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
|
||||
msg := f.GetMessage()
|
||||
var z *container.Container
|
||||
if z, err = a.makeContainer(
|
||||
ctx, msg, hostNet,
|
||||
ctx, msg, f.cache.flags, hostNet,
|
||||
f.GetTempDir(), f.GetWorkDir(),
|
||||
f.GetArtifact,
|
||||
f.cache.Ident,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
z.SetScheduler = f.cache.flags&CSchedIdle != 0
|
||||
|
||||
var status io.Writer
|
||||
if status, err = f.GetStatusWriter(); err != nil {
|
||||
|
||||
@@ -521,6 +521,14 @@ const (
|
||||
// was caused by an incorrect checksum accidentally left behind while
|
||||
// bumping a package. Only enable this if you are really sure you need it.
|
||||
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
|
||||
|
||||
@@ -24,6 +24,8 @@ import (
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/landlock"
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/stub"
|
||||
"hakurei.app/message"
|
||||
@@ -289,8 +291,20 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
|
||||
msg := message.New(log.New(os.Stderr, "cache: ", 0))
|
||||
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
|
||||
if c, err := pkg.Open(t.Context(), msg, tc.flags, 1<<4, base); err != nil {
|
||||
if c, err := pkg.Open(t.Context(), msg, flags, 1<<4, base); err != nil {
|
||||
t.Fatalf("Open: error = %v", err)
|
||||
} else {
|
||||
t.Cleanup(c.Close)
|
||||
|
||||
@@ -73,7 +73,7 @@ func (t Toolchain) newGoLatest() (pkg.Artifact, string) {
|
||||
case "amd64":
|
||||
bootstrapExtra = append(bootstrapExtra, t.newGoBootstrap())
|
||||
|
||||
case "arm64":
|
||||
case "arm64", "riscv64":
|
||||
bootstrapEnv = append(bootstrapEnv, "GOROOT_BOOTSTRAP=/system")
|
||||
bootstrapExtra = t.AppendPresets(bootstrapExtra, gcc)
|
||||
finalEnv = append(finalEnv, "CGO_ENABLED=0")
|
||||
@@ -141,8 +141,8 @@ rm \
|
||||
)
|
||||
|
||||
const (
|
||||
version = "1.26.1"
|
||||
checksum = "DdC5Ea-aCYPUHNObQh_09uWU0vn4e-8Ben850Vq-5OoamDRrXhuYI4YQ_BOFgaT0"
|
||||
version = "1.26.2"
|
||||
checksum = "v-6BE89_1g3xYf-9oIYpJKFXlo3xKHYJj2_VGkaUq8ZVkIVQmLwrto-xGG03OISH"
|
||||
)
|
||||
return t.newGo(
|
||||
version,
|
||||
|
||||
11450
internal/rosa/kernel_riscv64.config
Normal file
11450
internal/rosa/kernel_riscv64.config
Normal file
File diff suppressed because it is too large
Load Diff
8
internal/rosa/kernel_riscv64.go
Normal file
8
internal/rosa/kernel_riscv64.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package rosa
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed kernel_riscv64.config
|
||||
var kernelConfig []byte
|
||||
|
||||
const kernelName = "bzImage"
|
||||
@@ -4,8 +4,8 @@ import "hakurei.app/internal/pkg"
|
||||
|
||||
func (t Toolchain) newLibcap() (pkg.Artifact, string) {
|
||||
const (
|
||||
version = "2.77"
|
||||
checksum = "2GOTFU4cl2QoS7Dv5wh0c9-hxsQwIzMB9Y_gfAo5xKHqcM13fiHt1RbPkfemzjmB"
|
||||
version = "2.78"
|
||||
checksum = "wFdUkBhFMD9InPnrBZyegWrlPSAg_9JiTBC-eSFyWWlmbzL2qjh2mKxr9Kx2a8ut"
|
||||
)
|
||||
return t.NewPackage("libcap", version, pkg.NewHTTPGetTar(
|
||||
nil, "https://git.kernel.org/pub/scm/libs/libcap/libcap.git/"+
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
// llvmAttr holds the attributes that will be applied to a new [pkg.Artifact]
|
||||
// containing a LLVM variant.
|
||||
type llvmAttr struct {
|
||||
// Passed through to PackageAttr.Flag.
|
||||
flags int
|
||||
// Enabled projects and runtimes.
|
||||
pr int
|
||||
|
||||
// Concatenated with default environment for PackageAttr.Env.
|
||||
env []string
|
||||
@@ -24,8 +24,6 @@ type llvmAttr struct {
|
||||
append []string
|
||||
// Passed through to PackageAttr.NonStage0.
|
||||
nonStage0 []pkg.Artifact
|
||||
// Passed through to PackageAttr.Paths.
|
||||
paths []pkg.ExecPath
|
||||
// Concatenated with default fixup for CMakeHelper.Script.
|
||||
script string
|
||||
|
||||
@@ -75,19 +73,18 @@ func llvmFlagName(flag int) string {
|
||||
|
||||
// newLLVMVariant returns a [pkg.Artifact] containing a LLVM variant.
|
||||
func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
|
||||
|
||||
if attr == nil {
|
||||
panic("LLVM attr must be non-nil")
|
||||
}
|
||||
|
||||
var projects, runtimes []string
|
||||
for i := 1; i < llvmProjectAll; i <<= 1 {
|
||||
if attr.flags&i != 0 {
|
||||
if attr.pr&i != 0 {
|
||||
projects = append(projects, llvmFlagName(i))
|
||||
}
|
||||
}
|
||||
for i := (llvmProjectAll + 1) << 1; i < llvmRuntimeAll; i <<= 1 {
|
||||
if attr.flags&i != 0 {
|
||||
if attr.pr&i != 0 {
|
||||
runtimes = append(runtimes, llvmFlagName(i))
|
||||
}
|
||||
}
|
||||
@@ -126,7 +123,7 @@ func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
|
||||
}...)
|
||||
}
|
||||
|
||||
if attr.flags&llvmProjectClang != 0 {
|
||||
if attr.pr&llvmProjectClang != 0 {
|
||||
cache = append(cache, []KV{
|
||||
{"CLANG_DEFAULT_LINKER", "lld"},
|
||||
{"CLANG_DEFAULT_CXX_STDLIB", "libc++"},
|
||||
@@ -134,30 +131,30 @@ func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
|
||||
{"CLANG_DEFAULT_UNWINDLIB", "libunwind"},
|
||||
}...)
|
||||
}
|
||||
if attr.flags&llvmProjectLld != 0 {
|
||||
if attr.pr&llvmProjectLld != 0 {
|
||||
script += `
|
||||
ln -s ld.lld /work/system/bin/ld
|
||||
`
|
||||
}
|
||||
if attr.flags&llvmRuntimeCompilerRT != 0 {
|
||||
if attr.pr&llvmRuntimeCompilerRT != 0 {
|
||||
if attr.append == nil {
|
||||
cache = append(cache, []KV{
|
||||
{"COMPILER_RT_USE_LLVM_UNWINDER", "ON"},
|
||||
}...)
|
||||
}
|
||||
}
|
||||
if attr.flags&llvmRuntimeLibunwind != 0 {
|
||||
if attr.pr&llvmRuntimeLibunwind != 0 {
|
||||
cache = append(cache, []KV{
|
||||
{"LIBUNWIND_USE_COMPILER_RT", "ON"},
|
||||
}...)
|
||||
}
|
||||
if attr.flags&llvmRuntimeLibcxx != 0 {
|
||||
if attr.pr&llvmRuntimeLibcxx != 0 {
|
||||
cache = append(cache, []KV{
|
||||
{"LIBCXX_HAS_MUSL_LIBC", "ON"},
|
||||
{"LIBCXX_USE_COMPILER_RT", "ON"},
|
||||
}...)
|
||||
}
|
||||
if attr.flags&llvmRuntimeLibcxxABI != 0 {
|
||||
if attr.pr&llvmRuntimeLibcxxABI != 0 {
|
||||
cache = append(cache, []KV{
|
||||
{"LIBCXXABI_USE_COMPILER_RT", "ON"},
|
||||
{"LIBCXXABI_USE_LLVM_UNWINDER", "ON"},
|
||||
@@ -170,7 +167,22 @@ ln -s ld.lld /work/system/bin/ld
|
||||
mustDecode(llvmChecksum),
|
||||
pkg.TarGzip,
|
||||
), &PackageAttr{
|
||||
Patches: attr.patches,
|
||||
Patches: slices.Concat(attr.patches, []KV{
|
||||
{"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,
|
||||
|
||||
Env: slices.Concat([]string{
|
||||
@@ -178,8 +190,7 @@ ln -s ld.lld /work/system/bin/ld
|
||||
"ROSA_LLVM_RUNTIMES=" + strings.Join(runtimes, ";"),
|
||||
}, attr.env),
|
||||
|
||||
Paths: attr.paths,
|
||||
Flag: TExclusive,
|
||||
Flag: TExclusive,
|
||||
}, &CMakeHelper{
|
||||
Variant: variant,
|
||||
|
||||
@@ -201,15 +212,19 @@ ln -s ld.lld /work/system/bin/ld
|
||||
|
||||
// newLLVM returns LLVM toolchain across multiple [pkg.Artifact].
|
||||
func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) {
|
||||
var target string
|
||||
switch runtime.GOARCH {
|
||||
case "386", "amd64":
|
||||
target = "X86"
|
||||
case "arm64":
|
||||
target = "AArch64"
|
||||
target := "'AArch64;RISCV;X86'"
|
||||
if t.isStage0() {
|
||||
switch runtime.GOARCH {
|
||||
case "386", "amd64":
|
||||
target = "X86"
|
||||
case "arm64":
|
||||
target = "AArch64"
|
||||
case "riscv64":
|
||||
target = "RISCV"
|
||||
|
||||
default:
|
||||
panic("unsupported target " + runtime.GOARCH)
|
||||
default:
|
||||
panic("unsupported target " + runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
||||
minimalDeps := []KV{
|
||||
@@ -231,7 +246,7 @@ func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) {
|
||||
{"CMAKE_CXX_COMPILER_TARGET", ""},
|
||||
|
||||
{"COMPILER_RT_BUILD_BUILTINS", "ON"},
|
||||
{"COMPILER_RT_DEFAULT_TARGET_ONLY", "ON"},
|
||||
{"COMPILER_RT_DEFAULT_TARGET_ONLY", "OFF"},
|
||||
{"COMPILER_RT_SANITIZERS_TO_BUILD", "asan"},
|
||||
{"LLVM_ENABLE_PER_TARGET_RUNTIME_DIR", "ON"},
|
||||
|
||||
@@ -275,7 +290,7 @@ ln -s \
|
||||
env: stage0ExclConcat(t, []string{},
|
||||
"LDFLAGS="+earlyLDFLAGS(false),
|
||||
),
|
||||
flags: llvmRuntimeLibunwind | llvmRuntimeLibcxx | llvmRuntimeLibcxxABI,
|
||||
pr: llvmRuntimeLibunwind | llvmRuntimeLibcxx | llvmRuntimeLibcxxABI,
|
||||
cmake: slices.Concat([]KV{
|
||||
// libc++ not yet available
|
||||
{"CMAKE_CXX_COMPILER_WORKS", "ON"},
|
||||
@@ -291,7 +306,7 @@ ln -s \
|
||||
})
|
||||
|
||||
clang = t.newLLVMVariant("clang", &llvmAttr{
|
||||
flags: llvmProjectClang | llvmProjectLld,
|
||||
pr: llvmProjectClang | llvmProjectLld,
|
||||
env: stage0ExclConcat(t, []string{},
|
||||
"CFLAGS="+earlyCFLAGS,
|
||||
"CXXFLAGS="+earlyCXXFLAGS(),
|
||||
@@ -314,7 +329,7 @@ ln -s clang++ /work/system/bin/c++
|
||||
ninja check-all
|
||||
`,
|
||||
|
||||
patches: slices.Concat([]KV{
|
||||
patches: []KV{
|
||||
{"add-rosa-vendor", `diff --git a/llvm/include/llvm/TargetParser/Triple.h b/llvm/include/llvm/TargetParser/Triple.h
|
||||
index 9c83abeeb3b1..5acfe5836a23 100644
|
||||
--- a/llvm/include/llvm/TargetParser/Triple.h
|
||||
@@ -486,7 +501,7 @@ index 64324a3f8b01..15ce70b68217 100644
|
||||
"/System/Library/Frameworks"};
|
||||
|
||||
`},
|
||||
}, clangPatches),
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
package rosa
|
||||
|
||||
// clangPatches are patches applied to the LLVM source tree for building clang.
|
||||
var clangPatches []KV
|
||||
@@ -1,12 +0,0 @@
|
||||
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"
|
||||
)
|
||||
@@ -1,11 +1,9 @@
|
||||
//go:build !arm64
|
||||
|
||||
package rosa
|
||||
|
||||
// latest version of LLVM, conditional to temporarily avoid broken new releases
|
||||
const (
|
||||
llvmVersionMajor = "22"
|
||||
llvmVersion = llvmVersionMajor + ".1.2"
|
||||
llvmVersion = llvmVersionMajor + ".1.3"
|
||||
|
||||
llvmChecksum = "FwsmurWDVyYYQlOowowFjekwIGSB5__aKTpW_VGP3eWoZGXvBny-bOn1DuQ1U5xE"
|
||||
llvmChecksum = "CUwnpzua_y28HZ9oI0NmcKL2wClsSjFpgY9do5-7cCZJHI5KNF64vfwGvY0TYyR3"
|
||||
)
|
||||
|
||||
@@ -4,8 +4,8 @@ import "hakurei.app/internal/pkg"
|
||||
|
||||
func (t Toolchain) newOpenSSL() (pkg.Artifact, string) {
|
||||
const (
|
||||
version = "3.6.1"
|
||||
checksum = "boMAj2SIVIFXHswZva3qHJuFEpc32rxCCu07wjMPsVe9nn_976BGMmW_5P1zthgg"
|
||||
version = "3.6.2"
|
||||
checksum = "jH004dXTiE01Hp0kyShkWXwrSHEksZi4i_3v47D9H9Uz9LQ1aMwF7mrl2Tb4t_XA"
|
||||
)
|
||||
return t.NewPackage("openssl", version, pkg.NewHTTPGetTar(
|
||||
nil, "https://github.com/openssl/openssl/releases/download/"+
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
func (t Toolchain) newPython() (pkg.Artifact, string) {
|
||||
const (
|
||||
version = "3.14.3"
|
||||
checksum = "ajEC32WPmn9Jvll0n4gGvlTvhMPUHb2H_j5_h9jf_esHmkZBRfAumDcKY7nTTsCH"
|
||||
version = "3.14.4"
|
||||
checksum = "X0VRAAGOlCVldh4J9tRAE-YrJtDvqfQTJaqxKPXNX6YTPlwpR9GwA5WRIZDO-63s"
|
||||
)
|
||||
return t.NewPackage("python", version, pkg.NewHTTPGetTar(
|
||||
nil, "https://www.python.org/ftp/python/"+version+
|
||||
|
||||
@@ -56,6 +56,8 @@ func linuxArch() string {
|
||||
return "x86_64"
|
||||
case "arm64":
|
||||
return "aarch64"
|
||||
case "riscv64":
|
||||
return "riscv64"
|
||||
|
||||
default:
|
||||
panic("unsupported target " + runtime.GOARCH)
|
||||
|
||||
@@ -67,6 +67,8 @@ func NewStage0() pkg.Artifact {
|
||||
seed = "tqM1Li15BJ-uFG8zU-XjgFxoN_kuzh1VxrSDVUVa0vGmo-NeWapSftH739sY8EAg"
|
||||
case "arm64":
|
||||
seed = "CJj3ZSnRyLmFHlWIQtTPQD9oikOZY4cD_mI3v_-LIYc2hhg-cq_CZFBLzQBAkFIn"
|
||||
case "riscv64":
|
||||
seed = "FcszJjcVWdKAnn-bt8qmUn5GUUTjv_xQjXOWkUpOplRkG3Ckob3StUoAi5KQ5-QF"
|
||||
|
||||
default:
|
||||
panic("unsupported target " + runtime.GOARCH)
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
func (t Toolchain) newTamaGo() (pkg.Artifact, string) {
|
||||
const (
|
||||
version = "1.26.1"
|
||||
checksum = "fimZnklQcYWGsTQU8KepLn-yCYaTfNdMI9DCg6NJVQv-3gOJnUEO9mqRCMAHnEXZ"
|
||||
version = "1.26.2"
|
||||
checksum = "5xlhWq2NGhYCjt0y73QkydJ386lxg6-HkiO84ne6ByQSJBDat7-HSVzNA6jy7Laz"
|
||||
)
|
||||
return t.New("tamago-go"+version, 0, t.AppendPresets(nil,
|
||||
Bash,
|
||||
|
||||
@@ -24,7 +24,7 @@ func entryEncode(w io.Writer, s *hst.State) error {
|
||||
}
|
||||
|
||||
// entryDecodeHeader calls entryReadHeader, returning [hst.AppError] for a non-nil error.
|
||||
func entryDecodeHeader(r io.Reader) (hst.Enablement, error) {
|
||||
func entryDecodeHeader(r io.Reader) (hst.Enablements, error) {
|
||||
if et, err := entryReadHeader(r); err != nil {
|
||||
return 0, &hst.AppError{Step: "decode state header", Err: err}
|
||||
} else {
|
||||
@@ -32,20 +32,23 @@ func entryDecodeHeader(r io.Reader) (hst.Enablement, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// entryDecode decodes [hst.State] from [io.Reader] and stores the result in the value pointed to by p.
|
||||
// entryDecode validates the embedded [hst.Config] value.
|
||||
// entryDecode decodes [hst.State] from [io.Reader] and stores the result in the
|
||||
// value pointed to by p. entryDecode validates the embedded [hst.Config] value.
|
||||
//
|
||||
// A non-nil error returned by entryDecode is of type [hst.AppError].
|
||||
func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error) {
|
||||
func entryDecode(r io.Reader, p *hst.State) (hst.Enablements, error) {
|
||||
if et, err := entryDecodeHeader(r); err != nil {
|
||||
return et, err
|
||||
} else if err = gob.NewDecoder(r).Decode(&p); err != nil {
|
||||
return et, &hst.AppError{Step: "decode state body", Err: err}
|
||||
} else if err = p.Config.Validate(); err != nil {
|
||||
} else if err = p.Config.Validate(hst.VAllowInsecure); err != nil {
|
||||
return et, err
|
||||
} else if p.Enablements.Unwrap() != et {
|
||||
return et, &hst.AppError{Step: "validate state enablement", Err: os.ErrInvalid,
|
||||
Msg: fmt.Sprintf("state entry %s has unexpected enablement byte %#x, %#x", p.ID.String(), byte(p.Enablements.Unwrap()), byte(et))}
|
||||
Msg: fmt.Sprintf(
|
||||
"state entry %s has unexpected enablement byte %#x, %#x",
|
||||
p.ID.String(), byte(p.Enablements.Unwrap()), byte(et),
|
||||
)}
|
||||
} else {
|
||||
return et, nil
|
||||
}
|
||||
|
||||
@@ -14,22 +14,25 @@ import (
|
||||
const (
|
||||
// entryHeaderMagic are magic bytes at the beginning of the state entry file.
|
||||
entryHeaderMagic = "\x00\xff\xca\xfe"
|
||||
// entryHeaderRevision follows entryHeaderMagic and is incremented for revisions of the format.
|
||||
// entryHeaderRevision follows entryHeaderMagic and is incremented for
|
||||
// revisions of the format.
|
||||
entryHeaderRevision = "\x00\x00"
|
||||
// entryHeaderSize is the fixed size of the header in bytes, including the enablement byte and its complement.
|
||||
// entryHeaderSize is the fixed size of the header in bytes, including the
|
||||
// enablement byte and its complement.
|
||||
entryHeaderSize = len(entryHeaderMagic+entryHeaderRevision) + 2
|
||||
)
|
||||
|
||||
// entryHeaderEncode encodes a state entry header for a [hst.Enablement] byte.
|
||||
func entryHeaderEncode(et hst.Enablement) *[entryHeaderSize]byte {
|
||||
// entryHeaderEncode encodes a state entry header for a [hst.Enablements] byte.
|
||||
func entryHeaderEncode(et hst.Enablements) *[entryHeaderSize]byte {
|
||||
data := [entryHeaderSize]byte([]byte(
|
||||
entryHeaderMagic + entryHeaderRevision + string([]hst.Enablement{et, ^et}),
|
||||
entryHeaderMagic + entryHeaderRevision + string([]hst.Enablements{et, ^et}),
|
||||
))
|
||||
return &data
|
||||
}
|
||||
|
||||
// entryHeaderDecode validates a state entry header and returns the [hst.Enablement] byte.
|
||||
func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablement, error) {
|
||||
// entryHeaderDecode validates a state entry header and returns the
|
||||
// [hst.Enablements] byte.
|
||||
func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablements, error) {
|
||||
if magic := data[:len(entryHeaderMagic)]; string(magic) != entryHeaderMagic {
|
||||
return 0, errors.New("invalid header " + hex.EncodeToString(magic))
|
||||
}
|
||||
@@ -41,7 +44,7 @@ func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablement, error) {
|
||||
if et != ^data[len(entryHeaderMagic+entryHeaderRevision)+1] {
|
||||
return 0, errors.New("header enablement value is inconsistent")
|
||||
}
|
||||
return hst.Enablement(et), nil
|
||||
return hst.Enablements(et), nil
|
||||
}
|
||||
|
||||
// EntrySizeError is returned for a file too small to hold a state entry header.
|
||||
@@ -68,8 +71,8 @@ func entryCheckFile(fi os.FileInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// entryReadHeader reads [hst.Enablement] from an [io.Reader].
|
||||
func entryReadHeader(r io.Reader) (hst.Enablement, error) {
|
||||
// entryReadHeader reads [hst.Enablements] from an [io.Reader].
|
||||
func entryReadHeader(r io.Reader) (hst.Enablements, error) {
|
||||
var data [entryHeaderSize]byte
|
||||
if n, err := r.Read(data[:]); err != nil {
|
||||
return 0, err
|
||||
@@ -79,8 +82,8 @@ func entryReadHeader(r io.Reader) (hst.Enablement, error) {
|
||||
return entryHeaderDecode(&data)
|
||||
}
|
||||
|
||||
// entryWriteHeader writes [hst.Enablement] header to an [io.Writer].
|
||||
func entryWriteHeader(w io.Writer, et hst.Enablement) error {
|
||||
// entryWriteHeader writes [hst.Enablements] header to an [io.Writer].
|
||||
func entryWriteHeader(w io.Writer, et hst.Enablements) error {
|
||||
_, err := w.Write(entryHeaderEncode(et)[:])
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestEntryHeader(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
data [entryHeaderSize]byte
|
||||
et hst.Enablement
|
||||
et hst.Enablements
|
||||
err error
|
||||
}{
|
||||
{"complement mismatch", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
)
|
||||
|
||||
// EntryHandle is a handle on a state entry retrieved from a [Handle].
|
||||
//
|
||||
// Must only be used while its parent [Handle.Lock] is held.
|
||||
type EntryHandle struct {
|
||||
// Error returned while decoding pathname.
|
||||
@@ -27,6 +28,7 @@ type EntryHandle struct {
|
||||
}
|
||||
|
||||
// open opens the underlying state entry file.
|
||||
//
|
||||
// A non-nil error returned by open is of type [hst.AppError].
|
||||
func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
|
||||
if eh.DecodeErr != nil {
|
||||
@@ -41,6 +43,7 @@ func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
|
||||
}
|
||||
|
||||
// Destroy removes the underlying state entry.
|
||||
//
|
||||
// A non-nil error returned by Destroy is of type [hst.AppError].
|
||||
func (eh *EntryHandle) Destroy() error {
|
||||
// destroy does not go through open
|
||||
@@ -55,8 +58,10 @@ func (eh *EntryHandle) Destroy() error {
|
||||
}
|
||||
|
||||
// save encodes [hst.State] and writes it to the underlying file.
|
||||
//
|
||||
// An error is returned if a file already exists with the same identifier.
|
||||
// save does not validate the embedded [hst.Config].
|
||||
//
|
||||
// A non-nil error returned by save is of type [hst.AppError].
|
||||
func (eh *EntryHandle) save(state *hst.State) error {
|
||||
f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
||||
@@ -71,17 +76,19 @@ func (eh *EntryHandle) save(state *hst.State) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load loads and validates the state entry header, and returns the [hst.Enablement] byte.
|
||||
// for a non-nil v, the full state payload is decoded 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].
|
||||
func (eh *EntryHandle) Load(v *hst.State) (hst.Enablement, error) {
|
||||
// Load loads and validates the state entry header, and returns the
|
||||
// [hst.Enablements] byte. For a non-nil v, the full state payload is decoded
|
||||
// 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].
|
||||
func (eh *EntryHandle) Load(v *hst.State) (hst.Enablements, error) {
|
||||
f, err := eh.open(os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var et hst.Enablement
|
||||
var et hst.Enablements
|
||||
if v != nil {
|
||||
et, err = entryDecode(f, v)
|
||||
if err == nil && v.ID != eh.ID {
|
||||
@@ -99,6 +106,7 @@ func (eh *EntryHandle) Load(v *hst.State) (hst.Enablement, error) {
|
||||
}
|
||||
|
||||
// Handle is a handle on a [Store] segment.
|
||||
//
|
||||
// Initialised by [Store.Handle].
|
||||
type Handle struct {
|
||||
// Identity of instances tracked by this segment.
|
||||
@@ -113,8 +121,9 @@ type Handle struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Lock attempts to acquire a lock on [Handle].
|
||||
// If successful, Lock returns a non-nil unlock function.
|
||||
// Lock attempts to acquire a lock on [Handle]. If successful, Lock returns a
|
||||
// non-nil unlock function.
|
||||
//
|
||||
// A non-nil error returned by Lock is of type [hst.AppError].
|
||||
func (h *Handle) Lock() (unlock func(), err error) {
|
||||
if unlock, err = h.fileMu.Lock(); err != nil {
|
||||
@@ -123,20 +132,24 @@ func (h *Handle) Lock() (unlock func(), err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Save attempts to save [hst.State] as a segment entry, and returns its [EntryHandle].
|
||||
// Must be called while holding [Handle.Lock].
|
||||
// Save attempts to save [hst.State] as a segment entry, and returns its
|
||||
// [EntryHandle]. Must be called while holding [Handle.Lock].
|
||||
//
|
||||
// An error is returned if an entry already exists with the same identifier.
|
||||
// Save does not validate the embedded [hst.Config].
|
||||
//
|
||||
// A non-nil error returned by Save is of type [hst.AppError].
|
||||
func (h *Handle) Save(state *hst.State) (*EntryHandle, error) {
|
||||
eh := EntryHandle{nil, h.Path.Append(state.ID.String()), state.ID}
|
||||
return &eh, eh.save(state)
|
||||
}
|
||||
|
||||
// Entries returns an iterator over all [EntryHandle] held in this segment.
|
||||
// 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 returned by Entries is of type [hst.AppError].
|
||||
// Entries returns an iterator over all [EntryHandle] held in this segment. 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 returned by Entries is of type
|
||||
// [hst.AppError].
|
||||
func (h *Handle) Entries() (iter.Seq[*EntryHandle], int, error) {
|
||||
// for error reporting
|
||||
const step = "read store segment entries"
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// Made available here for direct validation of state entry files.
|
||||
//
|
||||
//go:linkname entryDecode hakurei.app/internal/store.entryDecode
|
||||
func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error)
|
||||
func entryDecode(r io.Reader, p *hst.State) (hst.Enablements, error)
|
||||
|
||||
// Made available here for direct access to known segment handles.
|
||||
//
|
||||
|
||||
@@ -17,20 +17,21 @@ func (sys *I) UpdatePerm(path *check.Absolute, perms ...acl.Perm) *I {
|
||||
return sys
|
||||
}
|
||||
|
||||
// UpdatePermType maintains [acl.Perms] on a file until its [Enablement] is no longer satisfied.
|
||||
func (sys *I) UpdatePermType(et hst.Enablement, path *check.Absolute, perms ...acl.Perm) *I {
|
||||
// UpdatePermType maintains [acl.Perms] on a file until its [hst.Enablements] is
|
||||
// no longer satisfied.
|
||||
func (sys *I) UpdatePermType(et hst.Enablements, path *check.Absolute, perms ...acl.Perm) *I {
|
||||
sys.ops = append(sys.ops, &aclUpdateOp{et, path.String(), perms})
|
||||
return sys
|
||||
}
|
||||
|
||||
// aclUpdateOp implements [I.UpdatePermType].
|
||||
type aclUpdateOp struct {
|
||||
et hst.Enablement
|
||||
et hst.Enablements
|
||||
path string
|
||||
perms acl.Perms
|
||||
}
|
||||
|
||||
func (a *aclUpdateOp) Type() hst.Enablement { return a.et }
|
||||
func (a *aclUpdateOp) Type() hst.Enablements { return a.et }
|
||||
|
||||
func (a *aclUpdateOp) apply(sys *I) error {
|
||||
sys.msg.Verbose("applying ACL", a)
|
||||
|
||||
@@ -31,7 +31,9 @@ func (sys *I) MustProxyDBus(
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via [dbus] and terminates it on revert.
|
||||
// ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via
|
||||
// [dbus] and terminates it on revert.
|
||||
//
|
||||
// This [Op] is always [Process] scoped.
|
||||
func (sys *I) ProxyDBus(
|
||||
session, system *hst.BusConfig,
|
||||
@@ -84,7 +86,7 @@ type dbusProxyOp struct {
|
||||
system bool
|
||||
}
|
||||
|
||||
func (d *dbusProxyOp) Type() hst.Enablement { return Process }
|
||||
func (d *dbusProxyOp) Type() hst.Enablements { return Process }
|
||||
|
||||
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])
|
||||
|
||||
@@ -21,12 +21,16 @@ type osFile interface {
|
||||
fs.File
|
||||
}
|
||||
|
||||
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
|
||||
// syscallDispatcher provides methods that make state-dependent system calls as
|
||||
// part of their behaviour.
|
||||
//
|
||||
// syscallDispatcher is embedded in [I], so all methods must be unexported.
|
||||
type syscallDispatcher interface {
|
||||
// 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,
|
||||
// just synchronising access is not enough, as this is for test instrumentation.
|
||||
//
|
||||
// A syscallDispatcher must never be used in any goroutine other than the
|
||||
// one owning it, just synchronising access is not enough, as this is for
|
||||
// test instrumentation.
|
||||
new(f func(k syscallDispatcher))
|
||||
|
||||
// stat provides os.Stat.
|
||||
|
||||
@@ -27,7 +27,7 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
|
||||
type opBehaviourTestCase struct {
|
||||
name string
|
||||
uid int
|
||||
ec hst.Enablement
|
||||
ec hst.Enablements
|
||||
op Op
|
||||
|
||||
apply []stub.Call
|
||||
@@ -158,7 +158,7 @@ type opMetaTestCase struct {
|
||||
name string
|
||||
op Op
|
||||
|
||||
wantType hst.Enablement
|
||||
wantType hst.Enablements
|
||||
wantPath string
|
||||
wantString string
|
||||
}
|
||||
|
||||
@@ -12,19 +12,19 @@ func (sys *I) Link(oldname, newname *check.Absolute) *I {
|
||||
return sys.LinkFileType(Process, oldname, newname)
|
||||
}
|
||||
|
||||
// LinkFileType maintains a hardlink until its [Enablement] is no longer satisfied.
|
||||
func (sys *I) LinkFileType(et hst.Enablement, oldname, newname *check.Absolute) *I {
|
||||
// LinkFileType maintains a hardlink until its [hst.Enablements] is no longer satisfied.
|
||||
func (sys *I) LinkFileType(et hst.Enablements, oldname, newname *check.Absolute) *I {
|
||||
sys.ops = append(sys.ops, &hardlinkOp{et, newname.String(), oldname.String()})
|
||||
return sys
|
||||
}
|
||||
|
||||
// hardlinkOp implements [I.LinkFileType].
|
||||
type hardlinkOp struct {
|
||||
et hst.Enablement
|
||||
et hst.Enablements
|
||||
dst, src string
|
||||
}
|
||||
|
||||
func (l *hardlinkOp) Type() hst.Enablement { return l.et }
|
||||
func (l *hardlinkOp) Type() hst.Enablements { return l.et }
|
||||
|
||||
func (l *hardlinkOp) apply(sys *I) error {
|
||||
sys.msg.Verbose("linking", l)
|
||||
|
||||
@@ -15,21 +15,22 @@ func (sys *I) Ensure(name *check.Absolute, perm os.FileMode) *I {
|
||||
return sys
|
||||
}
|
||||
|
||||
// Ephemeral ensures the existence of a directory until its [Enablement] is no longer satisfied.
|
||||
func (sys *I) Ephemeral(et hst.Enablement, name *check.Absolute, perm os.FileMode) *I {
|
||||
// Ephemeral ensures the existence of a directory until its [hst.Enablements] is
|
||||
// no longer satisfied.
|
||||
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})
|
||||
return sys
|
||||
}
|
||||
|
||||
// mkdirOp implements [I.Ensure] and [I.Ephemeral].
|
||||
type mkdirOp struct {
|
||||
et hst.Enablement
|
||||
et hst.Enablements
|
||||
path string
|
||||
perm os.FileMode
|
||||
ephemeral bool
|
||||
}
|
||||
|
||||
func (m *mkdirOp) Type() hst.Enablement { return m.et }
|
||||
func (m *mkdirOp) Type() hst.Enablements { return m.et }
|
||||
|
||||
func (m *mkdirOp) apply(sys *I) error {
|
||||
sys.msg.Verbose("ensuring directory", m)
|
||||
|
||||
@@ -12,8 +12,9 @@ import (
|
||||
)
|
||||
|
||||
// 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 is pathname only and is destroyed on revert.
|
||||
//
|
||||
// The socket stops accepting connections once the pipe referred to by sync is
|
||||
// closed. The socket is pathname only and is destroyed on revert.
|
||||
func (sys *I) PipeWire(dst *check.Absolute, appID, instanceID string) *I {
|
||||
sys.ops = append(sys.ops, &pipewireOp{nil, dst, appID, instanceID})
|
||||
return sys
|
||||
@@ -27,7 +28,7 @@ type pipewireOp struct {
|
||||
appID, instanceID string
|
||||
}
|
||||
|
||||
func (p *pipewireOp) Type() hst.Enablement { return Process }
|
||||
func (p *pipewireOp) Type() hst.Enablements { return Process }
|
||||
|
||||
func (p *pipewireOp) apply(sys *I) (err error) {
|
||||
var ctx *pipewire.Context
|
||||
|
||||
@@ -20,21 +20,21 @@ const (
|
||||
)
|
||||
|
||||
// Criteria specifies types of Op to revert.
|
||||
type Criteria hst.Enablement
|
||||
type Criteria hst.Enablements
|
||||
|
||||
func (ec *Criteria) hasType(t hst.Enablement) bool {
|
||||
func (ec *Criteria) hasType(t hst.Enablements) bool {
|
||||
// nil criteria: revert everything except User
|
||||
if ec == nil {
|
||||
return t != User
|
||||
}
|
||||
|
||||
return hst.Enablement(*ec)&t != 0
|
||||
return hst.Enablements(*ec)&t != 0
|
||||
}
|
||||
|
||||
// Op is a reversible system operation.
|
||||
type Op interface {
|
||||
// Type returns [Op]'s enablement type, for matching a revert criteria.
|
||||
Type() hst.Enablement
|
||||
Type() hst.Enablements
|
||||
|
||||
apply(sys *I) error
|
||||
revert(sys *I, ec *Criteria) error
|
||||
@@ -44,8 +44,8 @@ type Op interface {
|
||||
String() string
|
||||
}
|
||||
|
||||
// TypeString extends [hst.Enablement.String] to support [User] and [Process].
|
||||
func TypeString(e hst.Enablement) string {
|
||||
// TypeString extends [hst.Enablements] to support [User] and [Process].
|
||||
func TypeString(e hst.Enablements) string {
|
||||
switch e {
|
||||
case User:
|
||||
return "user"
|
||||
@@ -110,7 +110,9 @@ func (sys *I) Equal(target *I) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Commit applies all [Op] held by [I] and reverts all successful [Op] on first error encountered.
|
||||
// Commit applies all [Op] held by [I] and reverts all successful [Op] on first
|
||||
// error encountered.
|
||||
//
|
||||
// Commit must not be called more than once.
|
||||
func (sys *I) Commit() error {
|
||||
if sys.committed {
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestCriteria(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
ec, t hst.Enablement
|
||||
ec, t hst.Enablements
|
||||
want bool
|
||||
}{
|
||||
{"nil", 0xff, hst.EWayland, true},
|
||||
@@ -47,7 +47,7 @@ func TestTypeString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
e hst.Enablement
|
||||
e hst.Enablements
|
||||
want string
|
||||
}{
|
||||
{hst.EWayland, hst.EWayland.String()},
|
||||
@@ -190,7 +190,7 @@ func TestCommitRevert(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
f func(sys *I)
|
||||
ec hst.Enablement
|
||||
ec hst.Enablements
|
||||
|
||||
commit []stub.Call
|
||||
wantErrCommit error
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
)
|
||||
|
||||
// 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 is pathname only and is destroyed on revert.
|
||||
//
|
||||
// The socket stops accepting connections once the pipe referred to by sync is
|
||||
// closed. The socket is pathname only and is destroyed on revert.
|
||||
func (sys *I) Wayland(dst, src *check.Absolute, appID, instanceID string) *I {
|
||||
sys.ops = append(sys.ops, &waylandOp{nil,
|
||||
dst, src, appID, instanceID})
|
||||
@@ -26,7 +27,7 @@ type waylandOp struct {
|
||||
appID, instanceID string
|
||||
}
|
||||
|
||||
func (w *waylandOp) Type() hst.Enablement { return Process }
|
||||
func (w *waylandOp) Type() hst.Enablements { return Process }
|
||||
|
||||
func (w *waylandOp) apply(sys *I) (err error) {
|
||||
if w.ctx, err = sys.waylandNew(w.src, w.dst, w.appID, w.instanceID); err != nil {
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"hakurei.app/internal/xcb"
|
||||
)
|
||||
|
||||
// ChangeHosts inserts the target user into X11 hosts and deletes it once its [Enablement] is no longer satisfied.
|
||||
// ChangeHosts inserts the target user into X11 hosts and deletes it once its
|
||||
// [hst.Enablements] is no longer satisfied.
|
||||
func (sys *I) ChangeHosts(username string) *I {
|
||||
sys.ops = append(sys.ops, xhostOp(username))
|
||||
return sys
|
||||
@@ -14,7 +15,7 @@ func (sys *I) ChangeHosts(username string) *I {
|
||||
// xhostOp implements [I.ChangeHosts].
|
||||
type xhostOp string
|
||||
|
||||
func (x xhostOp) Type() hst.Enablement { return hst.EX11 }
|
||||
func (x xhostOp) Type() hst.Enablements { return hst.EX11 }
|
||||
|
||||
func (x xhostOp) apply(sys *I) error {
|
||||
sys.msg.Verbosef("inserting entry %s to X11", x)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// synthAdd is prepared bytes written to uevent to cause a synthetic add event
|
||||
@@ -26,6 +27,7 @@ var synthAdd = []byte(KOBJ_ADD.String())
|
||||
func Coldboot(
|
||||
ctx context.Context,
|
||||
pathname string,
|
||||
uuid *UUID,
|
||||
visited chan<- string,
|
||||
handleWalkErr func(error) error,
|
||||
) error {
|
||||
@@ -39,6 +41,11 @@ func Coldboot(
|
||||
}
|
||||
}
|
||||
|
||||
add := synthAdd
|
||||
if uuid != nil {
|
||||
add = slices.Concat(add, []byte{' '}, []byte(uuid.String()))
|
||||
}
|
||||
|
||||
return filepath.WalkDir(filepath.Join(pathname, "devices"), func(
|
||||
path string,
|
||||
d fs.DirEntry,
|
||||
@@ -54,7 +61,7 @@ func Coldboot(
|
||||
if d.IsDir() || d.Name() != "uevent" {
|
||||
return nil
|
||||
}
|
||||
if err = os.WriteFile(path, synthAdd, 0); err != nil {
|
||||
if err = os.WriteFile(path, add, 0); err != nil {
|
||||
return handleWalkErr(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestColdboot(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
err := uevent.Coldboot(t.Context(), d, visited, func(err error) error {
|
||||
err := uevent.Coldboot(t.Context(), d, nil, visited, func(err error) error {
|
||||
t.Errorf("handleWalkErr: %v", err)
|
||||
return err
|
||||
})
|
||||
@@ -219,7 +219,7 @@ func TestColdbootError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := uevent.Coldboot(ctx, d, visited, handleWalkErr); !reflect.DeepEqual(err, wantErr) {
|
||||
if err := uevent.Coldboot(ctx, d, new(uevent.UUID), visited, handleWalkErr); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Errorf("Coldboot: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,10 +5,14 @@ package uevent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/internal/netlink"
|
||||
)
|
||||
@@ -121,6 +125,117 @@ func (c *Conn) receiveEvent(ctx context.Context) (*Message, error) {
|
||||
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
|
||||
// [Recoverable] and [NeedsColdboot] errors via caller-supplied functions,
|
||||
// entering coldboot when required.
|
||||
@@ -145,6 +260,7 @@ func (c *Conn) receiveEvent(ctx context.Context) (*Message, error) {
|
||||
func (c *Conn) Consume(
|
||||
ctx context.Context,
|
||||
sysfs string,
|
||||
uuid *UUID,
|
||||
events chan<- *Message,
|
||||
coldboot bool,
|
||||
|
||||
@@ -193,7 +309,7 @@ coldboot:
|
||||
ctxColdboot, cancelColdboot := context.WithCancel(ctx)
|
||||
var coldbootErr error
|
||||
go func() {
|
||||
coldbootErr = Coldboot(ctxColdboot, sysfs, visited, handleWalkErr)
|
||||
coldbootErr = Coldboot(ctxColdboot, sysfs, uuid, visited, handleWalkErr)
|
||||
close(visited)
|
||||
}()
|
||||
for pathname := range visited {
|
||||
|
||||
@@ -3,8 +3,10 @@ package uevent_test
|
||||
import (
|
||||
"context"
|
||||
"encoding"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
@@ -23,6 +25,12 @@ func adeT[V any, S interface {
|
||||
*V
|
||||
}](t *testing.T, name string, v V, want string, wantErr, wantErrE error) {
|
||||
t.Helper()
|
||||
|
||||
noEncode := strings.HasSuffix(name, "\x00")
|
||||
if noEncode {
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
|
||||
f := func(t *testing.T) {
|
||||
if name != "" {
|
||||
t.Parallel()
|
||||
@@ -46,6 +54,10 @@ func adeT[V any, S interface {
|
||||
}
|
||||
})
|
||||
|
||||
if noEncode {
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("encode", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Helper()
|
||||
@@ -114,6 +126,45 @@ 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) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -157,7 +208,7 @@ func TestDialConsume(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
consume := func(c *uevent.Conn, ctx context.Context) error {
|
||||
return c.Consume(ctx, fhs.Sys, events, false, func(path string) {
|
||||
return c.Consume(ctx, fhs.Sys, nil, events, false, func(path string) {
|
||||
t.Log("coldboot visited", path)
|
||||
}, func(err error) bool {
|
||||
t.Log(err)
|
||||
@@ -244,6 +295,11 @@ func TestErrors(t *testing.T) {
|
||||
{"BadPortError", &uevent.BadPortError{
|
||||
Pid: 1,
|
||||
}, "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 {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container"
|
||||
@@ -27,9 +26,6 @@ const (
|
||||
|
||||
// lddName is the file name of ldd(1) passed to exec.LookPath.
|
||||
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].
|
||||
@@ -52,7 +48,7 @@ func Resolve(
|
||||
}
|
||||
}
|
||||
|
||||
c, cancel := context.WithTimeout(ctx, lddTimeout)
|
||||
c, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var toolPath *check.Absolute
|
||||
|
||||
@@ -265,7 +265,7 @@ in
|
||||
'';
|
||||
in
|
||||
pkgs.writeShellScriptBin app.name ''
|
||||
exec hakurei${if app.verbose then " -v" else ""} run ${checkedConfig "hakurei-app-${app.name}.json" conf} $@
|
||||
exec hakurei${if app.verbose then " -v" else ""}${if app.insecureWayland then " --insecure" else ""} run ${checkedConfig "hakurei-app-${app.name}.json" conf} $@
|
||||
''
|
||||
)
|
||||
]
|
||||
|
||||
41
options.md
41
options.md
@@ -35,7 +35,7 @@ package
|
||||
|
||||
|
||||
*Default:*
|
||||
` <derivation hakurei-static-x86_64-unknown-linux-musl-0.3.6> `
|
||||
` <derivation hakurei-static-x86_64-unknown-linux-musl-0.4.0> `
|
||||
|
||||
|
||||
|
||||
@@ -542,6 +542,43 @@ 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
|
||||
|
||||
|
||||
@@ -805,7 +842,7 @@ package
|
||||
|
||||
|
||||
*Default:*
|
||||
` <derivation hakurei-hsu-0.3.6> `
|
||||
` <derivation hakurei-hsu-0.4.0> `
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
buildGo126Module rec {
|
||||
pname = "hakurei";
|
||||
version = "0.3.7";
|
||||
version = "0.4.0";
|
||||
|
||||
srcFiltered = builtins.path {
|
||||
name = "${pname}-src";
|
||||
@@ -82,9 +82,6 @@ buildGo126Module rec {
|
||||
env = {
|
||||
# use clang instead of gcc
|
||||
CC = "clang -O3 -Werror";
|
||||
|
||||
# nix build environment does not allow acls
|
||||
HAKUREI_TEST_SKIP_ACL = 1;
|
||||
};
|
||||
|
||||
buildInputs = [
|
||||
@@ -116,7 +113,7 @@ buildGo126Module rec {
|
||||
];
|
||||
in
|
||||
''
|
||||
install -D --target-directory=$out/share/zsh/site-functions dist/comp/*
|
||||
install -D --target-directory=$out/share/zsh/site-functions cmd/dist/comp/*
|
||||
|
||||
mkdir "$out/libexec"
|
||||
mv "$out"/bin/* "$out/libexec/"
|
||||
|
||||
@@ -44,7 +44,7 @@ testers.nixosTest {
|
||||
|
||||
cd ${self.packages.${system}.hakurei.src}
|
||||
${fhs}/bin/hakurei-fhs -c \
|
||||
'CC="clang -O3 -Werror" go test ${if withRace then "-race" else "-count 16"} ./...' \
|
||||
'CC="clang -O3 -Werror" go test --tags=noskip ${if withRace then "-race" else "-count 16"} ./...' \
|
||||
&> /tmp/hakurei-test.log && \
|
||||
touch /tmp/hakurei-test-ok
|
||||
touch /tmp/hakurei-test-done
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
description = "Alice Foobar";
|
||||
password = "foobar";
|
||||
uid = 1000;
|
||||
extraGroups = [ "wheel" ];
|
||||
extraGroups = [
|
||||
"wheel"
|
||||
"sharefs"
|
||||
];
|
||||
};
|
||||
untrusted = {
|
||||
isNormalUser = true;
|
||||
|
||||
30
test/interactive/raceattr.nix
Normal file
30
test/interactive/raceattr.nix
Normal file
@@ -0,0 +1,30 @@
|
||||
{ lib, pkgs, ... }:
|
||||
{
|
||||
security.wrappers.raceattr =
|
||||
let
|
||||
inherit (pkgs) buildGoModule;
|
||||
in
|
||||
{
|
||||
setuid = true;
|
||||
owner = "root";
|
||||
group = "root";
|
||||
source = "${
|
||||
(buildGoModule rec {
|
||||
name = "raceattr";
|
||||
pname = name;
|
||||
tags = [ "raceattr" ];
|
||||
|
||||
src = builtins.path {
|
||||
name = "${pname}-src";
|
||||
path = lib.cleanSource ../../cmd/sharefs/test;
|
||||
filter = path: type: (type == "directory") || (type == "regular" && lib.hasSuffix ".go" path);
|
||||
};
|
||||
vendorHash = null;
|
||||
|
||||
preBuild = ''
|
||||
go mod init hakurei.app/raceattr >& /dev/null
|
||||
'';
|
||||
})
|
||||
}/bin/raceattr";
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user