1 Commits

Author SHA1 Message Date
a5bbc771a1 internal/netlink: make full response available
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m51s
Test / ShareFS (push) Successful in 4m44s
Test / Hakurei (push) Successful in 5m31s
Test / Sandbox (race detector) (push) Successful in 5m30s
Test / Hakurei (race detector) (push) Successful in 7m52s
Test / Flake checks (push) Successful in 1m22s
The previous API makes it impossible to retrieve remaining messages in the current iteration.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-23 15:16:38 +09:00
217 changed files with 2555 additions and 18474 deletions

31
.gitignore vendored
View File

@@ -1,7 +1,27 @@
# produced by tools and text editors
*.qcow2
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
*.pkg
/hakurei
# Test binary, built with `go test -c`
*.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
@@ -10,5 +30,8 @@
/internal/pkg/testdata/testtool
/internal/rosa/hakurei_current.tar.gz
# cmd/dist default destination
/dist
# release
/dist/hakurei-*
# interactive nixos vm
nixos.qcow2

6
all.sh
View File

@@ -1,6 +0,0 @@
#!/bin/sh -e
TOOLCHAIN_VERSION="$(go version)"
cd "$(dirname -- "$0")/"
echo "# Building cmd/dist using ${TOOLCHAIN_VERSION}."
go run -v --tags=dist ./cmd/dist

View File

