1
0
forked from rosa/hakurei

14 Commits

Author SHA1 Message Date
389844b1ea internal/rosa/gnu: mpc 1.3.1 to 1.4.0
This package now unfortunately switched to xz as well.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 23:56:20 +09:00
5b7ab35633 internal/rosa: iptables artifact
This also pulls in netlink libraries from netfilter project.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 23:47:16 +09:00
52b1a5a725 internal/rosa: use type P in helper interface
This is easier to type and serialises correctly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 23:45:01 +09:00
6b78df8714 internal/rosa: libmd and libbsd artifacts
These provide headers that are provided by glibc but not musl.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 22:57:08 +09:00
dadf170a46 internal/rosa: dbus artifact
Unfortunate ugly indirect dependency we cannot yet get rid of.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 21:47:05 +09:00
9594832302 internal/rosa/meson: disallow download
This will fail and waste time on KindExec, and cause nondeterminism in KindExecNet.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 21:38:09 +09:00
91a2d4d6e1 internal/uevent: integrate error handling in event loop
There are many subtleties when recovering from errors in the event loop, and coldboot requires internals to drain the receive buffer as synthetic uevents are being arranged.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 00:10:14 +09:00
a854719b9f internal/netlink: optional recvmsg without netpoll
For draining the socket receive buffer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 23:18:43 +09:00
f03c0fb249 internal/uevent: synthetic events for coldboot
This causes the kernel to regenerate events that happened before earlyinit started.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 23:01:08 +09:00
a6600be34a all: use filepath
This makes package check portable, and removes nonportable behaviour from package pkg, pipewire, and system. All other packages remain nonportable due to their nature. No latency increase was observed due to this change on amd64 and arm64 linux.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 18:24:53 +09:00
b5592633f5 internal/uevent: separate recvmsg helper
This enables messages to be received separately.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 02:53:26 +09:00
584e302168 internal/netlink: set receive buffer size
This is done by both systemd sd-device and AOSP ueventd to improve robustness. Rosa OS will still handle ENOBUFS via coldboot but a big buffer should mitigate this as well.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 02:14:47 +09:00
141958656f internal/uevent: handle state divergence
This requires the caller to arrange for a coldboot to happen, some time after this error is encountered, and to resume event processing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 01:50:09 +09:00
648079f42c internal/netlink: switch to recvmsg/sendmsg
These are more flexible than recvfrom/sendto.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-29 23:36:00 +09:00
43 changed files with 910 additions and 163 deletions

View File

