1
0
forked from rosa/hakurei

42 Commits

Author SHA1 Message Date
121fcfa406 internal/uevent: enumerate objects via sysfs
This is not a great way to implement cold boot, but I already have the implementation lying around.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-29 19:50:20 +09:00
19c76e0831 cmd: document Rosa OS programs
The earlyinit and mbf program are not covered by the compatibility promise, so specify that here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 22:43:25 +09:00
71fcc972ba cmd/hsu: alternative hsurc path for Rosa OS
Rosa OS does not have /etc.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 18:22:55 +09:00
62002efd08 cmd/hsu: document hsurc format and internals
This was previously only documented via an unexported function.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 18:17:31 +09:00
e33294db9c cmd/hakurei: document stable behaviour
These are undocumented anywhere else and is required by tools invoking hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 17:31:46 +09:00
b1ea3b4acf cmd/hakurei: rename app to run
The run command was a legacy holdover from very early days and is only useful for testing and demonstration these days. This change also renames it to exec.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 16:48:26 +09:00
2c254c70b8 cmd/hakurei: remove linkname directive
This used to be a function that did much more, and was later relocated to another package and exported.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 16:20:02 +09:00
ea014d6af2 internal/uevent: consume kernel-originated events
These are not possible to cover outside integration vm. Extreme care is required when dealing with this method, so keep it simple.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 15:39:16 +09:00
1b48484c16 internal/uevent: exclusive socket access
This is a much simplified mutex, since blocking is not required.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 01:01:06 +09:00
713bff3eb0 internal/uevent: decode uevent messages
The wire format and behaviour is entirely undocumented. This is implemented by reading lib/kobject_uevent.c, with testdata collected from the internal/rosa kernel.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 00:49:34 +09:00
30f459e690 internal/uevent: nontrivial errors
These errors are best represented as JSON.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 00:07:56 +09:00
8766fddcb3 internal/uevent: recoverable errors
This runs in the Rosa OS init, so recover as much as possible, as otherwise it is likely to require a full system reboot to resume event processing. The caller is responsible for reporting the error.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-27 22:58:16 +09:00
2745602be3 internal/uevent: wrap netlink socket
Unfortunately these messages do not have the same format as rtnetlink.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-27 22:46:18 +09:00
ee22847dde internal/uevent: kobject_action lookup
This is encoded as part of kobject uevent message headers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-27 22:39:43 +09:00
c61188649b internal/netlink: export generic connection
This enables abstractions around some families to be implemented in a separate package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-27 19:08:48 +09:00
6a87a96838 internal/rosa/kernel: 6.12.77 to 6.12.78
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-27 10:40:27 +09:00
2548a681e9 internal/rosa: key-value type
This type is used very frequently. The new type is much easier to type and can receive helper methods eventually if needed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 16:23:15 +09:00
d514d0679f internal/rosa: set PYTHONUNBUFFERED=1
Some python tools try to be clever and buffers output. This makes the build process appear to hang and is quite frustrating. Instead of trying to address this on a case-by-case basis, this is turned off globally for the interpreter.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 15:29:29 +09:00
4407892632 cmd/mbf: optionally enter cure container
This is very useful for troubleshooting failing tests and such. The ephemeral state is cleaned up by internal/pkg.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 15:10:11 +09:00
e661260607 internal/pkg: enter exec container
This enables much easier troubleshooting of failing cures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 15:05:04 +09:00
044490e0a5 cmd/mbf: retain session by default
This almost never make sense to be turned off.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 14:59:17 +09:00
af038c89ff internal/pkg: collection helper-artifact
This was moved from internal/rosa because it is considered generally useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 14:11:10 +09:00
d2f30173cd internal/pkg: isolate container params
This enables exporting container params for interactive troubleshooting within the cure container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 14:02:58 +09:00
5319ea994c internal/rosa/libseccomp: fix upstream out-of-bounds read
This was revealed by optimisation changes in the latest toolchain.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 10:43:11 +09:00
bbe178be3e internal/rosa/llvm: 22.1.1 to 22.1.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 09:56:34 +09:00
ca28e9936b internal/rosa/musl: 1.2.5 to 1.2.6
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 09:56:06 +09:00
f61c6ade56 internal/rosa/nss: 3.121 to 3.122
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 09:55:27 +09:00
fce3d63823 internal/rosa/gnu: autoconf 2.72 to 2.73
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 09:54:44 +09:00
722c3cc54f internal/netlink: optional check header as reply
Not every received message is a reply.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-25 19:33:01 +09:00
372d509e5c internal/netlink: expose multicast groups
This also gets rid of the cached pid value for port since that prevents multiple sockets from being open at once.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-25 17:55:35 +09:00
d62516ed1e internal/netlink: enlarge recvfrom buffer
This also uses an array type for the buffer since its size now uses the hardcoded value found in the kernel.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-25 17:18:56 +09:00
d2b635eb55 cmd/mbf: correctly describe --with-toolchain
The behaviour of this was changed to include the stage2 toolchain instead, but the help text was never updated.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-25 15:41:28 +09:00
50403e9d60 internal/netlink: wrap netpoll via context
This removes netpoll boilerplate for the most common use case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-25 15:39:29 +09:00
b98c5f2e21 internal/netlink: nonblocking socket I/O
This enables use with blocking calls like when used with NETLINK_KOBJECT_UEVENT.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-25 14:06:59 +09:00
d972cffe5a internal/netlink: make full response available
The previous API makes it impossible to retrieve remaining messages in the current iteration.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-23 16:39:25 +09:00
d8648304bb internal/netlink: isolate receive method
This enables use with epoll for receiving events only.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-23 15:03:15 +09:00
f7bfa9a6c2 internal/rosa/go: disable go1.25.7 smtp test
This uses certs that had just expired.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 17:52:54 +09:00
7035b4b598 internal/rosa/cmake: 4.2.3 to 4.3.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 16:39:57 +09:00
094b8400dd internal/rosa/qemu: 10.2.1 to 10.2.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 16:13:51 +09:00
4652d921d8 internal/rosa/wayland: 1.24.91 to 1.25.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 16:13:28 +09:00
066213c245 internal/rosa/libexpat: 2.7.4 to 2.7.5
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 16:00:50 +09:00
98832c21ee internal/rosa/fuse: 3.18.1 to 3.18.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 15:57:49 +09:00
78 changed files with 1784 additions and 432 deletions

View File

