Compare commits
42 Commits
master
...
wip-sysfs-
| Author | SHA1 | Date | |
|---|---|---|---|
|
121fcfa406
|
|||
|
19c76e0831
|
|||
|
71fcc972ba
|
|||
|
62002efd08
|
|||
|
e33294db9c
|
|||
|
b1ea3b4acf
|
|||
|
2c254c70b8
|
|||
|
ea014d6af2
|
|||
|
1b48484c16
|
|||
|
713bff3eb0
|
|||
|
30f459e690
|
|||
|
8766fddcb3
|
|||
|
2745602be3
|
|||
|
ee22847dde
|
|||
|
c61188649b
|
|||
|
6a87a96838
|
|||
|
2548a681e9
|
|||
|
d514d0679f
|
|||
|
4407892632
|
|||
|
e661260607
|
|||
|
044490e0a5
|
|||
|
af038c89ff
|
|||
|
d2f30173cd
|
|||
|
5319ea994c
|
|||
|
bbe178be3e
|
|||
|
ca28e9936b
|
|||
|
f61c6ade56
|
|||
|
fce3d63823
|
|||
|
722c3cc54f
|
|||
|
372d509e5c
|
|||
|
d62516ed1e
|
|||
|
d2b635eb55
|
|||
|
50403e9d60
|
|||
|
b98c5f2e21
|
|||
|
d972cffe5a
|
|||
|
d8648304bb
|
|||
|
f7bfa9a6c2
|
|||
|
7035b4b598
|
|||
|
094b8400dd
|
|||
|
4652d921d8
|
|||
|
066213c245
|
|||
|
98832c21ee
|
@@ -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 (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
7
cmd/hsu/conf.go
Normal 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
7
cmd/hsu/config_rosa.go
Normal 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"
|
||||
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
/* copied from hst and must never be changed */
|
||||
/* keep in sync with hst */
|
||||
|
||||
const (
|
||||
userOffset = 100000
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
10
dist/comp/_hakurei
vendored
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@ func (t Toolchain) newElfutils() (pkg.Artifact, string) {
|
||||
// nonstandard glibc extension
|
||||
SkipCheck: true,
|
||||
|
||||
Configure: [][2]string{
|
||||
Configure: []KV{
|
||||
{"enable-deterministic-archives"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func (t Toolchain) newGLib() (pkg.Artifact, string) {
|
||||
)),
|
||||
},
|
||||
}, &MesonHelper{
|
||||
Setup: [][2]string{
|
||||
Setup: []KV{
|
||||
{"Ddefault_library", "both"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,4 +15,4 @@ var hakureiSource = pkg.NewHTTPGetTar(
|
||||
)
|
||||
|
||||
// hakureiPatches are patches applied against a hakurei release.
|
||||
var hakureiPatches [][2]string
|
||||
var hakureiPatches []KV
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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/"+
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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...) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"},
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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"},
|
||||
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
85
internal/uevent/action.go
Normal 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)
|
||||
}
|
||||
43
internal/uevent/action_test.go
Normal file
43
internal/uevent/action_test.go
Normal 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
137
internal/uevent/message.go
Normal 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
|
||||
}
|
||||
132
internal/uevent/message_test.go
Normal file
132
internal/uevent/message_test.go
Normal 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
87
internal/uevent/sysfs.go
Normal 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
|
||||
})
|
||||
}
|
||||
28
internal/uevent/sysfs_test.go
Normal file
28
internal/uevent/sysfs_test.go
Normal 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
105
internal/uevent/uevent.go
Normal 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
|
||||
}
|
||||
}
|
||||
246
internal/uevent/uevent_test.go
Normal file
246
internal/uevent/uevent_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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} $@
|
||||
''
|
||||
)
|
||||
]
|
||||
|
||||
@@ -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}} "$@"
|
||||
|
||||
@@ -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}")
|
||||
|
||||
40
test/test.py
40
test/test.py
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user