@@ -5,7 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"path"
"path/filepath"
"slices"
"strings"
"syscall"
@@ -61,7 +61,7 @@ func (a *Absolute) Is(v *Absolute) bool {
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
func NewAbs(pathname string) (*Absolute, error) {
if !path.IsAbs(pathname) {
if !filepath.IsAbs(pathname) {
return nil, AbsoluteError(pathname)
}
return unsafeAbs(pathname), nil
@@ -76,13 +76,13 @@ func MustAbs(pathname string) *Absolute {
}
}
// Append calls [path.Join] with [Absolute] as the first element.
// Append calls [filepath.Join] with [Absolute] as the first element.
func (a *Absolute) Append(elem ...string) *Absolute {
return unsafeAbs(path.Join(append([]string{a.String()}, elem...)...))
return unsafeAbs(filepath.Join(append([]string{a.String()}, elem...)...))
}
// Dir calls [path.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return unsafeAbs(path.Dir(a.String())) }
// Dir calls [filepath.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }
// GobEncode returns the checked pathname.
func (a *Absolute) GobEncode() ([]byte, error) {
@@ -92,7 +92,7 @@ func (a *Absolute) GobEncode() ([]byte, error) {
// GobDecode stores data if it represents an absolute pathname.
func (a *Absolute) GobDecode(data []byte) error {
pathname := string(data)
if !path.IsAbs(pathname) {
if !filepath.IsAbs(pathname) {
return AbsoluteError(pathname)
}
a.pathname = unique.Make(pathname)
@@ -110,7 +110,7 @@ func (a *Absolute) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &pathname); err != nil {
return err
}
if !path.IsAbs(pathname) {
if !filepath.IsAbs(pathname) {
return AbsoluteError(pathname)
}
a.pathname = unique.Make(pathname)

View File

@@ -58,7 +58,7 @@ import (
"fmt"
"log"
"os"
"path"
"path/filepath"
"runtime"
"slices"
"strconv"
@@ -102,13 +102,13 @@ func main() {
log.Fatal("this program must not be started by root")
}
if !path.IsAbs(hakureiPath) {
if !filepath.IsAbs(hakureiPath) {
log.Fatal("this program is compiled incorrectly")
return
}
var toolPath string
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
pexe := filepath.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") {

View File

@@ -24,7 +24,7 @@ import (
"os"
"os/exec"
"os/signal"
"path"
"path/filepath"
"runtime"
"runtime/cgo"
"strconv"
@@ -145,9 +145,9 @@ func sharefs_destroy(private_data unsafe.Pointer) {
func showHelp(args *fuseArgs) {
executableName := sharefsName
if args.argc > 0 {
executableName = path.Base(C.GoString(*args.argv))
executableName = filepath.Base(C.GoString(*args.argv))
} else if name, err := os.Executable(); err == nil {
executableName = path.Base(name)
executableName = filepath.Base(name)
}
fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)

View File

@@ -179,7 +179,7 @@ func (direct) mustLoopback(ctx context.Context, msg message.Msg) {
lo = ifi.Index
}
c, err := netlink.DialRoute()
c, err := netlink.DialRoute(0)
if err != nil {
msg.GetLogger().Fatalln(err)
}

View File

@@ -8,7 +8,7 @@ import (
"os"
"os/exec"
"os/signal"
"path"
"path/filepath"
"slices"
"strconv"
"sync"
@@ -569,7 +569,7 @@ func TryArgv0(msg message.Msg) {
msg = message.New(log.Default())
}
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == initName {
Init(msg)
msg.BeforeExit()
os.Exit(0)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import (
"io"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"strconv"
"testing"
@@ -28,7 +28,7 @@ func TestUpdate(t *testing.T) {
t.Skip("acl test skipped")
}
testFilePath := path.Join(t.TempDir(), testFileName)
testFilePath := filepath.Join(t.TempDir(), testFileName)
if f, err := os.Create(testFilePath); err != nil {
t.Fatalf("Create: error = %v", err)

View File

@@ -46,7 +46,11 @@ type Conn struct {
// Dial returns the address of a newly connected generic netlink connection of
// specified family and groups.
func Dial(family int, groups uint32) (*Conn, error) {
//
// For a nonzero rcvbuf, the socket receive buffer size is set to its absolute
// value via SO_RCVBUF for a positive value, or SO_RCVBUFFORCE for a negative
// value.
func Dial(family int, groups uint32, rcvbuf int64) (*Conn, error) {
var c Conn
if fd, err := syscall.Socket(
syscall.AF_NETLINK,
@@ -75,6 +79,23 @@ func Dial(family int, groups uint32) (*Conn, error) {
return nil, syscall.ENOTRECOVERABLE
}
if rcvbuf != 0 {
opt := syscall.SO_RCVBUF
if rcvbuf < 0 {
opt = syscall.SO_RCVBUFFORCE
rcvbuf = -rcvbuf
}
if err = syscall.SetsockoptInt(
fd,
syscall.SOL_SOCKET,
opt,
int(rcvbuf),
); err != nil {
_ = syscall.Close(fd)
return nil, os.NewSyscallError("setsockopt", err)
}
}
c.family = family
c.f = os.NewFile(uintptr(fd), "netlink")
if c.raw, err = c.f.SyscallConn(); err != nil {
@@ -101,23 +122,40 @@ func (c *Conn) Close() error {
return c.f.Close()
}
// Recvfrom wraps recv(2) with nonblocking behaviour via the runtime network poller.
// Recvmsg 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(
// The returned slice is valid until the next call to Recvmsg.
func (c *Conn) Recvmsg(
ctx context.Context,
flags int,
) (data []byte, from syscall.Sockaddr, err error) {
) (data []byte, recvflags int, from syscall.Sockaddr, err error) {
if err = c.f.SetReadDeadline(time.Time{}); err != nil {
return
}
var n int
data = c.buf[:]
if ctx == nil {
rcErr := c.raw.Control(func(fd uintptr) {
n, _, recvflags, from, err = syscall.Recvmsg(int(fd), data, nil, flags)
})
if n >= 0 {
data = data[:n]
}
if err != nil {
err = os.NewSyscallError("recvmsg", err)
} else {
err = rcErr
}
return
}
done := make(chan error, 1)
go func() {
rcErr := c.raw.Read(func(fd uintptr) (done bool) {
n, from, err = syscall.Recvfrom(int(fd), data, flags)
n, _, recvflags, from, err = syscall.Recvmsg(int(fd), data, nil, flags)
return err != syscall.EWOULDBLOCK
})
if n >= 0 {
@@ -129,7 +167,7 @@ func (c *Conn) Recvfrom(
select {
case rcErr := <-done:
if err != nil {
err = os.NewSyscallError("recvfrom", err)
err = os.NewSyscallError("recvmsg", err)
} else {
err = rcErr
}
@@ -147,12 +185,12 @@ func (c *Conn) Recvfrom(
}
}
// Sendto wraps send(2) with nonblocking behaviour via the runtime network poller.
func (c *Conn) Sendto(
// Sendmsg wraps send(2) with nonblocking behaviour via the runtime network poller.
func (c *Conn) Sendmsg(
ctx context.Context,
p []byte,
flags int,
to syscall.Sockaddr,
flags int,
) (err error) {
if err = c.f.SetWriteDeadline(time.Time{}); err != nil {
return
@@ -161,7 +199,7 @@ func (c *Conn) Sendto(
done := make(chan error, 1)
go func() {
done <- c.raw.Write(func(fd uintptr) (done bool) {
err = syscall.Sendto(int(fd), p, flags, to)
err = syscall.Sendmsg(int(fd), p, nil, to, flags)
return err != syscall.EWOULDBLOCK
})
}()
@@ -169,7 +207,7 @@ func (c *Conn) Sendto(
select {
case rcErr := <-done:
if err != nil {
err = os.NewSyscallError("sendto", err)
err = os.NewSyscallError("sendmsg", err)
} else {
err = rcErr
}
@@ -278,7 +316,7 @@ type HandlerFunc func(resp []syscall.NetlinkMessage) error
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 {
if data, _, _, err := c.Recvmsg(ctx, flags); err != nil {
return err
} else if len(data) < syscall.NLMSG_HDRLEN {
return syscall.EBADE
@@ -302,9 +340,9 @@ func (c *Conn) Roundtrip(ctx context.Context, f HandlerFunc) error {
}
defer func() { c.seq++ }()
if err := c.Sendto(ctx, c.pending(), 0, &syscall.SockaddrNetlink{
if err := c.Sendmsg(ctx, c.pending(), &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
}); err != nil {
}, 0); err != nil {
return err
}

View File

@@ -13,8 +13,8 @@ type RouteConn struct{ conn *Conn }
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, 0)
func DialRoute(rcvbuf int64) (*RouteConn, error) {
c, err := Dial(syscall.NETLINK_ROUTE, 0, rcvbuf)
if err != nil {
return nil, err
}

View File

@@ -5,7 +5,7 @@ import (
"errors"
"io/fs"
"os"
"path"
"path/filepath"
"slices"
"strconv"
"syscall"
@@ -165,9 +165,9 @@ func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error {
}
for _, pair := range entry.Values {
if pair[0] == "path" {
if path.IsAbs(pair[1]) {
if filepath.IsAbs(pair[1]) {
// get parent dir of socket
dir := path.Dir(pair[1])
dir := filepath.Dir(pair[1])
if dir == "." || dir == fhs.Root {
state.msg.Verbosef("dbus socket %q is in an unusual location", pair[1])
}

View File

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

View File

@@ -27,6 +27,31 @@ func TestFlatten(t *testing.T) {
fs.ModeCharDevice | 0400,
)},
{"coldboot", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"devices": {Mode: fs.ModeDir | 0700},
"devices/uevent": {Mode: 0600, Data: []byte("add")},
"devices/empty": {Mode: fs.ModeDir | 0700},
"devices/sub": {Mode: fs.ModeDir | 0700},
"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"block": {Mode: fs.ModeDir | 0700},
"block/uevent": {Mode: 0600, Data: []byte{}},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "block"},
{Mode: 0600, Path: "block/uevent", Data: []byte{}},
{Mode: fs.ModeDir | 0700, Path: "devices"},
{Mode: fs.ModeDir | 0700, Path: "devices/empty"},
{Mode: fs.ModeDir | 0700, Path: "devices/sub"},
{Mode: 0600, Path: "devices/sub/uevent", Data: []byte("add")},
{Mode: 0600, Path: "devices/uevent", Data: []byte("add")},
}, pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8"), nil},
{"empty", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},

View File

@@ -8,7 +8,7 @@ import (
"io"
"os"
"os/exec"
"path"
"path/filepath"
"slices"
"strconv"
"syscall"
@@ -189,7 +189,7 @@ func NewExec(
paths ...ExecPath,
) Artifact {
if name == "" {
name = "exec-" + path.Base(pathname.String())
name = "exec-" + filepath.Base(pathname.String())
}
if timeout <= 0 {
timeout = ExecTimeoutDefault

View File

@@ -16,7 +16,6 @@ import (
"iter"
"maps"
"os"
"path"
"path/filepath"
"runtime"
"slices"
@@ -894,7 +893,7 @@ func (c *Cache) Scrub(checks int) error {
se.DanglingIdentifiers = append(se.DanglingIdentifiers, *want)
seMu.Unlock()
return false
} else if err = Decode(got, path.Base(linkname)); err != nil {
} else if err = Decode(got, filepath.Base(linkname)); err != nil {
seMu.Lock()
lnp := dir.Append(linkname)
se.Errs[lnp.Handle()] = append(se.Errs[lnp.Handle()], err)
@@ -1488,7 +1487,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
return
}
buf := c.getIdentBuf()
err = Decode((*Checksum)(buf[:]), path.Base(name))
err = Decode((*Checksum)(buf[:]), filepath.Base(name))
if err == nil {
checksum = unique.Make(Checksum(buf[:]))
}

View File

@@ -10,7 +10,7 @@ import (
"io/fs"
"net/http"
"os"
"path"
"path/filepath"
)
const (
@@ -169,7 +169,7 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
}
if typeflag >= '0' && typeflag <= '9' && typeflag != tar.TypeDir {
if err = root.MkdirAll(path.Dir(header.Name), 0700); err != nil {
if err = root.MkdirAll(filepath.Dir(header.Name), 0700); err != nil {
return
}
}

View File

@@ -7,7 +7,7 @@ import (
"log"
"net"
"os"
"path"
"path/filepath"
"reflect"
"slices"
"strings"
@@ -68,7 +68,7 @@ func main() {
if got, err := os.Executable(); err != nil {
log.Fatalf("Executable: error = %v", err)
} else {
iftPath = path.Join(path.Dir(path.Dir(got)), "ift")
iftPath = filepath.Join(filepath.Dir(filepath.Dir(got)), "ift")
if got != wantExec {
switch got {
@@ -161,7 +161,7 @@ func main() {
}
}
if !layers {
if path.Base(lowerdir) != checksumEmptyDir {
if filepath.Base(lowerdir) != checksumEmptyDir {
log.Fatal("unexpected artifact checksum")
}
} else {
@@ -187,8 +187,8 @@ func main() {
}
if len(lowerdirs) != 2 ||
path.Base(lowerdirs[0]) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" ||
path.Base(lowerdirs[1]) != "nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK" {
filepath.Base(lowerdirs[0]) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" ||
filepath.Base(lowerdirs[1]) != "nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK" {
log.Fatalf("unexpected lowerdirs %s", strings.Join(lowerdirs, ", "))
}
}
@@ -202,12 +202,12 @@ func main() {
}
next()
if path.Base(m.Root) != "OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb" {
if filepath.Base(m.Root) != "OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb" {
log.Fatal("unexpected file artifact checksum")
}
next()
if path.Base(m.Root) != checksumEmptyDir {
if filepath.Base(m.Root) != checksumEmptyDir {
log.Fatal("unexpected artifact checksum")
}
}
@@ -226,13 +226,13 @@ func main() {
log.Fatal("unexpected work mount entry")
}
} else {
if path.Base(m.Root) != ident || m.Target != "/work" {
if filepath.Base(m.Root) != ident || m.Target != "/work" {
log.Fatal("unexpected work mount entry")
}
}
next()
if path.Base(m.Root) != ident || m.Target != "/tmp" {
if filepath.Base(m.Root) != ident || m.Target != "/tmp" {
log.Fatal("unexpected temp mount entry")
}

View File

@@ -49,6 +49,7 @@ const (
CMake
Coreutils
Curl
DBus
DTC
Diffutils
Elfutils
@@ -68,14 +69,19 @@ const (
Gzip
Hakurei
HakureiDist
IPTables
Kmod
LibXau
Libbsd
Libcap
Libexpat
Libiconv
Libpsl
Libffi
Libgd
Libmd
Libmnl
Libnftnl
Libtool
Libseccomp
Libucontext

View File

@@ -1,7 +1,7 @@
package rosa
import (
"path"
"path/filepath"
"slices"
"strings"
@@ -144,11 +144,11 @@ func (attr *CMakeHelper) name(name, version string) string {
}
// extra returns a hardcoded slice of [CMake] and [Ninja].
func (attr *CMakeHelper) extra(int) []PArtifact {
func (attr *CMakeHelper) extra(int) P {
if attr != nil && attr.Make {
return []PArtifact{CMake, Make}
return P{CMake, Make}
}
return []PArtifact{CMake, Ninja}
return P{CMake, Ninja}
}
// wantsChmod returns false.
@@ -200,7 +200,7 @@ cmake -G ` + generate + ` \
}
}), " \\\n\t") + ` \
-DCMAKE_INSTALL_PREFIX=/system \
'/usr/src/` + name + `/` + path.Join(attr.Append...) + `'
'/usr/src/` + name + `/` + filepath.Join(attr.Append...) + `'
cmake --build .` + jobs + `
cmake --install . --prefix=/work/system
` + attr.Script

46
internal/rosa/dbus.go Normal file
View File

@@ -0,0 +1,46 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newDBus() (pkg.Artifact, string) {
const (
version = "1.16.2"
checksum = "INwOuNdrDG7XW5ilW_vn8JSxEa444rRNc5ho97i84I1CNF09OmcFcV-gzbF4uCyg"
)
return t.NewPackage("dbus", version, pkg.NewHTTPGetTar(
nil, "https://gitlab.freedesktop.org/dbus/dbus/-/archive/"+
"dbus-"+version+"/dbus-dbus-"+version+".tar.bz2",
mustDecode(checksum),
pkg.TarBzip2,
), &PackageAttr{
// OSError: [Errno 30] Read-only file system: '/usr/src/dbus/subprojects/packagecache'
Writable: true,
// PermissionError: [Errno 13] Permission denied: '/usr/src/dbus/subprojects/packagecache'
Chmod: true,
}, &MesonHelper{
Setup: []KV{
{"Depoll", "enabled"},
{"Dinotify", "enabled"},
{"Dx11_autolaunch", "disabled"},
},
},
GLib,
Libexpat,
), version
}
func init() {
artifactsM[DBus] = Metadata{
f: Toolchain.newDBus,
Name: "dbus",
Description: "a message bus system",
Website: "https://www.freedesktop.org/wiki/Software/dbus/",
Dependencies: P{
GLib,
Libexpat,
},
ID: 5356,
}
}

View File

@@ -864,15 +864,17 @@ func init() {
func (t Toolchain) newMPC() (pkg.Artifact, string) {
const (
version = "1.3.1"
checksum = "o8r8K9R4x7PuRx0-JE3-bC5jZQrtxGV2nkB773aqJ3uaxOiBDCID1gKjPaaDxX4V"
version = "1.4.0"
checksum = "75Sgr2hcDTltHYgFaHsRGsFgW74i2jqAUS0oXaBdJYKjMj_CvEeJ1zwGbNYjEl1H"
)
return t.NewPackage("mpc", version, pkg.NewHTTPGetTar(
nil, "https://gcc.gnu.org/pub/gcc/infrastructure/"+
"mpc-"+version+".tar.gz",
return t.NewPackage("mpc", version, pkg.NewHTTPGet(
nil, "https://ftpmirror.gnu.org/gnu/mpc/mpc-"+version+".tar.xz",
mustDecode(checksum),
pkg.TarGzip,
), nil, (*MakeHelper)(nil),
), &PackageAttr{
SourceKind: SourceKindTarXZ,
}, (*MakeHelper)(nil),
XZ,
MPFR,
), version
}

57
internal/rosa/libbsd.go Normal file
View File

@@ -0,0 +1,57 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newLibmd() (pkg.Artifact, string) {
const (
version = "1.1.0"
checksum = "72w7Na04b9ji6nOe2h-Tz5JeQ6iStDZN3FOG1JNZ9M_jMO8K2FceG6DZv7lYThZJ"
)
return t.NewPackage("libmd", version, pkg.NewHTTPGet(
nil, "https://libbsd.freedesktop.org/releases/libmd-"+version+".tar.xz",
mustDecode(checksum),
), &PackageAttr{
SourceKind: SourceKindTarXZ,
}, (*MakeHelper)(nil),
XZ,
), version
}
func init() {
artifactsM[Libmd] = Metadata{
f: Toolchain.newLibmd,
Name: "libmd",
Description: "Message Digest functions from BSD systems",
Website: "https://www.hadrons.org/software/libmd/",
ID: 15525,
}
}
func (t Toolchain) newLibbsd() (pkg.Artifact, string) {
const (
version = "0.12.2"
checksum = "MEJ9MuLai32-gSJUrfmlDgGl7rszjdSxgb3ph9AcI5jv70VwlwwXJy1kxdAixm5Y"
)
return t.NewPackage("libbsd", version, pkg.NewHTTPGet(
nil, "https://libbsd.freedesktop.org/releases/libbsd-"+version+".tar.xz",
mustDecode(checksum),
), &PackageAttr{
SourceKind: SourceKindTarXZ,
}, (*MakeHelper)(nil),
XZ,
Libmd,
), version
}
func init() {
artifactsM[Libbsd] = Metadata{
f: Toolchain.newLibbsd,
Name: "libbsd",
Description: "provides useful functions commonly found on BSD systems",
Website: "https://libbsd.freedesktop.org/",
ID: 1567,
}
}

View File

@@ -84,8 +84,8 @@ func (*MakeHelper) name(name, version string) string {
}
// extra returns make and other optional dependencies.
func (attr *MakeHelper) extra(flag int) []PArtifact {
extra := []PArtifact{Make}
func (attr *MakeHelper) extra(flag int) P {
extra := P{Make}
if (attr == nil || !attr.OmitDefaults) && flag&TEarly == 0 {
extra = append(extra,
Gawk,

View File

@@ -72,9 +72,7 @@ func (*MesonHelper) name(name, version string) string {
}
// extra returns hardcoded meson runtime dependencies.
func (*MesonHelper) extra(int) []PArtifact {
return []PArtifact{Meson}
}
func (*MesonHelper) extra(int) P { return P{Meson} }
// wantsChmod returns false.
func (*MesonHelper) wantsChmod() bool { return false }
@@ -114,6 +112,7 @@ cd "$(mktemp -d)"
meson setup \
` + strings.Join(slices.Collect(func(yield func(string) bool) {
for _, v := range append([]KV{
{"wrap-mode", "nodownload"},
{"prefix", "/system"},
{"buildtype", "release"},
}, attr.Setup...) {

149
internal/rosa/netfilter.go Normal file
View File

@@ -0,0 +1,149 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newLibmnl() (pkg.Artifact, string) {
const (
version = "1.0.5"
checksum = "DN-vbbvQDpxXJm0TJ6xlluILvfrB86avrCTX50XyE9SEFSAZ_o8nuKc5Gu0Am7-u"
)
return t.NewPackage("libmnl", version, pkg.NewHTTPGetTar(
nil, "https://www.netfilter.org/projects/libmnl/files/"+
"libmnl-"+version+".tar.bz2",
mustDecode(checksum),
pkg.TarBzip2,
), &PackageAttr{
Patches: []KV{
{"libbsd-sys-queue", `diff --git a/examples/netfilter/nfct-daemon.c b/examples/netfilter/nfct-daemon.c
index d223ac2..a7878d0 100644
--- a/examples/netfilter/nfct-daemon.c
+++ b/examples/netfilter/nfct-daemon.c
@@ -20,7 +20,7 @@
#include <linux/netfilter/nfnetlink.h>
#include <linux/netfilter/nfnetlink_conntrack.h>
-#include <sys/queue.h>
+#include <bsd/sys/queue.h>
struct nstats {
LIST_ENTRY(nstats) list;
`},
},
}, &MakeHelper{
Configure: []KV{
{"enable-static"},
},
},
Libbsd,
KernelHeaders,
), version
}
func init() {
artifactsM[Libmnl] = Metadata{
f: Toolchain.newLibmnl,
Name: "libmnl",
Description: "a minimalistic user-space library oriented to Netlink developers",
Website: "https://www.netfilter.org/projects/libmnl/",
ID: 1663,
}
}
func (t Toolchain) newLibnftnl() (pkg.Artifact, string) {
const (
version = "1.3.1"
checksum = "A6EFNv2TbOcjcsXX2hQ-pKsF5FvlSh-BNEf9LrgnVH4nDjcv6NbtyHkTriz9kIEu"
)
return t.NewPackage("libnftnl", version, pkg.NewHTTPGet(
nil, "https://www.netfilter.org/projects/libnftnl/files/"+
"libnftnl-"+version+".tar.xz",
mustDecode(checksum),
), &PackageAttr{
SourceKind: SourceKindTarXZ,
Env: []string{
"CFLAGS=-D_GNU_SOURCE",
},
}, &MakeHelper{
Configure: []KV{
{"enable-static"},
},
},
XZ,
PkgConfig,
Libmnl,
KernelHeaders,
), version
}
func init() {
artifactsM[Libnftnl] = Metadata{
f: Toolchain.newLibnftnl,
Name: "libnftnl",
Description: "a userspace library providing a low-level netlink API to the in-kernel nf_tables subsystem",
Website: "https://www.netfilter.org/projects/libnftnl/",
Dependencies: P{
Libmnl,
},
ID: 1681,
}
}
func (t Toolchain) newIPTables() (pkg.Artifact, string) {
const (
version = "1.8.13"
checksum = "JsNI7dyZHnHLtDkKWAxzAIMZ5t-ff3LkSPqNJsn5VM5Eq2m1bA5NKI-XfMRpQsg6"
)
return t.NewPackage("iptables", version, pkg.NewHTTPGet(
nil, "https://www.netfilter.org/projects/iptables/files/"+
"iptables-"+version+".tar.xz",
mustDecode(checksum),
), &PackageAttr{
SourceKind: SourceKindTarXZ,
ScriptEarly: `
rm \
extensions/libxt_connlabel.txlate \
extensions/libxt_conntrack.txlate
sed -i \
's/de:ad:0:be:ee:ff/DE:AD:00:BE:EE:FF/g' \
extensions/libebt_dnat.txlate \
extensions/libebt_snat.txlate
`,
}, &MakeHelper{
Configure: []KV{
{"enable-static"},
},
ScriptCheckEarly: `
ln -s ../system/bin/bash /bin/
chmod +w /etc/ && ln -s ../usr/src/iptables/etc/ethertypes /etc/
`,
},
XZ,
PkgConfig,
Bash,
Python,
Libnftnl,
KernelHeaders,
), version
}
func init() {
artifactsM[IPTables] = Metadata{
f: Toolchain.newIPTables,
Name: "iptables",
Description: "the userspace command line program used to configure the Linux 2.4.x and later packet filtering ruleset",
Website: "https://www.netfilter.org/projects/iptables/",
Dependencies: P{
Libnftnl,
},
ID: 1394,
}
}

View File

@@ -135,7 +135,7 @@ func (t Toolchain) newViaPerlMakeMaker(
{"PREFIX", "/system"},
},
Check: []string{"test"},
}, slices.Concat(extra, []PArtifact{
}, slices.Concat(extra, P{
Perl,
})...)
}

View File

@@ -3,7 +3,7 @@ package rosa_test
import (
"errors"
"os"
"path"
"path/filepath"
"syscall"
"testing"
"unique"
@@ -13,7 +13,7 @@ import (
)
func TestReportZeroLength(t *testing.T) {
report := path.Join(t.TempDir(), "report")
report := filepath.Join(t.TempDir(), "report")
if err := os.WriteFile(report, nil, 0400); err != nil {
t.Fatal(err)
}
@@ -24,7 +24,7 @@ func TestReportZeroLength(t *testing.T) {
}
func TestReportSIGSEGV(t *testing.T) {
report := path.Join(t.TempDir(), "report")
report := filepath.Join(t.TempDir(), "report")
if err := os.WriteFile(report, make([]byte, 64), 0400); err != nil {
t.Fatal(err)
}

View File

@@ -409,7 +409,7 @@ type Helper interface {
// name returns the value passed to the name argument of [Toolchain.New].
name(name, version string) string
// extra returns helper-specific dependencies.
extra(flag int) []PArtifact
extra(flag int) P
// wantsChmod returns whether the source directory should be made writable.
wantsChmod() bool

View File

@@ -3,7 +3,7 @@ package system
import (
"fmt"
"os"
"path"
"path/filepath"
"syscall"
"testing"
"time"
@@ -18,7 +18,7 @@ func TestPipeWireOp(t *testing.T) {
checkOpBehaviour(t, checkNoParallel, []opBehaviourTestCase{
{"success", 0xbeef, 0xff, &pipewireOp{nil,
m(path.Join(t.TempDir(), "pipewire")),
m(filepath.Join(t.TempDir(), "pipewire")),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, []stub.Call{

View File

@@ -0,0 +1,71 @@
package uevent
import (
"context"
"errors"
"io/fs"
"log"
"os"
"path/filepath"
)
// synthAdd is prepared bytes written to uevent to cause a synthetic add event
// to be emitted during coldboot.
var synthAdd = []byte(KOBJ_ADD.String())
// Coldboot writes "add" to every uevent file that it finds in /sys/devices.
// This causes the kernel to regenerate the uevents for these paths.
//
// The specified pathname must present the sysfs root.
//
// Note that while [AOSP documentation] claims to also scan /sys/class and
// /sys/block, this is no longer the case, and the documentation was not updated
// when this changed.
//
// [AOSP documentation]: https://android.googlesource.com/platform/system/core/+/master/init/README.ueventd.md
func Coldboot(
ctx context.Context,
pathname string,
visited chan<- string,
handleWalkErr func(error) error,
) error {
if handleWalkErr == nil {
handleWalkErr = func(err error) error {
if errors.Is(err, fs.ErrNotExist) {
log.Println("coldboot", err)
return nil
}
return err
}
}
return filepath.WalkDir(filepath.Join(pathname, "devices"), func(
path string,
d fs.DirEntry,
err error,
) error {
if err != nil {
return handleWalkErr(err)
}
if err = ctx.Err(); err != nil {
return err
}
if d.IsDir() || d.Name() != "uevent" {
return nil
}
if err = os.WriteFile(path, synthAdd, 0); err != nil {
return handleWalkErr(err)
}
select {
case visited <- path:
break
case <-ctx.Done():
return ctx.Err()
}
return nil
})
}

View File

@@ -0,0 +1,227 @@
package uevent_test
import (
"context"
"os"
"path/filepath"
"reflect"
"slices"
"sync"
"syscall"
"testing"
"hakurei.app/check"
"hakurei.app/internal/pkg"
"hakurei.app/internal/uevent"
)
func TestColdboot(t *testing.T) {
t.Parallel()
d := t.TempDir()
if err := os.Chmod(d, 0700); err != nil {
t.Fatal(err)
}
for _, s := range []string{
"devices",
"devices/sub",
"devices/empty",
"block",
} {
if err := os.MkdirAll(filepath.Join(d, s), 0700); err != nil {
t.Fatal(err)
}
}
for _, f := range [][2]string{
{"devices/uevent", ""},
{"devices/sub/uevent", ""},
{"block/uevent", ""},
} {
if err := os.WriteFile(
filepath.Join(d, f[0]),
[]byte(f[1]),
0600,
); err != nil {
t.Fatal(err)
}
}
var wg sync.WaitGroup
defer wg.Wait()
visited := make(chan string)
var got []string
wg.Go(func() {
for path := range visited {
got = append(got, path)
}
})
err := uevent.Coldboot(t.Context(), d, visited, func(err error) error {
t.Errorf("handleWalkErr: %v", err)
return err
})
close(visited)
if err != nil {
t.Fatalf("Coldboot: error = %v", err)
}
wg.Wait()
want := []string{
"devices/sub/uevent",
"devices/uevent",
}
for i, rel := range want {
want[i] = filepath.Join(d, rel)
}
if !slices.Equal(got, want) {
t.Errorf("Coldboot: %#v, want %#v", got, want)
}
var checksum pkg.Checksum
if err = pkg.HashDir(&checksum, check.MustAbs(d)); err != nil {
t.Fatalf("HashDir: error = %v", err)
}
wantChecksum := pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8")
if checksum != wantChecksum {
t.Errorf(
"Coldboot: checksum = %s, want %s",
pkg.Encode(checksum),
pkg.Encode(wantChecksum),
)
}
}
func TestColdbootError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
dF func(t *testing.T, d string) (wantErr error)
vF func(<-chan string, context.Context, context.CancelFunc)
hF func(d string, err error) error
}{
{"walk", func(t *testing.T, d string) (wantErr error) {
wantErr = &os.PathError{
Op: "open",
Path: filepath.Join(d, "devices"),
Err: syscall.EACCES,
}
if err := os.Mkdir(filepath.Join(d, "devices"), 0); err != nil {
t.Fatal(err)
}
return
}, nil, nil},
{"write", func(t *testing.T, d string) (wantErr error) {
wantErr = &os.PathError{
Op: "open",
Path: filepath.Join(d, "devices/uevent"),
Err: syscall.EACCES,
}
if err := os.Mkdir(filepath.Join(d, "devices"), 0700); err != nil {
t.Fatal(err)
} else if err = os.WriteFile(filepath.Join(d, "devices/uevent"), nil, 0); err != nil {
t.Fatal(err)
}
return
}, nil, nil},
{"deref", func(t *testing.T, d string) (wantErr error) {
if err := os.Mkdir(filepath.Join(d, "devices"), 0700); err != nil {
t.Fatal(err)
} else if err = os.Symlink("/proc/nonexistent", filepath.Join(d, "devices/uevent")); err != nil {
t.Fatal(err)
}
return
}, nil, nil},
{"deref handle", func(t *testing.T, d string) (wantErr error) {
if err := os.Mkdir(filepath.Join(d, "devices"), 0700); err != nil {
t.Fatal(err)
} else if err = os.Symlink("/proc/nonexistent", filepath.Join(d, "devices/uevent")); err != nil {
t.Fatal(err)
}
return
}, nil, func(d string, err error) error {
if reflect.DeepEqual(err, &os.PathError{
Op: "open",
Path: filepath.Join(d, "devices/uevent"),
Err: syscall.ENOENT,
}) {
return nil
}
return err
}},
{"cancel early", func(t *testing.T, d string) (wantErr error) {
wantErr = context.Canceled
if err := os.Mkdir(filepath.Join(d, "devices"), 0700); err != nil {
t.Fatal(err)
}
return
}, func(visited <-chan string, ctx context.Context, cancel context.CancelFunc) {
if visited == nil {
cancel()
}
return
}, nil},
{"cancel", func(t *testing.T, d string) (wantErr error) {
wantErr = context.Canceled
if err := os.Mkdir(filepath.Join(d, "devices"), 0700); err != nil {
t.Fatal(err)
} else if err = os.WriteFile(filepath.Join(d, "devices/uevent"), nil, 0600); err != nil {
t.Fatal(err)
} else if err = os.Mkdir(filepath.Join(d, "devices/sub"), 0700); err != nil {
t.Fatal(err)
} else if err = os.WriteFile(filepath.Join(d, "devices/sub/uevent"), nil, 0600); err != nil {
t.Fatal(err)
}
return
}, func(visited <-chan string, ctx context.Context, cancel context.CancelFunc) {
if visited == nil {
return
}
<-visited
cancel()
return
}, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
d := t.TempDir()
wantErr := tc.dF(t, d)
var wg sync.WaitGroup
defer wg.Wait()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
var visited chan string
if tc.vF != nil {
tc.vF(nil, ctx, cancel)
visited = make(chan string)
defer close(visited)
wg.Go(func() { tc.vF(visited, ctx, cancel) })
}
var handleWalkErr func(error) error
if tc.hF != nil {
handleWalkErr = func(err error) error {
return tc.hF(d, err)
}
}
if err := uevent.Coldboot(ctx, d, visited, handleWalkErr); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Coldboot: error = %v, want %v", err, wantErr)
}
})
}
}

View File

@@ -18,6 +18,19 @@ type (
Recoverable interface{ recoverable() }
// Nontrivial is satisfied by errors preferring a JSON encoding.
Nontrivial interface{ nontrivial() }
// NeedsColdboot is satisfied by errors indicating divergence of local state
// from the kernel, usually from lost uevent data.
NeedsColdboot interface {
Recoverable
coldboot()
}
)
const (
exclConsume = iota
_exclLen
)
// Conn represents a NETLINK_KOBJECT_UEVENT socket.
@@ -25,27 +38,27 @@ type Conn struct {
conn *netlink.Conn
// Whether currently between a call to enterExcl and exitExcl.
excl atomic.Bool
excl [_exclLen]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) {
func (c *Conn) enterExcl(k int) error {
if !c.excl[k].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) }
func (c *Conn) exitExcl(k int) { c.excl[k].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) {
func Dial(rcvbuf int64) (*Conn, error) {
// kernel group is hard coded in lib/kobject_uevent.c, undocumented
c, err := netlink.Dial(syscall.NETLINK_KOBJECT_UEVENT, 1)
c, err := netlink.Dial(syscall.NETLINK_KOBJECT_UEVENT, 1, rcvbuf)
if err != nil {
return nil, err
}
@@ -58,6 +71,18 @@ var (
ErrBadSocket = errors.New("unexpected socket address")
)
// ReceiveBufferError indicates one or more [Message] being lost due to the
// socket receive buffer filling up. This is usually caused by epoll waking the
// receiving program up too late.
type ReceiveBufferError struct{ _ [0]*ReceiveBufferError }
var _ NeedsColdboot = ReceiveBufferError{}
func (ReceiveBufferError) recoverable() {}
func (ReceiveBufferError) coldboot() {}
func (ReceiveBufferError) Unwrap() error { return syscall.ENOBUFS }
func (e ReceiveBufferError) Error() string { return syscall.ENOBUFS.Error() }
// BadPortError is returned by [Conn.Consume] upon receiving a message that did
// not come from the kernel.
type BadPortError syscall.SockaddrNetlink
@@ -70,36 +95,128 @@ func (e *BadPortError) Error() string {
" on NETLINK_KOBJECT_UEVENT"
}
// Consume continuously receives and parses events from the kernel. It returns
// the first error it encounters.
// receiveEvent receives a single event and returns the address of its [Message].
func (c *Conn) receiveEvent(ctx context.Context) (*Message, error) {
data, _, from, err := c.conn.Recvmsg(ctx, 0)
if err != nil {
if errors.Is(err, syscall.ENOBUFS) {
return nil, ReceiveBufferError{}
}
return nil, err
}
// lib/kobject_uevent.c:
// set portid 0 to inform userspace message comes from kernel
if v, ok := from.(*syscall.SockaddrNetlink); !ok {
return nil, ErrBadSocket
} else if v.Pid != 0 {
return nil, (*BadPortError)(v)
}
var msg Message
if err = msg.UnmarshalBinary(data); err != nil {
return nil, err
}
return &msg, err
}
// Consume continuously receives and parses events from the kernel and handles
// [Recoverable] and [NeedsColdboot] errors via caller-supplied functions,
// entering coldboot when required.
//
// For each uevent file visited by [Coldboot], handleColdbootVisited is called
// with its pathname. This function must never block.
//
// When consuming events, a non-nil error not satisfying [Recoverable] is
// returned immediately. Otherwise, handleConsumeErr is called with the error
// value. If the error satisfies [NeedsColdboot], a [Coldboot] is arranged
// before event processing resumes. If handleConsumeErr returns false, the error
// value is immediately returned as is.
//
// Callers are expected to reject excessively frequent [NeedsColdboot] errors
// in handleConsumeErr to avoid being stuck in a [Coldboot] loop. Event
// processing is allowed to restart without initial coldboot after recovering
// from such a condition, provided the caller adequately reports the degraded,
// diverging state to the user.
//
// 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 {
func (c *Conn) Consume(
ctx context.Context,
sysfs string,
events chan<- *Message,
coldboot bool,
handleColdbootVisited func(string),
handleConsumeErr func(error) bool,
handleWalkErr func(error) error,
) error {
if err := c.enterExcl(exclConsume); err != nil {
return err
}
defer c.exitExcl()
defer c.exitExcl(exclConsume)
for {
data, from, err := c.conn.Recvfrom(ctx, 0)
if err != nil {
return err
filterErr := func(err error) (error, bool) {
if _, ok := err.(Recoverable); !ok {
return err, true
}
// 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)
// avoids dropping pending coldboot
if _, ok := err.(NeedsColdboot); ok {
coldboot = true
}
var msg Message
if err = msg.UnmarshalBinary(data); err != nil {
return err
}
events <- &msg
return err, !handleConsumeErr(err)
}
retry:
if coldboot {
goto coldboot
}
for {
msg, err := c.receiveEvent(ctx)
if err == nil {
events <- msg
continue
}
if _, ok := filterErr(err); ok {
return err
}
}
coldboot:
coldboot = false
visited := make(chan string)
ctxColdboot, cancelColdboot := context.WithCancel(ctx)
var coldbootErr error
go func() {
coldbootErr = Coldboot(ctxColdboot, sysfs, visited, handleWalkErr)
close(visited)
}()
for pathname := range visited {
handleColdbootVisited(pathname)
for {
msg, err := c.receiveEvent(nil)
if err == nil {
events <- msg
continue
}
if errors.Is(err, syscall.EWOULDBLOCK) {
break
}
if filteredErr, ok := filterErr(err); ok {
cancelColdboot()
return filteredErr
}
}
}
cancelColdboot()
if coldbootErr != nil {
return coldbootErr
}
goto retry
}

View File

@@ -10,6 +10,7 @@ import (
"testing"
"time"
"hakurei.app/fhs"
"hakurei.app/internal/uevent"
)
@@ -116,7 +117,7 @@ func adeB[V any, S interface {
func TestDialConsume(t *testing.T) {
t.Parallel()
c, err := uevent.Dial()
c, err := uevent.Dial(0)
if err != nil {
t.Fatalf("Dial: error = %v", err)
}
@@ -127,7 +128,7 @@ func TestDialConsume(t *testing.T) {
})
// check kernel-assigned port id
c0, err0 := uevent.Dial()
c0, err0 := uevent.Dial(0)
if err0 != nil {
t.Fatalf("Dial: error = %v", err)
}
@@ -155,13 +156,23 @@ func TestDialConsume(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
consume := func(c *uevent.Conn, ctx context.Context) error {
return c.Consume(ctx, fhs.Sys, events, false, func(path string) {
t.Log("coldboot visited", path)
}, func(err error) bool {
t.Log(err)
_, ok := err.(uevent.NeedsColdboot)
return !ok
}, nil)
}
wg.Go(func() {
if err = c.Consume(ctx, events); err != context.Canceled {
if err = consume(c, ctx); err != context.Canceled {
panic(err)
}
})
wg.Go(func() {
if err0 = c0.Consume(ctx, events); err0 != context.Canceled {
if err0 = consume(c0, ctx); err0 != context.Canceled {
panic(err0)
}
})
@@ -185,11 +196,11 @@ func TestDialConsume(t *testing.T) {
exclExit := make(chan struct{})
wg.Go(func() {
defer func() { exclExit <- struct{}{} }()
errs[0] = c.Consume(ctx, events)
errs[0] = consume(c, ctx)
})
wg.Go(func() {
defer func() { exclExit <- struct{}{} }()
errs[1] = c.Consume(ctx, events)
errs[1] = consume(c, ctx)
})
<-exclExit
cancel()

View File

@@ -3,7 +3,7 @@ package wayland
import (
"errors"
"os"
"path"
"path/filepath"
"reflect"
"syscall"
"testing"
@@ -19,7 +19,7 @@ func TestSecurityContextClose(t *testing.T) {
}
var ctx SecurityContext
if f, err := os.Create(path.Join(t.TempDir(), "remove")); err != nil {
if f, err := os.Create(filepath.Join(t.TempDir(), "remove")); err != nil {
t.Fatal(err)
} else {
ctx.bindPath = check.MustAbs(f.Name())

View File

@@ -17,7 +17,7 @@ import (
"log"
"net"
"os"
"path"
"path/filepath"
"syscall"
)
@@ -54,7 +54,7 @@ func (t *T) MustCheckFile(wantFilePath string) {
}
func mustAbs(s string) string {
if !path.IsAbs(s) {
if !filepath.IsAbs(s) {
fatalf("[FAIL] %q is not absolute", s)
panic("unreachable")
}
@@ -68,7 +68,7 @@ func (t *T) MustCheck(want *TestCase) {
os.Getenv("XDG_RUNTIME_DIR"),
}
for _, a := range checkWritableDirPaths {
pathname := path.Join(mustAbs(a), ".hakurei-check")
pathname := filepath.Join(mustAbs(a), ".hakurei-check")
if err := os.WriteFile(pathname, make([]byte, 1<<8), 0600); err != nil {
fatalf("[FAIL] %s", err)
} else if err = os.Remove(pathname); err != nil {

View File

@@ -5,7 +5,7 @@ package sandbox
import (
"encoding/json"
"os"
"path"
"path/filepath"
"testing"
)
@@ -15,7 +15,7 @@ func SwapPrint(f F) (old F) { old = printfFunc; printfFunc = f; return }
func SwapFatal(f F) (old F) { old = fatalfFunc; fatalfFunc = f; return }
func MustWantFile(t *testing.T, v any) (wantFile string) {
wantFile = path.Join(t.TempDir(), "want.json")
wantFile = filepath.Join(t.TempDir(), "want.json")
if f, err := os.OpenFile(wantFile, os.O_CREATE|os.O_WRONLY, 0400); err != nil {
t.Fatalf("cannot create %q: %v", wantFile, err)
} else if err = json.NewEncoder(f).Encode(v); err != nil {

View File

@@ -6,7 +6,7 @@ import (
"errors"
"fmt"
"io/fs"
"path"
"path/filepath"
"strings"
)
@@ -68,7 +68,7 @@ func (s *FS) Compare(prefix string, e fs.FS) error {
printDir(prefix, dir)
return ErrFSInvalidEnt
} else {
name = path.Join(prefix, name)
name = filepath.Join(prefix, name)
if fi, err := got.Info(); err != nil {
return err

View File

@@ -4,7 +4,7 @@ package sandbox_test
import (
"os"
"path"
"path/filepath"
"testing"
"hakurei.app/test/internal/sandbox"
@@ -87,7 +87,7 @@ func TestMountinfo(t *testing.T) {
}
for _, tc := range testCases {
name := path.Join(t.TempDir(), "sample")
name := filepath.Join(t.TempDir(), "sample")
if err := os.WriteFile(name, []byte(tc.sample), 0400); err != nil {
t.Fatalf("cannot write sample: %v", err)
}

View File

@@ -5,7 +5,7 @@ import (
"errors"
"iter"
"os"
"path"
"path/filepath"
"reflect"
"slices"
"strconv"
@@ -394,7 +394,7 @@ func mn(
},
FirstChild: firstChild,
NextSibling: nextSibling,
Clean: path.Clean(target),
Clean: filepath.Clean(target),
Covered: covered,
}
}

View File

@@ -2,7 +2,7 @@ package vfs
import (
"iter"
"path"
"path/filepath"
"strings"
)
@@ -43,7 +43,7 @@ func (n *MountInfoNode) visit(yield func(*MountInfoNode) bool) bool {
// Unfold unfolds the mount hierarchy and resolves covered paths.
func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) {
targetClean := path.Clean(target)
targetClean := filepath.Clean(target)
var mountinfoSize int
for range d.Entries() {
@@ -61,7 +61,7 @@ func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) {
{
i := 0
for ent := range d.Entries() {
mountinfo[i] = &MountInfoNode{Clean: path.Clean(ent.Target), MountInfoEntry: ent}
mountinfo[i] = &MountInfoNode{Clean: filepath.Clean(ent.Target), MountInfoEntry: ent}
idIndex[ent.ID] = i
if mountinfo[i].Clean == targetClean {
targetIndex = i