@@ -2,10 +2,10 @@
package check
import (
"encoding"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"path"
"slices"
"strings"
"syscall"
@@ -30,16 +30,6 @@ 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{}) }
@@ -71,7 +61,7 @@ func (a *Absolute) Is(v *Absolute) bool {
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
func NewAbs(pathname string) (*Absolute, error) {
if !filepath.IsAbs(pathname) {
if !path.IsAbs(pathname) {
return nil, AbsoluteError(pathname)
}
return unsafeAbs(pathname), nil
@@ -86,35 +76,46 @@ func MustAbs(pathname string) *Absolute {
}
}
// Append calls [filepath.Join] with [Absolute] as the first element.
// Append calls [path.Join] with [Absolute] as the first element.
func (a *Absolute) Append(elem ...string) *Absolute {
return unsafeAbs(filepath.Join(append([]string{a.String()}, elem...)...))
return unsafeAbs(path.Join(append([]string{a.String()}, elem...)...))
}
// Dir calls [filepath.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }
// Dir calls [path.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return unsafeAbs(path.Dir(a.String())) }
// AppendText appends the checked pathname.
func (a *Absolute) AppendText(data []byte) ([]byte, error) {
return append(data, a.String()...), nil
// GobEncode returns the checked pathname.
func (a *Absolute) GobEncode() ([]byte, error) {
return []byte(a.String()), nil
}
// 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 {
// GobDecode stores data if it represents an absolute pathname.
func (a *Absolute) GobDecode(data []byte) error {
pathname := string(data)
if !filepath.IsAbs(pathname) {
if !path.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) }
// 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 !path.IsAbs(pathname) {
return AbsoluteError(pathname)
}
a.pathname = unique.Make(pathname)
return nil
}
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
func SortAbs(x []*Absolute) {

View File

@@ -170,20 +170,20 @@ func TestCodecAbsolute(t *testing.T) {
{"good", MustAbs("/etc"),
nil,
"\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",
"\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",
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
{"not absolute", nil,
AbsoluteError("etc"),
"\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",
"\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",
`"etc"`, `{"val":"etc","magic":3236757504}`},
{"zero", nil,
new(AbsoluteError),
"\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",
"\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",
`""`, `{"val":"","magic":3236757504}`},
}
@@ -347,6 +347,15 @@ func TestCodecAbsolute(t *testing.T) {
})
})
}
t.Run("json passthrough", func(t *testing.T) {
t.Parallel()
wantErr := "invalid character ':' looking for beginning of value"
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
}
})
}
func TestAbsoluteWrap(t *testing.T) {

237
cmd/dist/main.go vendored
View File

@@ -1,237 +0,0 @@
//go:build dist
package main
import (
"archive/tar"
"compress/gzip"
"context"
"crypto/sha512"
_ "embed"
"encoding/hex"
"fmt"
"io"
"io/fs"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
)
// getenv looks up an environment variable, and returns fallback if it is unset.
func getenv(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok {
return v
}
return fallback
}
// mustRun runs a command with the current process's environment and panics
// on error or non-zero exit code.
func mustRun(ctx context.Context, name string, arg ...string) {
cmd := exec.CommandContext(ctx, name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
//go:embed comp/_hakurei
var comp []byte
func main() {
fmt.Println()
log.SetFlags(0)
log.SetPrefix("# ")
version := getenv("HAKUREI_VERSION", "untagged")
prefix := getenv("PREFIX", "/usr")
destdir := getenv("DESTDIR", "dist")
if err := os.MkdirAll(destdir, 0755); err != nil {
log.Fatal(err)
}
s, err := os.MkdirTemp(destdir, ".dist.*")
if err != nil {
log.Fatal(err)
}
defer func() {
var code int
if err = os.RemoveAll(s); err != nil {
code = 1
log.Println(err)
}
if r := recover(); r != nil {
code = 1
log.Println(r)
}
os.Exit(code)
}()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
log.Println("Building hakurei.")
mustRun(ctx, "go", "generate", "./...")
mustRun(
ctx, "go", "build",
"-trimpath",
"-v", "-o", s,
"-ldflags=-s -w "+
"-buildid= -linkmode external -extldflags=-static "+
"-X hakurei.app/internal/info.buildVersion="+version+" "+
"-X hakurei.app/internal/info.hakureiPath="+prefix+"/bin/hakurei "+
"-X hakurei.app/internal/info.hsuPath="+prefix+"/bin/hsu "+
"-X main.hakureiPath="+prefix+"/bin/hakurei",
"./...",
)
fmt.Println()
log.Println("Testing Hakurei.")
mustRun(
ctx, "go", "test",
"-ldflags=-buildid= -linkmode external -extldflags=-static",
"./...",
)
fmt.Println()
log.Println("Creating distribution.")
const suffix = ".tar.gz"
distName := "hakurei-" + version + "-" + runtime.GOARCH
var f *os.File
if f, err = os.OpenFile(
filepath.Join(s, distName+suffix),
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
0644,
); err != nil {
panic(err)
}
defer func() {
if f == nil {
return
}
if err = f.Close(); err != nil {
log.Println(err)
}
}()
h := sha512.New()
gw := gzip.NewWriter(io.MultiWriter(f, h))
tw := tar.NewWriter(gw)
mustWriteHeader := func(name string, size int64, mode os.FileMode) {
header := tar.Header{
Name: filepath.Join(distName, name),
Size: size,
Mode: int64(mode),
Uname: "root",
Gname: "root",
}
if mode&os.ModeDir != 0 {
header.Typeflag = tar.TypeDir
fmt.Printf("%s %s\n", mode, name)
} else {
header.Typeflag = tar.TypeReg
fmt.Printf("%s %s (%d bytes)\n", mode, name, size)
}
if err = tw.WriteHeader(&header); err != nil {
panic(err)
}
}
mustWriteFile := func(name string, data []byte, mode os.FileMode) {
mustWriteHeader(name, int64(len(data)), mode)
if mode&os.ModeDir != 0 {
return
}
if _, err = tw.Write(data); err != nil {
panic(err)
}
}
mustWriteFromPath := func(dst, src string, mode os.FileMode) {
var r *os.File
if r, err = os.Open(src); err != nil {
panic(err)
}
var fi os.FileInfo
if fi, err = r.Stat(); err != nil {
_ = r.Close()
panic(err)
}
if mode == 0 {
mode = fi.Mode()
}
mustWriteHeader(dst, fi.Size(), mode)
if _, err = io.Copy(tw, r); err != nil {
_ = r.Close()
panic(err)
} else if err = r.Close(); err != nil {
panic(err)
}
}
mustWriteFile(".", nil, fs.ModeDir|0755)
mustWriteFile("comp/", nil, os.ModeDir|0755)
mustWriteFile("comp/_hakurei", comp, 0644)
mustWriteFile("install.sh", []byte(`#!/bin/sh -e
cd "$(dirname -- "$0")" || exit 1
install -vDm0755 "bin/hakurei" "${DESTDIR}`+prefix+`/bin/hakurei"
install -vDm0755 "bin/sharefs" "${DESTDIR}`+prefix+`/bin/sharefs"
install -vDm4511 "bin/hsu" "${DESTDIR}`+prefix+`/bin/hsu"
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
fi
install -vDm0644 "comp/_hakurei" "${DESTDIR}`+prefix+`/share/zsh/site-functions/_hakurei"
`), 0755)
mustWriteFromPath("README.md", "README.md", 0)
mustWriteFile("hsurc.default", []byte("1000 0"), 0400)
mustWriteFromPath("bin/hsu", filepath.Join(s, "hsu"), 04511)
for _, name := range []string{
"hakurei",
"sharefs",
} {
mustWriteFromPath(
filepath.Join("bin", name),
filepath.Join(s, name),
0,
)
}
if err = tw.Close(); err != nil {
panic(err)
} else if err = gw.Close(); err != nil {
panic(err)
} else if err = f.Close(); err != nil {
panic(err)
}
f = nil
if err = os.WriteFile(
filepath.Join(destdir, distName+suffix+".sha512"),
append(hex.AppendEncode(nil, h.Sum(nil)), " "+distName+suffix+"\n"...),
0644,
); err != nil {
panic(err)
}
if err = os.Rename(
filepath.Join(s, distName+suffix),
filepath.Join(destdir, distName+suffix),
); err != nil {
panic(err)
}
}

View File

@@ -1,7 +1,3 @@
// The earlyinit is part of the Rosa OS initramfs and serves as the system init.
//
// This program is an internal detail of Rosa OS and is not usable on its own.
// It is not covered by the compatibility promise.
package main
import (

View File

@@ -2,7 +2,6 @@ package main
import (
"context"
"errors"
"fmt"
"io"
"log"
@@ -12,6 +11,7 @@ import (
"strconv"
"sync"
"time"
_ "unsafe" // for go:linkname
"hakurei.app/check"
"hakurei.app/command"
@@ -27,20 +27,14 @@ import (
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
// if it is not nil, or the original value if it is.
func optionalErrorUnwrap(err error) error {
if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
return underlyingErr
}
return err
}
var errSuccess = errors.New("success")
//
//go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap
func optionalErrorUnwrap(err error) error
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var (
flagVerbose bool
flagInsecure bool
flagJSON bool
flagVerbose bool
flagJSON bool
)
c := command.New(out, log.Printf, "hakurei", func([]string) error {
msg.SwapVerbose(flagVerbose)
@@ -58,7 +52,6 @@ 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 })
@@ -67,9 +60,9 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
var (
flagIdentifierFile int
)
c.NewCommand("run", "Load and start container from configuration file", func(args []string) error {
c.NewCommand("app", "Load and start container from configuration file", func(args []string) error {
if len(args) < 1 {
log.Fatal("run requires at least 1 argument")
log.Fatal("app requires at least 1 argument")
}
config := tryPath(msg, args[0])
@@ -77,12 +70,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
config.Container.Args = append(config.Container.Args, args[1:]...)
}
var flags int
if flagInsecure {
flags |= hst.VAllowInsecure
}
outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
outcome.Main(ctx, msg, config, flagIdentifierFile)
panic("unreachable")
}).
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
@@ -110,7 +98,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
)
c.NewCommand("exec", "Configure and start a permissive container", func(args []string) error {
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error {
if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd {
log.Fatalf("identity %d out of range", flagIdentity)
}
@@ -152,7 +140,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
}
}
var et hst.Enablements
var et hst.Enablement
if flagWayland {
et |= hst.EWayland
}
@@ -170,7 +158,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
ID: flagID,
Identity: flagIdentity,
Groups: flagGroups,
Enablements: &et,
Enablements: hst.NewEnablements(et),
Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{
@@ -289,7 +277,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
}
}
outcome.Main(ctx, msg, &config, 0, -1)
outcome.Main(ctx, msg, &config, -1)
panic("unreachable")
}).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
@@ -335,7 +323,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagShort bool
flagNoStore bool
)
c.NewCommand("show", "Show live or local instance configuration", func(args []string) error {
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
switch len(args) {
case 0: // system
printShowSystem(os.Stdout, flagShort, flagJSON)

View File

@@ -20,12 +20,12 @@ func TestHelp(t *testing.T) {
}{
{
"main", []string{}, `
Usage: hakurei [-h | --help] [-v] [--insecure] [--json] COMMAND [OPTIONS]
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Commands:
run Load and start container from configuration file
exec Configure and start a permissive container
show Show live or local instance configuration
app Load and start container from configuration file
run Configure and start a permissive container
show Show live or local app configuration
ps List active instances
version Display version information
license Show full license text
@@ -35,8 +35,8 @@ Commands:
`,
},
{
"exec", []string{"exec", "-h"}, `
Usage: hakurei exec [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--policy <value>] [--priority <int>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS]
"run", []string{"run", "-h"}, `
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--policy <value>] [--priority <int>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS]
Flags:
-X Enable direct connection to X11

View File

@@ -1,42 +1,8 @@
// Hakurei runs user-specified containers as subordinate users.
//
// This program is generally invoked by another, higher level program, which
// creates container configuration via package [hst] or an implementation of it.
//
// The parent may leave files open and specify their file descriptor for various
// uses. In these cases, standard streams and netpoll files are treated as
// invalid file descriptors and rejected. All string representations must be in
// decimal.
//
// When specifying a [hst.Config] JSON stream or file to the run subcommand, the
// argument "-" is equivalent to stdin. Otherwise, file descriptor rules
// described above applies. Invalid file descriptors are treated as file names
// in their string representation, with the exception that if a netpoll file
// descriptor is attempted, the program fails.
//
// The flag --identifier-fd can be optionally specified to the run subcommand to
// receive the identifier of the newly started instance. File descriptor rules
// described above applies, and the file must be writable. This is sent after
// its state is made available, so the client must not attempt to poll for it.
// This uses the internal binary format of [hst.ID].
//
// For the show and ps subcommands, the flag --json can be applied to the main
// hakurei command to serialise output in JSON when applicable. Additionally,
// the flag --short targeting each subcommand is used to omit some information
// in both JSON and user-facing output. Only JSON-encoded output is covered
// under the compatibility promise.
//
// A template for [hst.Config] demonstrating all available configuration fields
// is returned by [hst.Template]. The JSON-encoded equivalent of this can be
// obtained via the template subcommand. Fields left unpopulated in the template
// (the direct_* family of fields, which are insecure under any configuration if
// enabled) are unsupported.
//
// For simple (but insecure) testing scenarios, the exec subcommand can be used
// to generate a simple, permissive configuration in-memory. See its help
// message for all available options.
package main
// this works around go:embed '..' limitation
//go:generate cp ../../LICENSE .
import (
"context"
_ "embed"
@@ -51,9 +17,12 @@ import (
"hakurei.app/message"
)
//go:generate cp ../../LICENSE .
//go:embed LICENSE
var license string
var (
errSuccess = errors.New("success")
//go:embed LICENSE
license string
)
// earlyHardeningErrs are errors collected while setting up early hardening feature.
type earlyHardeningErrs struct{ yamaLSM, dumpable error }
@@ -62,8 +31,8 @@ func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(nil)
log.SetFlags(0)
log.SetPrefix("hakurei: ")
log.SetFlags(0)
msg := message.New(log.Default())
early := earlyHardeningErrs{

View File

@@ -17,9 +17,8 @@ import (
)
// tryPath attempts to read [hst.Config] from multiple sources.
//
// tryPath reads from [os.Stdin] if name has value "-". Otherwise, name is
// passed to tryFd, and if that returns nil, name is passed to [os.Open].
// tryPath reads from [os.Stdin] if name has value "-".
// Otherwise, name is passed to tryFd, and if that returns nil, name is passed to [os.Open].
func tryPath(msg message.Msg, name string) (config *hst.Config) {
var r io.ReadCloser
config = new(hst.Config)
@@ -47,8 +46,7 @@ func tryPath(msg message.Msg, name string) (config *hst.Config) {
return
}
// tryFd returns a [io.ReadCloser] if name represents an integer corresponding
// to a valid file descriptor.
// tryFd returns a [io.ReadCloser] if name represents an integer corresponding to a valid file descriptor.
func tryFd(msg message.Msg, name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil {
if !errors.Is(err, strconv.ErrSyntax) {
@@ -62,12 +60,7 @@ func tryFd(msg message.Msg, name string) io.ReadCloser {
msg.Verbosef("trying config stream from %d", v)
fd := uintptr(v)
if _, _, errno := syscall.Syscall(
syscall.SYS_FCNTL,
fd,
syscall.F_GETFD,
0,
); errno != 0 {
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
if errors.Is(errno, syscall.EBADF) { // reject bad fd
return nil
}
@@ -82,12 +75,10 @@ func tryFd(msg message.Msg, name string) io.ReadCloser {
}
}
// shortLengthMin is the minimum length a short form identifier can have and
// still be interpreted as an identifier.
// shortLengthMin is the minimum length a short form identifier can have and still be interpreted as an identifier.
const shortLengthMin = 1 << 3
// shortIdentifier returns an eight character short representation of [hst.ID]
// from its random bytes.
// shortIdentifier returns an eight character short representation of [hst.ID] from its random bytes.
func shortIdentifier(id *hst.ID) string {
return shortIdentifierString(id.String())
}
@@ -97,8 +88,7 @@ func shortIdentifierString(s string) string {
return s[len(hst.ID{}) : len(hst.ID{})+shortLengthMin]
}
// tryIdentifier attempts to match [hst.State] from a [hex] representation of
// [hst.ID] or a prefix of its lower half.
// tryIdentifier attempts to match [hst.State] from a [hex] representation of [hst.ID] or a prefix of its lower half.
func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
const (
likeShort = 1 << iota
@@ -106,8 +96,7 @@ func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
)
var likely uintptr
// half the hex representation
if len(name) >= shortLengthMin && len(name) <= len(hst.ID{}) {
if len(name) >= shortLengthMin && len(name) <= len(hst.ID{}) { // half the hex representation
// cannot safely decode here due to unknown alignment
for _, c := range name {
if c >= '0' && c <= '9' {

View File

@@ -56,7 +56,7 @@ func printShowInstance(
t := newPrinter(output)
defer t.MustFlush()
if err := config.Validate(hst.VAllowInsecure); err != nil {
if err := config.Validate(); err != nil {
valid = false
if m, ok := message.GetMessage(err); ok {
mustPrint(output, "Error: "+m+"!\n\n")

View File

@@ -32,7 +32,7 @@ var (
PID: 0xbeef,
ShimPID: 0xcafe,
Config: &hst.Config{
Enablements: new(hst.EWayland | hst.EPipeWire),
Enablements: hst.NewEnablements(hst.EWayland | hst.EPipeWire),
Identity: 1,
Container: &hst.ContainerConfig{
Shell: check.MustAbs("/bin/sh"),

View File

@@ -1,7 +0,0 @@
//go:build !rosa
package main
// hsuConfPath is an absolute pathname to the hsu configuration file. Its
// contents are interpreted by parseConfig.
const hsuConfPath = "/etc/hsurc"

View File

@@ -1,7 +0,0 @@
//go:build rosa
package main
// hsuConfPath is the pathname to the hsu configuration file, specific to
// Rosa OS. Its contents are interpreted by parseConfig.
const hsuConfPath = "/system/etc/hsurc"

View File

@@ -1,6 +1,6 @@
package main
/* keep in sync with hst */
/* copied from hst and must never be changed */
const (
userOffset = 100000

View File

@@ -1,64 +1,13 @@
// hsu starts the hakurei shim as the target subordinate user.
//
// The hsu program must be installed with the setuid and setgid bit set, and
// owned by root. A configuration file must be installed at /etc/hsurc with
// permission bits 0400, and owned by root. Each line of the file specifies a
// hakurei userid to kernel uid mapping. A line consists of the decimal string
// representation of the uid of the user wishing to start hakurei containers,
// followed by a space, followed by the decimal string representation of its
// userid. Duplicate uid entries are ignored, with the first occurrence taking
// effect.
//
// For example, to map the kernel uid 1000 to the hakurei user id 0:
//
// 1000 0
//
// # Internals
//
// Hakurei and hsu holds pathnames pointing to each other set at link time. For
// this reason, a distribution of hakurei has fixed installation prefix. Since
// this program is never invoked by the user, behaviour described in the
// following paragraphs are considered an internal detail and not covered by the
// compatibility promise.
//
// After checking credentials, hsu checks via /proc/ the absolute pathname of
// its parent process, and fails if it does not match the hakurei pathname set
// at link time. This is not a security feature: the priv-side is considered
// trusted, and this feature makes no attempt to address the racy nature of
// querying /proc/, or debuggers attached to the parent process. Instead, this
// aims to discourage misuse and reduce confusion if the user accidentally
// stumbles upon this program. It also prevents accidental use of the incorrect
// installation of hsu in some environments.
//
// Since target container environment variables are set up in shim via the
// [container] infrastructure, the environment is used for parameters from the
// parent process.
//
// HAKUREI_SHIM specifies a single byte between '3' and '9' representing the
// setup pipe file descriptor. It is passed as is to the shim process and is the
// only value in the environment of the shim process. Since hsurc is not
// accessible to the parent process, leaving this unset causes hsu to print the
// corresponding hakurei user id of the parent and terminate.
//
// HAKUREI_IDENTITY specifies the identity of the instance being started and is
// used to produce the kernel uid alongside hakurei user id looked up from hsurc.
//
// HAKUREI_GROUPS specifies supplementary groups to inherit from the credentials
// of the parent process in a ' ' separated list of decimal string
// representations of gid. This has the unfortunate consequence of allowing
// users mapped via hsurc to effectively drop group membership, so special care
// must be taken to ensure this does not lead to an increase in access. This is
// not applicable to Rosa OS since unsigned code execution is not permitted
// outside hakurei containers, and is generally nonapplicable to the security
// model of hakurei, where all untrusted code runs within containers.
package main
// minimise imports to avoid inadvertently calling init or global variable functions
import (
"bytes"
"fmt"
"log"
"os"
"path/filepath"
"path"
"runtime"
"slices"
"strconv"
@@ -67,13 +16,10 @@ import (
)
const (
// envShim is the name of the environment variable holding a single byte
// representing the shim setup pipe file descriptor.
// envIdentity is the name of the environment variable holding a
// single byte representing the shim setup pipe file descriptor.
envShim = "HAKUREI_SHIM"
// envIdentity is the name of the environment variable holding a decimal
// string representation of the current application identity.
envIdentity = "HAKUREI_IDENTITY"
// envGroups holds a ' ' separated list of decimal string representations of
// envGroups holds a ' ' separated list of string representations of
// supplementary group gid. Membership requirements are enforced.
envGroups = "HAKUREI_GROUPS"
)
@@ -89,6 +35,7 @@ func main() {
log.SetFlags(0)
log.SetPrefix("hsu: ")
log.SetOutput(os.Stderr)
if os.Geteuid() != 0 {
log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
@@ -102,13 +49,13 @@ func main() {
log.Fatal("this program must not be started by root")
}
if !filepath.IsAbs(hakureiPath) {
if !path.IsAbs(hakureiPath) {
log.Fatal("this program is compiled incorrectly")
return
}
var toolPath string
pexe := filepath.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") {
@@ -152,6 +99,8 @@ func main() {
// last possible uid outcome
uidEnd = 999919999
)
// cast to int for use with library functions
uid := int(toUser(userid, identity))
// final bounds check to catch any bugs
@@ -187,6 +136,7 @@ func main() {
}
// careful! users in the allowlist is effectively allowed to drop groups via hsu
if err := syscall.Setresgid(uid, uid, uid); err != nil {
log.Fatalf("cannot set gid: %v", err)
}
@@ -196,21 +146,10 @@ func main() {
if err := syscall.Setresuid(uid, uid, uid); err != nil {
log.Fatalf("cannot set uid: %v", err)
}
if _, _, errno := syscall.AllThreadsSyscall(
syscall.SYS_PRCTL,
PR_SET_NO_NEW_PRIVS, 1,
0,
); errno != 0 {
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
}
if err := syscall.Exec(toolPath, []string{
"hakurei",
"shim",
}, []string{
envShim + "=" + shimSetupFd,
}); err != nil {
if err := syscall.Exec(toolPath, []string{"hakurei", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
log.Fatalf("cannot start shim: %v", err)
}

View File

@@ -18,9 +18,8 @@ const (
useridEnd = useridStart + rangeSize - 1
)
// parseUint32Fast parses a string representation of an unsigned 32-bit integer
// value using the fast path only. This limits the range of values it is defined
// in but is perfectly adequate for this use case.
// parseUint32Fast parses a string representation of an unsigned 32-bit integer value
// using the fast path only. This limits the range of values it is defined in.
func parseUint32Fast(s string) (uint32, error) {
sLen := len(s)
if sLen < 1 {
@@ -41,14 +40,12 @@ func parseUint32Fast(s string) (uint32, error) {
return n, nil
}
// parseConfig reads a list of allowed users from r until it encounters puid or
// [io.EOF].
// parseConfig reads a list of allowed users from r until it encounters puid or [io.EOF].
//
// Each line of the file specifies a hakurei userid to kernel uid mapping. A
// line consists of the string representation of the uid of the user wishing to
// start hakurei containers, followed by a space, followed by the string
// representation of its userid. Duplicate uid entries are ignored, with the
// first occurrence taking effect.
// Each line of the file specifies a hakurei userid to kernel uid mapping. A line consists
// of the string representation of the uid of the user wishing to start hakurei containers,
// followed by a space, followed by the string representation of its userid. Duplicate uid
// entries are ignored, with the first occurrence taking effect.
//
// All string representations are parsed by calling parseUint32Fast.
func parseConfig(r io.Reader, puid uint32) (userid uint32, ok bool, err error) {
@@ -84,6 +81,10 @@ func parseConfig(r io.Reader, puid uint32) (userid uint32, ok bool, err error) {
return useridEnd + 1, false, s.Err()
}
// hsuConfPath is an absolute pathname to the hsu configuration file.
// Its contents are interpreted by parseConfig.
const hsuConfPath = "/etc/hsurc"
// mustParseConfig calls parseConfig to interpret the contents of hsuConfPath,
// terminating the program if an error is encountered, the syntax is incorrect,
// or the current user is not authorised to use hsu because its uid is missing.
@@ -111,6 +112,10 @@ func mustParseConfig(puid int) (userid uint32) {
return
}
// envIdentity is the name of the environment variable holding a
// string representation of the current application identity.
var envIdentity = "HAKUREI_IDENTITY"
// mustReadIdentity calls parseUint32Fast to interpret the value stored in envIdentity,
// terminating the program if the value is not set, malformed, or out of bounds.
func mustReadIdentity() uint32 {

View File

@@ -1,94 +0,0 @@
package main
import (
"context"
"os"
"path/filepath"
"testing"
"hakurei.app/check"
"hakurei.app/internal/pkg"
"hakurei.app/message"
)
// cache refers to an instance of [pkg.Cache] that might be open.
type cache struct {
ctx context.Context
msg message.Msg
// Should generally not be used directly.
c *pkg.Cache
cures, jobs int
hostAbstract, idle bool
base string
}
// open opens the underlying [pkg.Cache].
func (cache *cache) open() (err error) {
if cache.c != nil {
return os.ErrInvalid
}
if cache.base == "" {
cache.base = "cache"
}
var base *check.Absolute
if cache.base, err = filepath.Abs(cache.base); err != nil {
return
} else if base, err = check.NewAbs(cache.base); err != nil {
return
}
var flags int
if cache.idle {
flags |= pkg.CSchedIdle
}
if cache.hostAbstract {
flags |= pkg.CHostAbstract
}
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-cache.ctx.Done():
if testing.Testing() {
return
}
os.Exit(2)
case <-done:
return
}
}()
cache.msg.Verbosef("opening cache at %s", base)
cache.c, err = pkg.Open(
cache.ctx,
cache.msg,
flags,
cache.cures,
cache.jobs,
base,
)
return
}
// Close closes the underlying [pkg.Cache] if it is open.
func (cache *cache) Close() {
if cache.c != nil {
cache.c.Close()
}
}
// Do calls f on the underlying cache and returns its error value.
func (cache *cache) Do(f func(cache *pkg.Cache) error) error {
if cache.c == nil {
if err := cache.open(); err != nil {
return err
}
}
return f(cache.c)
}

View File

@@ -1,37 +0,0 @@
package main
import (
"log"
"os"
"testing"
"hakurei.app/internal/pkg"
"hakurei.app/message"
)
func TestCache(t *testing.T) {
t.Parallel()
cm := cache{
ctx: t.Context(),
msg: message.New(log.New(os.Stderr, "check: ", 0)),
base: t.TempDir(),
hostAbstract: true, idle: true,
}
defer cm.Close()
cm.Close()
if err := cm.open(); err != nil {
t.Fatalf("open: error = %v", err)
}
if err := cm.open(); err != os.ErrInvalid {
t.Errorf("(duplicate) open: error = %v", err)
}
if err := cm.Do(func(cache *pkg.Cache) error {
return cache.Scrub(0)
}); err != nil {
t.Errorf("Scrub: error = %v", err)
}
}

View File

@@ -1,151 +0,0 @@
package main
import (
"context"
"errors"
"io"
"log"
"net"
"os"
"sync"
"testing"
"time"
"hakurei.app/check"
"hakurei.app/internal/pkg"
)
// daemonTimeout is the maximum amount of time cureFromIR will wait on I/O.
const daemonTimeout = 30 * time.Second
// daemonDeadline returns the deadline corresponding to daemonTimeout, or the
// zero value when running in a test.
func daemonDeadline() time.Time {
if testing.Testing() {
return time.Time{}
}
return time.Now().Add(daemonTimeout)
}
// cureFromIR services an IR curing request.
func cureFromIR(
ctx context.Context,
cache *pkg.Cache,
conn net.Conn,
) (pkg.Artifact, error) {
go func() {
<-ctx.Done()
_ = conn.SetDeadline(time.Now())
}()
if err := conn.SetReadDeadline(daemonDeadline()); err != nil {
return nil, errors.Join(err, conn.Close())
}
a, decodeErr := cache.NewDecoder(conn).Decode()
if decodeErr != nil {
_, err := conn.Write([]byte("\x00" + decodeErr.Error()))
return nil, errors.Join(decodeErr, err, conn.Close())
}
pathname, _, cureErr := cache.Cure(a)
if err := conn.SetWriteDeadline(daemonDeadline()); err != nil {
if !testing.Testing() || !errors.Is(err, io.ErrClosedPipe) {
return a, errors.Join(err, conn.Close())
}
}
if cureErr != nil {
_, err := conn.Write([]byte("\x00" + cureErr.Error()))
return a, errors.Join(cureErr, err, conn.Close())
}
_, err := conn.Write([]byte(pathname.String()))
if testing.Testing() && errors.Is(err, io.ErrClosedPipe) {
return a, nil
}
return a, errors.Join(err, conn.Close())
}
// serve services connections from a [net.UnixListener].
func serve(ctx context.Context, log *log.Logger, cm *cache, ul *net.UnixListener) error {
ul.SetUnlinkOnClose(true)
if cm.c == nil {
if err := cm.open(); err != nil {
return errors.Join(err, ul.Close())
}
}
var wg sync.WaitGroup
defer wg.Wait()
wg.Go(func() {
for {
if ctx.Err() != nil {
break
}
conn, err := ul.AcceptUnix()
if err != nil {
if !errors.Is(err, os.ErrDeadlineExceeded) {
log.Println(err)
}
continue
}
wg.Go(func() {
if a, _err := cureFromIR(ctx, cm.c, conn); _err != nil {
log.Println(_err)
} else {
log.Printf(
"fulfilled artifact %s",
pkg.Encode(cm.c.Ident(a).Value()),
)
}
})
}
})
<-ctx.Done()
if err := ul.SetDeadline(time.Now()); err != nil {
return errors.Join(err, ul.Close())
}
wg.Wait()
return ul.Close()
}
// cureRemote cures a [pkg.Artifact] on a daemon.
func cureRemote(
ctx context.Context,
addr *net.UnixAddr,
a pkg.Artifact,
) (*check.Absolute, error) {
conn, err := net.DialUnix("unix", nil, addr)
if err != nil {
return nil, err
}
go func() {
<-ctx.Done()
_ = conn.SetDeadline(time.Now())
}()
if err = pkg.NewIR().EncodeAll(conn, a); err != nil {
return nil, errors.Join(err, conn.Close())
} else if err = conn.CloseWrite(); err != nil {
return nil, errors.Join(err, conn.Close())
}
payload, recvErr := io.ReadAll(conn)
if err = errors.Join(recvErr, conn.Close()); err != nil {
if errors.Is(err, os.ErrDeadlineExceeded) {
err = ctx.Err()
}
return nil, err
}
if len(payload) > 0 && payload[0] == 0 {
return nil, errors.New(string(payload[1:]))
}
var p *check.Absolute
p, err = check.NewAbs(string(payload))
return p, err
}

View File

@@ -1,125 +0,0 @@
package main
import (
"bytes"
"context"
"errors"
"io"
"log"
"net"
"os"
"path/filepath"
"testing"
"time"
"hakurei.app/check"
"hakurei.app/internal/pkg"
"hakurei.app/message"
)
func TestCureFromIR(t *testing.T) {
t.Parallel()
if !daemonDeadline().IsZero() {
t.Fatal("daemonDeadline did not return the zero value")
}
c, err := pkg.Open(
t.Context(),
message.New(log.New(os.Stderr, "cir: ", 0)),
0, 0, 0,
check.MustAbs(t.TempDir()),
)
if err != nil {
t.Fatalf("Open: error = %v", err)
}
defer c.Close()
client, server := net.Pipe()
done := make(chan struct{})
go func() {
defer close(done)
go func() {
<-t.Context().Done()
if _err := client.SetDeadline(time.Now()); _err != nil && !errors.Is(_err, io.ErrClosedPipe) {
panic(_err)
}
}()
if _err := c.EncodeAll(
client,
pkg.NewFile("check", []byte{0}),
); _err != nil {
panic(_err)
} else if _err = client.Close(); _err != nil {
panic(_err)
}
}()
a, cureErr := cureFromIR(t.Context(), c, server)
if cureErr != nil {
t.Fatalf("cureFromIR: error = %v", cureErr)
}
<-done
wantIdent := pkg.MustDecode("fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG")
if gotIdent := c.Ident(a).Value(); gotIdent != wantIdent {
t.Errorf(
"cureFromIR: %s, want %s",
pkg.Encode(gotIdent), pkg.Encode(wantIdent),
)
}
}
func TestDaemon(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
logger := log.New(&buf, "daemon: ", 0)
addr := net.UnixAddr{
Name: filepath.Join(t.TempDir(), "daemon"),
Net: "unix",
}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
cm := cache{
ctx: ctx,
msg: message.New(logger),
base: t.TempDir(),
}
defer cm.Close()
ul, err := net.ListenUnix("unix", &addr)
if err != nil {
t.Fatalf("ListenUnix: error = %v", err)
}
done := make(chan struct{})
go func() {
defer close(done)
if _err := serve(ctx, logger, &cm, ul); _err != nil {
panic(_err)
}
}()
var p *check.Absolute
p, err = cureRemote(ctx, &addr, pkg.NewFile("check", []byte{0}))
if err != nil {
t.Fatalf("cureRemote: error = %v", err)
}
cancel()
<-done
const want = "fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG"
if got := filepath.Base(p.String()); got != want {
t.Errorf("cureRemote: %s, want %s", got, want)
}
const wantLog = `daemon: fulfilled artifact fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG
`
if gotLog := buf.String(); gotLog != wantLog {
t.Errorf("serve: logged\n%s\nwant\n%s", gotLog, wantLog)
}
}

View File

@@ -1,127 +0,0 @@
package main
import (
"errors"
"fmt"
"io"
"os"
"strings"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
)
// commandInfo implements the info subcommand.
func commandInfo(
cm *cache,
args []string,
w io.Writer,
writeStatus bool,
reportPath string,
) (err error) {
if len(args) == 0 {
return errors.New("info requires at least 1 argument")
}
var r *rosa.Report
if reportPath != "" {
if r, err = rosa.OpenReport(reportPath); err != nil {
return err
}
defer func() {
if closeErr := r.Close(); err == nil {
err = closeErr
}
}()
defer r.HandleAccess(&err)()
}
// recovered by HandleAccess
mustPrintln := func(a ...any) {
if _, _err := fmt.Fprintln(w, a...); _err != nil {
panic(_err)
}
}
mustPrint := func(a ...any) {
if _, _err := fmt.Fprint(w, a...); _err != nil {
panic(_err)
}
}
for i, name := range args {
if p, ok := rosa.ResolveName(name); !ok {
return fmt.Errorf("unknown artifact %q", name)
} else {
var suffix string
if version := rosa.Std.Version(p); version != rosa.Unversioned {
suffix += "-" + version
}
mustPrintln("name : " + name + suffix)
meta := rosa.GetMetadata(p)
mustPrintln("description : " + meta.Description)
if meta.Website != "" {
mustPrintln("website : " +
strings.TrimSuffix(meta.Website, "/"))
}
if len(meta.Dependencies) > 0 {
mustPrint("depends on :")
for _, d := range meta.Dependencies {
s := rosa.GetMetadata(d).Name
if version := rosa.Std.Version(d); version != rosa.Unversioned {
s += "-" + version
}
mustPrint(" " + s)
}
mustPrintln()
}
const statusPrefix = "status : "
if writeStatus {
if r == nil {
var f io.ReadSeekCloser
err = cm.Do(func(cache *pkg.Cache) (err error) {
f, err = cache.OpenStatus(rosa.Std.Load(p))
return
})
if err != nil {
if errors.Is(err, os.ErrNotExist) {
mustPrintln(
statusPrefix + "not yet cured",
)
} else {
return
}
} else {
mustPrint(statusPrefix)
_, err = io.Copy(w, f)
if err = errors.Join(err, f.Close()); err != nil {
return
}
}
} else if err = cm.Do(func(cache *pkg.Cache) (err error) {
status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p)))
if status == nil {
mustPrintln(
statusPrefix + "not in report",
)
} else {
mustPrintln("size :", n)
mustPrint(statusPrefix)
if _, err = w.Write(status); err != nil {
return
}
}
return
}); err != nil {
return
}
}
if i != len(args)-1 {
mustPrintln()
}
}
}
return nil
}

View File

@@ -1,170 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"strings"
"syscall"
"testing"
"unsafe"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
"hakurei.app/message"
)
func TestInfo(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
args []string
status map[string]string
report string
want string
wantErr any
}{
{"qemu", []string{"qemu"}, nil, "", `
name : qemu-` + rosa.Std.Version(rosa.QEMU) + `
description : a generic and open source machine emulator and virtualizer
website : https://www.qemu.org
depends on : glib-` + rosa.Std.Version(rosa.GLib) + ` zstd-` + rosa.Std.Version(rosa.Zstd) + `
`, nil},
{"multi", []string{"hakurei", "hakurei-dist"}, nil, "", `
name : hakurei-` + rosa.Std.Version(rosa.Hakurei) + `
description : low-level userspace tooling for Rosa OS
website : https://hakurei.app
name : hakurei-dist-` + rosa.Std.Version(rosa.HakureiDist) + `
description : low-level userspace tooling for Rosa OS (distribution tarball)
website : https://hakurei.app
`, nil},
{"nonexistent", []string{"zlib", "\x00"}, nil, "", `
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
description : lossless data-compression library
website : https://zlib.net
`, fmt.Errorf("unknown artifact %q", "\x00")},
{"status cache", []string{"zlib", "zstd"}, map[string]string{
"zstd": "internal/pkg (amd64) on satori\n",
"hakurei": "internal/pkg (amd64) on satori\n\n",
}, "", `
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
description : lossless data-compression library
website : https://zlib.net
status : not yet cured
name : zstd-` + rosa.Std.Version(rosa.Zstd) + `
description : a fast compression algorithm
website : https://facebook.github.io/zstd
status : internal/pkg (amd64) on satori
`, nil},
{"status cache perm", []string{"zlib"}, map[string]string{
"zlib": "\x00",
}, "", `
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
description : lossless data-compression library
website : https://zlib.net
`, func(cm *cache) error {
return &os.PathError{
Op: "open",
Path: filepath.Join(cm.base, "status", pkg.Encode(cm.c.Ident(rosa.Std.Load(rosa.Zlib)).Value())),
Err: syscall.EACCES,
}
}},
{"status report", []string{"zlib"}, nil, strings.Repeat("\x00", len(pkg.Checksum{})+8), `
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
description : lossless data-compression library
website : https://zlib.net
status : not in report
`, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var (
cm *cache
buf strings.Builder
rp string
)
if tc.status != nil || tc.report != "" {
cm = &cache{
ctx: context.Background(),
msg: message.New(log.New(os.Stderr, "info: ", 0)),
base: t.TempDir(),
}
defer cm.Close()
}
if tc.report != "" {
rp = filepath.Join(t.TempDir(), "report")
if err := os.WriteFile(
rp,
unsafe.Slice(unsafe.StringData(tc.report), len(tc.report)),
0400,
); err != nil {
t.Fatal(err)
}
}
if tc.status != nil {
for name, status := range tc.status {
p, ok := rosa.ResolveName(name)
if !ok {
t.Fatalf("invalid name %q", name)
}
perm := os.FileMode(0400)
if status == "\x00" {
perm = 0
}
if err := cm.Do(func(cache *pkg.Cache) error {
return os.WriteFile(filepath.Join(
cm.base,
"status",
pkg.Encode(cache.Ident(rosa.Std.Load(p)).Value()),
), unsafe.Slice(unsafe.StringData(status), len(status)), perm)
}); err != nil {
t.Fatalf("Do: error = %v", err)
}
}
}
var wantErr error
switch c := tc.wantErr.(type) {
case error:
wantErr = c
case func(cm *cache) error:
wantErr = c(cm)
default:
if tc.wantErr != nil {
t.Fatalf("invalid wantErr %#v", tc.wantErr)
}
}
if err := commandInfo(
cm,
tc.args,
&buf,
cm != nil,
rp,
); !reflect.DeepEqual(err, wantErr) {
t.Fatalf("commandInfo: error = %v, want %v", err, wantErr)
}
if got := buf.String(); got != strings.TrimPrefix(tc.want, "\n") {
t.Errorf("commandInfo:\n%s\nwant\n%s", got, tc.want)
}
})
}
}

View File

@@ -1,30 +1,17 @@
// The mbf program is a frontend for [hakurei.app/internal/rosa].
//
// This program is not covered by the compatibility promise. The command line
// interface, available packages and their behaviour, and even the on-disk
// format, may change at any time.
//
// # Name
//
// The name mbf stands for maiden's best friend, as a tribute to the DOOM source
// port of [the same name]. This name is a placeholder and is subject to change.
//
// [the same name]: https://www.doomwiki.org/wiki/MBF
package main
import (
"context"
"crypto/sha512"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
@@ -54,13 +41,14 @@ func main() {
log.Fatal("this program must not run as root")
}
var cache *pkg.Cache
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer stop()
var cm cache
defer func() {
cm.Close()
if cache != nil {
cache.Close()
}
if r := recover(); r != nil {
fmt.Println(r)
@@ -69,66 +57,61 @@ func main() {
}()
var (
flagQuiet bool
addr net.UnixAddr
flagQuiet bool
flagCures int
flagBase string
flagTShift int
flagIdle bool
)
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) error {
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) {
msg.SwapVerbose(!flagQuiet)
cm.ctx, cm.msg = ctx, msg
cm.base = os.ExpandEnv(cm.base)
addr.Net = "unix"
addr.Name = os.ExpandEnv(addr.Name)
if addr.Name == "" {
addr.Name = "daemon"
flagBase = os.ExpandEnv(flagBase)
if flagBase == "" {
flagBase = "cache"
}
return nil
var base *check.Absolute
if flagBase, err = filepath.Abs(flagBase); err != nil {
return
} else if base, err = check.NewAbs(flagBase); err != nil {
return
}
if cache, err = pkg.Open(ctx, msg, flagCures, base); err == nil {
if flagTShift < 0 {
cache.SetThreshold(0)
} else if flagTShift > 31 {
cache.SetThreshold(1 << 31)
} else {
cache.SetThreshold(1 << flagTShift)
}
}
if flagIdle {
pkg.SetSchedIdle = true
}
return
}).Flag(
&flagQuiet,
"q", command.BoolFlag(false),
"Do not print cure messages",
).Flag(
&cm.cures,
&flagCures,
"cures", command.IntFlag(0),
"Maximum number of dependencies to cure at any given time",
).Flag(
&cm.jobs,
"jobs", command.IntFlag(0),
"Preferred number of jobs to run, when applicable",
).Flag(
&cm.base,
&flagBase,
"d", command.StringFlag("$MBF_CACHE_DIR"),
"Directory to store cured artifacts",
).Flag(
&cm.idle,
&flagTShift,
"tshift", command.IntFlag(-1),
"Dependency graph size exponent, to the power of 2",
).Flag(
&flagIdle,
"sched-idle", command.BoolFlag(false),
"Set SCHED_IDLE scheduling policy",
).Flag(
&cm.hostAbstract,
"host-abstract", command.BoolFlag(
os.Getenv("MBF_HOST_ABSTRACT") != "",
),
"Do not restrict networked cure containers from connecting to host "+
"abstract UNIX sockets",
).Flag(
&addr.Name,
"socket", command.StringFlag("$MBF_DAEMON_SOCKET"),
"Pathname of socket to bind to",
)
c.NewCommand(
"checksum", "Compute checksum of data read from standard input",
func([]string) error {
go func() { <-ctx.Done(); os.Exit(1) }()
h := sha512.New384()
if _, err := io.Copy(h, os.Stdin); err != nil {
return err
}
log.Println(pkg.Encode(pkg.Checksum(h.Sum(nil))))
return nil
},
)
{
@@ -142,9 +125,7 @@ func main() {
if flagShifts < 0 || flagShifts > 31 {
flagShifts = 12
}
return cm.Do(func(cache *pkg.Cache) error {
return cache.Scrub(runtime.NumCPU() << flagShifts)
})
return cache.Scrub(runtime.NumCPU() << flagShifts)
},
).Flag(
&flagShifts,
@@ -162,17 +143,105 @@ func main() {
"info",
"Display out-of-band metadata of an artifact",
func(args []string) (err error) {
return commandInfo(&cm, args, os.Stdout, flagStatus, flagReport)
if len(args) == 0 {
return errors.New("info requires at least 1 argument")
}
var r *rosa.Report
if flagReport != "" {
if r, err = rosa.OpenReport(flagReport); err != nil {
return err
}
defer func() {
if closeErr := r.Close(); err == nil {
err = closeErr
}
}()
defer r.HandleAccess(&err)()
}
for i, name := range args {
if p, ok := rosa.ResolveName(name); !ok {
return fmt.Errorf("unknown artifact %q", name)
} else {
var suffix string
if version := rosa.Std.Version(p); version != rosa.Unversioned {
suffix += "-" + version
}
fmt.Println("name : " + name + suffix)
meta := rosa.GetMetadata(p)
fmt.Println("description : " + meta.Description)
if meta.Website != "" {
fmt.Println("website : " +
strings.TrimSuffix(meta.Website, "/"))
}
if len(meta.Dependencies) > 0 {
fmt.Print("depends on :")
for _, d := range meta.Dependencies {
s := rosa.GetMetadata(d).Name
if version := rosa.Std.Version(d); version != rosa.Unversioned {
s += "-" + version
}
fmt.Print(" " + s)
}
fmt.Println()
}
const statusPrefix = "status : "
if flagStatus {
if r == nil {
var f io.ReadSeekCloser
f, err = cache.OpenStatus(rosa.Std.Load(p))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Println(
statusPrefix + "not yet cured",
)
} else {
return
}
} else {
fmt.Print(statusPrefix)
_, err = io.Copy(os.Stdout, f)
if err = errors.Join(err, f.Close()); err != nil {
return
}
}
} else {
status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p)))
if status == nil {
fmt.Println(
statusPrefix + "not in report",
)
} else {
fmt.Println("size :", n)
fmt.Print(statusPrefix)
if _, err = os.Stdout.Write(status); err != nil {
return
}
}
}
}
if i != len(args)-1 {
fmt.Println()
}
}
}
return nil
},
).Flag(
&flagStatus,
"status", command.BoolFlag(false),
"Display cure status if available",
).Flag(
&flagReport,
"report", command.StringFlag(""),
"Load cure status from this report file instead of cache",
)
).
Flag(
&flagStatus,
"status", command.BoolFlag(false),
"Display cure status if available",
).
Flag(
&flagReport,
"report", command.StringFlag(""),
"Load cure status from this report file instead of cache",
)
}
c.NewCommand(
@@ -206,9 +275,7 @@ func main() {
if ext.Isatty(int(w.Fd())) {
return errors.New("output appears to be a terminal")
}
return cm.Do(func(cache *pkg.Cache) error {
return rosa.WriteReport(msg, w, cache)
})
return rosa.WriteReport(msg, w, cache)
},
)
@@ -271,26 +338,14 @@ func main() {
" package(s) are out of date"))
}
return errors.Join(errs...)
}).Flag(
&flagJobs,
"j", command.IntFlag(32),
"Maximum number of simultaneous connections",
)
}).
Flag(
&flagJobs,
"j", command.IntFlag(32),
"Maximum number of simultaneous connections",
)
}
c.NewCommand(
"daemon",
"Service artifact IR with Rosa OS extensions",
func(args []string) error {
ul, err := net.ListenUnix("unix", &addr)
if err != nil {
return err
}
log.Printf("listening on pathname socket at %s", addr.Name)
return serve(ctx, log.Default(), &cm, ul)
},
)
{
var (
flagGentoo string
@@ -315,37 +370,25 @@ func main() {
rosa.SetGentooStage3(flagGentoo, checksum)
}
_, _, _, stage1 := (t - 2).NewLLVM()
_, _, _, stage2 := (t - 1).NewLLVM()
_, _, _, stage3 := t.NewLLVM()
var (
pathname *check.Absolute
checksum [2]unique.Handle[pkg.Checksum]
)
if err = cm.Do(func(cache *pkg.Cache) (err error) {
pathname, _, err = cache.Cure(
(t - 2).Load(rosa.Clang),
)
return
}); err != nil {
return
if pathname, _, err = cache.Cure(stage1); err != nil {
return err
}
log.Println("stage1:", pathname)
if err = cm.Do(func(cache *pkg.Cache) (err error) {
pathname, checksum[0], err = cache.Cure(
(t - 1).Load(rosa.Clang),
)
return
}); err != nil {
return
if pathname, checksum[0], err = cache.Cure(stage2); err != nil {
return err
}
log.Println("stage2:", pathname)
if err = cm.Do(func(cache *pkg.Cache) (err error) {
pathname, checksum[1], err = cache.Cure(
t.Load(rosa.Clang),
)
return
}); err != nil {
return
if pathname, checksum[1], err = cache.Cure(stage3); err != nil {
return err
}
log.Println("stage3:", pathname)
@@ -362,40 +405,38 @@ func main() {
}
if flagStage0 {
if err = cm.Do(func(cache *pkg.Cache) (err error) {
pathname, _, err = cache.Cure(
t.Load(rosa.Stage0),
)
return
}); err != nil {
return
if pathname, _, err = cache.Cure(
t.Load(rosa.Stage0),
); err != nil {
return err
}
log.Println(pathname)
}
return
},
).Flag(
&flagGentoo,
"gentoo", command.StringFlag(""),
"Bootstrap from a Gentoo stage3 tarball",
).Flag(
&flagChecksum,
"checksum", command.StringFlag(""),
"Checksum of Gentoo stage3 tarball",
).Flag(
&flagStage0,
"stage0", command.BoolFlag(false),
"Create bootstrap stage0 tarball",
)
).
Flag(
&flagGentoo,
"gentoo", command.StringFlag(""),
"Bootstrap from a Gentoo stage3 tarball",
).
Flag(
&flagChecksum,
"checksum", command.StringFlag(""),
"Checksum of Gentoo stage3 tarball",
).
Flag(
&flagStage0,
"stage0", command.BoolFlag(false),
"Create bootstrap stage0 tarball",
)
}
{
var (
flagDump string
flagEnter bool
flagExport string
flagRemote bool
)
c.NewCommand(
"cure",
@@ -404,18 +445,10 @@ func main() {
if len(args) != 1 {
return errors.New("cure requires 1 argument")
}
p, ok := rosa.ResolveName(args[0])
if !ok {
if p, ok := rosa.ResolveName(args[0]); !ok {
return fmt.Errorf("unknown artifact %q", args[0])
}
switch {
default:
var pathname *check.Absolute
err := cm.Do(func(cache *pkg.Cache) (err error) {
pathname, _, err = cache.Cure(rosa.Std.Load(p))
return
})
} else if flagDump == "" {
pathname, _, err := cache.Cure(rosa.Std.Load(p))
if err != nil {
return err
}
@@ -444,8 +477,7 @@ func main() {
}
return nil
case flagDump != "":
} else {
f, err := os.OpenFile(
flagDump,
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
@@ -455,49 +487,25 @@ func main() {
return err
}
if err = pkg.NewIR().EncodeAll(f, rosa.Std.Load(p)); err != nil {
if err = cache.EncodeAll(f, rosa.Std.Load(p)); err != nil {
_ = f.Close()
return err
}
return f.Close()
case flagEnter:
return cm.Do(func(cache *pkg.Cache) error {
return cache.EnterExec(
ctx,
rosa.Std.Load(p),
true, os.Stdin, os.Stdout, os.Stderr,
rosa.AbsSystem.Append("bin", "mksh"),
"sh",
)
})
case flagRemote:
pathname, err := cureRemote(ctx, &addr, rosa.Std.Load(p))
if err == nil {
log.Println(pathname)
}
return err
}
},
).Flag(
&flagDump,
"dump", command.StringFlag(""),
"Write IR to specified pathname and terminate",
).Flag(
&flagExport,
"export", command.StringFlag(""),
"Export cured artifact to specified pathname",
).Flag(
&flagEnter,
"enter", command.BoolFlag(false),
"Enter cure container with an interactive shell",
).Flag(
&flagRemote,
"daemon", command.BoolFlag(false),
"Cure artifact on the daemon",
)
).
Flag(
&flagDump,
"dump", command.StringFlag(""),
"Write IR to specified pathname and terminate",
).
Flag(
&flagExport,
"export", command.StringFlag(""),
"Export cured artifact to specified pathname",
)
}
{
@@ -511,7 +519,7 @@ func main() {
"shell",
"Interactive shell in the specified Rosa OS environment",
func(args []string) error {
presets := make([]rosa.PArtifact, len(args)+3)
presets := make([]rosa.PArtifact, len(args))
for i, arg := range args {
p, ok := rosa.ResolveName(arg)
if !ok {
@@ -519,26 +527,23 @@ func main() {
}
presets[i] = p
}
base := rosa.Clang
if !flagWithToolchain {
base = rosa.Musl
}
presets = append(presets,
base,
rosa.Mksh,
rosa.Toybox,
)
root := make(pkg.Collect, 0, 6+len(args))
root := make(rosa.Collect, 0, 6+len(args))
root = rosa.Std.AppendPresets(root, presets...)
if err := cm.Do(func(cache *pkg.Cache) error {
_, _, err := cache.Cure(&root)
return err
}); err == nil {
if flagWithToolchain {
musl, compilerRT, runtimes, clang := (rosa.Std - 1).NewLLVM()
root = append(root, musl, compilerRT, runtimes, clang)
} else {
root = append(root, rosa.Std.Load(rosa.Musl))
}
root = append(root,
rosa.Std.Load(rosa.Mksh),
rosa.Std.Load(rosa.Toybox),
)
if _, _, err := cache.Cure(&root); err == nil {
return errors.New("unreachable")
} else if !pkg.IsCollected(err) {
} else if !errors.Is(err, rosa.Collected{}) {
return err
}
@@ -548,22 +553,11 @@ func main() {
}
cured := make(map[pkg.Artifact]cureRes)
for _, a := range root {
if err := cm.Do(func(cache *pkg.Cache) error {
pathname, checksum, err := cache.Cure(a)
if err == nil {
cured[a] = cureRes{pathname, checksum}
}
return err
}); err != nil {
return err
}
}
// explicitly open for direct error-free use from this point
if cm.c == nil {
if err := cm.open(); err != nil {
pathname, checksum, err := cache.Cure(a)
if err != nil {
return err
}
cured[a] = cureRes{pathname, checksum}
}
layers := pkg.PromoteLayers(root, func(a pkg.Artifact) (
@@ -573,7 +567,7 @@ func main() {
res := cured[a]
return res.pathname, res.checksum
}, func(i int, d pkg.Artifact) {
r := pkg.Encode(cm.c.Ident(d).Value())
r := pkg.Encode(cache.Ident(d).Value())
if s, ok := d.(fmt.Stringer); ok {
if name := s.String(); name != "" {
r += "-" + name
@@ -592,9 +586,6 @@ func main() {
z.Hostname = "localhost"
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
if s, ok := os.LookupEnv("TERM"); ok {
z.Env = append(z.Env, "TERM="+s)
}
var tempdir *check.Absolute
if s, err := filepath.Abs(os.TempDir()); err != nil {
@@ -637,19 +628,22 @@ func main() {
}
return z.Wait()
},
).Flag(
&flagNet,
"net", command.BoolFlag(false),
"Share host net namespace",
).Flag(
&flagSession,
"session", command.BoolFlag(true),
"Retain session",
).Flag(
&flagWithToolchain,
"with-toolchain", command.BoolFlag(false),
"Include the stage2 LLVM toolchain",
)
).
Flag(
&flagNet,
"net", command.BoolFlag(false),
"Share host net namespace",
).
Flag(
&flagSession,
"session", command.BoolFlag(false),
"Retain session",
).
Flag(
&flagWithToolchain,
"with-toolchain", command.BoolFlag(false),
"Include the stage3 LLVM toolchain",
)
}
@@ -660,7 +654,9 @@ func main() {
)
c.MustParse(os.Args[1:], func(err error) {
cm.Close()
if cache != nil {
cache.Close()
}
if w, ok := err.(interface{ Unwrap() []error }); !ok {
log.Fatal(err)
} else {

View File

@@ -7,8 +7,8 @@
#endif
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
#define SHAREFS_PERM_DIR 0770 /* permission bits for directories presented to userspace */
#define SHAREFS_PERM_REG 0660 /* permission bits for regular files presented to userspace */
#define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
#define SHAREFS_PERM_REG 0600 /* permission bits for regular files presented to userspace */
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
/* sharefs_private is populated by sharefs_init and contains process-wide context */

View File

@@ -19,11 +19,12 @@ import (
"encoding/gob"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"path"
"runtime"
"runtime/cgo"
"strconv"
@@ -84,10 +85,7 @@ func destroySetup(private_data unsafe.Pointer) (ok bool) {
}
//export sharefs_init
func sharefs_init(
_ *C.struct_fuse_conn_info,
cfg *C.struct_fuse_config,
) unsafe.Pointer {
func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.Pointer {
ctx := C.fuse_get_context()
priv := (*C.struct_sharefs_private)(ctx.private_data)
setup := cgo.Handle(priv.setup).Value().(*setupState)
@@ -105,11 +103,7 @@ func sharefs_init(
cfg.negative_timeout = 0
// all future filesystem operations happen through this dirfd
if fd, err := syscall.Open(
setup.Source.String(),
syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC,
0,
); err != nil {
if fd, err := syscall.Open(setup.Source.String(), syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC, 0); err != nil {
log.Printf("cannot open %q: %v", setup.Source, err)
goto fail
} else if err = syscall.Fchdir(fd); err != nil {
@@ -144,9 +138,9 @@ func sharefs_destroy(private_data unsafe.Pointer) {
func showHelp(args *fuseArgs) {
executableName := sharefsName
if args.argc > 0 {
executableName = filepath.Base(C.GoString(*args.argv))
executableName = path.Base(C.GoString(*args.argv))
} else if name, err := os.Executable(); err == nil {
executableName = filepath.Base(name)
executableName = path.Base(name)
}
fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
@@ -175,11 +169,8 @@ func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
// Decimal string representation of gid to set when running as root.
setgid *C.char
// Decimal string representation of open file descriptor to read
// setupState from.
//
// This is an internal detail for containerisation and must not be
// specified directly.
// Decimal string representation of open file descriptor to read setupState from.
// This is an internal detail for containerisation and must not be specified directly.
setup *C.char
}
@@ -262,8 +253,7 @@ func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
return true
}
// copyArgs returns a heap allocated copy of an argument slice in fuse_args
// representation.
// copyArgs returns a heap allocated copy of an argument slice in fuse_args representation.
func copyArgs(s ...string) fuseArgs {
if len(s) == 0 {
return fuseArgs{argc: 0, argv: nil, allocated: 0}
@@ -279,7 +269,6 @@ func copyArgs(s ...string) fuseArgs {
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }
// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
//
// The last byte of arg must be 0.
func unsafeAddArgument(args *fuseArgs, arg string) {
C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
@@ -299,8 +288,8 @@ func _main(s ...string) (exitCode int) {
args := copyArgs(s...)
defer freeArgs(&args)
// this causes the kernel to enforce access control based on struct stat
// populated by sharefs_getattr
// this causes the kernel to enforce access control based on
// struct stat populated by sharefs_getattr
unsafeAddArgument(&args, "-odefault_permissions\x00")
var priv C.struct_sharefs_private
@@ -464,19 +453,15 @@ func _main(s ...string) (exitCode int) {
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
}
z.Bind(z.Path, z.Path, 0)
setup.Fuse = int(proc.ExtraFileSlice(
&z.ExtraFiles,
os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"),
))
setup.Fuse = int(proc.ExtraFileSlice(&z.ExtraFiles, os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse")))
var setupPipe [2]*os.File
if r, w, err := os.Pipe(); err != nil {
var setupWriter io.WriteCloser
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
log.Println(err)
return 5
} else {
z.Args = append(z.Args, "-osetup="+strconv.Itoa(3+len(z.ExtraFiles)))
z.ExtraFiles = append(z.ExtraFiles, r)
setupPipe[0], setupPipe[1] = r, w
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
setupWriter = w
}
if err := z.Start(); err != nil {
@@ -487,9 +472,6 @@ 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)
@@ -499,10 +481,10 @@ func _main(s ...string) (exitCode int) {
return 5
}
if err := gob.NewEncoder(setupPipe[1]).Encode(&setup); err != nil {
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
log.Println(err)
return 5
} else if err = setupPipe[1].Close(); err != nil {
} else if err = setupWriter.Close(); err != nil {
log.Println(err)
}

View File

@@ -1,10 +1,3 @@
// The sharefs FUSE filesystem is a permissionless shared filesystem.
//
// This filesystem is the primary means of file sharing between hakurei
// application containers. It serves the same purpose in Rosa OS as /sdcard
// does in AOSP.
//
// See help message for all available options.
package main
import (

View File

@@ -1,122 +0,0 @@
//go:build raceattr
// The raceattr program reproduces vfs inode file attribute race.
//
// Even though libfuse high-level API presents the address of a struct stat
// alongside struct fuse_context, file attributes are actually inherent to the
// inode, instead of the specific call from userspace. The kernel implementation
// in fs/fuse/xattr.c appears to make stale data in the inode (set by a previous
// call) impossible or very unlikely to reach userspace via the stat family of
// syscalls. However, when using default_permissions to have the VFS check
// permissions, this race still happens, despite the resulting struct stat being
// correct when overriding the check via capabilities otherwise.
//
// This program reproduces the failure, but because of its continuous nature, it
// is provided independent of the vm integration test suite.
package main
import (
"context"
"flag"
"log"
"os"
"os/signal"
"runtime"
"sync"
"sync/atomic"
"syscall"
)
func newStatAs(
ctx context.Context, cancel context.CancelFunc,
n *atomic.Uint64, ok *atomic.Bool,
uid uint32, pathname string,
continuous bool,
) func() {
return func() {
runtime.LockOSThread()
defer cancel()
if _, _, errno := syscall.Syscall(
syscall.SYS_SETUID, uintptr(uid),
0, 0,
); errno != 0 {
cancel()
log.Printf("cannot set uid to %d: %s", uid, errno)
}
var stat syscall.Stat_t
for {
if ctx.Err() != nil {
return
}
if err := syscall.Lstat(pathname, &stat); err != nil {
// SHAREFS_PERM_DIR not world executable, or
// SHAREFS_PERM_REG not world readable
if !continuous {
cancel()
}
ok.Store(true)
log.Printf("uid %d: %v", uid, err)
} else if stat.Uid != uid {
// appears to be unreachable
if !continuous {
cancel()
}
ok.Store(true)
log.Printf("got uid %d instead of %d", stat.Uid, uid)
}
n.Add(1)
}
}
}
func main() {
log.SetFlags(0)
log.SetPrefix("raceattr: ")
p := flag.String("target", "/sdcard/raceattr", "pathname of test file")
u0 := flag.Int("uid0", 1<<10-1, "first uid")
u1 := flag.Int("uid1", 1<<10-2, "second uid")
count := flag.Int("count", 1, "threads per uid")
continuous := flag.Bool("continuous", false, "keep running even after reproduce")
flag.Parse()
if os.Geteuid() != 0 {
log.Fatal("this program must run as root")
}
ctx, cancel := signal.NotifyContext(
context.Background(),
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGHUP,
)
if err := os.WriteFile(*p, nil, 0); err != nil {
log.Fatal(err)
}
var (
wg sync.WaitGroup
n atomic.Uint64
ok atomic.Bool
)
if *count < 1 {
*count = 1
}
for range *count {
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u0), *p, *continuous))
if *u1 >= 0 {
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u1), *p, *continuous))
}
}
wg.Wait()
if !*continuous && ok.Load() {
log.Printf("reproduced after %d calls", n.Load())
}
}

View File

@@ -21,7 +21,6 @@ import (
"hakurei.app/container/std"
"hakurei.app/ext"
"hakurei.app/fhs"
"hakurei.app/internal/landlock"
"hakurei.app/message"
)
@@ -29,6 +28,9 @@ 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 (
@@ -51,7 +53,7 @@ type (
ExtraFiles []*os.File
// Write end of a pipe connected to the init to deliver [Params].
setup [2]*os.File
setup *os.File
// Cancels the context passed to the underlying cmd.
cancel context.CancelFunc
// Closed after Wait returns. Keeps the spawning thread alive.
@@ -285,16 +287,14 @@ func (p *Container) Start() error {
}
// place setup pipe before user supplied extra files, this is later restored by init
if r, w, err := os.Pipe(); err != nil {
if fd, f, err := Setup(&p.cmd.ExtraFiles); err != nil {
return &StartError{
Fatal: true,
Step: "set up params stream",
Err: err,
}
} else {
fd := 3 + len(p.cmd.ExtraFiles)
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, r)
p.setup[0], p.setup[1] = r, w
p.setup = f
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,17 +318,15 @@ func (p *Container) Start() error {
// landlock: depends on per-thread state but acts on a process group
{
rulesetAttr := &landlock.RulesetAttr{
Scoped: landlock.LANDLOCK_SCOPE_SIGNAL,
}
rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL}
if !p.HostAbstract {
rulesetAttr.Scoped |= landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
}
if abi, err := landlock.GetABI(); err != nil {
if p.HostAbstract || !p.HostNet {
if abi, err := LandlockGetABI(); err != nil {
if p.HostAbstract {
// landlock can be skipped here as it restricts access
// to resources already covered by namespaces (pid, net)
// to resources already covered by namespaces (pid)
goto landlockOut
}
return &StartError{Step: "get landlock ABI", Err: err}
@@ -354,7 +352,7 @@ func (p *Container) Start() error {
}
} else {
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = landlock.RestrictSelf(rulesetFd, 0); err != nil {
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd)
return &StartError{
Fatal: true,
@@ -430,33 +428,24 @@ func (p *Container) Start() error {
// Serve serves [Container.Params] to the container init.
//
// Serve must only be called once.
func (p *Container) Serve() (err error) {
if p.setup[0] == nil || p.setup[1] == nil {
func (p *Container) Serve() error {
if p.setup == nil {
panic("invalid serve")
}
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 {
setup := p.setup
p.setup = nil
if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil {
return &StartError{
Fatal: true,
Step: "close read end of init pipe",
Step: "set init pipe deadline",
Err: err,
Passthrough: true,
}
}
if p.Path == nil {
p.cancel()
return &StartError{
Step: "invalid executable pathname",
Err: EINVAL,
@@ -472,27 +461,18 @@ func (p *Container) Serve() (err error) {
p.SeccompRules = make([]std.NativeRule, 0)
}
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{
err := gob.NewEncoder(setup).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

View File

@@ -16,6 +16,7 @@ import (
"strings"
"syscall"
"testing"
"time"
"hakurei.app/check"
"hakurei.app/command"
@@ -25,9 +26,6 @@ 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"
@@ -86,9 +84,9 @@ func TestStartError(t *testing.T) {
{"params env", &container.StartError{
Fatal: true,
Step: "set up params stream",
Err: params.ErrReceiveEnv,
Err: container.ErrReceiveEnv,
}, "set up params stream: environment variable not set",
params.ErrReceiveEnv, syscall.EBADF,
container.ErrReceiveEnv, syscall.EBADF,
"cannot set up params stream: environment variable not set"},
{"params", &container.StartError{
@@ -438,8 +436,11 @@ func TestContainer(t *testing.T) {
wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel()
var libPaths []*check.Absolute
c := helperNewContainerLibPaths(t.Context(), &libPaths, "container", strconv.Itoa(i))
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid
c.Gid = tc.gid
c.Hostname = hostnameFromTestCase(tc.name)
@@ -449,6 +450,7 @@ func TestContainer(t *testing.T) {
} else {
c.Stdout, c.Stderr = os.Stdout, os.Stderr
}
c.WaitDelay = helperDefaultTimeout
*c.Ops = append(*c.Ops, *wantOps...)
c.SeccompRules = tc.rules
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
@@ -456,15 +458,6 @@ 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).
@@ -560,10 +553,11 @@ func testContainerCancel(
) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(t.Context())
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block")
c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.WaitDelay = helperDefaultTimeout
if containerExtra != nil {
containerExtra(c)
}
@@ -744,7 +738,8 @@ func init() {
const (
envDoCheck = "HAKUREI_TEST_DO_CHECK"
helperInnerPath = "/usr/bin/helper"
helperDefaultTimeout = 5 * time.Second
helperInnerPath = "/usr/bin/helper"
)
var (

View File

@@ -1,7 +1,6 @@
package container
import (
"context"
"io"
"io/fs"
"net"
@@ -16,7 +15,6 @@ import (
"hakurei.app/container/std"
"hakurei.app/ext"
"hakurei.app/internal/netlink"
"hakurei.app/internal/params"
"hakurei.app/message"
)
@@ -57,7 +55,7 @@ type syscallDispatcher interface {
// isatty provides [Isatty].
isatty(fd int) bool
// receive provides [Receive].
receive(key string, e any, fdp *int) (closeFunc func() error, err error)
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// bindMount provides procPaths.bindMount.
bindMount(msg message.Msg, source, target string, flags uintptr) error
@@ -68,7 +66,7 @@ type syscallDispatcher interface {
// ensureFile provides ensureFile.
ensureFile(name string, perm, pperm os.FileMode) error
// mustLoopback provides mustLoopback.
mustLoopback(ctx context.Context, msg message.Msg)
mustLoopback(msg message.Msg)
// seccompLoad provides [seccomp.Load].
seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error
@@ -148,7 +146,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) }
@@ -156,8 +154,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 *int) (func() error, error) {
return params.Receive(key, e, fdp)
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return Receive(key, e, fdp)
}
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
@@ -172,7 +170,7 @@ func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
return ensureFile(name, perm, pperm)
}
func (direct) mustLoopback(ctx context.Context, msg message.Msg) {
func (direct) mustLoopback(msg message.Msg) {
var lo int
if ifi, err := net.InterfaceByName("lo"); err != nil {
msg.GetLogger().Fatalln(err)
@@ -180,7 +178,7 @@ func (direct) mustLoopback(ctx context.Context, msg message.Msg) {
lo = ifi.Index
}
c, err := netlink.DialRoute(0)
c, err := netlink.DialRoute()
if err != nil {
msg.GetLogger().Fatalln(err)
}
@@ -201,14 +199,11 @@ func (direct) mustLoopback(ctx context.Context, msg message.Msg) {
msg.GetLogger().Fatalf("RTNETLINK answers: %v", err)
default:
if err == context.DeadlineExceeded || err == context.Canceled {
msg.GetLogger().Fatalf("interrupted RTNETLINK operation")
}
msg.GetLogger().Fatal("RTNETLINK answers with malformed message")
msg.GetLogger().Fatalf("RTNETLINK answers with malformed message")
}
}
must(c.SendNewaddrLo(ctx, uint32(lo)))
must(c.SendIfInfomsg(ctx, syscall.RTM_NEWLINK, 0, &syscall.IfInfomsg{
must(c.SendNewaddrLo(uint32(lo)))
must(c.SendIfInfomsg(syscall.RTM_NEWLINK, 0, &syscall.IfInfomsg{
Family: syscall.AF_UNSPEC,
Index: int32(lo),
Flags: syscall.IFF_UP,

View File

@@ -2,7 +2,6 @@ package container
import (
"bytes"
"context"
"fmt"
"io"
"io/fs"
@@ -390,7 +389,7 @@ func (k *kstub) isatty(fd int) bool {
return expect.Ret.(bool)
}
func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, err error) {
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
k.Helper()
expect := k.Expects("receive")
@@ -408,17 +407,10 @@ func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, er
}
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", fdpComp, 2))
stub.CheckArgReflect(k.Stub, "fdp", fdp, 2))
// 3 is unused so stores params
if expect.Args[3] != nil {
@@ -433,7 +425,7 @@ func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, er
if expect.Args[4] != nil {
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
if fdp != nil {
*fdp = int(v)
*fdp = v
}
}
}
@@ -476,7 +468,7 @@ func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
stub.CheckArg(k.Stub, "pperm", pperm, 2))
}
func (*kstub) mustLoopback(context.Context, message.Msg) { /* noop */ }
func (*kstub) mustLoopback(message.Msg) { /* noop */ }
func (k *kstub) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
k.Helper()

View File

@@ -7,8 +7,7 @@ import (
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"path"
"slices"
"strconv"
"sync"
@@ -19,7 +18,6 @@ import (
"hakurei.app/container/seccomp"
"hakurei.app/ext"
"hakurei.app/fhs"
"hakurei.app/internal/params"
"hakurei.app/message"
)
@@ -148,38 +146,36 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
var (
param initParams
closeSetup func() error
setupFd int
params initParams
closeSetup func() error
setupFd uintptr
offsetSetup int
)
if f, err := k.receive(setupEnv, &param, &setupFd); err != nil {
if f, err := k.receive(setupEnv, &params, &setupFd); err != nil {
if errors.Is(err, EBADF) {
k.fatal(msg, "invalid setup descriptor")
}
if errors.Is(err, params.ErrReceiveEnv) {
if errors.Is(err, ErrReceiveEnv) {
k.fatal(msg, setupEnv+" not set")
}
k.fatalf(msg, "cannot decode init setup payload: %v", err)
} else {
if param.Ops == nil {
if params.Ops == nil {
k.fatal(msg, "invalid setup parameters")
}
if param.ParentPerm == 0 {
param.ParentPerm = 0755
if params.ParentPerm == 0 {
params.ParentPerm = 0755
}
msg.SwapVerbose(param.Verbose)
msg.SwapVerbose(params.Verbose)
msg.Verbose("received setup parameters")
closeSetup = f
offsetSetup = int(setupFd + 1)
}
if !param.HostNet {
ctx, cancel := signal.NotifyContext(context.Background(), CancelSignal,
os.Interrupt, SIGTERM, SIGQUIT)
defer cancel() // for panics
k.mustLoopback(ctx, msg)
cancel()
if !params.HostNet {
k.mustLoopback(msg)
}
// write uid/gid map here so parent does not need to set dumpable
@@ -187,7 +183,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(param.Uid)+" "+strconv.Itoa(param.HostUid)+" 1\n"...),
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
0); err != nil {
k.fatalf(msg, "%v", err)
}
@@ -197,7 +193,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(param.Gid)+" "+strconv.Itoa(param.HostGid)+" 1\n"...),
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
0); err != nil {
k.fatalf(msg, "%v", err)
}
@@ -206,8 +202,8 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
oldmask := k.umask(0)
if param.Hostname != "" {
if err := k.sethostname([]byte(param.Hostname)); err != nil {
if params.Hostname != "" {
if err := k.sethostname([]byte(params.Hostname)); err != nil {
k.fatalf(msg, "cannot set hostname: %v", err)
}
}
@@ -220,7 +216,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
ctx, cancel := context.WithCancel(context.Background())
state := &setupState{process: make(map[int]WaitStatus), Params: &param.Params, Msg: msg, Context: ctx}
state := &setupState{process: make(map[int]WaitStatus), Params: &params.Params, Msg: msg, Context: ctx}
defer cancel()
/* early is called right before pivot_root into intermediate root;
@@ -228,7 +224,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 *param.Ops {
for i, op := range *params.Ops {
if op == nil || !op.Valid() {
k.fatalf(msg, "invalid op at index %d", i)
}
@@ -271,7 +267,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 *param.Ops {
for i, op := range *params.Ops {
// ops already checked during early setup
if prefix, ok := op.prefix(); ok {
msg.Verbosef("%s %s", prefix, op)
@@ -327,7 +323,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 param.Privileged && i == CAP_SYS_ADMIN {
if params.Privileged && i == CAP_SYS_ADMIN {
continue
}
if err := k.capBoundingSetDrop(i); err != nil {
@@ -336,7 +332,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
var keep [2]uint32
if param.Privileged {
if params.Privileged {
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
@@ -350,13 +346,13 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot capset: %v", err)
}
if !param.SeccompDisable {
rules := param.SeccompRules
if !params.SeccompDisable {
rules := params.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets
msg.Verbosef("resolving presets %#x", param.SeccompPresets)
rules = seccomp.Preset(param.SeccompPresets, param.SeccompFlags)
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
}
if err := k.seccompLoad(rules, param.SeccompFlags); err != nil {
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
k.fatalf(msg, "cannot load syscall filter: %v", err)
}
@@ -365,10 +361,10 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
msg.Verbose("syscall filter not configured")
}
extraFiles := make([]*os.File, param.Count)
extraFiles := make([]*os.File, params.Count)
for i := range extraFiles {
// setup fd is placed before all extra files
extraFiles[i] = k.newFile(uintptr(setupFd+1+i), "extra file "+strconv.Itoa(i))
extraFiles[i] = k.newFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
}
k.umask(oldmask)
@@ -446,7 +442,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 *param.Ops {
for i, op := range *params.Ops {
// ops already checked during early setup
if err := op.late(state, k); err != nil {
if m, ok := messageFromError(err); ok {
@@ -467,14 +463,14 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot close setup pipe: %v", err)
}
cmd := exec.Command(param.Path.String())
cmd := exec.Command(params.Path.String())
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = param.Args
cmd.Env = param.Env
cmd.Args = params.Args
cmd.Env = params.Env
cmd.ExtraFiles = extraFiles
cmd.Dir = param.Dir.String()
cmd.Dir = params.Dir.String()
msg.Verbosef("starting initial process %s", param.Path)
msg.Verbosef("starting initial process %s", params.Path)
if err := k.start(cmd); err != nil {
k.fatalf(msg, "%v", err)
}
@@ -492,9 +488,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
for {
select {
case s := <-sig:
if s == CancelSignal && param.ForwardCancel && cmd.Process != nil {
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
msg.Verbose("forwarding context cancellation")
if err := k.signal(cmd, os.Interrupt); err != nil && !errors.Is(err, os.ErrProcessDone) {
if err := k.signal(cmd, os.Interrupt); err != nil {
k.printf(msg, "cannot forward cancellation: %v", err)
}
continue
@@ -524,7 +520,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
cancel()
// start timeout early
go func() { time.Sleep(param.AdoptWaitDelay); close(timeout) }()
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
// close initial process files; this also keeps them alive
for _, f := range extraFiles {
@@ -568,7 +564,7 @@ func TryArgv0(msg message.Msg) {
msg = message.New(log.Default())
}
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == initName {
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
Init(msg)
msg.BeforeExit()
os.Exit(0)

View File

@@ -10,7 +10,6 @@ import (
"hakurei.app/check"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/internal/params"
"hakurei.app/internal/stub"
)
@@ -41,7 +40,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, params.ErrReceiveEnv),
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, ErrReceiveEnv),
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SETUP not set"}}, nil, nil),
},
}, nil},

View File

@@ -3,7 +3,7 @@ package container
import (
"encoding/gob"
"fmt"
"path/filepath"
"path"
. "syscall"
"hakurei.app/check"
@@ -46,7 +46,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
}
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
targetPath := filepath.Join(target, name)
targetPath := path.Join(target, name)
if err := k.ensureFile(targetPath, 0444, state.ParentPerm); err != nil {
return err
}
@@ -62,7 +62,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
for i, name := range []string{"stdin", "stdout", "stderr"} {
if err := k.symlink(
fhs.Proc+"self/fd/"+string(rune(i+'0')),
filepath.Join(target, name),
path.Join(target, name),
); err != nil {
return err
}
@@ -72,13 +72,13 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
{fhs.Proc + "kcore", "core"},
{"pts/ptmx", "ptmx"},
} {
if err := k.symlink(pair[0], filepath.Join(target, pair[1])); err != nil {
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
return err
}
}
devShmPath := filepath.Join(target, "shm")
devPtsPath := filepath.Join(target, "pts")
devShmPath := path.Join(target, "shm")
devPtsPath := path.Join(target, "pts")
for _, name := range []string{devShmPath, devPtsPath} {
if err := k.mkdir(name, state.ParentPerm); err != nil {
return err
@@ -92,7 +92,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
if state.RetainSession {
if k.isatty(Stdout) {
consolePath := filepath.Join(target, "console")
consolePath := path.Join(target, "console")
if err := k.ensureFile(consolePath, 0444, state.ParentPerm); err != nil {
return err
}
@@ -110,7 +110,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
}
if d.Mqueue {
mqueueTarget := filepath.Join(target, "mqueue")
mqueueTarget := path.Join(target, "mqueue")
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
return err
}

View File

@@ -3,7 +3,7 @@ package container
import (
"encoding/gob"
"fmt"
"path/filepath"
"path"
"hakurei.app/check"
)
@@ -30,7 +30,7 @@ func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkN
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
if l.Dereference {
if !filepath.IsAbs(l.LinkName) {
if !path.IsAbs(l.LinkName) {
return check.AbsoluteError(l.LinkName)
}
if name, err := k.readlink(l.LinkName); err != nil {
@@ -44,7 +44,7 @@ func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
target := toSysroot(l.Target.String())
if err := k.mkdirAll(filepath.Dir(target), state.ParentPerm); err != nil {
if err := k.mkdirAll(path.Dir(target), state.ParentPerm); err != nil {
return err
}
return k.symlink(l.LinkName, target)

View File

@@ -1,4 +1,4 @@
package landlock
package container
import (
"strings"
@@ -14,11 +14,11 @@ const (
LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
)
// AccessFS is bitmask of handled filesystem actions.
type AccessFS uint64
// LandlockAccessFS is bitmask of handled filesystem actions.
type LandlockAccessFS uint64
const (
LANDLOCK_ACCESS_FS_EXECUTE AccessFS = 1 << iota
LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 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 [AccessFS] flags.
func (f AccessFS) String() string {
// String returns a space-separated string of [LandlockAccessFS] flags.
func (f LandlockAccessFS) String() string {
switch f {
case LANDLOCK_ACCESS_FS_EXECUTE:
return "execute"
@@ -90,8 +90,8 @@ func (f AccessFS) String() string {
return "fs_ioctl_dev"
default:
var c []AccessFS
for i := AccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
var c []LandlockAccessFS
for i := LandlockAccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
if f&i != 0 {
c = append(c, i)
}
@@ -107,18 +107,18 @@ func (f AccessFS) String() string {
}
}
// AccessNet is bitmask of handled network actions.
type AccessNet uint64
// LandlockAccessNet is bitmask of handled network actions.
type LandlockAccessNet uint64
const (
LANDLOCK_ACCESS_NET_BIND_TCP AccessNet = 1 << iota
LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota
LANDLOCK_ACCESS_NET_CONNECT_TCP
_LANDLOCK_ACCESS_NET_DELIM
)
// String returns a space-separated string of [AccessNet] flags.
func (f AccessNet) String() string {
// String returns a space-separated string of [LandlockAccessNet] flags.
func (f LandlockAccessNet) String() string {
switch f {
case LANDLOCK_ACCESS_NET_BIND_TCP:
return "bind_tcp"
@@ -127,8 +127,8 @@ func (f AccessNet) String() string {
return "connect_tcp"
default:
var c []AccessNet
for i := AccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
var c []LandlockAccessNet
for i := LandlockAccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
if f&i != 0 {
c = append(c, i)
}
@@ -144,18 +144,18 @@ func (f AccessNet) String() string {
}
}
// Scope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
type Scope uint64
// LandlockScope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
type LandlockScope uint64
const (
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET Scope = 1 << iota
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota
LANDLOCK_SCOPE_SIGNAL
_LANDLOCK_SCOPE_DELIM
)
// String returns a space-separated string of [Scope] flags.
func (f Scope) String() string {
// String returns a space-separated string of [LandlockScope] flags.
func (f LandlockScope) String() string {
switch f {
case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET:
return "abstract_unix_socket"
@@ -164,8 +164,8 @@ func (f Scope) String() string {
return "signal"
default:
var c []Scope
for i := Scope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
var c []LandlockScope
for i := LandlockScope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
if f&i != 0 {
c = append(c, i)
}
@@ -184,12 +184,12 @@ func (f Scope) String() string {
// RulesetAttr is equivalent to struct landlock_ruleset_attr.
type RulesetAttr struct {
// Bitmask of handled filesystem actions.
HandledAccessFS AccessFS
HandledAccessFS LandlockAccessFS
// Bitmask of handled network actions.
HandledAccessNet AccessNet
HandledAccessNet LandlockAccessNet
// Bitmask of scopes restricting a Landlock domain from accessing outside
// resources (e.g. IPCs).
Scoped Scope
Scoped LandlockScope
}
// 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
}
// GetABI returns the ABI version supported by the kernel.
func GetABI() (int, error) {
// LandlockGetABI returns the ABI version supported by the kernel.
func LandlockGetABI() (int, error) {
return (*RulesetAttr)(nil).Create(LANDLOCK_CREATE_RULESET_VERSION)
}
// RestrictSelf applies a loaded ruleset to the calling thread.
func RestrictSelf(rulesetFd int, flags uintptr) error {
// LandlockRestrictSelf applies a loaded ruleset to the calling thread.
func LandlockRestrictSelf(rulesetFd int, flags uintptr) error {
r, _, errno := syscall.Syscall(
ext.SYS_LANDLOCK_RESTRICT_SELF,
uintptr(rulesetFd),

View File

@@ -0,0 +1,65 @@
package container_test
import (
"testing"
"unsafe"
"hakurei.app/container"
)
func TestLandlockString(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
rulesetAttr *container.RulesetAttr
want string
}{
{"nil", nil, "NULL"},
{"zero", new(container.RulesetAttr), "0"},
{"some", &container.RulesetAttr{Scoped: container.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
{"set", &container.RulesetAttr{
HandledAccessFS: container.LANDLOCK_ACCESS_FS_MAKE_SYM | container.LANDLOCK_ACCESS_FS_IOCTL_DEV | container.LANDLOCK_ACCESS_FS_WRITE_FILE,
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP,
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | container.LANDLOCK_SCOPE_SIGNAL,
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
{"all", &container.RulesetAttr{
HandledAccessFS: container.LANDLOCK_ACCESS_FS_EXECUTE |
container.LANDLOCK_ACCESS_FS_WRITE_FILE |
container.LANDLOCK_ACCESS_FS_READ_FILE |
container.LANDLOCK_ACCESS_FS_READ_DIR |
container.LANDLOCK_ACCESS_FS_REMOVE_DIR |
container.LANDLOCK_ACCESS_FS_REMOVE_FILE |
container.LANDLOCK_ACCESS_FS_MAKE_CHAR |
container.LANDLOCK_ACCESS_FS_MAKE_DIR |
container.LANDLOCK_ACCESS_FS_MAKE_REG |
container.LANDLOCK_ACCESS_FS_MAKE_SOCK |
container.LANDLOCK_ACCESS_FS_MAKE_FIFO |
container.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
container.LANDLOCK_ACCESS_FS_MAKE_SYM |
container.LANDLOCK_ACCESS_FS_REFER |
container.LANDLOCK_ACCESS_FS_TRUNCATE |
container.LANDLOCK_ACCESS_FS_IOCTL_DEV,
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP |
container.LANDLOCK_ACCESS_NET_CONNECT_TCP,
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
container.LANDLOCK_SCOPE_SIGNAL,
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.rulesetAttr.String(); got != tc.want {
t.Errorf("String: %s, want %s", got, tc.want)
}
})
}
}
func TestLandlockAttrSize(t *testing.T) {
t.Parallel()
want := 24
if got := unsafe.Sizeof(container.RulesetAttr{}); got != uintptr(want) {
t.Errorf("Sizeof: %d, want %d", got, want)
}
}

47
container/params.go Normal file
View File

@@ -0,0 +1,47 @@
package container
import (
"encoding/gob"
"errors"
"os"
"strconv"
"syscall"
)
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
func Setup(extraFiles *[]*os.File) (int, *os.File, error) {
if r, w, err := os.Pipe(); err != nil {
return -1, nil, err
} else {
fd := 3 + len(*extraFiles)
*extraFiles = append(*extraFiles, r)
return fd, w, nil
}
}
var (
ErrReceiveEnv = errors.New("environment variable not set")
)
// Receive retrieves setup fd from the environment and receives params.
func Receive(key string, e any, fdp *uintptr) (func() error, error) {
var setup *os.File
if s, ok := os.LookupEnv(key); !ok {
return nil, ErrReceiveEnv
} else {
if fd, err := strconv.Atoi(s); err != nil {
return nil, optionalErrorUnwrap(err)
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
return nil, syscall.EDOM
}
if fdp != nil {
*fdp = setup.Fd()
}
}
}
return setup.Close, gob.NewDecoder(setup).Decode(e)
}

View File

@@ -1,4 +1,4 @@
package params_test
package container_test
import (
"encoding/gob"
@@ -9,7 +9,7 @@ import (
"syscall"
"testing"
"hakurei.app/internal/params"
"hakurei.app/container"
)
func TestSetupReceive(t *testing.T) {
@@ -30,8 +30,8 @@ func TestSetupReceive(t *testing.T) {
})
}
if _, err := params.Receive(key, nil, nil); !errors.Is(err, params.ErrReceiveEnv) {
t.Errorf("Receive: error = %v, want %v", err, params.ErrReceiveEnv)
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrReceiveEnv) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrReceiveEnv)
}
})
@@ -39,7 +39,7 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_FORMAT"
t.Setenv(key, "")
if _, err := params.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
if _, err := container.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax)
}
})
@@ -48,7 +48,7 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_RANGE"
t.Setenv(key, "-1")
if _, err := params.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM)
}
})
@@ -60,22 +60,16 @@ func TestSetupReceive(t *testing.T) {
encoderDone := make(chan error, 1)
extraFiles := make([]*os.File, 0, 1)
if r, w, err := os.Pipe(); err != nil {
deadline, _ := t.Deadline()
if fd, f, err := container.Setup(&extraFiles); err != nil {
t.Fatalf("Setup: error = %v", err)
} else if fd != 3 {
t.Fatalf("Setup: fd = %d, want 3", fd)
} else {
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)
}
if err = f.SetDeadline(deadline); err != nil {
t.Fatal(err.Error())
}
go func() { encoderDone <- gob.NewEncoder(w).Encode(payload) }()
go func() { encoderDone <- gob.NewEncoder(f).Encode(payload) }()
}
if len(extraFiles) != 1 {
@@ -93,13 +87,13 @@ func TestSetupReceive(t *testing.T) {
var (
gotPayload []uint64
fdp *int
fdp *uintptr
)
if !useNilFdp {
fdp = new(int)
fdp = new(uintptr)
}
var closeFile func() error
if f, err := params.Receive(key, &gotPayload, fdp); err != nil {
if f, err := container.Receive(key, &gotPayload, fdp); err != nil {
t.Fatalf("Receive: error = %v", err)
} else {
closeFile = f
@@ -109,7 +103,7 @@ func TestSetupReceive(t *testing.T) {
}
}
if !useNilFdp {
if *fdp != dupFd {
if int(*fdp) != dupFd {
t.Errorf("Fd: %d, want %d", *fdp, dupFd)
}
}

View File

@@ -4,7 +4,7 @@ import (
"errors"
"io/fs"
"os"
"path/filepath"
"path"
"strconv"
"strings"
"syscall"
@@ -29,16 +29,16 @@ const (
func toSysroot(name string) string {
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
return filepath.Join(sysrootPath, name)
return path.Join(sysrootPath, name)
}
func toHost(name string) string {
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
return filepath.Join(hostPath, name)
return path.Join(hostPath, name)
}
func createFile(name string, perm, pperm os.FileMode, content []byte) error {
if err := os.MkdirAll(filepath.Dir(name), pperm); err != nil {
if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
return err
}
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)

View File

@@ -4,7 +4,7 @@ import (
"io"
"math"
"os"
"path/filepath"
"path"
"reflect"
"syscall"
"testing"
@@ -61,7 +61,7 @@ func TestCreateFile(t *testing.T) {
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
}
if err := createFile(filepath.Join(Nonexistent, ":3"), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
}
})
@@ -72,7 +72,7 @@ func TestCreateFile(t *testing.T) {
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
}
if err := createFile(filepath.Join(Nonexistent), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
}
})
@@ -80,7 +80,7 @@ func TestCreateFile(t *testing.T) {
t.Run("touch", func(t *testing.T) {
tempDir := t.TempDir()
pathname := filepath.Join(tempDir, "empty")
pathname := path.Join(tempDir, "empty")
if err := createFile(pathname, 0644, 0755, nil); err != nil {
t.Fatalf("createFile: error = %v", err)
}
@@ -93,7 +93,7 @@ func TestCreateFile(t *testing.T) {
t.Run("write", func(t *testing.T) {
tempDir := t.TempDir()
pathname := filepath.Join(tempDir, "zero")
pathname := path.Join(tempDir, "zero")
if err := createFile(pathname, 0644, 0755, []byte{0}); err != nil {
t.Fatalf("createFile: error = %v", err)
}
@@ -107,7 +107,7 @@ func TestCreateFile(t *testing.T) {
func TestEnsureFile(t *testing.T) {
t.Run("create", func(t *testing.T) {
if err := ensureFile(filepath.Join(t.TempDir(), "ensure"), 0644, 0755); err != nil {
if err := ensureFile(path.Join(t.TempDir(), "ensure"), 0644, 0755); err != nil {
t.Errorf("ensureFile: error = %v", err)
}
})
@@ -115,7 +115,7 @@ func TestEnsureFile(t *testing.T) {
t.Run("stat", func(t *testing.T) {
t.Run("inaccessible", func(t *testing.T) {
tempDir := t.TempDir()
pathname := filepath.Join(tempDir, "inaccessible")
pathname := path.Join(tempDir, "inaccessible")
if f, err := os.Create(pathname); err != nil {
t.Fatalf("Create: error = %v", err)
} else {
@@ -150,7 +150,7 @@ func TestEnsureFile(t *testing.T) {
t.Run("ensure", func(t *testing.T) {
tempDir := t.TempDir()
pathname := filepath.Join(tempDir, "ensure")
pathname := path.Join(tempDir, "ensure")
if f, err := os.Create(pathname); err != nil {
t.Fatalf("Create: error = %v", err)
} else {
@@ -195,12 +195,12 @@ func TestProcPaths(t *testing.T) {
t.Run("sample", func(t *testing.T) {
tempDir := t.TempDir()
if err := os.MkdirAll(filepath.Join(tempDir, "proc/self"), 0755); err != nil {
if err := os.MkdirAll(path.Join(tempDir, "proc/self"), 0755); err != nil {
t.Fatalf("MkdirAll: error = %v", err)
}
t.Run("clean", func(t *testing.T) {
if err := os.WriteFile(filepath.Join(tempDir, "proc/self/mountinfo"), []byte(`15 20 0:3 / /proc rw,relatime - proc /proc rw
if err := os.WriteFile(path.Join(tempDir, "proc/self/mountinfo"), []byte(`15 20 0:3 / /proc rw,relatime - proc /proc rw
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755`), 0644); err != nil {
t.Fatalf("WriteFile: error = %v", err)
@@ -243,8 +243,8 @@ func TestProcPaths(t *testing.T) {
})
t.Run("malformed", func(t *testing.T) {
filepath.Join(tempDir, "proc/self/mountinfo")
if err := os.WriteFile(filepath.Join(tempDir, "proc/self/mountinfo"), []byte{0}, 0644); err != nil {
path.Join(tempDir, "proc/self/mountinfo")
if err := os.WriteFile(path.Join(tempDir, "proc/self/mountinfo"), []byte{0}, 0644); err != nil {
t.Fatalf("WriteFile: error = %v", err)
}

View File

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

View File

@@ -1,11 +1,11 @@
#compdef hakurei
_hakurei_run() {
_hakurei_app() {
__hakurei_files
return $?
}
_hakurei_exec() {
_hakurei_run() {
_arguments \
'--id[Reverse-DNS style Application identifier, leave empty to inherit instance identifier]:id' \
'-a[Application identity]: :_numbers' \
@@ -57,9 +57,9 @@ __hakurei_instances() {
{
local -a _hakurei_cmds
_hakurei_cmds=(
"run:Load and start container from configuration file"
"exec:Configure and start a permissive container"
"show:Show live or local instance configuration"
"app:Load and start container from configuration file"
"run:Configure and start a permissive container"
"show:Show live or local app configuration"
"ps:List active instances"
"version:Display version information"
"license:Show full license text"

1
dist/hsurc.default vendored Normal file
View File

@@ -0,0 +1 @@
1000 0

12
dist/install.sh vendored Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
cd "$(dirname -- "$0")" || exit 1
install -vDm0755 "bin/hakurei" "${DESTDIR}/usr/bin/hakurei"
install -vDm0755 "bin/sharefs" "${DESTDIR}/usr/bin/sharefs"
install -vDm4511 "bin/hsu" "${DESTDIR}/usr/bin/hsu"
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
fi
install -vDm0644 "comp/_hakurei" "${DESTDIR}/usr/share/zsh/site-functions/_hakurei"

31
dist/release.sh vendored Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/sh -e
cd "$(dirname -- "$0")/.."
VERSION="${HAKUREI_VERSION:-untagged}"
pname="hakurei-${VERSION}-$(go env GOARCH)"
out="${DESTDIR:-dist}/${pname}"
echo '# Preparing distribution files.'
mkdir -p "${out}"
cp -v "README.md" "dist/hsurc.default" "dist/install.sh" "${out}"
cp -rv "dist/comp" "${out}"
echo
echo '# Building hakurei.'
go generate ./...
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w
-buildid= -linkmode external -extldflags=-static
-X hakurei.app/internal/info.buildVersion=${VERSION}
-X hakurei.app/internal/info.hakureiPath=/usr/bin/hakurei
-X hakurei.app/internal/info.hsuPath=/usr/bin/hsu
-X main.hakureiPath=/usr/bin/hakurei" ./...
echo
echo '# Testing hakurei.'
go test -ldflags='-buildid= -linkmode external -extldflags=-static' ./...
echo
echo '# Creating distribution.'
rm -f "${out}.tar.gz" && tar -C "${out}/.." -vczf "${out}.tar.gz" "${pname}"
rm -rf "${out}"
(cd "${out}/.." && sha512sum "${pname}.tar.gz" > "${pname}.tar.gz.sha512")
echo

View File

@@ -137,10 +137,11 @@
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}" \
./all.sh
./dist/release.sh
'';
}
);
@@ -195,7 +196,6 @@
./test/interactive/vm.nix
./test/interactive/hakurei.nix
./test/interactive/trace.nix
./test/interactive/raceattr.nix
self.nixosModules.hakurei
home-manager.nixosModules.home-manager

View File

@@ -140,29 +140,21 @@ 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(flags int) error {
const step = "validate configuration"
func (config *Config) Validate() error {
if config == nil {
return &AppError{Step: step, Err: ErrConfigNull,
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "invalid configuration"}
}
// this is checked again in hsu
if config.Identity < IdentityStart || config.Identity > IdentityEnd {
return &AppError{Step: step, Err: ErrIdentityBounds,
return &AppError{Step: "validate configuration", Err: ErrIdentityBounds,
Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
}
if config.SchedPolicy < 0 || config.SchedPolicy > ext.SCHED_LAST {
return &AppError{Step: step, Err: ErrSchedPolicyBounds,
return &AppError{Step: "validate configuration", Err: ErrSchedPolicyBounds,
Msg: "scheduling policy " +
strconv.Itoa(int(config.SchedPolicy)) +
" out of range"}
@@ -176,51 +168,34 @@ func (config *Config) Validate(flags int) error {
}
if config.Container == nil {
return &AppError{Step: step, Err: ErrConfigNull,
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "configuration missing container state"}
}
if config.Container.Home == nil {
return &AppError{Step: step, Err: ErrConfigNull,
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "container configuration missing path to home directory"}
}
if config.Container.Shell == nil {
return &AppError{Step: step, Err: ErrConfigNull,
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "container configuration missing path to shell"}
}
if config.Container.Path == nil {
return &AppError{Step: step, Err: ErrConfigNull,
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "container configuration missing path to initial program"}
}
for key := range config.Container.Env {
if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 {
return &AppError{Step: step, Err: ErrEnviron,
return &AppError{Step: "validate configuration", Err: ErrEnviron,
Msg: "invalid environment variable " + strconv.Quote(key)}
}
}
et := config.Enablements.Unwrap()
if !config.DirectPulse && et&EPulse != 0 {
return &AppError{Step: step, Err: ErrInsecure,
if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
return &AppError{Step: "validate configuration", Err: ErrInsecure,
Msg: "enablement PulseAudio is insecure and no longer supported"}
}
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
}

View File

@@ -14,109 +14,65 @@ func TestConfigValidate(t *testing.T) {
testCases := []struct {
name string
config *hst.Config
flags int
wantErr error
}{
{"nil", nil, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
{"nil", nil, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "invalid configuration"}},
{"identity lower", &hst.Config{Identity: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
{"identity lower", &hst.Config{Identity: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
Msg: "identity -1 out of range"}},
{"identity upper", &hst.Config{Identity: 10000}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
{"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
Msg: "identity 10000 out of range"}},
{"sched lower", &hst.Config{SchedPolicy: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
{"sched lower", &hst.Config{SchedPolicy: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
Msg: "scheduling policy -1 out of range"}},
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
Msg: "scheduling policy 51966 out of range"}},
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}}, 0,
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}},
&hst.BadInterfaceError{Interface: "", Segment: "session"}},
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}}, 0,
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}},
&hst.BadInterfaceError{Interface: "", Segment: "system"}},
{"container", &hst.Config{}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
{"container", &hst.Config{}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "configuration missing container state"}},
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to home directory"}},
{"shell", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to shell"}},
{"path", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to initial program"}},
{"env equals", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
Env: map[string]string{"TERM=": ""},
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
Msg: `invalid environment variable "TERM="`}},
{"env NUL", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
Env: map[string]string{"TERM\x00": ""},
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
Msg: `invalid environment variable "TERM\x00"`}},
{"insecure pulse", &hst.Config{Enablements: new(hst.EPulse), Container: &hst.ContainerConfig{
{"insecure pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "enablement PulseAudio is insecure and no longer supported"}},
{"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,
}}, 0, nil},
}}, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if err := tc.config.Validate(tc.flags); !reflect.DeepEqual(err, tc.wantErr) {
if err := tc.config.Validate(); !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr)
}
})

View File

@@ -7,12 +7,12 @@ import (
"syscall"
)
// Enablements denotes optional host service to export to the target user.
type Enablements byte
// Enablement represents an optional host service to export to the target user.
type Enablement byte
const (
// EWayland exposes a Wayland pathname socket via security-context-v1.
EWayland Enablements = 1 << iota
EWayland Enablement = 1 << iota
// EX11 adds the target user via X11 ChangeHosts and exposes the X11
// pathname socket.
EX11
@@ -28,8 +28,8 @@ const (
EM
)
// String returns a string representation of the flags set on [Enablements].
func (e Enablements) String() string {
// String returns a string representation of the flags set on [Enablement].
func (e Enablement) String() string {
switch e {
case 0:
return "(no enablements)"
@@ -47,7 +47,7 @@ func (e Enablements) String() string {
buf := new(strings.Builder)
buf.Grow(32)
for i := Enablements(1); i < EM; i <<= 1 {
for i := Enablement(1); i < EM; i <<= 1 {
if e&i != 0 {
buf.WriteString(", " + i.String())
}
@@ -60,6 +60,12 @@ func (e Enablements) String() string {
}
}
// NewEnablements returns the address of [Enablement] as [Enablements].
func NewEnablements(e Enablement) *Enablements { return (*Enablements)(&e) }
// Enablements is the [json] adapter for [Enablement].
type Enablements Enablement
// enablementsJSON is the [json] representation of [Enablements].
type enablementsJSON = struct {
Wayland bool `json:"wayland,omitempty"`
@@ -69,21 +75,24 @@ type enablementsJSON = struct {
Pulse bool `json:"pulse,omitempty"`
}
// Unwrap returns the value pointed to by e.
func (e *Enablements) Unwrap() Enablements {
// Unwrap returns the underlying [Enablement].
func (e *Enablements) Unwrap() Enablement {
if e == nil {
return 0
}
return *e
return Enablement(*e)
}
func (e Enablements) MarshalJSON() ([]byte, error) {
func (e *Enablements) MarshalJSON() ([]byte, error) {
if e == nil {
return nil, syscall.EINVAL
}
return json.Marshal(&enablementsJSON{
Wayland: e&EWayland != 0,
X11: e&EX11 != 0,
DBus: e&EDBus != 0,
PipeWire: e&EPipeWire != 0,
Pulse: e&EPulse != 0,
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,
})
}
@@ -97,21 +106,22 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
return err
}
*e = 0
var ve Enablement
if v.Wayland {
*e |= EWayland
ve |= EWayland
}
if v.X11 {
*e |= EX11
ve |= EX11
}
if v.DBus {
*e |= EDBus
ve |= EDBus
}
if v.PipeWire {
*e |= EPipeWire
ve |= EPipeWire
}
if v.Pulse {
*e |= EPulse
ve |= EPulse
}
*e = Enablements(ve)
return nil
}

View File

@@ -13,7 +13,7 @@ func TestEnablementString(t *testing.T) {
t.Parallel()
testCases := []struct {
flags hst.Enablements
flags hst.Enablement
want string
}{
{0, "(no enablements)"},
@@ -59,13 +59,13 @@ func TestEnablements(t *testing.T) {
sData string
}{
{"nil", nil, "null", `{"value":null,"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}`},
{"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}`},
}
for _, tc := range testCases {
@@ -137,7 +137,7 @@ func TestEnablements(t *testing.T) {
})
t.Run("val", func(t *testing.T) {
if got := new(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
if got := hst.NewEnablements(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
t.Errorf("Unwrap: %v", got)
}
})
@@ -146,6 +146,9 @@ 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)
}

View File

@@ -56,10 +56,8 @@ type Ops interface {
// ApplyState holds the address of [Ops] and any relevant application state.
type ApplyState struct {
// Prefix for [FSBind] in autoetc [FSBind.Special] condition.
// AutoEtcPrefix is the prefix for [FSBind] in autoetc [FSBind.Special] condition.
AutoEtcPrefix string
// Whether to skip remounting root.
NoRemountRoot bool
Ops
}

View File

@@ -2,7 +2,7 @@ package hst
import (
"encoding/gob"
"path/filepath"
"path"
"hakurei.app/check"
)
@@ -28,7 +28,7 @@ func (l *FSLink) Valid() bool {
if l == nil || l.Target == nil || l.Linkname == "" {
return false
}
return !l.Dereference || filepath.IsAbs(l.Linkname)
return !l.Dereference || path.IsAbs(l.Linkname)
}
func (l *FSLink) Path() *check.Absolute {

View File

@@ -5,7 +5,6 @@ import (
"strings"
"hakurei.app/check"
"hakurei.app/fhs"
)
func init() { gob.Register(new(FSOverlay)) }
@@ -70,12 +69,9 @@ func (o *FSOverlay) Apply(z *ApplyState) {
return
}
if o.Upper != nil && o.Work != nil {
if o.Upper != nil && o.Work != nil { // rw
z.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
if o.Target.Is(fhs.AbsRoot) {
z.NoRemountRoot = true
}
} else {
} else { // ro
z.OverlayReadonly(o.Target, o.Lower...)
}
}

View File

@@ -49,18 +49,5 @@ func TestFSOverlay(t *testing.T) {
Lower: ms("/tmp/.src0", "/tmp/.src1"),
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
"*/mnt/src:/tmp/.src0:/tmp/.src1"},
{"no remount root", &hst.FSOverlay{
Target: m("/"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
Upper: m("/tmp/upper"),
Work: m("/tmp/work"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
Upper: m("/tmp/upper"),
Work: m("/tmp/work"),
}}, m("/"), ms("/tmp/upper", "/tmp/work", "/tmp/.src0", "/tmp/.src1"),
"w*/:/tmp/upper:/tmp/work:/tmp/.src0:/tmp/.src1"},
})
}

View File

@@ -72,7 +72,7 @@ func Template() *Config {
return &Config{
ID: "org.chromium.Chromium",
Enablements: new(EWayland | EDBus | EPipeWire),
Enablements: NewEnablements(EWayland | EDBus | EPipeWire),
SessionBus: &BusConfig{
See: nil,

View File

@@ -8,14 +8,12 @@ import (
"io"
"os"
"os/exec"
"path/filepath"
"path"
"reflect"
"strconv"
"syscall"
"testing"
"hakurei.app/internal/acl"
"hakurei.app/internal/info"
)
const testFileName = "acl.test"
@@ -26,17 +24,11 @@ var (
)
func TestUpdate(t *testing.T) {
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)
}
if os.Getenv("HAKUREI_TEST_SKIP_ACL") == "1" {
t.Skip("acl test skipped")
}
testFilePath := filepath.Join(t.TempDir(), testFileName)
testFilePath := path.Join(t.TempDir(), testFileName)
if f, err := os.Create(testFilePath); err != nil {
t.Fatalf("Create: error = %v", err)

View File

@@ -1,7 +0,0 @@
//go:build !noskip
package info
// CanDegrade is whether tests are allowed to transparently degrade or skip due
// to required system features being denied or unavailable.
const CanDegrade = true

View File

@@ -1,5 +0,0 @@
//go:build noskip
package info
const CanDegrade = false

View File

@@ -1,90 +0,0 @@
package kobject
import (
"errors"
"strconv"
"strings"
"unsafe"
"hakurei.app/internal/uevent"
)
// Event is a [uevent.Message] with known environment variables processed.
type Event struct {
// alloc_uevent_skb: action_string
Action uevent.KobjectAction `json:"action"`
// alloc_uevent_skb: devpath
DevPath string `json:"devpath"`
// Uninterpreted environment variable pairs. An entry missing a separator
// gains the value "\x00".
Env map[string]string `json:"env"`
// SEQNUM value set by the kernel.
Sequence uint64 `json:"seqnum"`
// SYNTH_UUID value set on trigger, nil denotes a non-synthetic event.
Synth *uevent.UUID `json:"synth_uuid,omitempty"`
// SUBSYSTEM value set by the kernel.
Subsystem string `json:"subsystem"`
}
// Populate populates e with the contents of a [uevent.Message].
//
// The ACTION and DEVPATH environment variables are ignored and assumed to be
// consistent with the header.
func (e *Event) Populate(reportErr func(error), m *uevent.Message) {
if reportErr == nil {
reportErr = func(error) {}
}
*e = Event{
Action: m.Action,
DevPath: m.DevPath,
Env: make(map[string]string),
}
for _, s := range m.Env {
k, v, ok := strings.Cut(s, "=")
if !ok {
if _, ok = e.Env[s]; !ok {
e.Env[s] = "\x00"
}
continue
}
switch k {
case "ACTION", "DEVPATH":
continue
case "SEQNUM":
seq, err := strconv.ParseUint(v, 10, 64)
if err != nil {
if _e := errors.Unwrap(err); _e != nil {
err = _e
}
reportErr(err)
e.Env[k] = v
continue
}
e.Sequence = seq
case "SYNTH_UUID":
var uuid uevent.UUID
err := uuid.UnmarshalText(unsafe.Slice(unsafe.StringData(v), len(v)))
if err != nil {
reportErr(err)
e.Env[k] = v
continue
}
e.Synth = &uuid
case "SUBSYSTEM":
e.Subsystem = v
default:
e.Env[k] = v
}
}
}

View File

@@ -1,92 +0,0 @@
package kobject_test
import (
"reflect"
"strconv"
"testing"
"hakurei.app/internal/kobject"
"hakurei.app/internal/uevent"
)
func TestEvent(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
msg uevent.Message
want kobject.Event
errs []error
}{
{"sample coldboot qemu", uevent.Message{
Action: uevent.KOBJ_ADD,
DevPath: "/devices/LNXSYSTM:00/LNXPWRBN:00",
Env: []string{
"ACTION=add",
"DEVPATH=/devices/LNXSYSTM:00/LNXPWRBN:00",
"SUBSYSTEM=acpi",
"SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed",
"MODALIAS=acpi:LNXPWRBN:",
"SEQNUM=777",
}}, kobject.Event{
Action: uevent.KOBJ_ADD,
DevPath: "/devices/LNXSYSTM:00/LNXPWRBN:00",
Env: map[string]string{
"MODALIAS": "acpi:LNXPWRBN:",
},
Sequence: 777,
Synth: &uevent.UUID{
0xfe, 0x4d, 0x7c, 0x9d,
0xb8, 0xc6,
0x4a, 0x70,
0x9e, 0xf1,
0x3d, 0x8a, 0x58, 0xd1, 0x8e, 0xed,
},
Subsystem: "acpi",
}, []error{}},
{"nil reportErr", uevent.Message{Env: []string{
"SEQNUM=\x00",
}}, kobject.Event{Env: map[string]string{
"SEQNUM": "\x00",
}}, nil},
{"bad SEQNUM SYNTH_UUID", uevent.Message{Env: []string{
"SEQNUM=\x00",
"SYNTH_UUID=\x00",
"SUBSYSTEM=\x00",
}}, kobject.Event{Subsystem: "\x00", Env: map[string]string{
"SEQNUM": "\x00",
"SYNTH_UUID": "\x00",
}}, []error{strconv.ErrSyntax, uevent.UUIDSizeError(1)}},
{"bad sep", uevent.Message{Env: []string{
"SYNTH_UUID",
}}, kobject.Event{Env: map[string]string{
"SYNTH_UUID": "\x00",
}}, []error{}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var f func(error)
gotErrs := make([]error, 0)
if tc.errs != nil {
f = func(err error) {
gotErrs = append(gotErrs, err)
}
}
var got kobject.Event
got.Populate(f, &tc.msg)
if !reflect.DeepEqual(&got, &tc.want) {
t.Errorf("Populate: %#v, want %#v", got, tc.want)
}
if tc.errs != nil && !reflect.DeepEqual(gotErrs, tc.errs) {
t.Errorf("Populate: errs = %v, want %v", gotErrs, tc.errs)
}
})
}
}

View File

@@ -1,65 +0,0 @@
package landlock_test
import (
"testing"
"unsafe"
"hakurei.app/internal/landlock"
)
func TestLandlockString(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
rulesetAttr *landlock.RulesetAttr
want string
}{
{"nil", nil, "NULL"},
{"zero", new(landlock.RulesetAttr), "0"},
{"some", &landlock.RulesetAttr{Scoped: landlock.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
{"set", &landlock.RulesetAttr{
HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_MAKE_SYM | landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV | landlock.LANDLOCK_ACCESS_FS_WRITE_FILE,
HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP,
Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | landlock.LANDLOCK_SCOPE_SIGNAL,
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
{"all", &landlock.RulesetAttr{
HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_EXECUTE |
landlock.LANDLOCK_ACCESS_FS_WRITE_FILE |
landlock.LANDLOCK_ACCESS_FS_READ_FILE |
landlock.LANDLOCK_ACCESS_FS_READ_DIR |
landlock.LANDLOCK_ACCESS_FS_REMOVE_DIR |
landlock.LANDLOCK_ACCESS_FS_REMOVE_FILE |
landlock.LANDLOCK_ACCESS_FS_MAKE_CHAR |
landlock.LANDLOCK_ACCESS_FS_MAKE_DIR |
landlock.LANDLOCK_ACCESS_FS_MAKE_REG |
landlock.LANDLOCK_ACCESS_FS_MAKE_SOCK |
landlock.LANDLOCK_ACCESS_FS_MAKE_FIFO |
landlock.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
landlock.LANDLOCK_ACCESS_FS_MAKE_SYM |
landlock.LANDLOCK_ACCESS_FS_REFER |
landlock.LANDLOCK_ACCESS_FS_TRUNCATE |
landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV,
HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP |
landlock.LANDLOCK_ACCESS_NET_CONNECT_TCP,
Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
landlock.LANDLOCK_SCOPE_SIGNAL,
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.rulesetAttr.String(); got != tc.want {
t.Errorf("String: %s, want %s", got, tc.want)
}
})
}
}
func TestLandlockAttrSize(t *testing.T) {
t.Parallel()
want := 24
if got := unsafe.Sizeof(landlock.RulesetAttr{}); got != uintptr(want) {
t.Errorf("Sizeof: %d, want %d", got, want)
}
}

View File

@@ -2,32 +2,29 @@
package netlink
import (
"context"
"fmt"
"os"
"sync"
"syscall"
"time"
"unsafe"
)
// net/netlink/af_netlink.c
const maxRecvmsgLen = 32768
const (
// stateOpen denotes an open conn.
stateOpen uint32 = 1 << iota
// AF_NETLINK socket is never shared
var (
nlPid uint32
nlPidOnce sync.Once
)
// A Conn represents resources associated to a netlink socket.
type Conn struct {
// getpid returns a cached pid value.
func getpid() uint32 {
nlPidOnce.Do(func() { nlPid = uint32(os.Getpid()) })
return nlPid
}
// A conn represents resources associated to a netlink socket.
type conn struct {
// AF_NETLINK socket.
f *os.File
// For using runtime polling via f.
raw syscall.RawConn
// Port ID assigned by the kernel.
port uint32
// Internal connection status.
state uint32
fd int
// Kernel module or netlink group to communicate with.
family int
// Message sequence number.
@@ -36,193 +33,40 @@ type Conn struct {
typ, flags uint16
// Outgoing position in buf.
pos int
// Pages holding incoming and outgoing messages.
buf [maxRecvmsgLen]byte
// An instant some time after conn was established, but before the first
// I/O operation on f through raw. This serves as a cached deadline to
// cancel blocking I/O.
t time.Time
// A page holding incoming and outgoing messages.
buf []byte
}
// Dial returns the address of a newly connected generic netlink connection of
// specified family and groups.
//
// For a nonzero rcvbuf, the socket receive buffer size is set to its absolute
// value via SO_RCVBUF for a positive value, or SO_RCVBUFFORCE for a negative
// value.
func Dial(family int, groups uint32, rcvbuf int64) (*Conn, error) {
var c Conn
// dial returns the address of a newly connected conn of specified family.
func dial(family int) (*conn, error) {
var c conn
if fd, err := syscall.Socket(
syscall.AF_NETLINK,
syscall.SOCK_RAW|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC,
syscall.SOCK_RAW|syscall.SOCK_CLOEXEC,
family,
); err != nil {
return nil, os.NewSyscallError("socket", err)
} else if err = syscall.Bind(fd, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
Groups: groups,
Pid: getpid(),
}); err != nil {
_ = syscall.Close(fd)
return nil, os.NewSyscallError("bind", err)
} else {
var addr syscall.Sockaddr
if addr, err = syscall.Getsockname(fd); err != nil {
_ = syscall.Close(fd)
return nil, os.NewSyscallError("getsockname", err)
}
switch a := addr.(type) {
case *syscall.SockaddrNetlink:
c.port = a.Pid
default: // unreachable
_ = syscall.Close(fd)
return nil, syscall.ENOTRECOVERABLE
}
if rcvbuf != 0 {
opt := syscall.SO_RCVBUF
if rcvbuf < 0 {
opt = syscall.SO_RCVBUFFORCE
rcvbuf = -rcvbuf
}
if err = syscall.SetsockoptInt(
fd,
syscall.SOL_SOCKET,
opt,
int(rcvbuf),
); err != nil {
_ = syscall.Close(fd)
return nil, os.NewSyscallError("setsockopt", err)
}
}
c.family = family
c.f = os.NewFile(uintptr(fd), "netlink")
if c.raw, err = c.f.SyscallConn(); err != nil {
_ = c.f.Close()
return nil, err
}
c.state |= stateOpen
c.fd, c.family = fd, family
}
c.pos = syscall.NLMSG_HDRLEN
c.t = time.Now().UTC()
c.buf = make([]byte, os.Getpagesize())
return &c, nil
}
// ok returns whether conn is still open.
func (c *Conn) ok() bool { return c.state&stateOpen != 0 }
// Close closes the underlying socket.
func (c *Conn) Close() error {
if !c.ok() {
func (c *conn) Close() error {
if c.buf == nil {
return syscall.EINVAL
}
c.state &= ^stateOpen
return c.f.Close()
}
// Recvmsg wraps recv(2) with nonblocking behaviour via the runtime network poller.
//
// The returned slice is valid until the next call to Recvmsg.
func (c *Conn) Recvmsg(
ctx context.Context,
flags int,
) (data []byte, recvflags int, from syscall.Sockaddr, err error) {
if err = c.f.SetReadDeadline(time.Time{}); err != nil {
return
}
var n int
data = c.buf[:]
if ctx == nil {
rcErr := c.raw.Control(func(fd uintptr) {
n, _, recvflags, from, err = syscall.Recvmsg(int(fd), data, nil, flags)
})
if n >= 0 {
data = data[:n]
}
if err != nil {
err = os.NewSyscallError("recvmsg", err)
} else {
err = rcErr
}
return
}
done := make(chan error, 1)
go func() {
rcErr := c.raw.Read(func(fd uintptr) (done bool) {
n, _, recvflags, from, err = syscall.Recvmsg(int(fd), data, nil, flags)
return err != syscall.EWOULDBLOCK
})
if n >= 0 {
data = data[:n]
}
done <- rcErr
}()
select {
case rcErr := <-done:
if err != nil {
err = os.NewSyscallError("recvmsg", err)
} else {
err = rcErr
}
return
case <-ctx.Done():
cancelErr := c.f.SetReadDeadline(c.t)
<-done
if cancelErr != nil {
err = cancelErr
} else {
err = ctx.Err()
}
return
}
}
// Sendmsg wraps send(2) with nonblocking behaviour via the runtime network poller.
func (c *Conn) Sendmsg(
ctx context.Context,
p []byte,
to syscall.Sockaddr,
flags int,
) (err error) {
if err = c.f.SetWriteDeadline(time.Time{}); err != nil {
return
}
done := make(chan error, 1)
go func() {
done <- c.raw.Write(func(fd uintptr) (done bool) {
err = syscall.Sendmsg(int(fd), p, nil, to, flags)
return err != syscall.EWOULDBLOCK
})
}()
select {
case rcErr := <-done:
if err != nil {
err = os.NewSyscallError("sendmsg", err)
} else {
err = rcErr
}
return
case <-ctx.Done():
cancelErr := c.f.SetWriteDeadline(c.t)
<-done
if cancelErr != nil {
err = cancelErr
} else {
err = ctx.Err()
}
return
}
c.buf = nil
return syscall.Close(c.fd)
}
// Msg is type constraint for types sent over the wire via netlink.
@@ -244,7 +88,7 @@ func As[M Msg](data []byte) *M {
}
// add queues a value to be sent by conn.
func add[M Msg](c *Conn, p *M) bool {
func add[M Msg](c *conn, p *M) bool {
pos := c.pos
c.pos += int(unsafe.Sizeof(*p))
if c.pos > len(c.buf) {
@@ -278,16 +122,8 @@ func (e *InconsistentError) Error() string {
return s
}
// checkReply checks the message header of a reply from the kernel.
func (c *Conn) checkReply(header *syscall.NlMsghdr) error {
if header.Seq != c.seq || header.Pid != c.port {
return &InconsistentError{*header, c.seq, c.port}
}
return nil
}
// pending returns the valid slice of buf and initialises pos.
func (c *Conn) pending() []byte {
func (c *conn) pending() []byte {
buf := c.buf[:c.pos]
c.pos = syscall.NLMSG_HDRLEN
@@ -296,7 +132,7 @@ func (c *Conn) pending() []byte {
Type: c.typ,
Flags: c.flags,
Seq: c.seq,
Pid: c.port,
Pid: getpid(),
}
return buf
}
@@ -313,18 +149,32 @@ type HandlerFunc func(resp []syscall.NetlinkMessage) error
// receive receives from a socket with specified flags until a non-nil error is
// returned by f. An error of type [Complete] is returned as nil.
func (c *Conn) receive(ctx context.Context, f HandlerFunc, flags int) error {
func (c *conn) receive(
f HandlerFunc,
flags int,
) error {
for {
var resp []syscall.NetlinkMessage
if data, _, _, err := c.Recvmsg(ctx, flags); err != nil {
return err
} else if len(data) < syscall.NLMSG_HDRLEN {
buf := c.buf
if n, _, err := syscall.Recvfrom(c.fd, buf, flags); err != nil {
return os.NewSyscallError("recvfrom", err)
} else if n < syscall.NLMSG_HDRLEN {
return syscall.EBADE
} else if resp, err = syscall.ParseNetlinkMessage(data); err != nil {
} else {
buf = buf[:n]
}
resp, err := syscall.ParseNetlinkMessage(buf)
if err != nil {
return err
}
if err := f(resp); err != nil {
for i := range resp {
header := &resp[i].Header
if header.Seq != c.seq || header.Pid != getpid() {
return &InconsistentError{*header, c.seq, getpid()}
}
}
if err = f(resp); err != nil {
if err == (Complete{}) {
return nil
}
@@ -334,17 +184,17 @@ func (c *Conn) receive(ctx context.Context, f HandlerFunc, flags int) error {
}
// Roundtrip sends the pending message and handles the reply.
func (c *Conn) Roundtrip(ctx context.Context, f HandlerFunc) error {
if !c.ok() {
func (c *conn) Roundtrip(f HandlerFunc) error {
if c.buf == nil {
return syscall.EINVAL
}
defer func() { c.seq++ }()
if err := c.Sendmsg(ctx, c.pending(), &syscall.SockaddrNetlink{
if err := syscall.Sendto(c.fd, c.pending(), 0, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
}, 0); err != nil {
return err
}); err != nil {
return os.NewSyscallError("sendto", err)
}
return c.receive(ctx, f, 0)
return c.receive(f, 0)
}

View File

@@ -1,13 +1,16 @@
package netlink
import (
"os"
"syscall"
"testing"
)
func init() { nlPidOnce.Do(func() {}); nlPid = 1 }
type payloadTestCase struct {
name string
f func(c *Conn)
f func(c *conn)
want []byte
}
@@ -19,9 +22,11 @@ func checkPayload(t *testing.T, testCases []payloadTestCase) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Helper()
c := Conn{port: 1, pos: syscall.NLMSG_HDRLEN}
c := conn{
pos: syscall.NLMSG_HDRLEN,
buf: make([]byte, os.Getpagesize()),
}
tc.f(&c)
if got := c.pending(); string(got) != string(tc.want) {
t.Errorf("pending: %#v, want %#v", got, tc.want)

View File

@@ -1,20 +1,16 @@
package netlink
import (
"context"
"syscall"
"unsafe"
)
// RouteConn represents a NETLINK_ROUTE socket.
type RouteConn struct{ conn *Conn }
// Close closes the underlying socket.
func (c *RouteConn) Close() error { return c.conn.Close() }
type RouteConn struct{ *conn }
// DialRoute returns the address of a newly connected [RouteConn].
func DialRoute(rcvbuf int64) (*RouteConn, error) {
c, err := Dial(syscall.NETLINK_ROUTE, 0, rcvbuf)
func DialRoute() (*RouteConn, error) {
c, err := dial(syscall.NETLINK_ROUTE)
if err != nil {
return nil, err
}
@@ -22,12 +18,8 @@ func DialRoute(rcvbuf int64) (*RouteConn, error) {
}
// rtnlConsume consumes a message from rtnetlink.
func (c *RouteConn) rtnlConsume(resp []syscall.NetlinkMessage) error {
func rtnlConsume(resp []syscall.NetlinkMessage) error {
for i := range resp {
if err := c.conn.checkReply(&resp[i].Header); err != nil {
return err
}
switch resp[i].Header.Type {
case syscall.NLMSG_DONE:
return Complete{}
@@ -65,7 +57,7 @@ func (c *RouteConn) writeIfAddrmsg(
msg *syscall.IfAddrmsg,
attrs ...RtAttrMsg[InAddr],
) bool {
c.conn.typ, c.conn.flags = typ, syscall.NLM_F_REQUEST|syscall.NLM_F_ACK|flags
c.typ, c.flags = typ, syscall.NLM_F_REQUEST|syscall.NLM_F_ACK|flags
if !add(c.conn, msg) {
return false
}
@@ -80,7 +72,6 @@ func (c *RouteConn) writeIfAddrmsg(
// SendIfAddrmsg sends an ifaddrmsg structure to rtnetlink.
func (c *RouteConn) SendIfAddrmsg(
ctx context.Context,
typ, flags uint16,
msg *syscall.IfAddrmsg,
attrs ...RtAttrMsg[InAddr],
@@ -88,7 +79,7 @@ func (c *RouteConn) SendIfAddrmsg(
if !c.writeIfAddrmsg(typ, flags, msg, attrs...) {
return syscall.ENOMEM
}
return c.conn.Roundtrip(ctx, c.rtnlConsume)
return c.Roundtrip(rtnlConsume)
}
// writeNewaddrLo writes a RTM_NEWADDR message for the loopback address.
@@ -113,11 +104,11 @@ func (c *RouteConn) writeNewaddrLo(lo uint32) bool {
}
// SendNewaddrLo sends a RTM_NEWADDR message for the loopback address to the kernel.
func (c *RouteConn) SendNewaddrLo(ctx context.Context, lo uint32) error {
func (c *RouteConn) SendNewaddrLo(lo uint32) error {
if !c.writeNewaddrLo(lo) {
return syscall.ENOMEM
}
return c.conn.Roundtrip(ctx, c.rtnlConsume)
return c.Roundtrip(rtnlConsume)
}
// writeIfInfomsg writes an ifinfomsg structure to conn.
@@ -125,18 +116,17 @@ func (c *RouteConn) writeIfInfomsg(
typ, flags uint16,
msg *syscall.IfInfomsg,
) bool {
c.conn.typ, c.conn.flags = typ, syscall.NLM_F_REQUEST|syscall.NLM_F_ACK|flags
c.typ, c.flags = typ, syscall.NLM_F_REQUEST|syscall.NLM_F_ACK|flags
return add(c.conn, msg)
}
// SendIfInfomsg sends an ifinfomsg structure to rtnetlink.
func (c *RouteConn) SendIfInfomsg(
ctx context.Context,
typ, flags uint16,
msg *syscall.IfInfomsg,
) error {
if !c.writeIfInfomsg(typ, flags, msg) {
return syscall.ENOMEM
}
return c.conn.Roundtrip(ctx, c.rtnlConsume)
return c.Roundtrip(rtnlConsume)
}

View File

@@ -9,7 +9,7 @@ func TestPayloadRTNETLINK(t *testing.T) {
t.Parallel()
checkPayload(t, []payloadTestCase{
{"RTM_NEWADDR lo", func(c *Conn) {
{"RTM_NEWADDR lo", func(c *conn) {
(&RouteConn{c}).writeNewaddrLo(1)
}, []byte{
/* Len */ 0x28, 0, 0, 0,
@@ -33,7 +33,7 @@ func TestPayloadRTNETLINK(t *testing.T) {
/* in_addr */ 127, 0, 0, 1,
}},
{"RTM_NEWLINK", func(c *Conn) {
{"RTM_NEWLINK", func(c *conn) {
c.seq++
(&RouteConn{c}).writeIfInfomsg(
syscall.RTM_NEWLINK, 0,

View File

@@ -17,7 +17,6 @@ import (
"hakurei.app/ext"
"hakurei.app/internal/dbus"
"hakurei.app/internal/info"
"hakurei.app/internal/params"
"hakurei.app/message"
)
@@ -85,7 +84,7 @@ type syscallDispatcher interface {
// setDumpable provides [container.SetDumpable].
setDumpable(dumpable uintptr) error
// receive provides [container.Receive].
receive(key string, e any, fdp *int) (closeFunc func() error, err error)
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// containerStart provides the Start method of [container.Container].
containerStart(z *container.Container) error
@@ -155,8 +154,8 @@ func (direct) prctl(op, arg2, arg3 uintptr) error { return ext.Prctl(op, arg2, a
func (direct) overflowUid(msg message.Msg) int { return container.OverflowUid(msg) }
func (direct) 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 *int) (func() error, error) {
return params.Receive(key, e, fdp)
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return container.Receive(key, e, fdp)
}
func (direct) containerStart(z *container.Container) error { return z.Start() }

View File

@@ -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 *int) (closeFunc func() error, err error) {
func (k *kstub) receive(key string, e any, fdp *uintptr) (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 = int(expect.Args[2].(uintptr))
*fdp = 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, *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) 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) notifyContext(context.Context, ...os.Signal) (context.Context, context.CancelFunc) {
panic("unreachable")

View File

@@ -32,14 +32,7 @@ type outcome struct {
syscallDispatcher
}
// 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 {
func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, config *hst.Config) error {
if ctx == nil || id == nil {
// unreachable
panic("invalid call to finalise")
@@ -50,7 +43,7 @@ func (k *outcome) finalise(
}
k.ctx = ctx
if err := config.Validate(flags); err != nil {
if err := config.Validate(); err != nil {
return err
}

View File

@@ -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.Enablements
et hst.Enablement
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
directWayland bool

View File

@@ -13,6 +13,7 @@ import (
"time"
"hakurei.app/check"
"hakurei.app/container"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/info"
@@ -297,12 +298,12 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
// accumulate enablements of remaining instances
var (
// alive enablement bits
rt hst.Enablements
rt hst.Enablement
// alive instance count
n int
)
for eh := range entries {
var et hst.Enablements
var et hst.Enablement
if et, err = eh.Load(nil); err != nil {
perror(err, "read state header of instance "+eh.ID.String())
} else {
@@ -371,18 +372,17 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
// shim runs in the same session as monitor; see shim.go for behaviour
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
var shimPipe [2]*os.File
if r, w, err := os.Pipe(); err != nil {
var shimPipe *os.File
if fd, w, err := container.Setup(&cmd.ExtraFiles); 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(3+len(cmd.ExtraFiles)),
shimEnv + "=" + strconv.Itoa(fd),
// 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,16 +393,12 @@ 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, nil, &hst.AppError{Step: "start setuid wrapper", Err: err}
}
if err := shimPipe[0].Close(); err != nil {
msg.Verbose(err)
return cmd, shimPipe, &hst.AppError{Step: "start setuid wrapper", Err: err}
}
*startTime = time.Now().UTC()
return cmd, shimPipe[1], nil
return cmd, shimPipe, nil
}
// serveShim serves outcomeState through the shim setup pipe.
@@ -415,11 +411,11 @@ func serveShim(msg message.Msg, shimPipe *os.File, state *outcomeState) error {
msg.Verbose(err.Error())
}
if err := gob.NewEncoder(shimPipe).Encode(state); err != nil {
_ = shimPipe.Close()
msg.Resume()
return &hst.AppError{Step: "transmit shim config", Err: err}
}
return shimPipe.Close()
_ = shimPipe.Close()
return nil
}
// printMessageError prints the error message according to [message.GetMessage],

View File

@@ -18,13 +18,7 @@ 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,
flags int,
fd int,
) {
func Main(ctx context.Context, msg message.Msg, config *hst.Config, fd int) {
// avoids runtime internals or standard streams
if fd >= 0 {
if IsPollDescriptor(uintptr(fd)) || fd < 3 {
@@ -40,7 +34,7 @@ func Main(
k := outcome{syscallDispatcher: direct{msg}}
finaliseTime := time.Now()
if err := k.finalise(ctx, msg, &id, config, flags); err != nil {
if err := k.finalise(ctx, msg, &id, config); err != nil {
printMessageError(msg.GetLogger().Fatalln, "cannot seal app:", err)
panic("unreachable")
}

View File

@@ -288,7 +288,7 @@ func TestOutcomeRun(t *testing.T) {
},
Filter: true,
},
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{
@@ -427,7 +427,7 @@ func TestOutcomeRun(t *testing.T) {
DirectPipeWire: true,
ID: "org.chromium.Chromium",
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{
Env: nil,
Filesystem: []hst.FilesystemConfigJSON{

View File

@@ -20,7 +20,6 @@ import (
"hakurei.app/ext"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/params"
"hakurei.app/internal/pipewire"
"hakurei.app/message"
)
@@ -198,7 +197,7 @@ func shimEntrypoint(k syscallDispatcher) {
if errors.Is(err, syscall.EBADF) {
k.fatal("invalid config descriptor")
}
if errors.Is(err, params.ErrReceiveEnv) {
if errors.Is(err, container.ErrReceiveEnv) {
k.fatal(shimEnv + " not set")
}

View File

@@ -16,7 +16,6 @@ import (
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/env"
"hakurei.app/internal/params"
"hakurei.app/internal/stub"
)
@@ -173,7 +172,7 @@ func TestShimEntrypoint(t *testing.T) {
call("setDumpable", stub.ExpectArgs{uintptr(ext.SUID_DUMP_DISABLE)}, nil, nil),
call("getppid", stub.ExpectArgs{}, 0xbad, nil),
call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil),
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, params.ErrReceiveEnv),
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, container.ErrReceiveEnv),
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SHIM not set"}}, nil, nil),
// deferred

View File

@@ -5,7 +5,7 @@ import (
"errors"
"io/fs"
"os"
"path/filepath"
"path"
"slices"
"strconv"
"syscall"
@@ -165,9 +165,9 @@ func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error {
}
for _, pair := range entry.Values {
if pair[0] == "path" {
if filepath.IsAbs(pair[1]) {
if path.IsAbs(pair[1]) {
// get parent dir of socket
dir := filepath.Dir(pair[1])
dir := path.Dir(pair[1])
if dir == "." || dir == fhs.Root {
state.msg.Verbosef("dbus socket %q is in an unusual location", pair[1])
}
@@ -290,9 +290,7 @@ func (s *spFilesystemOp) toContainer(state *outcomeStateParams) error {
if state.Container.Flags&hst.FDevice == 0 {
state.params.Remount(fhs.AbsDev, syscall.MS_RDONLY)
}
if !state.as.NoRemountRoot {
state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY)
}
state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY)
state.params.Env = make([]string, 0, len(state.env))
for key, value := range state.env {

View File

@@ -21,7 +21,7 @@ func TestSpPulseOp(t *testing.T) {
newConfig := func() *hst.Config {
config := hst.Template()
config.DirectPulse = true
config.Enablements = new(hst.EPulse)
config.Enablements = hst.NewEnablements(hst.EPulse)
return config
}

View File

@@ -1,42 +0,0 @@
// Package params provides helpers for receiving setup payload from parent.
package params
import (
"encoding/gob"
"errors"
"os"
"strconv"
"syscall"
)
// ErrReceiveEnv is returned by [Receive] if setup fd is not present in environment.
var ErrReceiveEnv = errors.New("environment variable not set")
// Receive retrieves setup fd from the environment and receives params.
//
// The file descriptor written to the value pointed to by fdp must not be passed
// to any system calls. It is made available for ordering file descriptor only.
func Receive(key string, v any, fdp *int) (func() error, error) {
var setup *os.File
if s, ok := os.LookupEnv(key); !ok {
return nil, ErrReceiveEnv
} else {
if fd, err := strconv.Atoi(s); err != nil {
if _err := errors.Unwrap(err); _err != nil {
err = _err
}
return nil, err
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
return nil, syscall.EDOM
}
if fdp != nil {
*fdp = fd
}
}
}
return setup.Close, gob.NewDecoder(setup).Decode(v)
}

View File

@@ -20,7 +20,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"path"
"runtime"
"slices"
"strconv"
@@ -973,23 +973,23 @@ func connectName(name string, manager bool) (conn Conn, err error) {
return connectName(name+"-manager", false)
}
if filepath.IsAbs(name) || (len(name) > 0 && name[0] == '@') {
if path.IsAbs(name) || (len(name) > 0 && name[0] == '@') {
return Dial(name)
} else {
runtimeDir, ok := os.LookupEnv("PIPEWIRE_RUNTIME_DIR")
if !ok || !filepath.IsAbs(runtimeDir) {
if !ok || !path.IsAbs(runtimeDir) {
runtimeDir, ok = os.LookupEnv("XDG_RUNTIME_DIR")
}
if !ok || !filepath.IsAbs(runtimeDir) {
if !ok || !path.IsAbs(runtimeDir) {
// this is cargo culted from windows stuff and has no effect normally;
// keeping it to maintain compatibility in case someone sets this
runtimeDir, ok = os.LookupEnv("USERPROFILE")
}
if !ok || !filepath.IsAbs(runtimeDir) {
if !ok || !path.IsAbs(runtimeDir) {
runtimeDir = DEFAULT_SYSTEM_RUNTIME_DIR
}
return Dial(filepath.Join(runtimeDir, name))
return Dial(path.Join(runtimeDir, name))
}
}

View File

@@ -27,31 +27,6 @@ func TestFlatten(t *testing.T) {
fs.ModeCharDevice | 0400,
)},
{"coldboot", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"devices": {Mode: fs.ModeDir | 0700},
"devices/uevent": {Mode: 0600, Data: []byte("add")},
"devices/empty": {Mode: fs.ModeDir | 0700},
"devices/sub": {Mode: fs.ModeDir | 0700},
"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"block": {Mode: fs.ModeDir | 0700},
"block/uevent": {Mode: 0600, Data: []byte{}},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "block"},
{Mode: 0600, Path: "block/uevent", Data: []byte{}},
{Mode: fs.ModeDir | 0700, Path: "devices"},
{Mode: fs.ModeDir | 0700, Path: "devices/empty"},
{Mode: fs.ModeDir | 0700, Path: "devices/sub"},
{Mode: 0600, Path: "devices/sub/uevent", Data: []byte("add")},
{Mode: 0600, Path: "devices/uevent", Data: []byte("add")},
}, pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8"), nil},
{"empty", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
@@ -184,32 +159,6 @@ func TestFlatten(t *testing.T) {
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("WVpvsVqVKg9Nsh744x57h51AuWUoUR2nnh8Md-EYBQpk6ziyTuUn6PLtF2e0Eu_d"), nil},
{"sample no assume checksum", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M": {Mode: fs.ModeDir | 0500},
"checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M/check": {Mode: 0400, Data: []byte{}},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/_wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
"identifier/_wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M"},
{Mode: 0400, Path: "checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M/check", Data: []byte{}},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/_wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/_wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("OC290t23aimNo2Rp2pPwan5GI2KRLRdOwYxXQMD9jw0QROgHnNXWodoWdV0hwu2w"), nil},
{"sample tar step unpack", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500},

View File

@@ -8,7 +8,7 @@ import (
"io"
"os"
"os/exec"
"path/filepath"
"path"
"slices"
"strconv"
"syscall"
@@ -27,11 +27,6 @@ import (
// AbsWork is the container pathname [TContext.GetWorkDir] is mounted on.
var AbsWork = fhs.AbsRoot.Append("work/")
// EnvJobs is the name of the environment variable holding a decimal
// representation of the preferred job count. Its value must not affect cure
// outcome.
const EnvJobs = "CURE_JOBS"
// ExecPath is a slice of [Artifact] and the [check.Absolute] pathname to make
// it available at under in the container.
type ExecPath struct {
@@ -45,14 +40,14 @@ type ExecPath struct {
W bool
}
// GetArtifactFunc is the function signature of [FContext.GetArtifact].
type GetArtifactFunc func(Artifact) (*check.Absolute, unique.Handle[Checksum])
// SetSchedIdle is whether to set [std.SCHED_IDLE] scheduling priority.
var SetSchedIdle bool
// PromoteLayers returns artifacts with identical-by-content layers promoted to
// the highest priority instance, as if mounted via [ExecPath].
func PromoteLayers(
artifacts []Artifact,
getArtifact GetArtifactFunc,
getArtifact func(Artifact) (*check.Absolute, unique.Handle[Checksum]),
report func(i int, d Artifact),
) []*check.Absolute {
layers := make([]*check.Absolute, 0, len(artifacts))
@@ -72,14 +67,14 @@ func PromoteLayers(
}
// layers returns pathnames collected from A deduplicated via [PromoteLayers].
func (p *ExecPath) layers(
msg message.Msg,
getArtifact GetArtifactFunc,
ident func(a Artifact) unique.Handle[ID],
) []*check.Absolute {
return PromoteLayers(p.A, getArtifact, func(i int, d Artifact) {
func (p *ExecPath) layers(f *FContext) []*check.Absolute {
msg := f.GetMessage()
return PromoteLayers(p.A, f.GetArtifact, func(i int, d Artifact) {
if msg.IsVerbose() {
msg.Verbosef("promoted layer %d as %s", i, reportName(d, ident(d)))
msg.Verbosef(
"promoted layer %d as %s",
i, reportName(d, f.cache.Ident(d)),
)
}
})
}
@@ -191,7 +186,7 @@ func NewExec(
paths ...ExecPath,
) Artifact {
if name == "" {
name = "exec-" + filepath.Base(pathname.String())
name = "exec-" + path.Base(pathname.String())
}
if timeout <= 0 {
timeout = ExecTimeoutDefault
@@ -387,31 +382,17 @@ func scanVerbose(
}
}
var (
// ErrInvalidPaths is returned for an [Artifact] of [KindExec] or
// [KindExecNet] specified with invalid paths.
ErrInvalidPaths = errors.New("invalid mount point")
)
// SeccompPresets is the [seccomp] presets used by exec artifacts.
const SeccompPresets = std.PresetStrict &
^(std.PresetDenyNS | std.PresetDenyDevel)
// makeContainer sets up the specified temp and work directories and returns the
// corresponding [container.Container] that would have run for cure.
func (a *execArtifact) makeContainer(
ctx context.Context,
msg message.Msg,
flags, jobs int,
hostNet bool,
temp, work *check.Absolute,
getArtifact GetArtifactFunc,
ident func(a Artifact) unique.Handle[ID],
) (z *container.Container, err error) {
// cure is like Cure but allows optional host net namespace. This is used for
// the [KnownChecksum] variant where networking is allowed.
func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
overlayWorkIndex := -1
for i, p := range a.paths {
if p.P == nil || len(p.A) == 0 {
return nil, ErrInvalidPaths
return os.ErrInvalid
}
if p.P.Is(AbsWork) {
overlayWorkIndex = i
@@ -423,205 +404,29 @@ func (a *execArtifact) makeContainer(
artifactCount += len(p.A)
}
z = container.New(ctx, msg)
ctx, cancel := context.WithTimeout(f.Unwrap(), a.timeout)
defer cancel()
z := container.New(ctx, f.GetMessage())
z.WaitDelay = execWaitDelay
z.SeccompPresets = SeccompPresets
z.SeccompFlags |= seccomp.AllowMultiarch
z.ParentPerm = 0700
z.HostNet = hostNet
z.HostAbstract = flags&CHostAbstract != 0
z.Hostname = "cure"
z.SetScheduler = flags&CSchedIdle != 0
z.SetScheduler = SetSchedIdle
z.SchedPolicy = ext.SCHED_IDLE
if z.HostNet {
z.Hostname = "cure-net"
}
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
z.Dir, z.Path, z.Args = a.dir, a.path, a.args
z.Env = slices.Concat(a.env, []string{EnvJobs + "=" + strconv.Itoa(jobs)})
z.Grow(len(a.paths) + 4)
for i, b := range a.paths {
if i == overlayWorkIndex {
if err = os.MkdirAll(work.String(), 0700); err != nil {
return
}
tempWork := temp.Append(".work")
if err = os.MkdirAll(tempWork.String(), 0700); err != nil {
return
}
z.Overlay(
AbsWork,
work,
tempWork,
b.layers(msg, getArtifact, ident)...,
)
continue
}
if a.paths[i].W {
tempUpper, tempWork := temp.Append(
".upper", strconv.Itoa(i),
), temp.Append(
".work", strconv.Itoa(i),
)
if err = os.MkdirAll(tempUpper.String(), 0700); err != nil {
return
}
if err = os.MkdirAll(tempWork.String(), 0700); err != nil {
return
}
z.Overlay(b.P, tempUpper, tempWork, b.layers(msg, getArtifact, ident)...)
} else if len(b.A) == 1 {
pathname, _ := getArtifact(b.A[0])
z.Bind(pathname, b.P, 0)
} else {
z.OverlayReadonly(b.P, b.layers(msg, getArtifact, ident)...)
}
}
if overlayWorkIndex < 0 {
z.Bind(
work,
AbsWork,
std.BindWritable|std.BindEnsure,
)
}
z.Bind(
temp,
fhs.AbsTmp,
std.BindWritable|std.BindEnsure,
)
z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
return
}
var (
// ErrExecBusy is returned entering [Cache.EnterExec] while another
// goroutine has not yet returned from it.
ErrExecBusy = errors.New("scratch directories in use")
// ErrNotExec is returned for unsupported implementations of [Artifact]
// passed to [Cache.EnterExec].
ErrNotExec = errors.New("attempting to run a non-exec artifact")
)
// EnterExec runs the container of an [Artifact] of [KindExec] or [KindExecNet]
// with its entry point, argument, and standard streams replaced with values
// supplied by the caller.
func (c *Cache) EnterExec(
ctx context.Context,
a Artifact,
retainSession bool,
stdin io.Reader,
stdout, stderr io.Writer,
path *check.Absolute,
args ...string,
) (err error) {
if !c.inExec.CompareAndSwap(false, true) {
return ErrExecBusy
}
defer c.inExec.Store(false)
var hostNet bool
var e *execArtifact
switch f := a.(type) {
case *execArtifact:
e = f
case *execNetArtifact:
e = &f.execArtifact
hostNet = true
default:
return ErrNotExec
}
deps := Collect(a.Dependencies())
if _, _, err = c.Cure(&deps); err == nil {
return errors.New("unreachable")
} else if !IsCollected(err) {
return
}
dm := make(map[Artifact]cureRes)
for i, p := range deps {
var res cureRes
res.pathname, res.checksum, err = c.Cure(p)
if err != nil {
return
}
dm[deps[i]] = res
}
scratch := c.base.Append(dirExecScratch)
temp, work := scratch.Append("temp"), scratch.Append("work")
// work created during makeContainer
if err = os.MkdirAll(temp.String(), 0700); err != nil {
return
}
defer func() {
if chmodErr, removeErr := removeAll(scratch); chmodErr != nil || removeErr != nil {
err = errors.Join(err, chmodErr, removeErr)
}
}()
var z *container.Container
z, err = e.makeContainer(
ctx, c.msg,
c.flags,
c.jobs,
hostNet,
temp, work,
func(a Artifact) (*check.Absolute, unique.Handle[Checksum]) {
if res, ok := dm[a]; ok {
return res.pathname, res.checksum
}
panic(InvalidLookupError(c.Ident(a).Value()))
},
c.Ident,
)
if err != nil {
return
}
z.Stdin, z.Stdout, z.Stderr = stdin, stdout, stderr
z.Path, z.Args = path, args
z.RetainSession = retainSession
if stdin == os.Stdin {
if s, ok := os.LookupEnv("TERM"); ok {
z.Env = append(z.Env, "TERM="+s)
}
}
if err = z.Start(); err != nil {
return
}
if err = z.Serve(); err != nil {
return
}
return z.Wait()
}
// cure is like Cure but allows optional host net namespace.
func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
ctx, cancel := context.WithTimeout(f.Unwrap(), a.timeout)
defer cancel()
msg := f.GetMessage()
var z *container.Container
if z, err = a.makeContainer(
ctx, msg, f.cache.flags, f.GetJobs(), hostNet,
f.GetTempDir(), f.GetWorkDir(),
f.GetArtifact,
f.cache.Ident,
); err != nil {
return
}
var status io.Writer
if status, err = f.GetStatusWriter(); err != nil {
return
}
if msg.IsVerbose() {
if msg := f.GetMessage(); msg.IsVerbose() {
var stdout, stderr io.ReadCloser
if stdout, err = z.StdoutPipe(); err != nil {
return
@@ -659,6 +464,62 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
z.Stdout, z.Stderr = status, status
}
z.Dir, z.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args
z.Grow(len(a.paths) + 4)
temp, work := f.GetTempDir(), f.GetWorkDir()
for i, b := range a.paths {
if i == overlayWorkIndex {
if err = os.MkdirAll(work.String(), 0700); err != nil {
return
}
tempWork := temp.Append(".work")
if err = os.MkdirAll(tempWork.String(), 0700); err != nil {
return
}
z.Overlay(
AbsWork,
work,
tempWork,
b.layers(f)...,
)
continue
}
if a.paths[i].W {
tempUpper, tempWork := temp.Append(
".upper", strconv.Itoa(i),
), temp.Append(
".work", strconv.Itoa(i),
)
if err = os.MkdirAll(tempUpper.String(), 0700); err != nil {
return
}
if err = os.MkdirAll(tempWork.String(), 0700); err != nil {
return
}
z.Overlay(b.P, tempUpper, tempWork, b.layers(f)...)
} else if len(b.A) == 1 {
pathname, _ := f.GetArtifact(b.A[0])
z.Bind(pathname, b.P, 0)
} else {
z.OverlayReadonly(b.P, b.layers(f)...)
}
}
if overlayWorkIndex < 0 {
z.Bind(
work,
AbsWork,
std.BindWritable|std.BindEnsure,
)
}
z.Bind(
f.GetTempDir(),
fhs.AbsTmp,
std.BindWritable|std.BindEnsure,
)
z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
if err = z.Start(); err != nil {
return
}
@@ -671,7 +532,7 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
// do not allow empty directories to succeed
for {
err = syscall.Rmdir(f.GetWorkDir().String())
err = syscall.Rmdir(work.String())
if err != syscall.EINTR {
break
}

View File

@@ -33,7 +33,8 @@ func TestExec(t *testing.T) {
)
checkWithCache(t, []cacheTestCase{
{"offline", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"offline", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{
@@ -91,7 +92,7 @@ func TestExec(t *testing.T) {
[]string{"testtool"},
pkg.ExecPath{},
), nil, pkg.Checksum{}, pkg.ErrInvalidPaths},
), nil, pkg.Checksum{}, os.ErrInvalid},
})
// check init failure passthrough
@@ -110,7 +111,8 @@ func TestExec(t *testing.T) {
testtoolDestroy(t, base, c)
}, pkg.MustDecode("Q5DluWQCAeohLoiGRImurwFp3vdz9IfQCoj7Fuhh73s4KQPRHpEQEnHTdNHmB8Fx")},
{"net", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"net", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
testtool, testtoolDestroy := newTesttool()
wantChecksum := pkg.MustDecode(
@@ -144,7 +146,8 @@ func TestExec(t *testing.T) {
testtoolDestroy(t, base, c)
}, pkg.MustDecode("bPYvvqxpfV7xcC1EptqyKNK1klLJgYHMDkzBcoOyK6j_Aj5hb0mXNPwTwPSK5F6Z")},
{"overlay root", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"overlay root", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{
@@ -169,7 +172,8 @@ func TestExec(t *testing.T) {
testtoolDestroy(t, base, c)
}, pkg.MustDecode("PO2DSSCa4yoSgEYRcCSZfQfwow1yRigL3Ry-hI0RDI4aGuFBha-EfXeSJnG_5_Rl")},
{"overlay work", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"overlay work", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{
@@ -199,7 +203,8 @@ func TestExec(t *testing.T) {
testtoolDestroy(t, base, c)
}, pkg.MustDecode("iaRt6l_Wm2n-h5UsDewZxQkCmjZjyL8r7wv32QT2kyV55-Lx09Dq4gfg9BiwPnKs")},
{"multiple layers", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"multiple layers", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{
@@ -251,7 +256,8 @@ func TestExec(t *testing.T) {
testtoolDestroy(t, base, c)
}, pkg.MustDecode("O2YzyR7IUGU5J2CADy0hUZ3A5NkP_Vwzs4UadEdn2oMZZVWRtH0xZGJ3HXiimTnZ")},
{"overlay layer promotion", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"overlay layer promotion", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{

View File

@@ -11,7 +11,9 @@ func TestFile(t *testing.T) {
t.Parallel()
checkWithCache(t, []cacheTestCase{
{"file", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"file", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
cureMany(t, c, []cureStep{
{"short", pkg.NewFile("null", []byte{0}), base.Append(
"identifier",

View File

@@ -3,6 +3,7 @@ package pkg
import (
"bufio"
"bytes"
"context"
"crypto/sha512"
"encoding/binary"
"errors"
@@ -10,7 +11,6 @@ import (
"io"
"slices"
"strconv"
"sync"
"syscall"
"unique"
"unsafe"
@@ -39,45 +39,22 @@ func panicToError(errP *error) {
}
}
// irCache implements [IRCache].
type irCache struct {
// Artifact to [unique.Handle] of identifier cache.
artifact sync.Map
// Identifier free list, must not be accessed directly.
identPool sync.Pool
}
// zeroIRCache returns the initialised value of irCache.
func zeroIRCache() irCache {
return irCache{
identPool: sync.Pool{New: func() any { return new(extIdent) }},
}
}
// IRCache provides memory management and caching primitives for IR and
// identifier operations against [Artifact] implementations.
//
// The zero value is not safe for use.
type IRCache struct{ irCache }
// NewIR returns the address of a new [IRCache].
func NewIR() *IRCache {
return &IRCache{zeroIRCache()}
}
// IContext is passed to [Artifact.Params] and provides methods for writing
// values to the IR writer. It does not expose the underlying [io.Writer].
//
// IContext is valid until [Artifact.Params] returns.
type IContext struct {
// Address of underlying irCache, should be zeroed or made unusable after
// Address of underlying [Cache], should be zeroed or made unusable after
// [Artifact.Params] returns and must not be exposed directly.
ic *irCache
cache *Cache
// Written to by various methods, should be zeroed after [Artifact.Params]
// returns and must not be exposed directly.
w io.Writer
}
// Unwrap returns the underlying [context.Context].
func (i *IContext) Unwrap() context.Context { return i.cache.ctx }
// irZero is a zero IR word.
var irZero [wordSize]byte
@@ -159,11 +136,11 @@ func (i *IContext) mustWrite(p []byte) {
// WriteIdent is not defined for an [Artifact] not part of the slice returned by
// [Artifact.Dependencies].
func (i *IContext) WriteIdent(a Artifact) {
buf := i.ic.getIdentBuf()
defer i.ic.putIdentBuf(buf)
buf := i.cache.getIdentBuf()
defer i.cache.putIdentBuf(buf)
IRKindIdent.encodeHeader(0).put(buf[:])
*(*ID)(buf[wordSize:]) = i.ic.Ident(a).Value()
*(*ID)(buf[wordSize:]) = i.cache.Ident(a).Value()
i.mustWrite(buf[:])
}
@@ -206,19 +183,19 @@ func (i *IContext) WriteString(s string) {
// Encode writes a deterministic, efficient representation of a to w and returns
// the first non-nil error encountered while writing to w.
func (ic *irCache) Encode(w io.Writer, a Artifact) (err error) {
func (c *Cache) Encode(w io.Writer, a Artifact) (err error) {
deps := a.Dependencies()
idents := make([]*extIdent, len(deps))
for i, d := range deps {
dbuf, did := ic.unsafeIdent(d, true)
dbuf, did := c.unsafeIdent(d, true)
if dbuf == nil {
dbuf = ic.getIdentBuf()
dbuf = c.getIdentBuf()
binary.LittleEndian.PutUint64(dbuf[:], uint64(d.Kind()))
*(*ID)(dbuf[wordSize:]) = did.Value()
} else {
ic.storeIdent(d, dbuf)
c.storeIdent(d, dbuf)
}
defer ic.putIdentBuf(dbuf)
defer c.putIdentBuf(dbuf)
idents[i] = dbuf
}
slices.SortFunc(idents, func(a, b *extIdent) int {
@@ -244,10 +221,10 @@ func (ic *irCache) Encode(w io.Writer, a Artifact) (err error) {
}
func() {
i := IContext{ic, w}
i := IContext{c, w}
defer panicToError(&err)
defer func() { i.ic, i.w = nil, nil }()
defer func() { i.cache, i.w = nil, nil }()
a.Params(&i)
}()
@@ -256,7 +233,7 @@ func (ic *irCache) Encode(w io.Writer, a Artifact) (err error) {
}
var f IREndFlag
kcBuf := ic.getIdentBuf()
kcBuf := c.getIdentBuf()
sz := wordSize
if kc, ok := a.(KnownChecksum); ok {
f |= IREndKnownChecksum
@@ -266,13 +243,13 @@ func (ic *irCache) Encode(w io.Writer, a Artifact) (err error) {
IRKindEnd.encodeHeader(uint32(f)).put(kcBuf[:])
_, err = w.Write(kcBuf[:sz])
ic.putIdentBuf(kcBuf)
c.putIdentBuf(kcBuf)
return
}
// encodeAll implements EncodeAll by recursively encoding dependencies and
// performs deduplication by value via the encoded map.
func (ic *irCache) encodeAll(
func (c *Cache) encodeAll(
w io.Writer,
a Artifact,
encoded map[Artifact]struct{},
@@ -282,13 +259,13 @@ func (ic *irCache) encodeAll(
}
for _, d := range a.Dependencies() {
if err = ic.encodeAll(w, d, encoded); err != nil {
if err = c.encodeAll(w, d, encoded); err != nil {
return
}
}
encoded[a] = struct{}{}
return ic.Encode(w, a)
return c.Encode(w, a)
}
// EncodeAll writes a self-describing IR stream of a to w and returns the first
@@ -306,8 +283,8 @@ func (ic *irCache) encodeAll(
// the ident cache, nor does it contribute identifiers it computes back to the
// ident cache. Because of this, multiple invocations of EncodeAll will have
// similar cost and does not amortise when combined with a call to Cure.
func (ic *irCache) EncodeAll(w io.Writer, a Artifact) error {
return ic.encodeAll(w, a, make(map[Artifact]struct{}))
func (c *Cache) EncodeAll(w io.Writer, a Artifact) error {
return c.encodeAll(w, a, make(map[Artifact]struct{}))
}
// ErrRemainingIR is returned for a [IRReadFunc] that failed to call

View File

@@ -85,7 +85,7 @@ func TestIRRoundtrip(t *testing.T) {
testCasesCache := make([]cacheTestCase, len(testCases))
for i, tc := range testCases {
want := tc.a
testCasesCache[i] = cacheTestCase{tc.name, 0, nil,
testCasesCache[i] = cacheTestCase{tc.name, nil,
func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
r, w := io.Pipe()

View File

@@ -32,7 +32,7 @@ func TestHTTPGet(t *testing.T) {
}))
checkWithCache(t, []cacheTestCase{
{"direct", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"direct", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
var r pkg.RContext
rCacheVal := reflect.ValueOf(&r).Elem().FieldByName("cache")
reflect.NewAt(
@@ -94,7 +94,7 @@ func TestHTTPGet(t *testing.T) {
}
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C")},
{"cure", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"cure", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
var r pkg.RContext
rCacheVal := reflect.ValueOf(&r).Elem().FieldByName("cache")
reflect.NewAt(

View File

@@ -13,14 +13,15 @@ import (
"hash"
"io"
"io/fs"
"iter"
"maps"
"os"
"path"
"path/filepath"
"runtime"
"slices"
"strings"
"sync"
"sync/atomic"
"syscall"
"testing"
"unique"
@@ -188,10 +189,6 @@ func (c *common) Unwrap() context.Context { return c.cache.ctx }
// GetMessage returns [message.Msg] held by the underlying [Cache].
func (c *common) GetMessage() message.Msg { return c.cache.msg }
// GetJobs returns the preferred number of jobs to run, when applicable. Its
// value must not affect cure outcome.
func (c *common) GetJobs() int { return c.cache.jobs }
// GetWorkDir returns a pathname to a directory which [Artifact] is expected to
// write its output to. This is not the final resting place of the [Artifact]
// and this pathname should not be directly referred to in the final contents.
@@ -334,6 +331,23 @@ type FloodArtifact interface {
Artifact
}
// Flood returns an iterator over the dependency tree of an [Artifact].
func Flood(a Artifact) iter.Seq[Artifact] {
return func(yield func(Artifact) bool) {
for _, d := range a.Dependencies() {
if !yield(d) {
return
}
for d0 := range Flood(d) {
if !yield(d0) {
return
}
}
}
}
}
// TrivialArtifact refers to an [Artifact] that cures without requiring that
// any other [Artifact] is cured before it. Its dependency tree is ignored after
// computing its identifier.
@@ -352,7 +366,7 @@ type TrivialArtifact interface {
}
// KnownIdent is optionally implemented by [Artifact] and is used instead of
// [Cache.Ident] when it is available.
// [Kind.Ident] when it is available.
//
// This is very subtle to use correctly. The implementation must ensure that
// this value is globally unique, otherwise [Cache] can enter an inconsistent
@@ -425,11 +439,6 @@ const (
KindCustomOffset = 1 << 31
)
const (
// kindCollection is the kind of [Collect]. It never cures successfully.
kindCollection Kind = KindCustomOffset - 1 - iota
)
const (
// fileLock is the file name appended to Cache.base for guaranteeing
// exclusive access to the cache directory.
@@ -452,11 +461,6 @@ const (
// pathnames allocated during [Cache.Cure].
dirTemp = "temp"
// dirExecScratch is the directory name appended to Cache.base for scratch
// space setting up the container started by [Cache.EnterExec]. Exclusivity
// via Cache.inExec.
dirExecScratch = "scratch"
// checksumLinknamePrefix is prepended to the encoded [Checksum] value
// of an [Artifact] when creating a symbolic link to dirChecksum.
checksumLinknamePrefix = "../" + dirChecksum + "/"
@@ -472,7 +476,7 @@ type cureRes struct {
// subject to the cures limit. Values pointed to by result addresses are safe
// to access after the [sync.WaitGroup] associated with this pendingArtifactDep
// is done. pendingArtifactDep must not be reused or modified after it is sent
// to cure.
// to Cache.cureDep.
type pendingArtifactDep struct {
// Dependency artifact populated during [Cache.Cure].
a Artifact
@@ -492,49 +496,6 @@ type pendingArtifactDep struct {
*sync.WaitGroup
}
const (
// CValidateKnown arranges for [KnownChecksum] outcomes to be validated to
// match its intended checksum.
//
// A correct implementation of [KnownChecksum] does not successfully cure
// with output not matching its intended checksum. When an implementation
// fails to perform this validation correctly, the on-disk format enters
// an inconsistent state (correctable by [Cache.Scrub]).
//
// This flag causes [Cache.Cure] to always compute the checksum, and reject
// a cure if it does not match the intended checksum.
//
// This behaviour significantly reduces performance and is not recommended
// outside of testing a custom [Artifact] implementation.
CValidateKnown = 1 << iota
// CSchedIdle arranges for the [ext.SCHED_IDLE] scheduling priority to be
// set for [KindExec] and [KindExecNet] containers.
CSchedIdle
// CAssumeChecksum enables the use of [KnownChecksum] for duplicate function
// call suppression via the on-disk cache.
//
// This may cause incorrect cure outcome if an impossible checksum is
// specified that matches an output already present in the on-disk cache.
// This may be avoided by purposefully specifying a statistically
// unattainable checksum, like the zero value.
//
// While this optimisation might seem appealing, it is almost never
// applicable in real world use. Almost every time this path was taken, it
// 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
// cured [Artifact] data in a content addressed fashion.
type Cache struct {
@@ -554,13 +515,17 @@ type Cache struct {
// Directory where all [Cache] related files are placed.
base *check.Absolute
// Immutable cure options set by [Open].
flags int
// Immutable job count, when applicable.
jobs int
// Must not be exposed directly.
irCache
// Whether to validate [FileArtifact.Cure] for a [KnownChecksum] file. This
// significantly reduces performance.
strict bool
// Maximum size of a dependency graph.
threshold uintptr
// Artifact to [unique.Handle] of identifier cache.
artifact sync.Map
// Identifier free list, must not be accessed directly.
identPool sync.Pool
// Synchronises access to dirChecksum.
checksumMu sync.RWMutex
@@ -583,33 +548,46 @@ type Cache struct {
unlock func()
// Synchronises calls to Close.
closeOnce sync.Once
// Whether EnterExec has not yet returned.
inExec atomic.Bool
}
// IsStrict returns whether the [Cache] strictly verifies checksums.
func (c *Cache) IsStrict() bool { return c.strict }
// SetStrict sets whether the [Cache] strictly verifies checksums, even when
// the implementation promises to validate them internally. This significantly
// reduces performance and is not recommended outside of testing.
//
// This method is not safe for concurrent use with any other method.
func (c *Cache) SetStrict(strict bool) { c.strict = strict }
// SetThreshold imposes a maximum size on the dependency graph, checked on every
// call to Cure. The zero value disables this check entirely.
//
// This method is not safe for concurrent use with any other method.
func (c *Cache) SetThreshold(threshold uintptr) { c.threshold = threshold }
// extIdent is a [Kind] concatenated with [ID].
type extIdent [wordSize + len(ID{})]byte
// getIdentBuf returns the address of an extIdent for Ident.
func (ic *irCache) getIdentBuf() *extIdent { return ic.identPool.Get().(*extIdent) }
func (c *Cache) getIdentBuf() *extIdent { return c.identPool.Get().(*extIdent) }
// putIdentBuf adds buf to identPool.
func (ic *irCache) putIdentBuf(buf *extIdent) { ic.identPool.Put(buf) }
func (c *Cache) putIdentBuf(buf *extIdent) { c.identPool.Put(buf) }
// storeIdent adds an [Artifact] to the artifact cache.
func (ic *irCache) storeIdent(a Artifact, buf *extIdent) unique.Handle[ID] {
func (c *Cache) storeIdent(a Artifact, buf *extIdent) unique.Handle[ID] {
idu := unique.Make(ID(buf[wordSize:]))
ic.artifact.Store(a, idu)
c.artifact.Store(a, idu)
return idu
}
// Ident returns the identifier of an [Artifact].
func (ic *irCache) Ident(a Artifact) unique.Handle[ID] {
buf, idu := ic.unsafeIdent(a, false)
func (c *Cache) Ident(a Artifact) unique.Handle[ID] {
buf, idu := c.unsafeIdent(a, false)
if buf != nil {
idu = ic.storeIdent(a, buf)
ic.putIdentBuf(buf)
idu = c.storeIdent(a, buf)
c.putIdentBuf(buf)
}
return idu
}
@@ -617,17 +595,17 @@ func (ic *irCache) Ident(a Artifact) unique.Handle[ID] {
// unsafeIdent implements Ident but returns the underlying buffer for a newly
// computed identifier. Callers must return this buffer to identPool. encodeKind
// is only a hint, kind may still be encoded in the buffer.
func (ic *irCache) unsafeIdent(a Artifact, encodeKind bool) (
func (c *Cache) unsafeIdent(a Artifact, encodeKind bool) (
buf *extIdent,
idu unique.Handle[ID],
) {
if id, ok := ic.artifact.Load(a); ok {
if id, ok := c.artifact.Load(a); ok {
idu = id.(unique.Handle[ID])
return
}
if ki, ok := a.(KnownIdent); ok {
buf = ic.getIdentBuf()
buf = c.getIdentBuf()
if encodeKind {
binary.LittleEndian.PutUint64(buf[:], uint64(a.Kind()))
}
@@ -635,9 +613,9 @@ func (ic *irCache) unsafeIdent(a Artifact, encodeKind bool) (
return
}
buf = ic.getIdentBuf()
buf = c.getIdentBuf()
h := sha512.New384()
if err := ic.Encode(h, a); err != nil {
if err := c.Encode(h, a); err != nil {
// unreachable
panic(err)
}
@@ -902,7 +880,7 @@ func (c *Cache) Scrub(checks int) error {
se.DanglingIdentifiers = append(se.DanglingIdentifiers, *want)
seMu.Unlock()
return false
} else if err = Decode(got, filepath.Base(linkname)); err != nil {
} else if err = Decode(got, path.Base(linkname)); err != nil {
seMu.Lock()
lnp := dir.Append(linkname)
se.Errs[lnp.Handle()] = append(se.Errs[lnp.Handle()], err)
@@ -1067,7 +1045,7 @@ func (c *Cache) finaliseIdent(
// [FileArtifact] to the filesystem. If err is nil, the caller is responsible
// for closing the resulting [io.ReadCloser].
func (c *Cache) openFile(f FileArtifact) (r io.ReadCloser, err error) {
if kc, ok := f.(KnownChecksum); c.flags&CAssumeChecksum != 0 && ok {
if kc, ok := f.(KnownChecksum); ok {
c.checksumMu.RLock()
r, err = os.Open(c.base.Append(
dirChecksum,
@@ -1238,6 +1216,14 @@ func (e InvalidArtifactError) Error() string {
return "artifact " + Encode(e) + " cannot be cured"
}
// DependencyError refers to an artifact with a dependency tree larger than the
// threshold specified by a previous call to [Cache.SetThreshold].
type DependencyError struct{ A Artifact }
func (e DependencyError) Error() string {
return "artifact has too many dependencies"
}
// Cure cures the [Artifact] and returns its pathname and [Checksum]. Direct
// calls to Cure are not subject to the cures limit.
func (c *Cache) Cure(a Artifact) (
@@ -1253,6 +1239,18 @@ func (c *Cache) Cure(a Artifact) (
default:
}
if c.threshold > 0 {
var n uintptr
for range Flood(a) {
if n == c.threshold {
err = DependencyError{a}
return
}
n++
}
c.msg.Verbosef("visited %d artifacts", n)
}
return c.cure(a, true)
}
@@ -1476,7 +1474,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
return
}
buf := c.getIdentBuf()
err = Decode((*Checksum)(buf[:]), filepath.Base(name))
err = Decode((*Checksum)(buf[:]), path.Base(name))
if err == nil {
checksum = unique.Make(Checksum(buf[:]))
}
@@ -1510,18 +1508,16 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
checksums,
)
if c.flags&CAssumeChecksum != 0 {
c.checksumMu.RLock()
checksumFi, err = os.Stat(checksumPathname.String())
c.checksumMu.RUnlock()
c.checksumMu.RLock()
checksumFi, err = os.Stat(checksumPathname.String())
c.checksumMu.RUnlock()
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return
}
checksumFi, err = nil, nil
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return
}
checksumFi, err = nil, nil
}
}
@@ -1577,7 +1573,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
}
r, err = f.Cure(&RContext{common{c}})
if err == nil {
if checksumPathname == nil || c.flags&CValidateKnown != 0 {
if checksumPathname == nil || c.IsStrict() {
h := sha512.New384()
hbw := c.getWriter(h)
_, err = io.Copy(w, io.TeeReader(r, hbw))
@@ -1594,7 +1590,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
if checksumPathname == nil {
checksum = unique.Make(Checksum(buf[:]))
checksums = Encode(Checksum(buf[:]))
} else if c.flags&CValidateKnown != 0 {
} else if c.IsStrict() {
if got := Checksum(buf[:]); got != checksum.Value() {
err = &ChecksumMismatchError{
Got: got,
@@ -1822,7 +1818,7 @@ func (c *Cache) Close() {
// caller-supplied value, however direct calls to [Cache.Cure] is not subject
// to this limitation.
//
// A cures or jobs value of 0 or lower is equivalent to the value returned by
// A cures value of 0 or lower is equivalent to the value returned by
// [runtime.NumCPU].
//
// A successful call to Open guarantees exclusive access to the on-filesystem
@@ -1832,10 +1828,10 @@ func (c *Cache) Close() {
func Open(
ctx context.Context,
msg message.Msg,
flags, cures, jobs int,
cures int,
base *check.Absolute,
) (*Cache, error) {
return open(ctx, msg, flags, cures, jobs, base, true)
return open(ctx, msg, cures, base, true)
}
// open implements Open but allows omitting the [lockedfile] lock when called
@@ -1843,16 +1839,13 @@ func Open(
func open(
ctx context.Context,
msg message.Msg,
flags, cures, jobs int,
cures int,
base *check.Absolute,
lock bool,
) (*Cache, error) {
if cures < 1 {
cures = runtime.NumCPU()
}
if jobs < 1 {
jobs = runtime.NumCPU()
}
for _, name := range []string{
dirIdentifier,
@@ -1868,13 +1861,11 @@ func open(
c := Cache{
cures: make(chan struct{}, cures),
flags: flags,
jobs: jobs,
msg: msg,
base: base,
irCache: zeroIRCache(),
identPool: sync.Pool{New: func() any { return new(extIdent) }},
ident: make(map[unique.Handle[ID]]unique.Handle[Checksum]),
identErr: make(map[unique.Handle[ID]]error),
@@ -1899,33 +1890,3 @@ func open(
return &c, nil
}
// Collected is returned by [Collect.Cure] to indicate a successful collection.
type Collected struct{}
// Error returns a constant string to satisfy error, but should never be seen
// by the user.
func (Collected) Error() string { return "artifacts successfully collected" }
// IsCollected returns whether the underlying error contains that of the result
// of curing a [Collect] helper.
func IsCollected(err error) bool { return errors.As(err, new(Collected)) }
// Collect implements [pkg.FloodArtifact] to concurrently cure multiple
// [pkg.Artifact]. It returns [Collected].
type Collect []Artifact
// Cure returns [Collected].
func (*Collect) Cure(*FContext) error { return Collected{} }
// Kind returns the hardcoded [pkg.Kind] value.
func (*Collect) Kind() Kind { return kindCollection }
// Params is a noop: dependencies are already represented in the header.
func (*Collect) Params(*IContext) {}
// Dependencies returns [Collect] as is.
func (c *Collect) Dependencies() []Artifact { return *c }
// IsExclusive returns false: Cure is a noop.
func (*Collect) IsExclusive() bool { return false }

View File

@@ -24,8 +24,6 @@ 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"
@@ -35,7 +33,7 @@ import (
func unsafeOpen(
ctx context.Context,
msg message.Msg,
flags, cures, jobs int,
cures int,
base *check.Absolute,
lock bool,
) (*pkg.Cache, error)
@@ -230,7 +228,7 @@ func TestIdent(t *testing.T) {
var cache *pkg.Cache
if a, err := check.NewAbs(t.TempDir()); err != nil {
t.Fatal(err)
} else if cache, err = pkg.Open(t.Context(), msg, 0, 0, 0, a); err != nil {
} else if cache, err = pkg.Open(t.Context(), msg, 0, a); err != nil {
t.Fatal(err)
}
t.Cleanup(cache.Close)
@@ -254,7 +252,6 @@ func TestIdent(t *testing.T) {
// on test completion.
type cacheTestCase struct {
name string
flags int
early func(t *testing.T, base *check.Absolute)
f func(t *testing.T, base *check.Absolute, c *pkg.Cache)
want pkg.Checksum
@@ -291,20 +288,8 @@ 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, flags, 1<<4, 0, base); err != nil {
if c, err := pkg.Open(t.Context(), msg, 1<<4, base); err != nil {
t.Fatalf("Open: error = %v", err)
} else {
t.Cleanup(c.Close)
@@ -483,7 +468,9 @@ func TestCache(t *testing.T) {
}()
testCases := []cacheTestCase{
{"file", pkg.CValidateKnown | pkg.CAssumeChecksum, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"file", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
identifier := (pkg.ID)(bytes.Repeat([]byte{
0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f,
}, 8))
@@ -606,7 +593,7 @@ func TestCache(t *testing.T) {
if c0, err := unsafeOpen(
t.Context(),
message.New(nil),
0, 0, 0, base, false,
0, base, false,
); err != nil {
t.Fatalf("open: error = %v", err)
} else {
@@ -640,7 +627,7 @@ func TestCache(t *testing.T) {
}
}, pkg.MustDecode("St9rlE-mGZ5gXwiv_hzQ_B8bZP-UUvSNmf4nHUZzCMOumb6hKnheZSe0dmnuc4Q2")},
{"directory", pkg.CAssumeChecksum, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"directory", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
id := pkg.MustDecode(
"HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY",
)
@@ -817,7 +804,9 @@ func TestCache(t *testing.T) {
})
}, pkg.MustDecode("WVpvsVqVKg9Nsh744x57h51AuWUoUR2nnh8Md-EYBQpk6ziyTuUn6PLtF2e0Eu_d")},
{"pending", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"pending", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
wantErr := stub.UniqueError(0xcafe)
n, ready := make(chan struct{}), make(chan struct{})
go func() {
@@ -887,54 +876,7 @@ func TestCache(t *testing.T) {
<-wCureDone
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C")},
{"no assume checksum", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
makeGarbage := func(work *check.Absolute, wantErr error) error {
if err := os.Mkdir(work.String(), 0700); err != nil {
return err
}
if err := os.WriteFile(work.Append(
"check",
).String(), nil, 0400); err != nil {
return err
}
return wantErr
}
wantChecksum := pkg.MustDecode("Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")
cureMany(t, c, []cureStep{
{"create", overrideChecksum{wantChecksum, overrideIdent{pkg.ID{0xff, 0}, &stubArtifact{
kind: pkg.KindTar,
cure: func(t *pkg.TContext) error {
return makeGarbage(t.GetWorkDir(), nil)
},
}}}, base.Append(
"identifier",
pkg.Encode(pkg.ID{0xff, 0}),
), wantChecksum, nil},
{"reject", overrideChecksum{wantChecksum, overrideIdent{pkg.ID{0xfe, 1}, &stubArtifact{
kind: pkg.KindTar,
cure: func(t *pkg.TContext) error {
return makeGarbage(t.GetWorkDir(), stub.UniqueError(0xbad))
},
}}}, nil, pkg.Checksum{}, stub.UniqueError(0xbad)},
{"match", overrideChecksum{wantChecksum, overrideIdent{pkg.ID{0xff, 1}, &stubArtifact{
kind: pkg.KindTar,
cure: func(t *pkg.TContext) error {
return makeGarbage(t.GetWorkDir(), nil)
},
}}}, base.Append(
"identifier",
pkg.Encode(pkg.ID{0xff, 1}),
), wantChecksum, nil},
})
}, pkg.MustDecode("OC290t23aimNo2Rp2pPwan5GI2KRLRdOwYxXQMD9jw0QROgHnNXWodoWdV0hwu2w")},
{"scrub", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"scrub", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
cureMany(t, c, []cureStep{
{"bad measured file", newStubFile(
pkg.KindHTTPGet,
@@ -1240,7 +1182,7 @@ func (a earlyFailureF) Cure(*pkg.FContext) error {
func TestDependencyCureErrorEarly(t *testing.T) {
checkWithCache(t, []cacheTestCase{
{"early", 0, nil, func(t *testing.T, _ *check.Absolute, c *pkg.Cache) {
{"early", nil, func(t *testing.T, _ *check.Absolute, c *pkg.Cache) {
_, _, err := c.Cure(earlyFailureF(8))
if !errors.Is(err, stub.UniqueError(0xcafe)) {
t.Fatalf("Cure: error = %v", err)
@@ -1263,7 +1205,7 @@ func TestNew(t *testing.T) {
if _, err := pkg.Open(
t.Context(),
message.New(nil),
0, 0, 0, check.MustAbs(container.Nonexistent),
0, check.MustAbs(container.Nonexistent),
); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Open: error = %#v, want %#v", err, wantErr)
}
@@ -1291,7 +1233,7 @@ func TestNew(t *testing.T) {
if _, err := pkg.Open(
t.Context(),
message.New(nil),
0, 0, 0, tempDir.Append("cache"),
0, tempDir.Append("cache"),
); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Open: error = %#v, want %#v", err, wantErr)
}

View File

@@ -10,7 +10,7 @@ import (
"io/fs"
"net/http"
"os"
"path/filepath"
"path"
)
const (
@@ -43,7 +43,8 @@ var _ fmt.Stringer = new(tarArtifactNamed)
func (a *tarArtifactNamed) String() string { return a.name + "-unpack" }
// NewTar returns a new [Artifact] backed by the supplied [Artifact] and
// compression method. The source [Artifact] must be a [FileArtifact].
// compression method. The source [Artifact] must be compatible with
// [TContext.Open].
func NewTar(a Artifact, compression uint32) Artifact {
ta := tarArtifact{a, compression}
if s, ok := a.(fmt.Stringer); ok {
@@ -168,7 +169,7 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
}
if typeflag >= '0' && typeflag <= '9' && typeflag != tar.TypeDir {
if err = root.MkdirAll(filepath.Dir(header.Name), 0700); err != nil {
if err = root.MkdirAll(path.Dir(header.Name), 0700); err != nil {
return
}
}

View File

@@ -21,7 +21,7 @@ func TestTar(t *testing.T) {
t.Parallel()
checkWithCache(t, []cacheTestCase{
{"http", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"http", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
checkTarHTTP(t, base, c, fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
@@ -42,7 +42,7 @@ func TestTar(t *testing.T) {
))
}, pkg.MustDecode("NQTlc466JmSVLIyWklm_u8_g95jEEb98PxJU-kjwxLpfdjwMWJq0G8ze9R4Vo1Vu")},
{"http expand", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
{"http expand", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
checkTarHTTP(t, base, c, fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},

View File

@@ -7,11 +7,9 @@ import (
"log"
"net"
"os"
"path/filepath"
"path"
"reflect"
"runtime"
"slices"
"strconv"
"strings"
"hakurei.app/check"
@@ -23,10 +21,6 @@ func main() {
log.SetFlags(0)
log.SetPrefix("testtool: ")
environ := slices.DeleteFunc(slices.Clone(os.Environ()), func(s string) bool {
return s == "CURE_JOBS="+strconv.Itoa(runtime.NumCPU())
})
var hostNet, layers, promote bool
if len(os.Args) == 2 && os.Args[0] == "testtool" {
switch os.Args[1] {
@@ -54,15 +48,15 @@ func main() {
var overlayRoot bool
wantEnv := []string{"HAKUREI_TEST=1"}
if len(environ) == 2 {
if len(os.Environ()) == 2 {
overlayRoot = true
if !layers && !promote {
log.SetPrefix("testtool(overlay root): ")
}
wantEnv = []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"}
}
if !slices.Equal(wantEnv, environ) {
log.Fatalf("Environ: %q, want %q", environ, wantEnv)
if !slices.Equal(wantEnv, os.Environ()) {
log.Fatalf("Environ: %q, want %q", os.Environ(), wantEnv)
}
var overlayWork bool
@@ -74,7 +68,7 @@ func main() {
if got, err := os.Executable(); err != nil {
log.Fatalf("Executable: error = %v", err)
} else {
iftPath = filepath.Join(filepath.Dir(filepath.Dir(got)), "ift")
iftPath = path.Join(path.Dir(path.Dir(got)), "ift")
if got != wantExec {
switch got {
@@ -167,7 +161,7 @@ func main() {
}
}
if !layers {
if filepath.Base(lowerdir) != checksumEmptyDir {
if path.Base(lowerdir) != checksumEmptyDir {
log.Fatal("unexpected artifact checksum")
}
} else {
@@ -193,8 +187,8 @@ func main() {
}
if len(lowerdirs) != 2 ||
filepath.Base(lowerdirs[0]) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" ||
filepath.Base(lowerdirs[1]) != "nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK" {
path.Base(lowerdirs[0]) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" ||
path.Base(lowerdirs[1]) != "nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK" {
log.Fatalf("unexpected lowerdirs %s", strings.Join(lowerdirs, ", "))
}
}
@@ -208,12 +202,12 @@ func main() {
}
next()
if filepath.Base(m.Root) != "OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb" {
if path.Base(m.Root) != "OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb" {
log.Fatal("unexpected file artifact checksum")
}
next()
if filepath.Base(m.Root) != checksumEmptyDir {
if path.Base(m.Root) != checksumEmptyDir {
log.Fatal("unexpected artifact checksum")
}
}
@@ -232,13 +226,13 @@ func main() {
log.Fatal("unexpected work mount entry")
}
} else {
if filepath.Base(m.Root) != ident || m.Target != "/work" {
if path.Base(m.Root) != ident || m.Target != "/work" {
log.Fatal("unexpected work mount entry")
}
}
next()
if filepath.Base(m.Root) != ident || m.Target != "/tmp" {
if path.Base(m.Root) != ident || m.Target != "/tmp" {
log.Fatal("unexpected temp mount entry")
}

View File

@@ -7,13 +7,13 @@ func (t Toolchain) newAttr() (pkg.Artifact, string) {
version = "2.5.2"
checksum = "YWEphrz6vg1sUMmHHVr1CRo53pFXRhq_pjN-AlG8UgwZK1y6m7zuDhxqJhD0SV0l"
)
return t.NewPackage("attr", version, newTar(
"https://download.savannah.nongnu.org/releases/attr/"+
return t.NewPackage("attr", version, pkg.NewHTTPGetTar(
nil, "https://download.savannah.nongnu.org/releases/attr/"+
"attr-"+version+".tar.gz",
checksum,
mustDecode(checksum),
pkg.TarGzip,
), &PackageAttr{
Patches: []KV{
Patches: [][2]string{
{"libgen-basename", `From 8a80d895dfd779373363c3a4b62ecce5a549efb2 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Sat, 30 Mar 2024 10:17:10 +0100
@@ -81,10 +81,10 @@ func (t Toolchain) newACL() (pkg.Artifact, string) {
version = "2.3.2"
checksum = "-fY5nwH4K8ZHBCRXrzLdguPkqjKI6WIiGu4dBtrZ1o0t6AIU73w8wwJz_UyjIS0P"
)
return t.NewPackage("acl", version, newTar(
"https://download.savannah.nongnu.org/releases/acl/"+
return t.NewPackage("acl", version, pkg.NewHTTPGetTar(
nil, "https://download.savannah.nongnu.org/releases/acl/"+
"acl-"+version+".tar.gz",
checksum,
mustDecode(checksum),
pkg.TarGzip,
), nil, &MakeHelper{
// makes assumptions about uid_map/gid_map

View File

@@ -16,9 +16,9 @@ import (
type PArtifact int
const (
CompilerRT PArtifact = iota
LLVMCompilerRT PArtifact = iota
LLVMRuntimes
Clang
LLVMClang
// EarlyInit is the Rosa OS init program.
EarlyInit
@@ -47,10 +47,8 @@ const (
Bison
Bzip2
CMake
Connman
Coreutils
Curl
DBus
DTC
Diffutils
Elfutils
@@ -64,37 +62,23 @@ const (
GenInitCPIO
Gettext
Git
Glslang
GnuTLS
Go
Gperf
Grep
Gzip
Hakurei
HakureiDist
IPTables
Kmod
LibXau
Libbsd
Libcap
Libclc
Libdrm
Libev
Libexpat
Libiconv
Libpsl
Libffi
Libgd
Libglvnd
Libiconv
Libmd
Libmnl
Libpciaccess
Libnftnl
Libpsl
Libseccomp
Libtasn1
Libtool
Libseccomp
Libucontext
Libunistring
Libxml2
Libxslt
M4
@@ -111,7 +95,6 @@ const (
Nettle
Ninja
OpenSSL
P11Kit
PCRE2
Parallel
Patch
@@ -124,27 +107,21 @@ const (
PerlTermReadKey
PerlTextCharWidth
PerlTextWrapI18N
PerlUnicodeLineBreak
PerlUnicodeGCString
PerlYAMLTiny
PkgConfig
Procps
Python
PythonIniConfig
PythonMako
PythonMarkupSafe
PythonPackaging
PythonPluggy
PythonPyTest
PythonPyYAML
PythonPygments
QEMU
Rdfind
Readline
Rsync
Sed
Setuptools
SPIRVHeaders
SPIRVTools
SquashfsTools
Strace
TamaGo
@@ -158,17 +135,15 @@ const (
WaylandProtocols
XCB
XCBProto
XDGDBusProxy
XZ
Xproto
XZ
Zlib
Zstd
// PresetUnexportedStart is the first unexported preset.
PresetUnexportedStart
llvmSource = iota - 1
buildcatrust
buildcatrust = iota - 1
utilMacros
// Musl is a standalone libc that does not depend on the toolchain.
@@ -178,9 +153,6 @@ const (
// stages only. This preset and its direct output must never be exposed.
gcc
// nettle3 is an older version of [Nettle].
nettle3
// Stage0 is a tarball containing all compile-time dependencies of artifacts
// part of the [Std] toolchain.
Stage0
@@ -319,17 +291,6 @@ var (
artifactsOnce [_toolchainEnd][len(artifactsM)]sync.Once
)
// zero zeros the value pointed to by p.
func zero[T any](p *T) { var v T; *p = v }
// DropCaches arranges for all cached [pkg.Artifact] to be freed some time after
// it returns. Must not be used concurrently with any other function from this
// package.
func DropCaches() {
zero(&artifacts)
zero(&artifactsOnce)
}
// GetMetadata returns [Metadata] of a [PArtifact].
func GetMetadata(p PArtifact) *Metadata { return &artifactsM[p] }

View File

@@ -19,18 +19,6 @@ func TestLoad(t *testing.T) {
}
}
func BenchmarkAll(b *testing.B) {
for b.Loop() {
for i := range rosa.PresetEnd {
rosa.Std.Load(rosa.PArtifact(i))
}
b.StopTimer()
rosa.DropCaches()
b.StartTimer()
}
}
func TestResolveName(t *testing.T) {
t.Parallel()

View File

@@ -7,10 +7,10 @@ func (t Toolchain) newArgpStandalone() (pkg.Artifact, string) {
version = "1.3"
checksum = "vtW0VyO2pJ-hPyYmDI2zwSLS8QL0sPAUKC1t3zNYbwN2TmsaE-fADhaVtNd3eNFl"
)
return t.NewPackage("argp-standalone", version, newTar(
"http://www.lysator.liu.se/~nisse/misc/"+
return t.NewPackage("argp-standalone", version, pkg.NewHTTPGetTar(
nil, "http://www.lysator.liu.se/~nisse/misc/"+
"argp-standalone-"+version+".tar.gz",
checksum,
mustDecode(checksum),
pkg.TarGzip,
), &PackageAttr{
Env: []string{

View File

@@ -7,9 +7,9 @@ func (t Toolchain) newBzip2() (pkg.Artifact, string) {
version = "1.0.8"
checksum = "cTLykcco7boom-s05H1JVsQi1AtChYL84nXkg_92Dm1Xt94Ob_qlMg_-NSguIK-c"
)
return t.NewPackage("bzip2", version, newTar(
"https://sourceware.org/pub/bzip2/bzip2-"+version+".tar.gz",
checksum,
return t.NewPackage("bzip2", version, pkg.NewHTTPGetTar(
nil, "https://sourceware.org/pub/bzip2/bzip2-"+version+".tar.gz",
mustDecode(checksum),
pkg.TarGzip,
), &PackageAttr{
Writable: true,

View File

@@ -1,7 +1,7 @@
package rosa
import (
"path/filepath"
"path"
"slices"
"strings"
@@ -10,14 +10,13 @@ import (
func (t Toolchain) newCMake() (pkg.Artifact, string) {
const (
version = "4.3.1"
checksum = "RHpzZiM1kJ5bwLjo9CpXSeHJJg3hTtV9QxBYpQoYwKFtRh5YhGWpShrqZCSOzQN6"
version = "4.3.0"
checksum = "amBtnY2eGsEdlrB-cTRuOESBTsIqtyaxWlEKNlnp2EWLwAKWINjssilo4KXE6El9"
)
return t.NewPackage("cmake", version, newFromGitHubRelease(
"Kitware/CMake",
"v"+version,
"cmake-"+version+".tar.gz",
checksum,
return t.NewPackage("cmake", version, pkg.NewHTTPGetTar(
nil, "https://github.com/Kitware/CMake/releases/download/"+
"v"+version+"/cmake-"+version+".tar.gz",
mustDecode(checksum),
pkg.TarGzip,
), &PackageAttr{
// test suite expects writable source tree
@@ -26,7 +25,7 @@ func (t Toolchain) newCMake() (pkg.Artifact, string) {
// expected to be writable in the copy made during bootstrap
Chmod: true,
Patches: []KV{
Patches: [][2]string{
{"bootstrap-test-no-openssl", `diff --git a/Tests/BootstrapTest.cmake b/Tests/BootstrapTest.cmake
index 137de78bc1..b4da52e664 100644
--- a/Tests/BootstrapTest.cmake
@@ -89,9 +88,9 @@ index 2ead810437..f85cbb8b1c 100644
OmitDefaults: true,
ConfigureName: "/usr/src/cmake/bootstrap",
Configure: []KV{
Configure: [][2]string{
{"prefix", "/system"},
{"parallel", jobsE},
{"parallel", `"$(nproc)"`},
{"--"},
{"-DCMAKE_USE_OPENSSL", "OFF"},
{"-DCMake_TEST_NO_NETWORK", "ON"},
@@ -119,11 +118,14 @@ func init() {
// CMakeHelper is the [CMake] build system helper.
type CMakeHelper struct {
// Joined with name with a dash if non-empty.
Variant string
// Path elements joined with source.
Append []string
// CMake CACHE entries.
Cache []KV
Cache [][2]string
// Runs after install.
Script string
@@ -133,12 +135,20 @@ type CMakeHelper struct {
var _ Helper = new(CMakeHelper)
// extra returns a hardcoded slice of [CMake] and [Ninja].
func (attr *CMakeHelper) extra(int) P {
if attr != nil && attr.Make {
return P{CMake, Make}
// name returns its arguments and an optional variant string joined with '-'.
func (attr *CMakeHelper) name(name, version string) string {
if attr != nil && attr.Variant != "" {
name += "-" + attr.Variant
}
return P{CMake, Ninja}
return name + "-" + version
}
// extra returns a hardcoded slice of [CMake] and [Ninja].
func (attr *CMakeHelper) extra(int) []PArtifact {
if attr != nil && attr.Make {
return []PArtifact{CMake, Make}
}
return []PArtifact{CMake, Ninja}
}
// wantsChmod returns false.
@@ -160,7 +170,7 @@ func (*CMakeHelper) wantsDir() string { return "/cure/" }
func (attr *CMakeHelper) script(name string) string {
if attr == nil {
attr = &CMakeHelper{
Cache: []KV{
Cache: [][2]string{
{"CMAKE_BUILD_TYPE", "Release"},
},
}
@@ -170,8 +180,10 @@ func (attr *CMakeHelper) script(name string) string {
}
generate := "Ninja"
jobs := ""
if attr.Make {
generate = "'Unix Makefiles'"
jobs += ` "--parallel=$(nproc)"`
}
return `
@@ -188,8 +200,8 @@ cmake -G ` + generate + ` \
}
}), " \\\n\t") + ` \
-DCMAKE_INSTALL_PREFIX=/system \
'/usr/src/` + name + `/` + filepath.Join(attr.Append...) + `'
cmake --build . --parallel=` + jobsE + `
'/usr/src/` + name + `/` + path.Join(attr.Append...) + `'
cmake --build .` + jobs + `
cmake --install . --prefix=/work/system
` + attr.Script
}

View File

@@ -1,109 +0,0 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newConnman() (pkg.Artifact, string) {
const (
version = "2.0"
checksum = "MhVTdJOhndnZn2SWd8URKo_Pj7Zvc14tntEbrVOf9L3yVWJvpb3v3Q6104tWJgtW"
)
return t.NewPackage("connman", version, newTar(
"https://git.kernel.org/pub/scm/network/connman/connman.git/"+
"snapshot/connman-"+version+".tar.gz",
checksum,
pkg.TarGzip,
), &PackageAttr{
Patches: []KV{
{"alpine-musl-res", `musl does not implement res_ninit
--- a/gweb/gresolv.c
+++ b/gweb/gresolv.c
@@ -877,8 +877,6 @@
resolv->index = index;
resolv->nameserver_list = NULL;
- res_ninit(&resolv->res);
-
return resolv;
}
@@ -918,8 +916,6 @@
flush_nameservers(resolv);
- res_nclose(&resolv->res);
-
g_free(resolv);
}
@@ -1022,24 +1018,19 @@
debug(resolv, "hostname %s", hostname);
if (!resolv->nameserver_list) {
- int i;
-
- for (i = 0; i < resolv->res.nscount; i++) {
- char buf[100];
- int family = resolv->res.nsaddr_list[i].sin_family;
- void *sa_addr = &resolv->res.nsaddr_list[i].sin_addr;
-
- if (family != AF_INET &&
- resolv->res._u._ext.nsaddrs[i]) {
- family = AF_INET6;
- sa_addr = &resolv->res._u._ext.nsaddrs[i]->sin6_addr;
+ FILE *f = fopen("/etc/resolv.conf", "r");
+ if (f) {
+ char line[256], *s;
+ int i;
+ while (fgets(line, sizeof(line), f)) {
+ if (strncmp(line, "nameserver", 10) || !isspace(line[10]))
+ continue;
+ for (s = &line[11]; isspace(s[0]); s++);
+ for (i = 0; s[i] && !isspace(s[i]); i++);
+ s[i] = 0;
+ g_resolv_add_nameserver(resolv, s, 53, 0);
}
-
- if (family != AF_INET && family != AF_INET6)
- continue;
-
- if (inet_ntop(family, sa_addr, buf, sizeof(buf)))
- g_resolv_add_nameserver(resolv, buf, 53, 0);
+ fclose(f);
}
if (!resolv->nameserver_list)
`},
},
}, &MakeHelper{
Generate: "./bootstrap",
},
Automake,
Libtool,
PkgConfig,
DBus,
IPTables,
GnuTLS,
Readline,
KernelHeaders,
), version
}
func init() {
artifactsM[Connman] = Metadata{
f: Toolchain.newConnman,
Name: "connman",
Description: "a daemon for managing Internet connections",
Website: "https://git.kernel.org/pub/scm/network/connman/connman.git/",
Dependencies: P{
DBus,
IPTables,
GnuTLS,
Readline,
},
ID: 337,
}
}

View File

@@ -7,25 +7,25 @@ func (t Toolchain) newCurl() (pkg.Artifact, string) {
version = "8.19.0"
checksum = "YHuVLVVp8q_Y7-JWpID5ReNjq2Zk6t7ArHB6ngQXilp_R5l3cubdxu3UKo-xDByv"
)
return t.NewPackage("curl", version, newTar(
"https://curl.se/download/curl-"+version+".tar.bz2",
checksum,
return t.NewPackage("curl", version, pkg.NewHTTPGetTar(
nil, "https://curl.se/download/curl-"+version+".tar.bz2",
mustDecode(checksum),
pkg.TarBzip2,
), &PackageAttr{
// remove broken test
Writable: true,
ScriptEarly: `
chmod +w tests/data && rm -f tests/data/test459
chmod +w tests/data && rm tests/data/test459
`,
}, &MakeHelper{
Configure: []KV{
Configure: [][2]string{
{"with-openssl"},
{"with-ca-bundle", "/system/etc/ssl/certs/ca-bundle.crt"},
{"disable-smb"},
},
Check: []string{
"TFLAGS=" + jobsLFlagE,
`TFLAGS="-j$(expr "$(nproc)" '*' 2)"`,
"test-nonflaky",
},
},

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