@@ -1,3 +1,7 @@
// 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,6 +2,7 @@ package main
import (
"context"
"errors"
"fmt"
"io"
"log"
@@ -11,7 +12,6 @@ import (
"strconv"
"sync"
"time"
_ "unsafe" // for go:linkname
"hakurei.app/check"
"hakurei.app/command"
@@ -27,9 +27,14 @@ import (
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
// if it is not nil, or the original value if it is.
//
//go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap
func optionalErrorUnwrap(err error) error
func optionalErrorUnwrap(err error) error {
if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
return underlyingErr
}
return err
}
var errSuccess = errors.New("success")
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var (
@@ -60,9 +65,9 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
var (
flagIdentifierFile int
)
c.NewCommand("app", "Load and start container from configuration file", func(args []string) error {
c.NewCommand("run", "Load and start container from configuration file", func(args []string) error {
if len(args) < 1 {
log.Fatal("app requires at least 1 argument")
log.Fatal("run requires at least 1 argument")
}
config := tryPath(msg, args[0])
@@ -98,7 +103,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
)
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error {
c.NewCommand("exec", "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)
}
@@ -323,7 +328,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagShort bool
flagNoStore bool
)
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
c.NewCommand("show", "Show live or local instance configuration", func(args []string) error {
switch len(args) {
case 0: // system
printShowSystem(os.Stdout, flagShort, flagJSON)

View File

@@ -23,9 +23,9 @@ func TestHelp(t *testing.T) {
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Commands:
app Load and start container from configuration file
run Configure and start a permissive container
show Show live or local app configuration
run Load and start container from configuration file
exec Configure and start a permissive container
show Show live or local instance configuration
ps List active instances
version Display version information
license Show full license text
@@ -35,8 +35,8 @@ Commands:
`,
},
{
"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]
"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]
Flags:
-X Enable direct connection to X11

View File

@@ -1,8 +1,42 @@
// 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"
@@ -17,12 +51,9 @@ import (
"hakurei.app/message"
)
var (
errSuccess = errors.New("success")
//go:embed LICENSE
license string
)
//go:generate cp ../../LICENSE .
//go:embed LICENSE
var license string
// earlyHardeningErrs are errors collected while setting up early hardening feature.
type earlyHardeningErrs struct{ yamaLSM, dumpable error }
@@ -31,8 +62,8 @@ func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(nil)
log.SetPrefix("hakurei: ")
log.SetFlags(0)
log.SetPrefix("hakurei: ")
msg := message.New(log.Default())
early := earlyHardeningErrs{

View File

@@ -17,8 +17,9 @@ 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)
@@ -46,7 +47,8 @@ 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) {
@@ -60,7 +62,12 @@ 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
}
@@ -75,10 +82,12 @@ 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())
}
@@ -88,7 +97,8 @@ 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
@@ -96,7 +106,8 @@ func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
)
var likely uintptr
if len(name) >= shortLengthMin && len(name) <= len(hst.ID{}) { // half the hex representation
// half the hex representation
if len(name) >= shortLengthMin && len(name) <= len(hst.ID{}) {
// cannot safely decode here due to unknown alignment
for _, c := range name {
if c >= '0' && c <= '9' {

7
cmd/hsu/conf.go Normal file
View File

@@ -0,0 +1,7 @@
//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"

7
cmd/hsu/config_rosa.go Normal file
View File

@@ -0,0 +1,7 @@
//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
/* copied from hst and must never be changed */
/* keep in sync with hst */
const (
userOffset = 100000

View File

@@ -1,7 +1,58 @@
// 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"
@@ -16,10 +67,13 @@ import (
)
const (
// envIdentity is the name of the environment variable holding a
// single byte representing the shim setup pipe file descriptor.
// envShim is the name of the environment variable holding a single byte
// representing the shim setup pipe file descriptor.
envShim = "HAKUREI_SHIM"
// envGroups holds a ' ' separated list of string representations of
// 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
// supplementary group gid. Membership requirements are enforced.
envGroups = "HAKUREI_GROUPS"
)
@@ -35,7 +89,6 @@ 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")
@@ -99,8 +152,6 @@ 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
@@ -136,7 +187,6 @@ 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)
}
@@ -146,10 +196,21 @@ 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,8 +18,9 @@ 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.
// 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.
func parseUint32Fast(s string) (uint32, error) {
sLen := len(s)
if sLen < 1 {
@@ -40,12 +41,14 @@ 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) {
@@ -81,10 +84,6 @@ 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.
@@ -112,10 +111,6 @@ 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,3 +1,15 @@
// 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 (
@@ -436,6 +448,7 @@ func main() {
{
var (
flagDump string
flagEnter bool
flagExport string
)
c.NewCommand(
@@ -445,9 +458,13 @@ func main() {
if len(args) != 1 {
return errors.New("cure requires 1 argument")
}
if p, ok := rosa.ResolveName(args[0]); !ok {
p, ok := rosa.ResolveName(args[0])
if !ok {
return fmt.Errorf("unknown artifact %q", args[0])
} else if flagDump == "" {
}
switch {
default:
pathname, _, err := cache.Cure(rosa.Std.Load(p))
if err != nil {
return err
@@ -477,7 +494,8 @@ func main() {
}
return nil
} else {
case flagDump != "":
f, err := os.OpenFile(
flagDump,
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
@@ -493,6 +511,15 @@ func main() {
}
return f.Close()
case flagEnter:
return cache.EnterExec(
ctx,
rosa.Std.Load(p),
true, os.Stdin, os.Stdout, os.Stderr,
rosa.AbsSystem.Append("bin", "mksh"),
"sh",
)
}
},
).
@@ -505,6 +532,11 @@ func main() {
&flagExport,
"export", command.StringFlag(""),
"Export cured artifact to specified pathname",
).
Flag(
&flagEnter,
"enter", command.BoolFlag(false),
"Enter cure container with an interactive shell",
)
}
@@ -527,7 +559,7 @@ func main() {
}
presets[i] = p
}
root := make(rosa.Collect, 0, 6+len(args))
root := make(pkg.Collect, 0, 6+len(args))
root = rosa.Std.AppendPresets(root, presets...)
if flagWithToolchain {
@@ -543,7 +575,7 @@ func main() {
if _, _, err := cache.Cure(&root); err == nil {
return errors.New("unreachable")
} else if !errors.Is(err, rosa.Collected{}) {
} else if !pkg.IsCollected(err) {
return err
}
@@ -636,13 +668,13 @@ func main() {
).
Flag(
&flagSession,
"session", command.BoolFlag(false),
"session", command.BoolFlag(true),
"Retain session",
).
Flag(
&flagWithToolchain,
"with-toolchain", command.BoolFlag(false),
"Include the stage3 LLVM toolchain",
"Include the stage2 LLVM toolchain",
)
}

View File

@@ -85,7 +85,10 @@ 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)
@@ -103,7 +106,11 @@ func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.
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 {
@@ -169,8 +176,11 @@ 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
}
@@ -253,7 +263,8 @@ 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}
@@ -269,6 +280,7 @@ 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))))
@@ -288,8 +300,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
@@ -453,7 +465,10 @@ 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 setupWriter io.WriteCloser
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {

View File

@@ -1,3 +1,10 @@
// 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,6 +1,7 @@
package container
import (
"context"
"io"
"io/fs"
"net"
@@ -66,7 +67,7 @@ type syscallDispatcher interface {
// ensureFile provides ensureFile.
ensureFile(name string, perm, pperm os.FileMode) error
// mustLoopback provides mustLoopback.
mustLoopback(msg message.Msg)
mustLoopback(ctx context.Context, msg message.Msg)
// seccompLoad provides [seccomp.Load].
seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error
@@ -170,7 +171,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(msg message.Msg) {
func (direct) mustLoopback(ctx context.Context, msg message.Msg) {
var lo int
if ifi, err := net.InterfaceByName("lo"); err != nil {
msg.GetLogger().Fatalln(err)
@@ -199,11 +200,14 @@ func (direct) mustLoopback(msg message.Msg) {
msg.GetLogger().Fatalf("RTNETLINK answers: %v", err)
default:
msg.GetLogger().Fatalf("RTNETLINK answers with malformed message")
if err == context.DeadlineExceeded || err == context.Canceled {
msg.GetLogger().Fatalf("interrupted RTNETLINK operation")
}
msg.GetLogger().Fatal("RTNETLINK answers with malformed message")
}
}
must(c.SendNewaddrLo(uint32(lo)))
must(c.SendIfInfomsg(syscall.RTM_NEWLINK, 0, &syscall.IfInfomsg{
must(c.SendNewaddrLo(ctx, uint32(lo)))
must(c.SendIfInfomsg(ctx, syscall.RTM_NEWLINK, 0, &syscall.IfInfomsg{
Family: syscall.AF_UNSPEC,
Index: int32(lo),
Flags: syscall.IFF_UP,

View File

@@ -2,6 +2,7 @@ package container
import (
"bytes"
"context"
"fmt"
"io"
"io/fs"
@@ -468,7 +469,7 @@ func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
stub.CheckArg(k.Stub, "pperm", pperm, 2))
}
func (*kstub) mustLoopback(message.Msg) { /* noop */ }
func (*kstub) mustLoopback(context.Context, message.Msg) { /* noop */ }
func (k *kstub) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
k.Helper()

View File

@@ -7,6 +7,7 @@ import (
"log"
"os"
"os/exec"
"os/signal"
"path"
"slices"
"strconv"
@@ -175,7 +176,11 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
if !params.HostNet {
k.mustLoopback(msg)
ctx, cancel := signal.NotifyContext(context.Background(), CancelSignal,
os.Interrupt, SIGTERM, SIGQUIT)
defer cancel() // for panics
k.mustLoopback(ctx, msg)
cancel()
}
// write uid/gid map here so parent does not need to set dumpable

10
dist/comp/_hakurei vendored
View File

@@ -1,11 +1,11 @@
#compdef hakurei
_hakurei_app() {
_hakurei_run() {
__hakurei_files
return $?
}
_hakurei_run() {
_hakurei_exec() {
_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=(
"app:Load and start container from configuration file"
"run:Configure and start a permissive container"
"show:Show live or local app configuration"
"run:Load and start container from configuration file"
"exec:Configure and start a permissive container"
"show:Show live or local instance configuration"
"ps:List active instances"
"version:Display version information"
"license:Show full license text"

View File

@@ -2,29 +2,32 @@
package netlink
import (
"context"
"fmt"
"os"
"sync"
"syscall"
"time"
"unsafe"
)
// AF_NETLINK socket is never shared
var (
nlPid uint32
nlPidOnce sync.Once
// net/netlink/af_netlink.c
const maxRecvmsgLen = 32768
const (
// stateOpen denotes an open conn.
stateOpen uint32 = 1 << iota
)
// 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 {
// A Conn represents resources associated to a netlink socket.
type Conn struct {
// AF_NETLINK socket.
fd int
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
// Kernel module or netlink group to communicate with.
family int
// Message sequence number.
@@ -33,40 +36,155 @@ type conn struct {
typ, flags uint16
// Outgoing position in buf.
pos int
// A page holding incoming and outgoing messages.
buf []byte
// 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
}
// dial returns the address of a newly connected conn of specified family.
func dial(family int) (*conn, error) {
var c conn
// Dial returns the address of a newly connected generic netlink connection of
// specified family and groups.
func Dial(family int, groups uint32) (*Conn, error) {
var c Conn
if fd, err := syscall.Socket(
syscall.AF_NETLINK,
syscall.SOCK_RAW|syscall.SOCK_CLOEXEC,
syscall.SOCK_RAW|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC,
family,
); err != nil {
return nil, os.NewSyscallError("socket", err)
} else if err = syscall.Bind(fd, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
Pid: getpid(),
Groups: groups,
}); err != nil {
_ = syscall.Close(fd)
return nil, os.NewSyscallError("bind", err)
} else {
c.fd, c.family = fd, family
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
}
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.pos = syscall.NLMSG_HDRLEN
c.buf = make([]byte, os.Getpagesize())
c.t = time.Now().UTC()
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.buf == nil {
func (c *Conn) Close() error {
if !c.ok() {
return syscall.EINVAL
}
c.buf = nil
return syscall.Close(c.fd)
c.state &= ^stateOpen
return c.f.Close()
}
// Recvfrom wraps recv(2) with nonblocking behaviour via the runtime network poller.
//
// The returned slice is valid until the next call to Recvfrom.
func (c *Conn) Recvfrom(
ctx context.Context,
flags int,
) (data []byte, from syscall.Sockaddr, err error) {
if err = c.f.SetReadDeadline(time.Time{}); err != nil {
return
}
var n int
data = c.buf[:]
done := make(chan error, 1)
go func() {
rcErr := c.raw.Read(func(fd uintptr) (done bool) {
n, from, err = syscall.Recvfrom(int(fd), data, flags)
return err != syscall.EWOULDBLOCK
})
if n >= 0 {
data = data[:n]
}
done <- rcErr
}()
select {
case rcErr := <-done:
if err != nil {
err = os.NewSyscallError("recvfrom", 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
}
}
// Sendto wraps send(2) with nonblocking behaviour via the runtime network poller.
func (c *Conn) Sendto(
ctx context.Context,
p []byte,
flags int,
to syscall.Sockaddr,
) (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.Sendto(int(fd), p, flags, to)
return err != syscall.EWOULDBLOCK
})
}()
select {
case rcErr := <-done:
if err != nil {
err = os.NewSyscallError("sendto", 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
}
}
// Msg is type constraint for types sent over the wire via netlink.
@@ -88,7 +206,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) {
@@ -122,8 +240,16 @@ 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
@@ -132,7 +258,7 @@ func (c *conn) pending() []byte {
Type: c.typ,
Flags: c.flags,
Seq: c.seq,
Pid: getpid(),
Pid: c.port,
}
return buf
}
@@ -143,44 +269,44 @@ type Complete struct{}
// Error returns a hardcoded string that should never be displayed to the user.
func (Complete) Error() string { return "returning from roundtrip" }
// HandlerFunc handles [syscall.NetlinkMessage] and returns a non-nil error to
// discontinue the receiving of more messages.
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 {
for {
var resp []syscall.NetlinkMessage
if data, _, err := c.Recvfrom(ctx, flags); err != nil {
return err
} else if len(data) < syscall.NLMSG_HDRLEN {
return syscall.EBADE
} else if resp, err = syscall.ParseNetlinkMessage(data); err != nil {
return err
}
if err := f(resp); err != nil {
if err == (Complete{}) {
return nil
}
return err
}
}
}
// Roundtrip sends the pending message and handles the reply.
func (c *conn) Roundtrip(f func(msg *syscall.NetlinkMessage) error) error {
if c.buf == nil {
func (c *Conn) Roundtrip(ctx context.Context, f HandlerFunc) error {
if !c.ok() {
return syscall.EINVAL
}
defer func() { c.seq++ }()
if err := syscall.Sendto(c.fd, c.pending(), 0, &syscall.SockaddrNetlink{
if err := c.Sendto(ctx, c.pending(), 0, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
}); err != nil {
return os.NewSyscallError("sendto", err)
return err
}
for {
buf := c.buf
if n, _, err := syscall.Recvfrom(c.fd, buf, 0); err != nil {
return os.NewSyscallError("recvfrom", err)
} else if n < syscall.NLMSG_HDRLEN {
return syscall.EBADE
} else {
buf = buf[:n]
}
msgs, err := syscall.ParseNetlinkMessage(buf)
if err != nil {
return err
}
for _, msg := range msgs {
if msg.Header.Seq != c.seq || msg.Header.Pid != getpid() {
return &InconsistentError{msg.Header, c.seq, getpid()}
}
if err = f(&msg); err != nil {
if err == (Complete{}) {
return nil
}
return err
}
}
}
return c.receive(ctx, f, 0)
}

View File

@@ -1,16 +1,13 @@
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
}
@@ -22,11 +19,9 @@ 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{
pos: syscall.NLMSG_HDRLEN,
buf: make([]byte, os.Getpagesize()),
}
c := Conn{port: 1, pos: syscall.NLMSG_HDRLEN}
tc.f(&c)
if got := c.pending(); string(got) != string(tc.want) {
t.Errorf("pending: %#v, want %#v", got, tc.want)

View File

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

@@ -40,14 +40,17 @@ type ExecPath struct {
W bool
}
// SetSchedIdle is whether to set [std.SCHED_IDLE] scheduling priority.
// SetSchedIdle is whether to set [ext.SCHED_IDLE] scheduling priority.
var SetSchedIdle bool
// GetArtifactFunc is the function signature of [FContext.GetArtifact].
type GetArtifactFunc func(Artifact) (*check.Absolute, unique.Handle[Checksum])
// PromoteLayers returns artifacts with identical-by-content layers promoted to
// the highest priority instance, as if mounted via [ExecPath].
func PromoteLayers(
artifacts []Artifact,
getArtifact func(Artifact) (*check.Absolute, unique.Handle[Checksum]),
getArtifact GetArtifactFunc,
report func(i int, d Artifact),
) []*check.Absolute {
layers := make([]*check.Absolute, 0, len(artifacts))
@@ -67,14 +70,14 @@ func PromoteLayers(
}
// layers returns pathnames collected from A deduplicated via [PromoteLayers].
func (p *ExecPath) layers(f *FContext) []*check.Absolute {
msg := f.GetMessage()
return PromoteLayers(p.A, f.GetArtifact, func(i int, d Artifact) {
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) {
if msg.IsVerbose() {
msg.Verbosef(
"promoted layer %d as %s",
i, reportName(d, f.cache.Ident(d)),
)
msg.Verbosef("promoted layer %d as %s", i, reportName(d, ident(d)))
}
})
}
@@ -382,17 +385,30 @@ 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)
// 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) {
// 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,
hostNet bool,
temp, work *check.Absolute,
getArtifact GetArtifactFunc,
ident func(a Artifact) unique.Handle[ID],
) (z *container.Container, err error) {
overlayWorkIndex := -1
for i, p := range a.paths {
if p.P == nil || len(p.A) == 0 {
return os.ErrInvalid
return nil, ErrInvalidPaths
}
if p.P.Is(AbsWork) {
overlayWorkIndex = i
@@ -404,10 +420,7 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
artifactCount += len(p.A)
}
ctx, cancel := context.WithTimeout(f.Unwrap(), a.timeout)
defer cancel()
z := container.New(ctx, f.GetMessage())
z = container.New(ctx, msg)
z.WaitDelay = execWaitDelay
z.SeccompPresets = SeccompPresets
z.SeccompFlags |= seccomp.AllowMultiarch
@@ -421,12 +434,183 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
}
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
z.Dir, z.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args
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,
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 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, 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 := f.GetMessage(); msg.IsVerbose() {
if msg.IsVerbose() {
var stdout, stderr io.ReadCloser
if stdout, err = z.StdoutPipe(); err != nil {
return
@@ -464,62 +648,6 @@ 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
}
@@ -532,7 +660,7 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
// do not allow empty directories to succeed
for {
err = syscall.Rmdir(work.String())
err = syscall.Rmdir(f.GetWorkDir().String())
if err != syscall.EINTR {
break
}

View File

@@ -92,7 +92,7 @@ func TestExec(t *testing.T) {
[]string{"testtool"},
pkg.ExecPath{},
), nil, pkg.Checksum{}, os.ErrInvalid},
), nil, pkg.Checksum{}, pkg.ErrInvalidPaths},
})
// check init failure passthrough

View File

@@ -22,6 +22,7 @@ import (
"slices"
"strings"
"sync"
"sync/atomic"
"syscall"
"testing"
"unique"
@@ -366,7 +367,7 @@ type TrivialArtifact interface {
}
// KnownIdent is optionally implemented by [Artifact] and is used instead of
// [Kind.Ident] when it is available.
// [Cache.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
@@ -439,6 +440,11 @@ 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.
@@ -461,6 +467,11 @@ 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 + "/"
@@ -476,7 +487,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 Cache.cureDep.
// to cure.
type pendingArtifactDep struct {
// Dependency artifact populated during [Cache.Cure].
a Artifact
@@ -548,6 +559,9 @@ 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.
@@ -1890,3 +1904,33 @@ 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

@@ -13,7 +13,7 @@ func (t Toolchain) newAttr() (pkg.Artifact, string) {
mustDecode(checksum),
pkg.TarGzip,
), &PackageAttr{
Patches: [][2]string{
Patches: []KV{
{"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

View File

@@ -10,8 +10,8 @@ import (
func (t Toolchain) newCMake() (pkg.Artifact, string) {
const (
version = "4.2.3"
checksum = "Y4uYGnLrDQX78UdzH7fMzfok46Nt_1taDIHSmqgboU1yFi6f0iAXBDegMCu4eS-J"
version = "4.3.0"
checksum = "amBtnY2eGsEdlrB-cTRuOESBTsIqtyaxWlEKNlnp2EWLwAKWINjssilo4KXE6El9"
)
return t.NewPackage("cmake", version, pkg.NewHTTPGetTar(
nil, "https://github.com/Kitware/CMake/releases/download/"+
@@ -25,7 +25,7 @@ func (t Toolchain) newCMake() (pkg.Artifact, string) {
// expected to be writable in the copy made during bootstrap
Chmod: true,
Patches: [][2]string{
Patches: []KV{
{"bootstrap-test-no-openssl", `diff --git a/Tests/BootstrapTest.cmake b/Tests/BootstrapTest.cmake
index 137de78bc1..b4da52e664 100644
--- a/Tests/BootstrapTest.cmake
@@ -88,7 +88,7 @@ index 2ead810437..f85cbb8b1c 100644
OmitDefaults: true,
ConfigureName: "/usr/src/cmake/bootstrap",
Configure: [][2]string{
Configure: []KV{
{"prefix", "/system"},
{"parallel", `"$(nproc)"`},
{"--"},
@@ -125,7 +125,7 @@ type CMakeHelper struct {
Append []string
// CMake CACHE entries.
Cache [][2]string
Cache []KV
// Runs after install.
Script string
@@ -170,7 +170,7 @@ func (*CMakeHelper) wantsDir() string { return "/cure/" }
func (attr *CMakeHelper) script(name string) string {
if attr == nil {
attr = &CMakeHelper{
Cache: [][2]string{
Cache: []KV{
{"CMAKE_BUILD_TYPE", "Release"},
},
}

View File

@@ -18,7 +18,7 @@ func (t Toolchain) newCurl() (pkg.Artifact, string) {
chmod +w tests/data && rm tests/data/test459
`,
}, &MakeHelper{
Configure: [][2]string{
Configure: []KV{
{"with-openssl"},
{"with-ca-bundle", "/system/etc/ssl/certs/ca-bundle.crt"},

View File

@@ -18,7 +18,7 @@ func (t Toolchain) newDTC() (pkg.Artifact, string) {
Writable: true,
Chmod: true,
}, &MesonHelper{
Setup: [][2]string{
Setup: []KV{
{"Dyaml", "disabled"},
{"Dstatic-build", "true"},
},

View File

@@ -22,7 +22,7 @@ func (t Toolchain) newElfutils() (pkg.Artifact, string) {
// nonstandard glibc extension
SkipCheck: true,
Configure: [][2]string{
Configure: []KV{
{"enable-deterministic-archives"},
},
},

View File

@@ -25,7 +25,7 @@ func (a cureEtc) Cure(t *pkg.FContext) (err error) {
if err = os.MkdirAll(etc.String(), 0700); err != nil {
return
}
for _, f := range [][2]string{
for _, f := range []KV{
{"hosts", "127.0.0.1 localhost cure cure-net\n"},
{"passwd", `root:x:0:0:System administrator:/proc/nonexistent:/bin/sh
cure:x:1023:1023:Cure:/usr/src:/bin/sh

View File

@@ -13,7 +13,7 @@ func (t Toolchain) newFakeroot() (pkg.Artifact, string) {
mustDecode(checksum),
pkg.TarBzip2,
), &PackageAttr{
Patches: [][2]string{
Patches: []KV{
{"remove-broken-docs", `diff --git a/doc/Makefile.am b/doc/Makefile.am
index f135ad9..85c784c 100644
--- a/doc/Makefile.am

View File

@@ -4,8 +4,8 @@ import "hakurei.app/internal/pkg"
func (t Toolchain) newFuse() (pkg.Artifact, string) {
const (
version = "3.18.1"
checksum = "COb-BgJRWXLbt9XUkNeuiroQizpMifXqxgieE1SlkMXhs_WGSyJStrmyewAw2hd6"
version = "3.18.2"
checksum = "iL-7b7eUtmlVSf5cSq0dzow3UiqSjBmzV3cI_ENPs1tXcHdktkG45j1V12h-4jZe"
)
return t.NewPackage("fuse", version, pkg.NewHTTPGetTar(
nil, "https://github.com/libfuse/libfuse/releases/download/"+
@@ -13,7 +13,7 @@ func (t Toolchain) newFuse() (pkg.Artifact, string) {
mustDecode(checksum),
pkg.TarGzip,
), nil, &MesonHelper{
Setup: [][2]string{
Setup: []KV{
{"Ddefault_library", "both"},
{"Dtests", "true"},
{"Duseroot", "false"},

View File

@@ -88,8 +88,8 @@ func init() {
func (t Toolchain) newAutoconf() (pkg.Artifact, string) {
const (
version = "2.72"
checksum = "-c5blYkC-xLDer3TWEqJTyh1RLbOd1c5dnRLKsDnIrg_wWNOLBpaqMY8FvmUFJ33"
version = "2.73"
checksum = "yGabDTeOfaCUB0JX-h3REYLYzMzvpDwFmFFzHNR7QilChCUNE4hR6q7nma4viDYg"
)
return t.NewPackage("autoconf", version, pkg.NewHTTPGetTar(
nil, "https://ftpmirror.gnu.org/gnu/autoconf/autoconf-"+version+".tar.gz",
@@ -351,7 +351,7 @@ func (t Toolchain) newBash() (pkg.Artifact, string) {
Flag: TEarly,
}, &MakeHelper{
Script: "ln -s bash /work/system/bin/sh\n",
Configure: [][2]string{
Configure: []KV{
{"without-bash-malloc"},
},
}), version
@@ -390,7 +390,7 @@ test_disable 'int main(){return 0;}' gnulib-tests/test-fchownat.c
test_disable 'int main(){return 0;}' gnulib-tests/test-lchown.c
`,
Patches: [][2]string{
Patches: []KV{
{"tests-fix-job-control", `From 21d287324aa43aa3a31f39619ade0deac7fd6013 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <P@draigBrady.com>
Date: Tue, 24 Feb 2026 15:44:41 +0000
@@ -485,7 +485,7 @@ index 9a395416b..fbb043312 100755
Flag: TEarly,
}, &MakeHelper{
Configure: [][2]string{
Configure: []KV{
{"enable-single-binary", "symlinks"},
},
},
@@ -720,7 +720,7 @@ func (t Toolchain) newTar() (pkg.Artifact, string) {
mustDecode(checksum),
pkg.TarGzip,
), nil, &MakeHelper{
Configure: [][2]string{
Configure: []KV{
{"disable-acl"},
{"without-posix-acls"},
{"without-xattrs"},
@@ -903,7 +903,7 @@ func (t Toolchain) newGCC() (pkg.Artifact, string) {
mustDecode(checksum),
pkg.TarGzip,
), &PackageAttr{
Patches: [][2]string{
Patches: []KV{
{"musl-off64_t-loff_t", `diff --git a/libgo/sysinfo.c b/libgo/sysinfo.c
index 180f5c31d74..44d7ea73f7d 100644
--- a/libgo/sysinfo.c
@@ -1062,7 +1062,7 @@ ln -s system/lib /work/
// it also saturates the CPU for a consequential amount of time.
Flag: TExclusive,
}, &MakeHelper{
Configure: [][2]string{
Configure: []KV{
{"disable-multilib"},
{"with-multilib-list", `""`},
{"enable-default-pie"},

View File

@@ -135,7 +135,8 @@ sed -i \
cmd/link/internal/`+runtime.GOARCH+`/obj.go
rm \
os/root_unix_test.go
os/root_unix_test.go \
net/smtp/smtp_test.go
`, go123,
)

View File

@@ -35,7 +35,7 @@ func (t Toolchain) newGLib() (pkg.Artifact, string) {
)),
},
}, &MesonHelper{
Setup: [][2]string{
Setup: []KV{
{"Ddefault_library", "both"},
},
},

View File

@@ -66,7 +66,7 @@ mkdir -p /work/system/libexec/hakurei/
echo '# Building hakurei.'
go generate -v ./...
go build -trimpath -v -o /work/system/libexec/hakurei -ldflags="-s -w
go build -trimpath -v -tags=rosa -o /work/system/libexec/hakurei -ldflags="-s -w
-buildid=
-linkmode external
-extldflags=-static

View File

@@ -23,4 +23,4 @@ var hakureiSource = pkg.NewTar(pkg.NewFile(
), pkg.TarGzip)
// hakureiPatches are patches applied against the compile-time source tree.
var hakureiPatches [][2]string
var hakureiPatches []KV

View File

@@ -15,4 +15,4 @@ var hakureiSource = pkg.NewHTTPGetTar(
)
// hakureiPatches are patches applied against a hakurei release.
var hakureiPatches [][2]string
var hakureiPatches []KV

View File

@@ -2,12 +2,12 @@ package rosa
import "hakurei.app/internal/pkg"
const kernelVersion = "6.12.77"
const kernelVersion = "6.12.78"
var kernelSource = pkg.NewHTTPGetTar(
nil, "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/"+
"snapshot/linux-"+kernelVersion+".tar.gz",
mustDecode("_MyFL0MqqNwAJx4fP8L9FkUayXIqEJto5trAPr_9UJvaT5TK1tvlU8leS82Hw2uw"),
mustDecode("iUlZA-nv04TUOL0TmgDGBjaOe0sIaXTqLvuR4owYgHMZM8vecusnMMqbeuuZP4_G"),
pkg.TarGzip,
)
@@ -90,7 +90,7 @@ exec /system/sbin/depmod -m /lib/modules "$@"
`))),
},
Patches: [][2]string{
Patches: []KV{
{"f54a91f5337cd918eb86cf600320d25b6cfd8209", `From f54a91f5337cd918eb86cf600320d25b6cfd8209 Mon Sep 17 00:00:00 2001
From: Nathan Chancellor <nathan@kernel.org>
Date: Sat, 13 Dec 2025 19:58:10 +0900

View File

@@ -1,16 +1,16 @@
#
# Automatically generated file; DO NOT EDIT.
# Linux/x86 6.12.76 Kernel Configuration
# Linux/x86 6.12.78 Kernel Configuration
#
CONFIG_CC_VERSION_TEXT="clang version 22.1.1"
CONFIG_CC_VERSION_TEXT="clang version 22.1.2"
CONFIG_GCC_VERSION=0
CONFIG_CC_IS_CLANG=y
CONFIG_CLANG_VERSION=220101
CONFIG_CLANG_VERSION=220102
CONFIG_AS_IS_LLVM=y
CONFIG_AS_VERSION=220101
CONFIG_AS_VERSION=220102
CONFIG_LD_VERSION=0
CONFIG_LD_IS_LLD=y
CONFIG_LLD_VERSION=220101
CONFIG_LLD_VERSION=220102
CONFIG_RUSTC_VERSION=0
CONFIG_RUSTC_LLVM_VERSION=0
CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y

View File

@@ -1,16 +1,16 @@
#
# Automatically generated file; DO NOT EDIT.
# Linux/arm64 6.12.76 Kernel Configuration
# Linux/arm64 6.12.78 Kernel Configuration
#
CONFIG_CC_VERSION_TEXT="clang version 22.1.1"
CONFIG_CC_VERSION_TEXT="clang version 21.1.8"
CONFIG_GCC_VERSION=0
CONFIG_CC_IS_CLANG=y
CONFIG_CLANG_VERSION=220101
CONFIG_CLANG_VERSION=210108
CONFIG_AS_IS_LLVM=y
CONFIG_AS_VERSION=220101
CONFIG_AS_VERSION=210108
CONFIG_LD_VERSION=0
CONFIG_LD_IS_LLD=y
CONFIG_LLD_VERSION=220101
CONFIG_LLD_VERSION=210108
CONFIG_RUSTC_VERSION=0
CONFIG_RUSTC_LLVM_VERSION=0
CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y
@@ -73,6 +73,7 @@ CONFIG_IRQ_MSI_IOMMU=y
CONFIG_IRQ_FORCED_THREADING=y
CONFIG_SPARSE_IRQ=y
# CONFIG_GENERIC_IRQ_DEBUGFS is not set
CONFIG_GENERIC_IRQ_KEXEC_CLEAR_VM_FORWARD=y
# end of IRQ subsystem
CONFIG_GENERIC_TIME_VSYSCALL=y

View File

@@ -13,7 +13,7 @@ func (t Toolchain) newKmod() (pkg.Artifact, string) {
mustDecode(checksum),
pkg.TarGzip,
), nil, &MesonHelper{
Setup: [][2]string{
Setup: []KV{
{"Dmoduledir", "/system/lib/modules"},
{"Dsysconfdir", "/system/etc"},
{"Dbashcompletiondir", "no"},

View File

@@ -8,8 +8,8 @@ import (
func (t Toolchain) newLibexpat() (pkg.Artifact, string) {
const (
version = "2.7.4"
checksum = "W6NI2FESBjrTqRPcvs15fK5c3nwF6f9RT8U-XHKQKblXVzJB3nt_ez5B5jO0ZVDG"
version = "2.7.5"
checksum = "vTRUjjg-qbHSXUBYKXgzVHkUO7UNyuhrkSYrE7ikApQm0g-OvQ8tspw4w55M-1Tp"
)
return t.NewPackage("libexpat", version, pkg.NewHTTPGetTar(
nil, "https://github.com/libexpat/libexpat/releases/download/"+

View File

@@ -16,6 +16,23 @@ func (t Toolchain) newLibseccomp() (pkg.Artifact, string) {
ScriptEarly: `
ln -s ../system/bin/bash /bin/
`,
Patches: []KV{
{"fix-export-oob-read", `diff --git a/src/api.c b/src/api.c
index adccef3..65a277a 100644
--- a/src/api.c
+++ b/src/api.c
@@ -786,7 +786,7 @@ API int seccomp_export_bpf_mem(const scmp_filter_ctx ctx, void *buf,
if (BPF_PGM_SIZE(program) > *len)
rc = _rc_filter(-ERANGE);
else
- memcpy(buf, program->blks, *len);
+ memcpy(buf, program->blks, BPF_PGM_SIZE(program));
}
*len = BPF_PGM_SIZE(program);
`},
},
}, (*MakeHelper)(nil),
Bash,
Diffutils,

View File

@@ -19,7 +19,7 @@ type llvmAttr struct {
// Concatenated with default environment for PackageAttr.Env.
env []string
// Concatenated with generated entries for CMakeHelper.Cache.
cmake [][2]string
cmake []KV
// Override CMakeHelper.Append.
append []string
// Passed through to PackageAttr.NonStage0.
@@ -30,7 +30,7 @@ type llvmAttr struct {
script string
// Patch name and body pairs.
patches [][2]string
patches []KV
}
const (
@@ -94,43 +94,45 @@ func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
var script string
cache := [][2]string{
cache := []KV{
{"CMAKE_BUILD_TYPE", "Release"},
{"LLVM_HOST_TRIPLE", `"${ROSA_TRIPLE}"`},
{"LLVM_DEFAULT_TARGET_TRIPLE", `"${ROSA_TRIPLE}"`},
}
if len(projects) > 0 {
cache = append(cache,
[2]string{"LLVM_ENABLE_PROJECTS", `"${ROSA_LLVM_PROJECTS}"`})
cache = append(cache, []KV{
{"LLVM_ENABLE_PROJECTS", `"${ROSA_LLVM_PROJECTS}"`},
}...)
}
if len(runtimes) > 0 {
cache = append(cache,
[2]string{"LLVM_ENABLE_RUNTIMES", `"${ROSA_LLVM_RUNTIMES}"`})
cache = append(cache, []KV{
{"LLVM_ENABLE_RUNTIMES", `"${ROSA_LLVM_RUNTIMES}"`},
}...)
}
cmakeAppend := []string{"llvm"}
if attr.append != nil {
cmakeAppend = attr.append
} else {
cache = append(cache,
[2]string{"LLVM_ENABLE_LIBCXX", "ON"},
[2]string{"LLVM_USE_LINKER", "lld"},
cache = append(cache, []KV{
{"LLVM_ENABLE_LIBCXX", "ON"},
{"LLVM_USE_LINKER", "lld"},
[2]string{"LLVM_INSTALL_BINUTILS_SYMLINKS", "ON"},
[2]string{"LLVM_INSTALL_CCTOOLS_SYMLINKS", "ON"},
{"LLVM_INSTALL_BINUTILS_SYMLINKS", "ON"},
{"LLVM_INSTALL_CCTOOLS_SYMLINKS", "ON"},
[2]string{"LLVM_LIT_ARGS", "'--verbose'"},
)
{"LLVM_LIT_ARGS", "'--verbose'"},
}...)
}
if attr.flags&llvmProjectClang != 0 {
cache = append(cache,
[2]string{"CLANG_DEFAULT_LINKER", "lld"},
[2]string{"CLANG_DEFAULT_CXX_STDLIB", "libc++"},
[2]string{"CLANG_DEFAULT_RTLIB", "compiler-rt"},
[2]string{"CLANG_DEFAULT_UNWINDLIB", "libunwind"},
)
cache = append(cache, []KV{
{"CLANG_DEFAULT_LINKER", "lld"},
{"CLANG_DEFAULT_CXX_STDLIB", "libc++"},
{"CLANG_DEFAULT_RTLIB", "compiler-rt"},
{"CLANG_DEFAULT_UNWINDLIB", "libunwind"},
}...)
}
if attr.flags&llvmProjectLld != 0 {
script += `
@@ -139,25 +141,27 @@ ln -s ld.lld /work/system/bin/ld
}
if attr.flags&llvmRuntimeCompilerRT != 0 {
if attr.append == nil {
cache = append(cache,
[2]string{"COMPILER_RT_USE_LLVM_UNWINDER", "ON"})
cache = append(cache, []KV{
{"COMPILER_RT_USE_LLVM_UNWINDER", "ON"},
}...)
}
}
if attr.flags&llvmRuntimeLibunwind != 0 {
cache = append(cache,
[2]string{"LIBUNWIND_USE_COMPILER_RT", "ON"})
cache = append(cache, []KV{
{"LIBUNWIND_USE_COMPILER_RT", "ON"},
}...)
}
if attr.flags&llvmRuntimeLibcxx != 0 {
cache = append(cache,
[2]string{"LIBCXX_HAS_MUSL_LIBC", "ON"},
[2]string{"LIBCXX_USE_COMPILER_RT", "ON"},
)
cache = append(cache, []KV{
{"LIBCXX_HAS_MUSL_LIBC", "ON"},
{"LIBCXX_USE_COMPILER_RT", "ON"},
}...)
}
if attr.flags&llvmRuntimeLibcxxABI != 0 {
cache = append(cache,
[2]string{"LIBCXXABI_USE_COMPILER_RT", "ON"},
[2]string{"LIBCXXABI_USE_LLVM_UNWINDER", "ON"},
)
cache = append(cache, []KV{
{"LIBCXXABI_USE_COMPILER_RT", "ON"},
{"LIBCXXABI_USE_LLVM_UNWINDER", "ON"},
}...)
}
return t.NewPackage("llvm", llvmVersion, pkg.NewHTTPGetTar(
@@ -208,7 +212,7 @@ func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) {
panic("unsupported target " + runtime.GOARCH)
}
minimalDeps := [][2]string{
minimalDeps := []KV{
{"LLVM_ENABLE_ZLIB", "OFF"},
{"LLVM_ENABLE_ZSTD", "OFF"},
{"LLVM_ENABLE_LIBXML2", "OFF"},
@@ -222,7 +226,7 @@ func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) {
env: stage0ExclConcat(t, []string{},
"LDFLAGS="+earlyLDFLAGS(false),
),
cmake: [][2]string{
cmake: []KV{
// libc++ not yet available
{"CMAKE_CXX_COMPILER_TARGET", ""},
@@ -272,7 +276,7 @@ ln -s \
"LDFLAGS="+earlyLDFLAGS(false),
),
flags: llvmRuntimeLibunwind | llvmRuntimeLibcxx | llvmRuntimeLibcxxABI,
cmake: slices.Concat([][2]string{
cmake: slices.Concat([]KV{
// libc++ not yet available
{"CMAKE_CXX_COMPILER_WORKS", "ON"},
@@ -293,7 +297,7 @@ ln -s \
"CXXFLAGS="+earlyCXXFLAGS(),
"LDFLAGS="+earlyLDFLAGS(false),
),
cmake: slices.Concat([][2]string{
cmake: slices.Concat([]KV{
{"LLVM_TARGETS_TO_BUILD", target},
{"CMAKE_CROSSCOMPILING", "OFF"},
{"CXX_SUPPORTS_CUSTOM_LINKER", "ON"},
@@ -310,7 +314,7 @@ ln -s clang++ /work/system/bin/c++
ninja check-all
`,
patches: slices.Concat([][2]string{
patches: slices.Concat([]KV{
{"add-rosa-vendor", `diff --git a/llvm/include/llvm/TargetParser/Triple.h b/llvm/include/llvm/TargetParser/Triple.h
index 9c83abeeb3b1..5acfe5836a23 100644
--- a/llvm/include/llvm/TargetParser/Triple.h

View File

@@ -1,4 +1,4 @@
package rosa
// clangPatches are patches applied to the LLVM source tree for building clang.
var clangPatches [][2]string
var clangPatches []KV

View File

@@ -1,7 +1,7 @@
package rosa
// clangPatches are patches applied to the LLVM source tree for building clang.
var clangPatches [][2]string
var clangPatches []KV
// one version behind, latest fails 5 tests with 2 flaky on arm64
const (

View File

@@ -5,7 +5,7 @@ package rosa
// latest version of LLVM, conditional to temporarily avoid broken new releases
const (
llvmVersionMajor = "22"
llvmVersion = llvmVersionMajor + ".1.1"
llvmVersion = llvmVersionMajor + ".1.2"
llvmChecksum = "bQvV6D8AZvQykg7-uQb_saTbVavnSo1ykNJ3g57F5iE-evU3HuOYtcRnVIXTK76e"
llvmChecksum = "FwsmurWDVyYYQlOowowFjekwIGSB5__aKTpW_VGP3eWoZGXvBny-bOn1DuQ1U5xE"
)

View File

@@ -60,7 +60,7 @@ type MakeHelper struct {
// Alternative name for the configure script.
ConfigureName string
// Flags passed to the configure script.
Configure [][2]string
Configure []KV
// Host target triple, zero value is equivalent to the Rosa OS triple.
Host string
// Target triple, zero value is equivalent to the Rosa OS triple.

View File

@@ -59,7 +59,7 @@ type MesonHelper struct {
Script string
// Flags passed to the setup command.
Setup [][2]string
Setup []KV
// Whether to skip meson test.
SkipTest bool
}
@@ -113,7 +113,7 @@ meson test \
cd "$(mktemp -d)"
meson setup \
` + strings.Join(slices.Collect(func(yield func(string) bool) {
for _, v := range append([][2]string{
for _, v := range append([]KV{
{"prefix", "/system"},
{"buildtype", "release"},
}, attr.Setup...) {

View File

@@ -8,8 +8,8 @@ func (t Toolchain) newMusl(
extra ...pkg.Artifact,
) (pkg.Artifact, string) {
const (
version = "1.2.5"
checksum = "y6USdIeSdHER_Fw2eT2CNjqShEye85oEg2jnOur96D073ukmIpIqDOLmECQroyDb"
version = "1.2.6"
checksum = "WtWb_OV_XxLDAB5NerOL9loLlHVadV00MmGk65PPBU1evaolagoMHfvpZp_vxEzS"
)
name := "musl"

View File

@@ -15,7 +15,7 @@ func (t Toolchain) newNcurses() (pkg.Artifact, string) {
// "tests" are actual demo programs, not a test suite.
SkipCheck: true,
Configure: [][2]string{
Configure: []KV{
{"with-pkg-config"},
{"enable-pc-files"},
},

View File

@@ -8,8 +8,8 @@ import (
func (t Toolchain) newNSS() (pkg.Artifact, string) {
const (
version = "3.121"
checksum = "MTS4Eg-1vBN3T7gdUAdNO0y_e9x9BE3f_k_DHdM_BIovc7y57vhsZTfB5f6BeQfi"
version = "3.122"
checksum = "QvC6TBO4BAUEh6wmgUrb1hwH5podQAN-QdcAaWL32cWEppmZs6oKkZpD9GvZf59S"
version0 = "4_38_2"
checksum0 = "25x2uJeQnOHIiq_zj17b4sYqKgeoU8-IsySUptoPcdHZ52PohFZfGuIisBreWzx0"

View File

@@ -20,7 +20,7 @@ func (t Toolchain) newOpenSSL() (pkg.Artifact, string) {
OmitDefaults: true,
ConfigureName: "/usr/src/openssl/Configure",
Configure: [][2]string{
Configure: []KV{
{"prefix", "/system"},
{"libdir", "lib"},
{"openssldir", "etc/ssl"},

View File

@@ -20,7 +20,7 @@ func (t Toolchain) newPCRE2() (pkg.Artifact, string) {
ln -s ../system/bin/toybox /bin/echo
`,
}, &MakeHelper{
Configure: [][2]string{
Configure: []KV{
{"enable-jit"},
{"enable-pcre2-8"},
{"enable-pcre2-16"},

View File

@@ -31,7 +31,7 @@ rm -f /system/bin/ps # perl does not like toybox ps
InPlace: true,
ConfigureName: "./Configure",
Configure: [][2]string{
Configure: []KV{
{"-des"},
{"Dprefix", "/system"},
{"Dcc", "clang"},
@@ -67,7 +67,7 @@ func init() {
func (t Toolchain) newViaPerlModuleBuild(
name, version string,
source pkg.Artifact,
patches [][2]string,
patches []KV,
extra ...PArtifact,
) pkg.Artifact {
if name == "" || version == "" {
@@ -116,7 +116,7 @@ func init() {
func (t Toolchain) newViaPerlMakeMaker(
name, version string,
source pkg.Artifact,
patches [][2]string,
patches []KV,
extra ...PArtifact,
) pkg.Artifact {
return t.NewPackage("perl-"+name, version, source, &PackageAttr{
@@ -131,7 +131,7 @@ func (t Toolchain) newViaPerlMakeMaker(
InPlace: true,
ConfigureName: "perl Makefile.PL",
Configure: [][2]string{
Configure: []KV{
{"PREFIX", "/system"},
},
Check: []string{"test"},

View File

@@ -13,7 +13,7 @@ func (t Toolchain) newPkgConfig() (pkg.Artifact, string) {
mustDecode(checksum),
pkg.TarGzip,
), nil, &MakeHelper{
Configure: [][2]string{
Configure: []KV{
{"CFLAGS", "'-Wno-int-conversion'"},
{"with-internal-glib"},
},

View File

@@ -14,7 +14,7 @@ func (t Toolchain) newProcps() (pkg.Artifact, string) {
pkg.TarBzip2,
), nil, &MakeHelper{
Generate: "./autogen.sh",
Configure: [][2]string{
Configure: []KV{
{"without-ncurses"},
},
},

View File

@@ -4,15 +4,15 @@ import "hakurei.app/internal/pkg"
func (t Toolchain) newQEMU() (pkg.Artifact, string) {
const (
version = "10.2.1"
checksum = "rjLTSgHJd3X3Vgpxrsus_ZZiaYLiNix1YhcHaGbLd_odYixwZjCcAIt8CVQPJGdZ"
version = "10.2.2"
checksum = "uNzRxlrVoLWe-EmZmBp75SezymgE512iE5XN90Bl7wi6CjE_oQGQB-9ocs7E16QG"
)
return t.NewPackage("qemu", version, pkg.NewHTTPGetTar(
nil, "https://download.qemu.org/qemu-"+version+".tar.bz2",
mustDecode(checksum),
pkg.TarBzip2,
), &PackageAttr{
Patches: [][2]string{
Patches: []KV{
{"disable-mcast-test", `diff --git a/tests/qtest/netdev-socket.c b/tests/qtest/netdev-socket.c
index b731af0ad9..b5cbed4801 100644
--- a/tests/qtest/netdev-socket.c
@@ -58,7 +58,7 @@ _notrun 'appears to spuriously fail on zfs'
EOF
`,
}, &MakeHelper{
Configure: [][2]string{
Configure: []KV{
{"disable-download"},
{"disable-docs"},

View File

@@ -20,9 +20,6 @@ const (
// kindBusyboxBin is the kind of [pkg.Artifact] of busyboxBin.
kindBusyboxBin
// kindCollection is the kind of [Collect]. It never cures successfully.
kindCollection
)
// mustDecode is like [pkg.MustDecode], but replaces the zero value and prints
@@ -40,6 +37,9 @@ func mustDecode(s string) pkg.Checksum {
return pkg.MustDecode(s)
}
// KV is a key-value pair of strings.
type KV [2]string
var (
// AbsUsrSrc is the conventional directory to place source code under.
AbsUsrSrc = fhs.AbsUsr.Append("src")
@@ -202,6 +202,10 @@ func lastIndexFunc[S ~[]E, E any](s S, f func(E) bool) (i int) {
// fixupEnviron fixes up PATH, prepends extras and returns the resulting slice.
func fixupEnviron(env, extras []string, paths ...string) []string {
// some python tools try to be clever and buffers their output, making the
// build process appear to hang
env = append(env, "PYTHONUNBUFFERED=1")
const pathPrefix = "PATH="
pathVal := strings.Join(paths, ":")
@@ -363,7 +367,7 @@ func (t Toolchain) NewPatchedSource(
name, version string,
source pkg.Artifact,
passthrough bool,
patches ...[2]string,
patches ...KV,
) pkg.Artifact {
if passthrough && len(patches) == 0 {
return source
@@ -445,7 +449,7 @@ type PackageAttr struct {
ScriptEarly string
// Passed to [Toolchain.NewPatchedSource].
Patches [][2]string
Patches []KV
// Kind of source artifact.
SourceKind int
@@ -591,29 +595,3 @@ cd '/usr/src/` + name + `/'
})...,
)
}
// 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" }
// Collect implements [pkg.FloodArtifact] to concurrently cure multiple
// [pkg.Artifact]. It returns [Collected].
type Collect []pkg.Artifact
// Cure returns [Collected].
func (*Collect) Cure(*pkg.FContext) error { return Collected{} }
// Kind returns the hardcoded [pkg.Kind] value.
func (*Collect) Kind() pkg.Kind { return kindCollection }
// Params does not write anything, dependencies are already represented in the header.
func (*Collect) Params(*pkg.IContext) {}
// Dependencies returns [Collect] as is.
func (c *Collect) Dependencies() []pkg.Artifact { return *c }
// IsExclusive returns false: Cure is a noop.
func (*Collect) IsExclusive() bool { return false }

View File

@@ -15,7 +15,7 @@ func (t Toolchain) newRsync() (pkg.Artifact, string) {
), &PackageAttr{
Flag: TEarly,
}, &MakeHelper{
Configure: [][2]string{
Configure: []KV{
{"disable-openssl"},
{"disable-xxhash"},
{"disable-zstd"},

View File

@@ -23,7 +23,7 @@ sed -i 's/unsigned int msg_len;$/uint32_t msg_len;/g' \
tests/nlattr.c
`,
}, &MakeHelper{
Configure: [][2]string{
Configure: []KV{
// tests broken on clang
{"disable-gcc-Werror"},

View File

@@ -22,7 +22,7 @@ func (t Toolchain) newUtilLinux() (pkg.Artifact, string) {
ln -s ../system/bin/bash /bin/
`,
}, &MakeHelper{
Configure: [][2]string{
Configure: []KV{
{"disable-use-tty-group"},
{"disable-makeinstall-setuid"},
{"disable-makeinstall-chown"},

View File

@@ -4,8 +4,8 @@ import "hakurei.app/internal/pkg"
func (t Toolchain) newWayland() (pkg.Artifact, string) {
const (
version = "1.24.91"
checksum = "SQkjYShk2TutoBOfmeJcdLU9iDExVKOg0DZhLeL8U_qjc9olLTC7h3vuUBvVtx9w"
version = "1.25.0"
checksum = "q-4dYXme46JPgLGtXAxyZGTy7udll9RfT0VXtcW2YRR1WWViUhvdZXZneXzLqpCg"
)
return t.NewPackage("wayland", version, pkg.NewHTTPGetTar(
nil, "https://gitlab.freedesktop.org/wayland/wayland/"+
@@ -20,7 +20,7 @@ chmod +w tests tests/sanity-test.c
echo 'int main(){}' > tests/sanity-test.c
`,
}, &MesonHelper{
Setup: [][2]string{
Setup: []KV{
{"Ddefault_library", "both"},
{"Ddocumentation", "false"},
{"Dtests", "true"},
@@ -63,7 +63,7 @@ func (t Toolchain) newWaylandProtocols() (pkg.Artifact, string) {
mustDecode(checksum),
pkg.TarBzip2,
), &PackageAttr{
Patches: [][2]string{
Patches: []KV{
{"build-only", `From 8b4c76275fa1b6e0a99a53494151d9a2c907144d Mon Sep 17 00:00:00 2001
From: "A. Wilcox" <AWilcox@Wilcox-Tech.com>
Date: Fri, 8 Nov 2024 11:27:25 -0600

View File

@@ -12,7 +12,7 @@ func (t Toolchain) newZlib() (pkg.Artifact, string) {
mustDecode(checksum),
pkg.TarGzip,
), nil, &CMakeHelper{
Cache: [][2]string{
Cache: []KV{
{"CMAKE_BUILD_TYPE", "Release"},
{"ZLIB_BUILD_TESTING", "OFF"},

View File

@@ -14,7 +14,7 @@ func (t Toolchain) newZstd() (pkg.Artifact, string) {
pkg.TarGzip,
), nil, &CMakeHelper{
Append: []string{"build", "cmake"},
Cache: [][2]string{
Cache: []KV{
{"CMAKE_BUILD_TYPE", "Release"},
},
}), version

85
internal/uevent/action.go Normal file
View File

@@ -0,0 +1,85 @@
package uevent
import (
"strconv"
"syscall"
)
// KobjectAction represents enum kobject_action found in include/linux/kobject.h
// and their corresponding string representations in lib/kobject_uevent.c.
type KobjectAction uint32
// include/linux/kobject.h
const (
KOBJ_ADD KobjectAction = iota
KOBJ_REMOVE
KOBJ_CHANGE
KOBJ_MOVE
KOBJ_ONLINE
KOBJ_OFFLINE
KOBJ_BIND
KOBJ_UNBIND
// Synthetic denotes a [Message] that originates from outside the kernel. It
// is not valid in the wire format and is only meaningful within this package.
Synthetic KobjectAction = 0xfeed
)
// lib/kobject_uevent.c
var kobject_actions = [...]string{
KOBJ_ADD: "add",
KOBJ_REMOVE: "remove",
KOBJ_CHANGE: "change",
KOBJ_MOVE: "move",
KOBJ_ONLINE: "online",
KOBJ_OFFLINE: "offline",
KOBJ_BIND: "bind",
KOBJ_UNBIND: "unbind",
}
// Valid returns whether the value of act is defined.
func (act KobjectAction) Valid() bool { return int(act) < len(kobject_actions) }
// String returns the corresponding string sent over netlink.
func (act KobjectAction) String() string {
if act == Synthetic {
return "synthetic"
}
if !act.Valid() {
return "unsupported kobject_action " + strconv.Itoa(int(act))
}
return kobject_actions[act]
}
func (act KobjectAction) AppendText(b []byte) ([]byte, error) {
if !act.Valid() && act != Synthetic {
return b, syscall.EINVAL
}
return append(b, act.String()...), nil
}
func (act KobjectAction) MarshalText() ([]byte, error) {
return act.AppendText(nil)
}
// An UnsupportedActionError describes a string representation of [KobjectAction]
// not yet supported by this package.
type UnsupportedActionError string
var _ Recoverable = UnsupportedActionError("")
func (UnsupportedActionError) recoverable() {}
func (e UnsupportedActionError) Error() string {
return "unsupported kobject_action " + strconv.Quote(string(e))
}
func (act *KobjectAction) UnmarshalText(data []byte) error {
for v, s := range kobject_actions {
if string(data) == s {
*act = KobjectAction(v)
return nil
}
}
return UnsupportedActionError(data)
}

View File

@@ -0,0 +1,43 @@
package uevent_test
import (
"syscall"
"testing"
"hakurei.app/internal/uevent"
)
func TestKobjectAction(t *testing.T) {
t.Parallel()
adeT(t, "add", uevent.KOBJ_ADD, "add", nil, nil)
adeT(t, "remove", uevent.KOBJ_REMOVE, "remove", nil, nil)
adeT(t, "change", uevent.KOBJ_CHANGE, "change", nil, nil)
adeT(t, "move", uevent.KOBJ_MOVE, "move", nil, nil)
adeT(t, "online", uevent.KOBJ_ONLINE, "online", nil, nil)
adeT(t, "offline", uevent.KOBJ_OFFLINE, "offline", nil, nil)
adeT(t, "bind", uevent.KOBJ_BIND, "bind", nil, nil)
adeT(t, "unbind", uevent.KOBJ_UNBIND, "unbind", nil, nil)
adeT(t, "unsupported", uevent.KobjectAction(0xbad), "explode",
uevent.UnsupportedActionError("explode"), syscall.EINVAL)
t.Run("oob string", func(t *testing.T) {
t.Parallel()
const want = "unsupported kobject_action 2989"
if got := uevent.KobjectAction(0xbad).String(); got != want {
t.Errorf("String: %q, want %q", got, want)
}
})
adeT(t, "synthetic", uevent.Synthetic, "synthetic",
uevent.UnsupportedActionError("synthetic"), nil)
t.Run("validate synthetic", func(t *testing.T) {
t.Parallel()
if uevent.Synthetic.Valid() {
t.Errorf("Valid unexpectedly succeeded")
}
})
}

137
internal/uevent/message.go Normal file
View File

@@ -0,0 +1,137 @@
package uevent
import (
"bytes"
"strconv"
"strings"
)
// A Message represents a kernel message to userspace.
type Message struct {
// alloc_uevent_skb: action_string
Action KobjectAction `json:"action"`
// alloc_uevent_skb: devpath
DevPath string `json:"devpath"`
// add_uevent_var: key value strings
Env []string `json:"env"`
}
// String returns a multiline user-facing string representation of [Message].
func (msg *Message) String() string {
var buf strings.Builder
buf.WriteString(msg.Action.String() + " event")
if msg.DevPath != "" {
buf.WriteString(" on " + msg.DevPath)
}
buf.WriteString(":")
for _, s := range msg.Env {
buf.WriteString("\n" + s)
}
return buf.String()
}
var (
// zero is a single pre-allocated NUL character.
zero = []byte{0}
// sepHeader is the separator in a [Message] header.
sepHeader = []byte{'@'}
)
func (msg *Message) AppendBinary(b []byte) (_ []byte, err error) {
if b, err = msg.Action.AppendText(b); err != nil {
return
}
b = append(b, sepHeader...)
b = append(b, msg.DevPath...)
b = append(b, zero...)
for _, s := range msg.Env {
b = append(b, s...)
b = append(b, zero...)
}
return b, nil
}
func (msg *Message) MarshalBinary() ([]byte, error) {
return msg.AppendBinary(nil)
}
// MissingHeaderError is an invalid representation of [Message] which is missing
// its header added by alloc_uevent_skb.
type MissingHeaderError string
var _ Recoverable = MissingHeaderError("")
func (MissingHeaderError) recoverable() {}
func (e MissingHeaderError) Error() string {
return "message " + strconv.Quote(string(e)) + " has no header"
}
// MessageError describes a malformed representation of [Message].
type MessageError struct {
// Full offending data.
Data string `json:"data"`
// Offending section.
Section string `json:"section"`
// Part of header in Section.
Kind int `json:"kind"`
}
var _ Recoverable = new(MessageError)
var _ Nontrivial = new(MessageError)
const (
// MErrorKindHeaderSep denotes a message header missing its separator.
MErrorKindHeaderSep = iota
// MErrorKindFinalNUL denotes a message body missing its final NUL terminator.
MErrorKindFinalNUL
)
func (*MessageError) recoverable() {}
func (*MessageError) nontrivial() {}
func (e *MessageError) Error() string {
switch e.Kind {
case MErrorKindHeaderSep:
return "header " + strconv.Quote(e.Section) + " missing separator"
case MErrorKindFinalNUL:
return "entry " + strconv.Quote(e.Section) + " missing NUL"
default:
return "section " + strconv.Quote(e.Section) + " is invalid"
}
}
func (msg *Message) UnmarshalBinary(data []byte) error {
header, body, ok := bytes.Cut(data, zero)
if !ok {
return MissingHeaderError(data)
}
action_string, devpath, ok := bytes.Cut(header, sepHeader)
if !ok {
return &MessageError{string(data), string(header), MErrorKindHeaderSep}
}
if err := msg.Action.UnmarshalText(action_string); err != nil {
return err
}
msg.DevPath = string(devpath)
if len(body) == 0 {
msg.Env = nil
return nil
}
msg.Env = make([]string, 0, bytes.Count(body, zero))
var s []byte
for len(body) != 0 {
var r []byte
s, r, ok = bytes.Cut(body, zero)
if !ok {
return &MessageError{string(data), string(body), MErrorKindFinalNUL}
}
body = r
msg.Env = append(msg.Env, string(s))
}
return nil
}

View File

@@ -0,0 +1,132 @@
package uevent_test
import (
"syscall"
"testing"
"hakurei.app/internal/uevent"
)
func TestMessage(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
v uevent.Message
want string
wantErr error
wantErrE error
s string
}{
{"sample virtio-sound-pci add", uevent.Message{
Action: uevent.KOBJ_ADD,
DevPath: "/devices/pci0000:00/0000:00:04.0/virtio1",
Env: []string{
"ACTION=add",
"DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1",
"SUBSYSTEM=virtio",
"MODALIAS=virtio:d00000019v00001AF4",
"SEQNUM=779",
},
}, "add@/devices/pci0000:00/0000:00:04.0/virtio1\x00" +
"ACTION=add\x00" +
"DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1\x00" +
"SUBSYSTEM=virtio\x00" +
"MODALIAS=virtio:d00000019v00001AF4\x00" +
"SEQNUM=779\x00",
nil, nil, `add event on /devices/pci0000:00/0000:00:04.0/virtio1:
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1
SUBSYSTEM=virtio
MODALIAS=virtio:d00000019v00001AF4
SEQNUM=779`},
{"sample virtio-sound-pci bind", uevent.Message{
Action: uevent.KOBJ_BIND,
DevPath: "/devices/pci0000:00/0000:00:04.0",
Env: []string{
"ACTION=bind",
"DEVPATH=/devices/pci0000:00/0000:00:04.0",
"SUBSYSTEM=pci",
"DRIVER=virtio-pci",
"PCI_CLASS=40100",
"PCI_ID=1AF4:1059",
"PCI_SUBSYS_ID=1AF4:1100",
"PCI_SLOT_NAME=0000:00:04.0",
"MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00",
"SEQNUM=780",
},
}, "bind@/devices/pci0000:00/0000:00:04.0\x00" +
"ACTION=bind\x00" +
"DEVPATH=/devices/pci0000:00/0000:00:04.0\x00" +
"SUBSYSTEM=pci\x00" +
"DRIVER=virtio-pci\x00" +
"PCI_CLASS=40100\x00" +
"PCI_ID=1AF4:1059\x00" +
"PCI_SUBSYS_ID=1AF4:1100\x00" +
"PCI_SLOT_NAME=0000:00:04.0\x00" +
"MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00\x00" +
"SEQNUM=780\x00", nil, nil, `bind event on /devices/pci0000:00/0000:00:04.0:
ACTION=bind
DEVPATH=/devices/pci0000:00/0000:00:04.0
SUBSYSTEM=pci
DRIVER=virtio-pci
PCI_CLASS=40100
PCI_ID=1AF4:1059
PCI_SUBSYS_ID=1AF4:1100
PCI_SLOT_NAME=0000:00:04.0
MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00
SEQNUM=780`},
{"zero devpath env", uevent.Message{
Action: uevent.KOBJ_MOVE,
}, "move@\x00", nil, nil, "move event:"},
{"d final NUL e bad action", uevent.Message{
Action: 0xbad,
}, "move@\x00truncated", &uevent.MessageError{
Data: "move@\x00truncated",
Section: "truncated",
Kind: uevent.MErrorKindFinalNUL,
}, syscall.EINVAL, "unsupported kobject_action 2989 event:"},
{"bad action", uevent.Message{
Action: 0xbad,
}, "nonexistent@\x00", uevent.UnsupportedActionError(
"nonexistent",
), syscall.EINVAL, "unsupported kobject_action 2989 event:"},
{"d header sep e bad action", uevent.Message{
Action: 0xbad,
}, "move\x00", &uevent.MessageError{
Data: "move\x00",
Section: "move",
Kind: uevent.MErrorKindHeaderSep,
}, syscall.EINVAL, "unsupported kobject_action 2989 event:"},
{"d missing header e bad action", uevent.Message{
Action: 0xbad,
}, "move", uevent.MissingHeaderError(
"move",
), syscall.EINVAL, "unsupported kobject_action 2989 event:"},
{"synthetic", uevent.Message{
Action: uevent.Synthetic,
}, "synthetic@\x00", uevent.UnsupportedActionError(
"synthetic",
), nil, "synthetic event:"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
adeB(t, "", tc.v, tc.want, tc.wantErr, tc.wantErrE)
t.Run("string", func(t *testing.T) {
if got := tc.v.String(); got != tc.s {
t.Errorf("String: %q, want %q", got, tc.s)
}
})
})
}
}

87
internal/uevent/sysfs.go Normal file
View File

@@ -0,0 +1,87 @@
package uevent
import (
"bytes"
"errors"
"io/fs"
"log"
"path/filepath"
"unsafe"
)
// Enumerate scans sysfs and emits [Synthetic] events. It returns the first
// error it encounters.
//
// The specified filesystem must present the sysfs root.
func Enumerate(
sysfs fs.FS,
handleWalkErr func(error) error,
events chan<- *Message,
) error {
if handleWalkErr == nil {
handleWalkErr = func(err error) error {
if errors.Is(err, fs.ErrNotExist) {
log.Println("enumerate", err)
return nil
}
return err
}
}
return fs.WalkDir(sysfs, "devices", func(
path string,
d fs.DirEntry,
err error,
) error {
if err != nil {
return handleWalkErr(err)
}
if d.IsDir() || d.Name() != "uevent" {
return nil
}
msg := Message{
Action: Synthetic,
// cleans path, appears to be compatible with kernel behaviour
DevPath: filepath.Dir(path),
}
var target string
if target, err = fs.ReadLink(
sysfs,
filepath.Join(msg.DevPath, "subsystem"),
); err != nil {
if err = handleWalkErr(err); err != nil {
return err
}
} else {
msg.Env = append(msg.Env, "SUBSYSTEM="+filepath.Base(target))
}
// read entire file: slicing does not copy
var env []byte
if env, err = fs.ReadFile(sysfs, path); err != nil {
return handleWalkErr(err)
}
for _, s := range bytes.Split(env, []byte{'\n'}) {
if len(s) == 0 {
continue
}
msg.Env = append(msg.Env, unsafe.String(unsafe.SliceData(s), len(s)))
}
if len(msg.Env) == 0 {
// this implies absent subsystem, its error is already handled
return nil
}
if msg.DevPath != "" && msg.DevPath[0] != '/' {
msg.DevPath = "/" + msg.DevPath
}
events <- &msg
return nil
})
}

View File

@@ -0,0 +1,28 @@
package uevent_test
import (
"os"
"sync"
"testing"
"hakurei.app/internal/uevent"
)
func TestEnumerate(t *testing.T) {
t.Parallel()
var wg sync.WaitGroup
defer wg.Wait()
events := make(chan *uevent.Message, 1<<10)
wg.Go(func() {
for msg := range events {
t.Log(msg)
}
})
if err := uevent.Enumerate(os.DirFS("/sys"), nil, events); err != nil {
t.Fatalf("Enumerate: error = %v", err)
}
close(events)
}

105
internal/uevent/uevent.go Normal file
View File

@@ -0,0 +1,105 @@
// Package uevent provides userspace client for consuming events from a
// NETLINK_KOBJECT_UEVENT socket, as well as helpers for supplementing
// events received from the kernel.
package uevent
import (
"context"
"errors"
"strconv"
"sync/atomic"
"syscall"
"hakurei.app/internal/netlink"
)
type (
// Recoverable is satisfied by errors that are safe to recover from.
Recoverable interface{ recoverable() }
// Nontrivial is satisfied by errors preferring a JSON encoding.
Nontrivial interface{ nontrivial() }
)
// Conn represents a NETLINK_KOBJECT_UEVENT socket.
type Conn struct {
conn *netlink.Conn
// Whether currently between a call to enterExcl and exitExcl.
excl atomic.Bool
}
// enterExcl must be called entering a critical section that interacts with conn.
func (c *Conn) enterExcl() error {
if !c.excl.CompareAndSwap(false, true) {
return syscall.EAGAIN
}
return nil
}
// exitExcl must be called exiting a critical section that interacts with conn.
func (c *Conn) exitExcl() { c.excl.Store(false) }
// Close closes the underlying socket.
func (c *Conn) Close() error { return c.conn.Close() }
// Dial returns the address of a newly connected [Conn].
func Dial() (*Conn, error) {
// kernel group is hard coded in lib/kobject_uevent.c, undocumented
c, err := netlink.Dial(syscall.NETLINK_KOBJECT_UEVENT, 1)
if err != nil {
return nil, err
}
return &Conn{conn: c}, err
}
var (
// ErrBadSocket is returned by [Conn.Consume] for a reply from a
// syscall.Sockaddr with unexpected concrete type.
ErrBadSocket = errors.New("unexpected socket address")
)
// BadPortError is returned by [Conn.Consume] upon receiving a message that did
// not come from the kernel.
type BadPortError syscall.SockaddrNetlink
var _ Recoverable = new(BadPortError)
func (*BadPortError) recoverable() {}
func (e *BadPortError) Error() string {
return "unexpected message from port id " + strconv.Itoa(int(e.Pid)) +
" on NETLINK_KOBJECT_UEVENT"
}
// Consume continuously receives and parses events from the kernel. It returns
// the first error it encounters.
//
// Callers must not restart event processing after a non-nil error that does not
// satisfy [Recoverable] is returned.
func (c *Conn) Consume(ctx context.Context, events chan<- *Message) error {
if err := c.enterExcl(); err != nil {
return err
}
defer c.exitExcl()
for {
data, from, err := c.conn.Recvfrom(ctx, 0)
if err != nil {
return err
}
// lib/kobject_uevent.c:
// set portid 0 to inform userspace message comes from kernel
if v, ok := from.(*syscall.SockaddrNetlink); !ok {
return ErrBadSocket
} else if v.Pid != 0 {
return (*BadPortError)(v)
}
var msg Message
if err = msg.UnmarshalBinary(data); err != nil {
return err
}
events <- &msg
}
}

View File

@@ -0,0 +1,246 @@
package uevent_test
import (
"context"
"encoding"
"os"
"reflect"
"sync"
"syscall"
"testing"
"time"
"hakurei.app/internal/uevent"
)
// adeT sets up a parallel subtest for a textual appender/decoder/encoder.
func adeT[V any, S interface {
encoding.TextAppender
encoding.TextMarshaler
encoding.TextUnmarshaler
*V
}](t *testing.T, name string, v V, want string, wantErr, wantErrE error) {
t.Helper()
f := func(t *testing.T) {
if name != "" {
t.Parallel()
}
t.Helper()
t.Run("decode", func(t *testing.T) {
t.Parallel()
t.Helper()
var got V
if err := S(&got).UnmarshalText([]byte(want)); !reflect.DeepEqual(err, wantErr) {
t.Fatalf("UnmarshalText: error = %v, want %v", err, wantErr)
}
if wantErr != nil {
return
}
if !reflect.DeepEqual(&got, &v) {
t.Errorf("UnmarshalText: %#v, want %#v", got, v)
}
})
t.Run("encode", func(t *testing.T) {
t.Parallel()
t.Helper()
if got, err := S(&v).MarshalText(); !reflect.DeepEqual(err, wantErrE) {
t.Fatalf("MarshalText: error = %v, want %v", err, wantErrE)
} else if err == nil && string(got) != want {
t.Errorf("MarshalText: %q, want %q", string(got), want)
}
})
}
if name != "" {
t.Run(name, f)
} else {
f(t)
}
}
// adeT sets up a binary subtest for a textual appender/decoder/encoder.
func adeB[V any, S interface {
encoding.BinaryAppender
encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
*V
}](t *testing.T, name string, v V, want string, wantErr, wantErrE error) {
t.Helper()
f := func(t *testing.T) {
if name != "" {
t.Parallel()
}
t.Helper()
t.Run("decode", func(t *testing.T) {
t.Parallel()
t.Helper()
var got V
if err := S(&got).UnmarshalBinary([]byte(want)); !reflect.DeepEqual(err, wantErr) {
t.Fatalf("UnmarshalBinary: error = %v, want %v", err, wantErr)
}
if wantErr != nil {
return
}
if !reflect.DeepEqual(&got, &v) {
t.Errorf("UnmarshalBinary: %#v, want %#v", got, v)
}
})
t.Run("encode", func(t *testing.T) {
t.Parallel()
t.Helper()
if got, err := S(&v).MarshalBinary(); !reflect.DeepEqual(err, wantErrE) {
t.Fatalf("MarshalBinary: error = %v, want %v", err, wantErrE)
} else if err == nil && string(got) != want {
t.Errorf("MarshalBinary: %q, want %q", string(got), want)
}
})
}
if name != "" {
t.Run(name, f)
} else {
f(t)
}
}
func TestDialConsume(t *testing.T) {
t.Parallel()
c, err := uevent.Dial()
if err != nil {
t.Fatalf("Dial: error = %v", err)
}
t.Cleanup(func() {
if closeErr := c.Close(); closeErr != nil {
t.Fatal(err)
}
})
// check kernel-assigned port id
c0, err0 := uevent.Dial()
if err0 != nil {
t.Fatalf("Dial: error = %v", err)
}
t.Cleanup(func() {
if closeErr := c0.Close(); closeErr != nil {
t.Fatal(closeErr)
}
})
var wg sync.WaitGroup
done := make(chan struct{})
events := make(chan *uevent.Message, 1<<10)
go func() {
defer close(done)
for msg := range events {
t.Log(msg)
}
}()
t.Cleanup(func() {
wg.Wait()
close(events)
<-done
})
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
wg.Go(func() {
if err = c.Consume(ctx, events); err != context.Canceled {
panic(err)
}
})
wg.Go(func() {
if err0 = c0.Consume(ctx, events); err0 != context.Canceled {
panic(err0)
}
})
if testing.Verbose() {
if d, perr := time.ParseDuration(os.Getenv(
"ROSA_UEVENT_TEST_DURATION",
)); perr != nil {
t.Logf("skipping long test: error = %v", perr)
} else {
time.Sleep(d)
}
}
cancel()
wg.Wait()
ctx, cancel = context.WithCancel(t.Context())
defer cancel()
var errs [2]error
exclExit := make(chan struct{})
wg.Go(func() {
defer func() { exclExit <- struct{}{} }()
errs[0] = c.Consume(ctx, events)
})
wg.Go(func() {
defer func() { exclExit <- struct{}{} }()
errs[1] = c.Consume(ctx, events)
})
<-exclExit
cancel()
<-exclExit
if errs[0] != syscall.EAGAIN && errs[1] != syscall.EAGAIN {
t.Fatalf("enterExcl: err0 = %v, err1 = %v", errs[0], errs[1])
}
}
func TestErrors(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
want string
}{
{"UnsupportedActionError", uevent.UnsupportedActionError("explode"),
`unsupported kobject_action "explode"`},
{"MissingHeaderError", uevent.MissingHeaderError("move"),
`message "move" has no header`},
{"MessageError MErrorKindHeaderSep", &uevent.MessageError{
Data: "move\x00",
Section: "move",
Kind: uevent.MErrorKindHeaderSep,
}, `header "move" missing separator`},
{"MessageError MErrorKindFinalNUL", &uevent.MessageError{
Data: "move\x00truncated",
Section: "truncated",
Kind: uevent.MErrorKindFinalNUL,
}, `entry "truncated" missing NUL`},
{"MessageError bad", &uevent.MessageError{
Data: "\x00",
Kind: 0xbad,
}, `section "" is invalid`},
{"BadPortError", &uevent.BadPortError{
Pid: 1,
}, "unexpected message from port id 1 on NETLINK_KOBJECT_UEVENT"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want)
}
})
}
}

View File

@@ -265,7 +265,7 @@ in
'';
in
pkgs.writeShellScriptBin app.name ''
exec hakurei${if app.verbose then " -v" else ""} app ${checkedConfig "hakurei-app-${app.name}.json" conf} $@
exec hakurei${if app.verbose then " -v" else ""} run ${checkedConfig "hakurei-app-${app.name}.json" conf} $@
''
)
]

View File

@@ -30,7 +30,7 @@ in
# For checking pd outcome:
(pkgs.writeShellScriptBin "check-sandbox-pd" ''
hakurei -v run hakurei-test \
hakurei -v exec hakurei-test \
-p "/var/tmp/.hakurei-check-ok.0" \
-t ${toString (builtins.toFile "hakurei-pd-want.json" (builtins.toJSON testCases.pd.want))} \
-s ${testCases.pd.expectedFilter.${pkgs.stdenv.hostPlatform.system}} "$@"

View File

@@ -42,23 +42,23 @@ machine.wait_for_file("/run/user/1000/wayland-1")
machine.wait_for_file("/tmp/sway-ipc.sock")
# Check pd seccomp outcome:
swaymsg("exec hakurei run cat")
swaymsg("exec hakurei exec cat")
check_filter(0, "pdlike", "cat")
# Check fd leak:
swaymsg("exec exec 127</proc/cmdline && hakurei -v run sleep infinity")
swaymsg("exec exec 127</proc/cmdline && hakurei -v exec sleep infinity")
pd_identity0_sleep_pid = int(machine.wait_until_succeeds("pgrep -U 10000 -x sleep", timeout=60))
print(machine.succeed(f"hakurei-test fd {pd_identity0_sleep_pid}"))
machine.succeed(f"kill -INT {pd_identity0_sleep_pid}")
# Verify capabilities/securebits in user namespace:
print(machine.succeed("sudo -u alice -i hakurei run capsh --print"))
print(machine.succeed("sudo -u alice -i hakurei run capsh --has-no-new-privs"))
print(machine.fail("sudo -u alice -i hakurei run capsh --has-a=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i hakurei run capsh --has-b=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i hakurei run capsh --has-i=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i hakurei run capsh --has-p=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i hakurei run umount -R /dev"))
print(machine.succeed("sudo -u alice -i hakurei exec capsh --print"))
print(machine.succeed("sudo -u alice -i hakurei exec capsh --has-no-new-privs"))
print(machine.fail("sudo -u alice -i hakurei exec capsh --has-a=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i hakurei exec capsh --has-b=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i hakurei exec capsh --has-i=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i hakurei exec capsh --has-p=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i hakurei exec umount -R /dev"))
# Check sandbox outcome:
machine.succeed("install -dm0777 /tmp/.hakurei-store-rw/{upper,work}")

View File

@@ -87,9 +87,9 @@ machine.wait_for_file("/tmp/sway-ipc.sock")
swaymsg("exec hakurei-test")
# Deny unmapped uid:
denyOutput = machine.fail("sudo -u untrusted -i hakurei run &>/dev/stdout")
denyOutput = machine.fail("sudo -u untrusted -i hakurei exec &>/dev/stdout")
print(denyOutput)
denyOutputVerbose = machine.fail("sudo -u untrusted -i hakurei -v run &>/dev/stdout")
denyOutputVerbose = machine.fail("sudo -u untrusted -i hakurei -v exec &>/dev/stdout")
print(denyOutputVerbose)
# Fail direct hsu call:
@@ -118,11 +118,11 @@ def hakurei_identity(offset):
# Start hakurei permissive defaults outside Wayland session:
print(machine.succeed("sudo -u alice -i hakurei -v run -a 0 touch /tmp/pd-bare-ok"))
print(machine.succeed("sudo -u alice -i hakurei -v exec -a 0 touch /tmp/pd-bare-ok"))
machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-bare-ok", timeout=5)
# Verify silent output permissive defaults:
output = machine.succeed("sudo -u alice -i hakurei run -a 0 true &>/dev/stdout")
output = machine.succeed("sudo -u alice -i hakurei exec -a 0 true &>/dev/stdout")
if output != "":
raise Exception(f"unexpected output\n{output}")
@@ -131,12 +131,12 @@ def silent_output_interrupt(flags):
swaymsg("exec foot")
wait_for_window("alice@machine")
# identity 0 does not have home-manager
machine.send_chars(f"exec hakurei run {flags}-a 0 sh -c 'export PATH=/run/current-system/sw/bin:$PATH && touch /tmp/pd-silent-ready && sleep infinity' &>/tmp/pd-silent\n")
machine.send_chars(f"exec hakurei exec {flags}-a 0 sh -c 'export PATH=/run/current-system/sw/bin:$PATH && touch /tmp/pd-silent-ready && sleep infinity' &>/tmp/pd-silent\n")
machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-silent-ready", timeout=15)
machine.succeed("rm /tmp/hakurei.0/tmpdir/0/pd-silent-ready")
machine.send_key("ctrl-c")
machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_until_fails(f"pgrep -u alice -f 'hakurei run {flags}-a 0 '", timeout=5)
machine.wait_until_fails(f"pgrep -u alice -f 'hakurei exec {flags}-a 0 '", timeout=5)
output = machine.succeed("cat /tmp/pd-silent && rm /tmp/pd-silent")
if output != "":
raise Exception(f"unexpected output\n{output}")
@@ -147,10 +147,10 @@ silent_output_interrupt("--dbus ") # this one is especially painful as it mainta
silent_output_interrupt("--wayland -X --dbus --pulse ")
# Verify graceful failure on bad Wayland display name:
print(machine.fail("sudo -u alice -i hakurei -v run --wayland true"))
print(machine.fail("sudo -u alice -i hakurei -v exec --wayland true"))
# Start hakurei permissive defaults within Wayland session:
hakurei('-v run --wayland --dbus --dbus-log notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-ok')
hakurei('-v exec --wayland --dbus --dbus-log notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-ok')
machine.wait_for_file("/tmp/dbus-ok", timeout=15)
collect_state_ui("dbus_notify_exited")
# not in pid namespace, verify termination
@@ -158,10 +158,10 @@ machine.wait_until_fails("pgrep xdg-dbus-proxy")
machine.succeed("pkill -9 mako")
# Check revert type selection:
hakurei("-v run --wayland -X --dbus --pulse -u p0 foot && touch /tmp/p0-exit-ok")
hakurei("-v exec --wayland -X --dbus --pulse -u p0 foot && touch /tmp/p0-exit-ok")
wait_for_window("p0@machine")
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
hakurei("-v run --wayland -X --dbus --pulse -u p1 foot && touch /tmp/p1-exit-ok")
hakurei("-v exec --wayland -X --dbus --pulse -u p1 foot && touch /tmp/p1-exit-ok")
wait_for_window("p1@machine")
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
machine.send_chars("exit\n")
@@ -173,14 +173,14 @@ machine.wait_for_file("/tmp/p0-exit-ok", timeout=15)
machine.fail("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")
# Check invalid identifier fd behaviour:
machine.fail('echo \'{"container":{"shell":"/proc/nonexistent","home":"/proc/nonexistent","path":"/proc/nonexistent"}}\' | sudo -u alice -i hakurei -v app --identifier-fd 32767 - 2>&1 | tee > /tmp/invalid-identifier-fd')
machine.fail('echo \'{"container":{"shell":"/proc/nonexistent","home":"/proc/nonexistent","path":"/proc/nonexistent"}}\' | sudo -u alice -i hakurei -v run --identifier-fd 32767 - 2>&1 | tee > /tmp/invalid-identifier-fd')
machine.wait_for_file("/tmp/invalid-identifier-fd")
print(machine.succeed('grep "^hakurei: cannot write identifier: bad file descriptor$" /tmp/invalid-identifier-fd'))
# Check interrupt shim behaviour:
swaymsg("exec sh -c 'ne-foot; echo -n $? > /tmp/monitor-exit-code'")
wait_for_window(f"u0_a{hakurei_identity(0)}@machine")
machine.succeed("pkill -INT -f 'hakurei -v app '")
machine.succeed("pkill -INT -f 'hakurei -v run '")
machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/monitor-exit-code")
interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code"))
@@ -190,7 +190,7 @@ if interrupt_exit_code != 230:
# Check interrupt shim behaviour immediate termination:
swaymsg("exec sh -c 'ne-foot-immediate; echo -n $? > /tmp/monitor-exit-code'")
wait_for_window(f"u0_a{hakurei_identity(0)}@machine")
machine.succeed("pkill -INT -f 'hakurei -v app '")
machine.succeed("pkill -INT -f 'hakurei -v run '")
machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/monitor-exit-code")
interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code"))
@@ -201,19 +201,19 @@ if interrupt_exit_code != 254:
swaymsg("exec sh -c 'ne-foot &> /tmp/shim-cont-unexpected-pid'")
wait_for_window(f"u0_a{hakurei_identity(0)}@machine")
machine.succeed("pkill -CONT -f 'hakurei shim'")
machine.succeed("pkill -INT -f 'hakurei -v app '")
machine.succeed("pkill -INT -f 'hakurei -v run '")
machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/shim-cont-unexpected-pid")
print(machine.succeed('grep "shim: got SIGCONT from unexpected process$" /tmp/shim-cont-unexpected-pid'))
# Check setscheduler:
sched_unset = int(machine.succeed("sudo -u alice -i hakurei -v run cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
sched_unset = int(machine.succeed("sudo -u alice -i hakurei -v exec cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
if sched_unset != 0:
raise Exception(f"unexpected unset policy: {sched_unset}")
sched_idle = int(machine.succeed("sudo -u alice -i hakurei -v run --policy=idle cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
sched_idle = int(machine.succeed("sudo -u alice -i hakurei -v exec --policy=idle cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
if sched_idle != 5:
raise Exception(f"unexpected idle policy: {sched_idle}")
sched_rr = int(machine.succeed("sudo -u alice -i hakurei -v run --policy=rr cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
sched_rr = int(machine.succeed("sudo -u alice -i hakurei -v exec --policy=rr cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
if sched_rr != 2:
raise Exception(f"unexpected round-robin policy: {sched_idle}")
@@ -243,11 +243,11 @@ machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_until_fails("pgrep -x hakurei", timeout=5)
machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")
# Test PipeWire SecurityContext:
machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pulse pactl info")
machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pulse pactl set-sink-mute @DEFAULT_SINK@ toggle")
machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pulse pactl info")
machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pulse pactl set-sink-mute @DEFAULT_SINK@ toggle")
# Test PipeWire direct access:
machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 pw-dump")
machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pipewire pw-dump")
machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pipewire pw-dump")
# Test XWayland (foot does not support X):
swaymsg("exec x11-alacritty")