aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/acl/acl.go99
-rw-r--r--internal/acl/acl_test.go200
-rw-r--r--internal/acl/libacl-helper.c175
-rw-r--r--internal/acl/libacl-helper.go40
-rw-r--r--internal/acl/libacl-helper.h21
-rw-r--r--internal/acl/libacl-helper_test.go47
-rw-r--r--internal/acl/perms.go26
-rw-r--r--internal/acl/perms_test.go20
-rw-r--r--internal/lockedfile/lockedfile.go19
-rw-r--r--internal/lockedfile/lockedfile_filelock.go6
-rw-r--r--internal/lockedfile/lockedfile_plan9.go2
-rw-r--r--internal/outcome/dispatcher_test.go4
-rw-r--r--internal/outcome/finalise.go2
-rw-r--r--internal/outcome/outcome.go17
-rw-r--r--internal/outcome/run_test.go90
-rw-r--r--internal/outcome/shim-signal.c81
-rw-r--r--internal/outcome/shim-signal.h8
-rw-r--r--internal/outcome/shim.go7
-rw-r--r--internal/outcome/spcontainer.go14
-rw-r--r--internal/outcome/spcontainer_test.go26
-rw-r--r--internal/outcome/spdbus.go4
-rw-r--r--internal/outcome/spdbus_test.go14
-rw-r--r--internal/outcome/sppipewire_test.go2
-rw-r--r--internal/outcome/sppulse_test.go24
-rw-r--r--internal/outcome/spruntime.go7
-rw-r--r--internal/outcome/spruntime_test.go28
-rw-r--r--internal/outcome/sptmpdir.go7
-rw-r--r--internal/outcome/sptmpdir_test.go7
-rw-r--r--internal/outcome/spwayland.go2
-rw-r--r--internal/outcome/spwayland_test.go10
-rw-r--r--internal/outcome/spx11.go2
-rw-r--r--internal/outcome/spx11_test.go4
-rw-r--r--internal/rosa/llvm.go2
-rw-r--r--internal/rosa/llvm_test.go2
-rw-r--r--internal/rosa/mirror.go1
-rw-r--r--internal/rosa/package/acl/package.az4
-rw-r--r--internal/rosa/package/cairo.az4
-rw-r--r--internal/rosa/package/cmake/package.az4
-rw-r--r--internal/rosa/package/cryptsetup.az4
-rw-r--r--internal/rosa/package/curl.az4
-rw-r--r--internal/rosa/package/dbus.az4
-rw-r--r--internal/rosa/package/firmware.az4
-rw-r--r--internal/rosa/package/freetype.az6
-rw-r--r--internal/rosa/package/fribidi.az4
-rw-r--r--internal/rosa/package/fuse/package.az10
-rw-r--r--internal/rosa/package/git.az6
-rw-r--r--internal/rosa/package/glib/loopvar-cast.patch13
-rw-r--r--internal/rosa/package/glib/package.az5
-rw-r--r--internal/rosa/package/glslang.az6
-rw-r--r--internal/rosa/package/gnu.az34
-rw-r--r--internal/rosa/package/go.az4
-rw-r--r--internal/rosa/package/gzip/late-include.patch21
-rw-r--r--internal/rosa/package/gzip/package.az18
-rw-r--r--internal/rosa/package/hakurei.az52
-rw-r--r--internal/rosa/package/harfbuzz.az4
-rw-r--r--internal/rosa/package/hwdata.az4
-rw-r--r--internal/rosa/package/kernel/amd64.config15
-rw-r--r--internal/rosa/package/kernel/arm64.config20
-rw-r--r--internal/rosa/package/kernel/dml-frame_warn_limit.patch13
-rw-r--r--internal/rosa/package/kernel/package.az12
-rw-r--r--internal/rosa/package/kernel/version.h4
-rw-r--r--internal/rosa/package/libarchive/CTestCustom.cmake1
-rw-r--r--internal/rosa/package/libarchive/package.az4
-rw-r--r--internal/rosa/package/libbpf.az4
-rw-r--r--internal/rosa/package/libevdev.az4
-rw-r--r--internal/rosa/package/libexpat.az4
-rw-r--r--internal/rosa/package/libglvnd.az1
-rw-r--r--internal/rosa/package/libinput.az4
-rw-r--r--internal/rosa/package/libliftoff/0001-test-Disable-alloc-deadline-on-tests.patch139
-rw-r--r--internal/rosa/package/libliftoff/package.az (renamed from internal/rosa/package/libliftoff.az)1
-rw-r--r--internal/rosa/package/libmd.az4
-rw-r--r--internal/rosa/package/libpng.az4
-rw-r--r--internal/rosa/package/libtirpc.az8
-rw-r--r--internal/rosa/package/llvm/package.az4
-rw-r--r--internal/rosa/package/lvm2/package.az4
-rw-r--r--internal/rosa/package/mesa/include.patch12
-rw-r--r--internal/rosa/package/mesa/package.az4
-rw-r--r--internal/rosa/package/meson/package.az4
-rw-r--r--internal/rosa/package/muon/package.az (renamed from internal/rosa/package/muon.az)17
-rw-r--r--internal/rosa/package/muon/remove-broken-tests.patch30
-rw-r--r--internal/rosa/package/muon/remove-cl.patch39
-rw-r--r--internal/rosa/package/muon/respect-colour.patch14
-rw-r--r--internal/rosa/package/noto.az4
-rw-r--r--internal/rosa/package/nss.az4
-rw-r--r--internal/rosa/package/openssl.az6
-rw-r--r--internal/rosa/package/pcre2.az4
-rw-r--r--internal/rosa/package/pipewire.az8
-rw-r--r--internal/rosa/package/pkgconf.az4
-rw-r--r--internal/rosa/package/python/package.az58
-rw-r--r--internal/rosa/package/python/wheel-test-colour.patch21
-rw-r--r--internal/rosa/package/qemu/package.az4
-rw-r--r--internal/rosa/package/spirv.az12
-rw-r--r--internal/rosa/package/sway/package.az1
-rw-r--r--internal/rosa/package/tamago.az4
-rw-r--r--internal/rosa/package/toybox.az4
-rw-r--r--internal/rosa/package/utf8proc.az4
-rw-r--r--internal/rosa/package/util-linux.az (renamed from internal/rosa/package/util-linux/package.az)5
-rw-r--r--internal/rosa/package/util-linux/a323dddbcd1ed05a10e7e870b3e1a48b4ed44a43.patch35
-rw-r--r--internal/rosa/package/x.az20
-rw-r--r--internal/rosa/package/xz.az4
-rw-r--r--internal/rosa/state.go39
-rw-r--r--internal/system/acl.go17
-rw-r--r--internal/system/acl_test.go100
-rw-r--r--internal/system/dispatcher.go6
-rw-r--r--internal/system/dispatcher_test.go19
-rw-r--r--internal/system/mkdir.go40
-rw-r--r--internal/system/mkdir_test.go86
-rw-r--r--internal/system/pipewire.go2
-rw-r--r--internal/system/pipewire_test.go2
-rw-r--r--internal/system/system.go53
-rw-r--r--internal/system/system_test.go74
-rw-r--r--internal/system/wayland.go2
-rw-r--r--internal/system/wayland_test.go4
-rw-r--r--internal/testsuite/fs.go129
-rw-r--r--internal/testsuite/fs_test.go85
-rw-r--r--internal/testsuite/mountinfo/mountinfo.go177
-rw-r--r--internal/testsuite/mountinfo/mountinfo_guard.go15
-rw-r--r--internal/testsuite/mountinfo/mountinfo_test.go146
-rw-r--r--internal/testsuite/proc.go353
-rw-r--r--internal/testsuite/proc_test.go33
-rw-r--r--internal/testsuite/ptrace.go151
-rw-r--r--internal/testsuite/ptrace_test.go13
-rw-r--r--internal/testsuite/testsuite.go910
-rw-r--r--internal/testsuite/testsuite_guard.go15
-rw-r--r--internal/testsuite/testsuite_root.go17
-rw-r--r--internal/wayland/security-context-v1-protocol.c2
-rw-r--r--internal/wayland/security-context-v1-protocol.h2
-rw-r--r--internal/wayland/wayland-client-helper.c185
-rw-r--r--internal/wayland/wayland-client-helper.h64
-rw-r--r--internal/workflows/Containerfile93
-rwxr-xr-xinternal/workflows/build.sh5
-rw-r--r--internal/workflows/doc.go162
-rw-r--r--internal/workflows/main.go151
-rw-r--r--internal/workflows/release.go50
-rw-r--r--internal/workflows/seccomp.patch18
-rw-r--r--internal/workflows/step.go60
-rw-r--r--internal/workflows/test.go86
137 files changed, 4199 insertions, 974 deletions
diff --git a/internal/acl/acl.go b/internal/acl/acl.go
index 366ad533..d5b53095 100644
--- a/internal/acl/acl.go
+++ b/internal/acl/acl.go
@@ -8,26 +8,103 @@ package acl
*/
import "C"
+import (
+ "os"
+
+ "hakurei.app/ext"
+)
+
+// Perm represents acl_perm_t.
type Perm C.acl_perm_t
const (
- Read Perm = C.ACL_READ
- Write Perm = C.ACL_WRITE
+ // Read represents ACL_READ.
+ Read Perm = C.ACL_READ
+ // Write represents ACL_WRITE.
+ Write Perm = C.ACL_WRITE
+ // Execute represents ACL_EXECUTE.
Execute Perm = C.ACL_EXECUTE
)
+// Status is the outcome of an ACL update operation.
+type Status = C.enum_hakurei_acl_status
+
+// An Error describes an error encountered during [Update].
+type Error struct {
+ // Pathname of the file to update ACL for.
+ Path string
+ // Function that returned the error.
+ Op Status
+ // Underlying errno.
+ Err error
+}
+
+// Unwrap returns an [os.PathError] describing e.
+func (e Error) Unwrap() error {
+ v := os.PathError{Path: e.Path, Err: e.Err}
+ switch e.Op {
+ case C.HAKUREI_ACL_OK:
+ return os.ErrInvalid
+
+ case C.HAKUREI_ACL_GET_FILE:
+ v.Op = "acl_get_file"
+ case C.HAKUREI_ACL_GET_TAG_TYPE:
+ v.Op = "acl_get_tag_type"
+ case C.HAKUREI_ACL_GET_QUALIFIER:
+ v.Op = "acl_get_qualifier"
+ case C.HAKUREI_ACL_DELETE_ENTRY:
+ v.Op = "acl_delete_entry"
+ case C.HAKUREI_ACL_CREATE_ENTRY:
+ v.Op = "acl_create_entry"
+ case C.HAKUREI_ACL_GET_PERMSET:
+ v.Op = "acl_get_permset"
+ case C.HAKUREI_ACL_ADD_PERM:
+ v.Op = "acl_add_perm"
+ case C.HAKUREI_ACL_SET_TAG_TYPE:
+ v.Op = "acl_set_tag_type"
+ case C.HAKUREI_ACL_SET_QUALIFIER:
+ v.Op = "acl_set_qualifier"
+ case C.HAKUREI_ACL_CALC_MASK:
+ v.Op = "acl_calc_mask"
+ case C.HAKUREI_ACL_VALID:
+ v.Op = "acl_valid"
+ case C.HAKUREI_ACL_SET_FILE:
+ v.Op = "acl_set_file"
+ case C.HAKUREI_ACL_GET_PERM:
+ v.Op = "acl_get_perm"
+ case C.HAKUREI_ACL_DUP:
+ v.Op = "acl_dup"
+
+ default: // unreachable
+ v.Op = "setfacl"
+ }
+ return &v
+}
+
+// Error returns the message of the resulting [os.PathError].
+func (e Error) Error() string {
+ return e.Unwrap().Error()
+}
+
// Update replaces ACL_USER entry with qualifier uid.
-func Update(name string, uid int, perms ...Perm) error {
- var p *Perm
- if len(perms) > 0 {
- p = &perms[0]
+func Update(name string, uid int, perms Perm) error {
+ f, _err := os.OpenFile(name, ext.O_PATH, 0)
+ if _err != nil {
+ return _err
}
- r, err := C.hakurei_acl_update_file_by_uid(
- C.CString(name),
+ r, err := C.hakurei_update_acl(
+ C.int(f.Fd()),
C.uid_t(uid),
- (*C.acl_perm_t)(p),
- C.size_t(len(perms)),
+ C.acl_perm_t(perms),
)
- return newAclPathError(name, int(r), err)
+
+ if _err = f.Close(); _err != nil {
+ return _err
+ }
+
+ if r == C.HAKUREI_ACL_OK {
+ return nil
+ }
+ return &Error{name, r, err}
}
diff --git a/internal/acl/acl_test.go b/internal/acl/acl_test.go
index 60c4b702..14f0ca18 100644
--- a/internal/acl/acl_test.go
+++ b/internal/acl/acl_test.go
@@ -18,93 +18,131 @@ import (
"hakurei.app/internal/info"
)
-const testFileName = "acl.test"
-
var (
uid = os.Geteuid()
- cred = int32(os.Geteuid())
+ cred = int32(uid)
)
+// testFile returns the pathname of a newly created test file.
+func testFile(t *testing.T) string {
+ name := filepath.Join(t.TempDir(), "f")
+ f, err := os.Create(name)
+ if err != nil {
+ t.Fatal(err)
+ } else if err = f.Close(); err != nil {
+ t.Fatal(err)
+ }
+ return name
+}
+
func TestUpdate(t *testing.T) {
if info.CanDegrade {
name := filepath.Join(t.TempDir(), "check-degrade")
if err := os.WriteFile(name, nil, 0); err != nil {
t.Fatal(err)
}
- if err := acl.Update(name, os.Geteuid()); errors.Is(err, syscall.ENOTSUP) {
+ if err := acl.Update(name, uid, 0); errors.Is(err, syscall.ENOTSUP) {
t.Skip(err)
}
}
- testFilePath := filepath.Join(t.TempDir(), testFileName)
+ t.Parallel()
- if f, err := os.Create(testFilePath); err != nil {
- t.Fatalf("Create: error = %v", err)
- } else {
- if err = f.Close(); err != nil {
- t.Fatalf("Close: error = %v", err)
- }
+ pathname := testFile(t)
+ cur := getfacl(t, pathname)
+ if len(cur) != 3 {
+ t.Fatalf("unexpected test file acl length %d", len(cur))
}
- defer func() {
- if err := os.Remove(testFilePath); err != nil {
- t.Fatalf("Remove: error = %v", err)
- }
- }()
- cur := getfacl(t, testFilePath)
+ if err := acl.Update(pathname, uid, 0); err != nil {
+ t.Fatalf("(clear) Update: error = %v", err)
+ }
+ if cur = getfacl(t, pathname); len(cur) != 4 {
+ t.Fatalf("(clear) Update: %v", cur)
+ }
- t.Run("default entry count", func(t *testing.T) {
- if len(cur) != 3 {
- t.Fatalf("unexpected test file acl length %d", len(cur))
- }
- })
+ if err := acl.Update(pathname, uid, 0); err != nil {
+ t.Fatalf("(clear*) Update: error = %v", err)
+ }
+ if val := getfacl(t, pathname); !reflect.DeepEqual(val, cur) {
+ t.Fatalf("(clear*) Update: %v, want %v", val, cur)
+ }
- t.Run("default clear mask", func(t *testing.T) {
- if err := acl.Update(testFilePath, uid); err != nil {
- t.Fatalf("Update: error = %v", err)
- }
- if cur = getfacl(t, testFilePath); len(cur) != 4 {
- t.Fatalf("Update: %v", cur)
- }
- })
+ testUpdate(t, "r--", "", cur, acl.Read, true)
+ testUpdate(t, "-w-", "", cur, acl.Write, true)
+ testUpdate(t, "--x", "", cur, acl.Execute, true)
- t.Run("default clear consistency", func(t *testing.T) {
- if err := acl.Update(testFilePath, uid); err != nil {
- t.Fatalf("Update: error = %v", err)
- }
- if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) {
- t.Fatalf("Update: %v, want %v", val, cur)
- }
- })
+ testUpdate(
+ t, "-wx", "", cur,
+ acl.Write|acl.Execute,
+ true,
+ )
+ testUpdate(
+ t, "r-x", "", cur,
+ acl.Read|acl.Execute,
+ true,
+ )
+ testUpdate(
+ t, "rw-", "", cur,
+ acl.Read|acl.Write,
+ true,
+ )
+ testUpdate(
+ t, "rwx", "", cur,
+ acl.Read|acl.Write|acl.Execute,
+ true,
+ )
- testUpdate(t, testFilePath, "r--", cur, fAclPermRead, acl.Read)
- testUpdate(t, testFilePath, "-w-", cur, fAclPermWrite, acl.Write)
- testUpdate(t, testFilePath, "--x", cur, fAclPermExecute, acl.Execute)
- testUpdate(t, testFilePath, "-wx", cur, fAclPermWrite|fAclPermExecute, acl.Write, acl.Execute)
- testUpdate(t, testFilePath, "r-x", cur, fAclPermRead|fAclPermExecute, acl.Read, acl.Execute)
- testUpdate(t, testFilePath, "rw-", cur, fAclPermRead|fAclPermWrite, acl.Read, acl.Write)
- testUpdate(t, testFilePath, "rwx", cur, fAclPermRead|fAclPermWrite|fAclPermExecute, acl.Read, acl.Write, acl.Execute)
+ for i := range 16 {
+ testUpdate(
+ t, "r-x", " "+strconv.Itoa(i), cur,
+ acl.Read|acl.Execute,
+ false,
+ )
+ }
+
+ testUpdate(
+ t, "rwx", " cleanup", cur,
+ acl.Read|acl.Write|acl.Execute,
+ true,
+ )
}
-func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val fAclPerm, perms ...acl.Perm) {
- t.Run(name, func(t *testing.T) {
- t.Cleanup(func() {
- if err := acl.Update(testFilePath, uid); err != nil {
- t.Fatalf("Update: error = %v", err)
- }
- if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) {
- t.Fatalf("Update: %v, want %v", v, cur)
- }
- })
+func testUpdate(
+ t *testing.T,
+ name, suffix string,
+ cur []*getFACLResp,
+ perms acl.Perm,
+ cleanup bool,
+) {
+ t.Run(name+suffix, func(t *testing.T) {
+ t.Parallel()
- if err := acl.Update(testFilePath, uid, perms...); err != nil {
+ pathname := testFile(t)
+ if cleanup {
+ t.Cleanup(func() {
+ if err := acl.Update(pathname, uid, 0); err != nil {
+ t.Fatalf("Update: error = %v", err)
+ }
+ if v := getfacl(t, pathname); !reflect.DeepEqual(v, cur) {
+ t.Fatalf("Update: %v, want %v", v, cur)
+ }
+ })
+ }
+
+ if err := acl.Update(pathname, uid, perms); err != nil {
t.Fatalf("Update: error = %v", err)
}
- r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred)
+ r := respByCred(getfacl(t, pathname), fAclTypeUser, cred)
if r == nil {
t.Fatalf("Update did not add an ACL entry")
}
- if !r.equals(fAclTypeUser, cred, val) {
+
+ if !r.equals(&getFACLResp{
+ typ: fAclTypeUser,
+ cred: cred,
+ val: perms,
+ }) {
t.Fatalf("Update(%s) = %s", name, r)
}
})
@@ -113,31 +151,24 @@ func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val
type (
getFAclInvocation struct {
cmd *exec.Cmd
- val []*getFAclResp
+ val []*getFACLResp
pe []error
}
- getFAclResp struct {
+ getFACLResp struct {
typ fAclType
cred int32
- val fAclPerm
+ val acl.Perm
raw []byte
}
- fAclPerm uintptr
fAclType uint8
)
const fAclBufSize = 16
const (
- fAclPermRead fAclPerm = 1 << iota
- fAclPermWrite
- fAclPermExecute
-)
-
-const (
fAclTypeUser fAclType = iota
fAclTypeGroup
fAclTypeMask
@@ -149,7 +180,13 @@ func (c *getFAclInvocation) run(name string) error {
panic("attempted to run twice")
}
- c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name)
+ c.cmd = exec.Command(
+ "getfacl",
+ "--omit-header",
+ "--absolute-names",
+ "--numeric",
+ name,
+ )
c.cmd.Stderr = os.Stderr
scanErr := make(chan error, 1)
@@ -166,18 +203,17 @@ func (c *getFAclInvocation) run(name string) error {
return errors.Join(<-scanErr, c.cmd.Wait())
}
-func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
- c.val = make([]*getFAclResp, 0, 4+fAclBufSize)
+func (c *getFAclInvocation) parse(r io.Reader, scanErr chan error) {
+ c.val = make([]*getFACLResp, 0, 4+fAclBufSize)
- s := bufio.NewScanner(pipe)
+ s := bufio.NewScanner(r)
for s.Scan() {
fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3)
if len(fields) != 3 {
continue
}
- resp := getFAclResp{}
-
+ var resp getFACLResp
switch string(fields[0]) {
case "user":
resp.typ = fAclTypeUser
@@ -213,7 +249,7 @@ func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
} else {
switch fields[2][0] {
case 'r':
- resp.val |= fAclPermRead
+ resp.val |= acl.Read
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0]))
@@ -221,7 +257,7 @@ func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
}
switch fields[2][1] {
case 'w':
- resp.val |= fAclPermWrite
+ resp.val |= acl.Write
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1]))
@@ -229,7 +265,7 @@ func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
}
switch fields[2][2] {
case 'x':
- resp.val |= fAclPermExecute
+ resp.val |= acl.Execute
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2]))
@@ -244,7 +280,7 @@ func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
scanErr <- s.Err()
}
-func (r *getFAclResp) String() string {
+func (r *getFACLResp) String() string {
if r.raw != nil && len(r.raw) > 0 {
return string(r.raw)
}
@@ -252,11 +288,11 @@ func (r *getFAclResp) String() string {
return "(user-initialised resp value)"
}
-func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool {
- return r.typ == typ && r.cred == cred && r.val == val
+func (r *getFACLResp) equals(v *getFACLResp) bool {
+ return r.typ == v.typ && r.cred == v.cred && r.val == v.val
}
-func getfacl(t *testing.T, name string) []*getFAclResp {
+func getfacl(t *testing.T, name string) []*getFACLResp {
c := new(getFAclInvocation)
if err := c.run(name); err != nil {
t.Fatalf("getfacl: error = %v", err)
@@ -267,7 +303,7 @@ func getfacl(t *testing.T, name string) []*getFAclResp {
return c.val
}
-func respByCred(v []*getFAclResp, typ fAclType, cred int32) *getFAclResp {
+func respByCred(v []*getFACLResp, typ fAclType, cred int32) *getFACLResp {
j := -1
for i, r := range v {
if r.typ == typ && r.cred == cred {
diff --git a/internal/acl/libacl-helper.c b/internal/acl/libacl-helper.c
index 259dfb2b..c56e6436 100644
--- a/internal/acl/libacl-helper.c
+++ b/internal/acl/libacl-helper.c
@@ -1,90 +1,133 @@
+#define _GNU_SOURCE /* Definition of AT_* constants */
+
#include "libacl-helper.h"
#include <acl/libacl.h>
#include <stdbool.h>
#include <stdlib.h>
+#include <fcntl.h>
#include <sys/acl.h>
-int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
- acl_perm_t *perms, size_t plen) {
- int ret;
- bool v;
- int i;
- acl_t acl;
- acl_entry_t entry;
- acl_tag_t tag_type;
- void *qualifier_p;
- acl_permset_t permset;
+enum hakurei_acl_status hakurei_update_acl(int fd, uid_t uid, acl_perm_t perms)
+{
+ enum hakurei_acl_status ret;
+
+ bool v;
+ int i;
+ acl_t acl, acl_d = NULL;
+ acl_entry_t entry;
+ acl_tag_t tag_type;
+ void *qualifier_p;
+ acl_permset_t permset;
+ acl_perm_t has_perms;
+
+ ret = HAKUREI_ACL_GET_FILE;
+ acl = acl_get_file_at(fd, "", AT_EMPTY_PATH, ACL_TYPE_ACCESS);
+ if (acl == NULL)
+ goto out;
+
+ /* prune entries by uid */
+ for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
+ i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
+ ret = HAKUREI_ACL_GET_TAG_TYPE;
+ if (acl_get_tag_type(entry, &tag_type) != 0)
+ goto out;
+ if (tag_type != ACL_USER)
+ continue;
+
+ ret = HAKUREI_ACL_GET_QUALIFIER;
+ qualifier_p = acl_get_qualifier(entry);
+ if (qualifier_p == NULL)
+ goto out;
+ v = *(uid_t *)qualifier_p == uid;
+ acl_free(qualifier_p);
+
+ if (!v)
+ continue;
+
+ ret = HAKUREI_ACL_GET_PERMSET;
+ if (acl_get_permset(entry, &permset) != 0)
+ goto out;
+
+ if (perms == 0)
+ goto delete;
- ret = -1; /* acl_get_file */
- acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
- if (acl == NULL)
- goto out;
+ /* this is quite ugly, but the standard makes it impossible to
+ * directly obtain these bits */
+ has_perms = 0;
+#define COPY_PERM_BIT(perm) \
+ do { \
+ ret = HAKUREI_ACL_GET_PERM; \
+ i = acl_get_perm(permset, perm); \
+ if (i < 0) \
+ goto out; \
+ if (i > 0) \
+ has_perms |= perm; \
+ } while (0)
+ COPY_PERM_BIT(ACL_READ);
+ COPY_PERM_BIT(ACL_WRITE);
+ COPY_PERM_BIT(ACL_EXECUTE);
- /* prune entries by uid */
- for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
- i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
- ret = -2; /* acl_get_tag_type */
- if (acl_get_tag_type(entry, &tag_type) != 0)
- goto out;
- if (tag_type != ACL_USER)
- continue;
+ if (perms == has_perms) {
+ ret = HAKUREI_ACL_DUP;
+ acl_d = acl_dup(acl);
+ if (acl_d == NULL)
+ goto out;
- ret = -3; /* acl_get_qualifier */
- qualifier_p = acl_get_qualifier(entry);
- if (qualifier_p == NULL)
- goto out;
- v = *(uid_t *)qualifier_p == uid;
- acl_free(qualifier_p);
+ ret = HAKUREI_ACL_CALC_MASK;
+ if (acl_calc_mask(&acl_d) != 0)
+ goto out;
- if (!v)
- continue;
+ ret = HAKUREI_ACL_OK;
+ if (acl_cmp(acl, acl_d) == 0)
+ goto out;
+ }
- ret = -4; /* acl_delete_entry */
- if (acl_delete_entry(acl, entry) != 0)
- goto out;
- }
+delete:
+ ret = HAKUREI_ACL_DELETE_ENTRY;
+ if (acl_delete_entry(acl, entry) != 0)
+ goto out;
+ }
- if (plen == 0)
- goto set;
+ if (perms == 0)
+ goto set;
- ret = -5; /* acl_create_entry */
- if (acl_create_entry(&acl, &entry) != 0)
- goto out;
+ ret = HAKUREI_ACL_CREATE_ENTRY;
+ if (acl_create_entry(&acl, &entry) != 0)
+ goto out;
- ret = -6; /* acl_get_permset */
- if (acl_get_permset(entry, &permset) != 0)
- goto out;
+ ret = HAKUREI_ACL_GET_PERMSET;
+ if (acl_get_permset(entry, &permset) != 0)
+ goto out;
- ret = -7; /* acl_add_perm */
- for (i = 0; i < plen; i++) {
- if (acl_add_perm(permset, perms[i]) != 0)
- goto out;
- }
+ ret = HAKUREI_ACL_ADD_PERM;
+ if (acl_add_perm(permset, perms) != 0)
+ goto out;
- ret = -8; /* acl_set_tag_type */
- if (acl_set_tag_type(entry, ACL_USER) != 0)
- goto out;
+ ret = HAKUREI_ACL_SET_TAG_TYPE;
+ if (acl_set_tag_type(entry, ACL_USER) != 0)
+ goto out;
- ret = -9; /* acl_set_qualifier */
- if (acl_set_qualifier(entry, (void *)&uid) != 0)
- goto out;
+ ret = HAKUREI_ACL_SET_QUALIFIER;
+ if (acl_set_qualifier(entry, (void *)&uid) != 0)
+ goto out;
set:
- ret = -10; /* acl_calc_mask */
- if (acl_calc_mask(&acl) != 0)
- goto out;
+ ret = HAKUREI_ACL_CALC_MASK;
+ if (acl_calc_mask(&acl) != 0)
+ goto out;
- ret = -11; /* acl_valid */
- if (acl_valid(acl) != 0)
- goto out;
+ ret = HAKUREI_ACL_VALID;
+ if (acl_valid(acl) != 0)
+ goto out;
- ret = -12; /* acl_set_file */
- if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
- ret = 0;
+ ret = HAKUREI_ACL_SET_FILE;
+ if (acl_set_file_at(fd, "", AT_EMPTY_PATH, ACL_TYPE_ACCESS, acl) == 0)
+ ret = HAKUREI_ACL_OK;
out:
- free((void *)path_p);
- if (acl != NULL)
- acl_free((void *)acl);
- return ret;
+ if (acl != NULL)
+ acl_free((void *)acl);
+ if (acl_d != NULL)
+ acl_free((void *)acl_d);
+ return ret;
}
diff --git a/internal/acl/libacl-helper.go b/internal/acl/libacl-helper.go
deleted file mode 100644
index 22575296..00000000
--- a/internal/acl/libacl-helper.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package acl
-
-import "os"
-
-func newAclPathError(name string, r int, err error) error {
- pathError := &os.PathError{Path: name, Err: err}
- switch r {
- case 0:
- return nil
-
- case -1:
- pathError.Op = "acl_get_file"
- case -2:
- pathError.Op = "acl_get_tag_type"
- case -3:
- pathError.Op = "acl_get_qualifier"
- case -4:
- pathError.Op = "acl_delete_entry"
- case -5:
- pathError.Op = "acl_create_entry"
- case -6:
- pathError.Op = "acl_get_permset"
- case -7:
- pathError.Op = "acl_add_perm"
- case -8:
- pathError.Op = "acl_set_tag_type"
- case -9:
- pathError.Op = "acl_set_qualifier"
- case -10:
- pathError.Op = "acl_calc_mask"
- case -11:
- pathError.Op = "acl_valid"
- case -12:
- pathError.Op = "acl_set_file"
-
- default: // unreachable
- pathError.Op = "setfacl"
- }
- return pathError
-}
diff --git a/internal/acl/libacl-helper.h b/internal/acl/libacl-helper.h
index b86eb170..111fac4a 100644
--- a/internal/acl/libacl-helper.h
+++ b/internal/acl/libacl-helper.h
@@ -1,4 +1,21 @@
#include <sys/acl.h>
-int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
- acl_perm_t *perms, size_t plen);
+enum hakurei_acl_status {
+ HAKUREI_ACL_OK,
+ HAKUREI_ACL_GET_FILE,
+ HAKUREI_ACL_GET_TAG_TYPE,
+ HAKUREI_ACL_GET_QUALIFIER,
+ HAKUREI_ACL_DELETE_ENTRY,
+ HAKUREI_ACL_CREATE_ENTRY,
+ HAKUREI_ACL_GET_PERMSET,
+ HAKUREI_ACL_ADD_PERM,
+ HAKUREI_ACL_SET_TAG_TYPE,
+ HAKUREI_ACL_SET_QUALIFIER,
+ HAKUREI_ACL_CALC_MASK,
+ HAKUREI_ACL_VALID,
+ HAKUREI_ACL_SET_FILE,
+ HAKUREI_ACL_GET_PERM,
+ HAKUREI_ACL_DUP,
+};
+
+enum hakurei_acl_status hakurei_update_acl(int fd, uid_t uid, acl_perm_t perms);
diff --git a/internal/acl/libacl-helper_test.go b/internal/acl/libacl-helper_test.go
index 13d23a3c..05c43639 100644
--- a/internal/acl/libacl-helper_test.go
+++ b/internal/acl/libacl-helper_test.go
@@ -1,4 +1,4 @@
-package acl
+package acl_test
import (
"os"
@@ -7,9 +7,12 @@ import (
"testing"
"hakurei.app/container"
+ "hakurei.app/internal/acl"
)
-func TestNewAclPathError(t *testing.T) {
+func TestError(t *testing.T) {
+ t.Parallel()
+
testCases := []struct {
name string
path string
@@ -17,43 +20,51 @@ func TestNewAclPathError(t *testing.T) {
err error
want error
}{
- {"nil", container.Nonexistent, 0, syscall.ENOTRECOVERABLE, nil},
+ {"nil", container.Nonexistent, 0, syscall.ENOTRECOVERABLE, os.ErrInvalid},
- {"acl_get_file", container.Nonexistent, -1, syscall.ENOTRECOVERABLE,
+ {"acl_get_file", container.Nonexistent, 1, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_get_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
- {"acl_get_tag_type", container.Nonexistent, -2, syscall.ENOTRECOVERABLE,
+ {"acl_get_tag_type", container.Nonexistent, 2, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_get_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
- {"acl_get_qualifier", container.Nonexistent, -3, syscall.ENOTRECOVERABLE,
+ {"acl_get_qualifier", container.Nonexistent, 3, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_get_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
- {"acl_delete_entry", container.Nonexistent, -4, syscall.ENOTRECOVERABLE,
+ {"acl_delete_entry", container.Nonexistent, 4, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_delete_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
- {"acl_create_entry", container.Nonexistent, -5, syscall.ENOTRECOVERABLE,
+ {"acl_create_entry", container.Nonexistent, 5, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_create_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
- {"acl_get_permset", container.Nonexistent, -6, syscall.ENOTRECOVERABLE,
+ {"acl_get_permset", container.Nonexistent, 6, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_get_permset", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
- {"acl_add_perm", container.Nonexistent, -7, syscall.ENOTRECOVERABLE,
+ {"acl_add_perm", container.Nonexistent, 7, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_add_perm", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
- {"acl_set_tag_type", container.Nonexistent, -8, syscall.ENOTRECOVERABLE,
+ {"acl_set_tag_type", container.Nonexistent, 8, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_set_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
- {"acl_set_qualifier", container.Nonexistent, -9, syscall.ENOTRECOVERABLE,
+ {"acl_set_qualifier", container.Nonexistent, 9, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_set_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
- {"acl_calc_mask", container.Nonexistent, -10, syscall.ENOTRECOVERABLE,
+ {"acl_calc_mask", container.Nonexistent, 10, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_calc_mask", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
- {"acl_valid", container.Nonexistent, -11, syscall.ENOTRECOVERABLE,
+ {"acl_valid", container.Nonexistent, 11, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_valid", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
- {"acl_set_file", container.Nonexistent, -12, syscall.ENOTRECOVERABLE,
+ {"acl_set_file", container.Nonexistent, 12, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_set_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_get_perm", container.Nonexistent, 13, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_get_perm", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_dup", container.Nonexistent, 14, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_dup", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
- {"acl", container.Nonexistent, -13, syscall.ENOTRECOVERABLE,
+ {"acl", container.Nonexistent, 15, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "setfacl", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"invalid", container.Nonexistent, -0xdead, nil,
&os.PathError{Op: "setfacl", Path: container.Nonexistent}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- err := newAclPathError(tc.path, tc.r, tc.err)
+ err := acl.Error{
+ Path: tc.path,
+ Op: acl.Status(tc.r),
+ Err: tc.err,
+ }.Unwrap()
if !reflect.DeepEqual(err, tc.want) {
- t.Errorf("newAclPathError: %v, want %v", err, tc.want)
+ t.Errorf("Unwrap: %v, want %v", err, tc.want)
}
})
}
diff --git a/internal/acl/perms.go b/internal/acl/perms.go
index fbc99e82..829b134f 100644
--- a/internal/acl/perms.go
+++ b/internal/acl/perms.go
@@ -1,18 +1,18 @@
package acl
-type Perms []Perm
+import "unsafe"
-func (ps Perms) String() string {
- var s = []byte("---")
- for _, p := range ps {
- switch p {
- case Read:
- s[0] = 'r'
- case Write:
- s[1] = 'w'
- case Execute:
- s[2] = 'x'
- }
+// String returns a text representation of perms.
+func (perms Perm) String() string {
+ buf := []byte{'-', '-', '-'}
+ if perms&Read != 0 {
+ buf[0] = 'r'
}
- return string(s)
+ if perms&Write != 0 {
+ buf[1] = 'w'
+ }
+ if perms&Execute != 0 {
+ buf[2] = 'x'
+ }
+ return unsafe.String(unsafe.SliceData(buf), len(buf))
}
diff --git a/internal/acl/perms_test.go b/internal/acl/perms_test.go
index 3920046d..9fa24dc8 100644
--- a/internal/acl/perms_test.go
+++ b/internal/acl/perms_test.go
@@ -7,18 +7,20 @@ import (
)
func TestPerms(t *testing.T) {
+ t.Parallel()
+
testCases := []struct {
name string
- perms acl.Perms
+ perms acl.Perm
}{
- {"---", acl.Perms{}},
- {"r--", acl.Perms{acl.Read}},
- {"-w-", acl.Perms{acl.Write}},
- {"--x", acl.Perms{acl.Execute}},
- {"rw-", acl.Perms{acl.Read, acl.Read, acl.Write}},
- {"r-x", acl.Perms{acl.Read, acl.Execute, acl.Execute}},
- {"-wx", acl.Perms{acl.Write, acl.Write, acl.Execute, acl.Execute}},
- {"rwx", acl.Perms{acl.Read, acl.Write, acl.Execute}},
+ {"---", 0},
+ {"r--", acl.Read},
+ {"-w-", acl.Write},
+ {"--x", acl.Execute},
+ {"rw-", acl.Read | acl.Write},
+ {"r-x", acl.Read | acl.Execute},
+ {"-wx", acl.Write | acl.Execute},
+ {"rwx", acl.Read | acl.Write | acl.Execute},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
diff --git a/internal/lockedfile/lockedfile.go b/internal/lockedfile/lockedfile.go
index f48124ff..c7baff60 100644
--- a/internal/lockedfile/lockedfile.go
+++ b/internal/lockedfile/lockedfile.go
@@ -34,15 +34,12 @@ type osFile struct {
*os.File
}
-// OpenFile is like os.OpenFile, but returns a locked file.
-// If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked;
-// otherwise, it is read-locked.
-func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) {
+func openFile(name string, flag int, perm fs.FileMode, w bool) (*File, error) {
var (
f = new(File)
err error
)
- f.osFile.File, err = openFile(name, flag, perm)
+ f.osFile.File, err = osOpenFile(name, flag, perm, w)
if err != nil {
return nil, err
}
@@ -59,11 +56,23 @@ func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) {
return f, nil
}
+// OpenFile is like os.OpenFile, but returns a locked file.
+// If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked;
+// otherwise, it is read-locked.
+func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) {
+ return openFile(name, flag, perm, false)
+}
+
// Open is like os.Open, but returns a read-locked file.
func Open(name string) (*File, error) {
return OpenFile(name, os.O_RDONLY, 0)
}
+// OpenDir is like Open, but returns a write-locked directory.
+func OpenDir(name string) (*File, error) {
+ return openFile(name, os.O_RDONLY, 0, true)
+}
+
// Create is like os.Create, but returns a write-locked file.
func Create(name string) (*File, error) {
return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
diff --git a/internal/lockedfile/lockedfile_filelock.go b/internal/lockedfile/lockedfile_filelock.go
index 9fc16a9b..1f5ca310 100644
--- a/internal/lockedfile/lockedfile_filelock.go
+++ b/internal/lockedfile/lockedfile_filelock.go
@@ -13,7 +13,7 @@ import (
"hakurei.app/internal/lockedfile/internal/filelock"
)
-func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
+func osOpenFile(name string, flag int, perm fs.FileMode, w bool) (*os.File, error) {
// On BSD systems, we could add the O_SHLOCK or O_EXLOCK flag to the OpenFile
// call instead of locking separately, but we have to support separate locking
// calls for Linux and Windows anyway, so it's simpler to use that approach
@@ -24,6 +24,10 @@ func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
return nil, err
}
+ if w {
+ flag |= os.O_WRONLY
+ }
+
switch flag & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) {
case os.O_WRONLY, os.O_RDWR:
err = filelock.Lock(f)
diff --git a/internal/lockedfile/lockedfile_plan9.go b/internal/lockedfile/lockedfile_plan9.go
index a2ce794b..45e2b87c 100644
--- a/internal/lockedfile/lockedfile_plan9.go
+++ b/internal/lockedfile/lockedfile_plan9.go
@@ -42,7 +42,7 @@ func isLocked(err error) bool {
return false
}
-func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
+func osOpenFile(name string, flag int, perm fs.FileMode, _ bool) (*os.File, error) {
// Plan 9 uses a mode bit instead of explicit lock/unlock syscalls.
//
// Per http://man.cat-v.org/plan_9/5/stat: “Exclusive use files may be open
diff --git a/internal/outcome/dispatcher_test.go b/internal/outcome/dispatcher_test.go
index 00771e56..3f4732b5 100644
--- a/internal/outcome/dispatcher_test.go
+++ b/internal/outcome/dispatcher_test.go
@@ -209,7 +209,7 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
if err := s.populateLocal(k, k); err != nil {
t.Fatalf("populateLocal: error = %v", err)
}
- stateSys := s.newSys(config, system.New(panicMsgContext{}, k, checkExpectUid))
+ stateSys := s.newSys(config, system.New(panicMsgContext{}, k, checkExpectUid, nil))
if tc.pStateSys != nil {
tc.pStateSys(stateSys)
}
@@ -279,7 +279,7 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
}
}
-func newI() *system.I { return system.New(panicMsgContext{}, panicMsgContext{}, checkExpectUid) }
+func newI() *system.I { return system.New(panicMsgContext{}, panicMsgContext{}, checkExpectUid, nil) }
// simpleTestCase is a simple freeform test case utilising kstub.
type simpleTestCase struct {
diff --git a/internal/outcome/finalise.go b/internal/outcome/finalise.go
index 4049b8d3..7e5faab6 100644
--- a/internal/outcome/finalise.go
+++ b/internal/outcome/finalise.go
@@ -74,7 +74,7 @@ func (k *outcome) finalise(
return err
}
- sys := system.New(k.ctx, msg, s.uid.unwrap())
+ sys := system.New(k.ctx, msg, s.uid.unwrap(), s.sc.SharePath.Append("lock"))
if err := s.newSys(config, sys).toSystem(); err != nil {
return err
}
diff --git a/internal/outcome/outcome.go b/internal/outcome/outcome.go
index 0fe0016e..19ca9c7b 100644
--- a/internal/outcome/outcome.go
+++ b/internal/outcome/outcome.go
@@ -6,6 +6,7 @@ import (
"errors"
"maps"
"strconv"
+ "time"
"hakurei.app/check"
"hakurei.app/container"
@@ -125,6 +126,14 @@ func newOutcomeState(k syscallDispatcher, msg message.Msg, id *hst.ID, config *h
s.Shim.WaitDelay = s.Container.WaitDelay
}
+ if s.Container.AdoptWaitDelay > 5*time.Second {
+ s.Shim.AdoptWaitDelay = 5 * time.Second
+ } else if s.Container.AdoptWaitDelay < 0 {
+ s.Shim.AdoptWaitDelay = -1
+ } else {
+ s.Shim.AdoptWaitDelay = s.Container.AdoptWaitDelay
+ }
+
if s.Container.Flags&hst.FMapRealUID != 0 {
s.Mapuid, s.Mapgid = k.getuid(), k.getgid()
} else {
@@ -240,8 +249,8 @@ func (state *outcomeStateSys) ensureRuntimeDir() {
state.useRuntimeDir = true
state.sys.
// ensure this dir in case XDG_RUNTIME_DIR is unset
- Ensure(state.sc.RuntimePath, 0700).UpdatePermType(system.User, state.sc.RuntimePath, acl.Execute).
- Ensure(state.sc.RunDirPath, 0700).UpdatePermType(system.User, state.sc.RunDirPath, acl.Execute)
+ Ensure(state.sc.RuntimePath, 0700, true).UpdatePermType(system.User, state.sc.RuntimePath, acl.Execute).
+ Ensure(state.sc.RunDirPath, 0700, true).UpdatePermType(system.User, state.sc.RunDirPath, acl.Execute)
}
// instance returns the pathname to a process-specific directory within TMPDIR.
@@ -252,7 +261,7 @@ func (state *outcomeStateSys) instance() *check.Absolute {
return state.sharePath
}
state.sharePath = state.instancePath()
- state.sys.Ephemeral(system.Process, state.sharePath, 0711)
+ state.sys.Ephemeral(system.Process, state.sharePath, 0711, false)
return state.sharePath
}
@@ -265,7 +274,7 @@ func (state *outcomeStateSys) runtime() *check.Absolute {
}
state.ensureRuntimeDir()
state.runtimeSharePath = state.runtimePath()
- state.sys.Ephemeral(system.Process, state.runtimeSharePath, 0700)
+ state.sys.Ephemeral(system.Process, state.runtimeSharePath, 0700, true)
state.sys.UpdatePerm(state.runtimeSharePath, acl.Execute)
return state.runtimeSharePath
}
diff --git a/internal/outcome/run_test.go b/internal/outcome/run_test.go
index a1d83ec9..d9a41f53 100644
--- a/internal/outcome/run_test.go
+++ b/internal/outcome/run_test.go
@@ -40,24 +40,22 @@ func TestOutcomeRun(t *testing.T) {
wantSys *system.I
wantParams *container.Params
}{
- {"template", new(stubNixOS), hst.Template(), checkExpectInstanceId, system.New(panicMsgContext{}, message.New(nil), 10009).
+ {"template", new(stubNixOS), hst.Template(), checkExpectInstanceId, system.New(panicMsgContext{}, message.New(nil), 10009, nil).
// spParamsOp
- Ensure(m("/tmp/hakurei.0"), 0711).
+ Ensure(m("/tmp/hakurei.0"), 0711, false).
// spRuntimeOp
- Ensure(m("/tmp/hakurei.0/runtime"), 0700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/runtime/9"), 0700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute).
+ Ensure(m("/tmp/hakurei.0/runtime"), 0711, false).
+ Ensure(m("/tmp/hakurei.0/runtime/9"), 0700, true).
+ UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read|acl.Write|acl.Execute).
// spTmpdirOp
- Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
+ Ensure(m("/tmp/hakurei.0/tmpdir"), 0711, false).
+ Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700, true).
+ UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read|acl.Write|acl.Execute).
// instance
- Ephemeral(system.Process, m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0711).
+ Ephemeral(system.Process, m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0711, false).
// spWaylandOp
Wayland(
@@ -84,13 +82,13 @@ func TestOutcomeRun(t *testing.T) {
"unix:path=/var/run/dbus/system_bus_socket",
"/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket",
},
- ).UpdatePerm(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), acl.Read, acl.Write).
- UpdatePerm(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), acl.Read, acl.Write).
+ ).UpdatePerm(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), acl.Read|acl.Write).
+ UpdatePerm(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), acl.Read|acl.Write).
// spFilesystemOp
- Ensure(m("/var/lib/hakurei/u0"), 0700).
+ Ensure(m("/var/lib/hakurei/u0"), 0700, true).
UpdatePermType(system.User, m("/var/lib/hakurei/u0"), acl.Execute).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"), acl.Read, acl.Write, acl.Execute), &container.Params{
+ UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"), acl.Read|acl.Write|acl.Execute), &container.Params{
Dir: m("/data/data/org.chromium.Chromium"),
Env: []string{
@@ -200,16 +198,14 @@ func TestOutcomeRun(t *testing.T) {
0x96, 0xd7, 0xbc, 0x15,
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
- }, system.New(t.Context(), msg, 10000).
- Ensure(m("/tmp/hakurei.0"), 0711).
- Ensure(m("/tmp/hakurei.0/runtime"), 0700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/runtime/0"), 0700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/0"), acl.Read, acl.Write, acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir/0"), 01700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/0"), acl.Read, acl.Write, acl.Execute), &container.Params{
+ }, system.New(t.Context(), msg, 10000, nil).
+ Ensure(m("/tmp/hakurei.0"), 0711, false).
+ Ensure(m("/tmp/hakurei.0/runtime"), 0711, false).
+ Ensure(m("/tmp/hakurei.0/runtime/0"), 0700, true).
+ UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/0"), acl.Read|acl.Write|acl.Execute).
+ Ensure(m("/tmp/hakurei.0/tmpdir"), 0711, false).
+ Ensure(m("/tmp/hakurei.0/tmpdir/0"), 01700, true).
+ UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/0"), acl.Read|acl.Write|acl.Execute), &container.Params{
Dir: m("/home/chronos"),
Path: m("/run/current-system/sw/bin/zsh"),
@@ -326,13 +322,13 @@ func TestOutcomeRun(t *testing.T) {
0xb1, 0x75, 0x91, 0x17,
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
- }, system.New(t.Context(), msg, 10009).
- Ensure(m("/tmp/hakurei.0"), 0711).
- Ensure(m("/tmp/hakurei.0/runtime"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/runtime/9"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
- Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711).
+ }, system.New(t.Context(), msg, 10009, nil).
+ Ensure(m("/tmp/hakurei.0"), 0711, false).
+ Ensure(m("/tmp/hakurei.0/runtime"), 0711, false).
+ Ensure(m("/tmp/hakurei.0/runtime/9"), 0700, true).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read|acl.Write|acl.Execute).
+ Ensure(m("/tmp/hakurei.0/tmpdir"), 0711, false).
+ Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700, true).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read|acl.Write|acl.Execute).
+ Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711, false).
Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
PipeWire(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
MustProxyDBus(&hst.BusConfig{
@@ -371,8 +367,8 @@ func TestOutcomeRun(t *testing.T) {
"unix:path=/var/run/dbus/system_bus_socket",
"/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket",
}).
- UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), acl.Read, acl.Write).
- UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), acl.Read, acl.Write), &container.Params{
+ UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), acl.Read|acl.Write).
+ UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), acl.Read|acl.Write), &container.Params{
Dir: m("/home/chronos"),
Path: m("/run/current-system/sw/bin/zsh"),
@@ -477,16 +473,16 @@ func TestOutcomeRun(t *testing.T) {
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
- }, system.New(t.Context(), msg, 10001).
- Ensure(m("/tmp/hakurei.0"), 0711).
- Ensure(m("/tmp/hakurei.0/runtime"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/runtime/1"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/1"), acl.Read, acl.Write, acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir/1"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/1"), acl.Read, acl.Write, acl.Execute).
- Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
- Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
- UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute).
- Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711).
+ }, system.New(t.Context(), msg, 10001, nil).
+ Ensure(m("/tmp/hakurei.0"), 0711, false).
+ Ensure(m("/tmp/hakurei.0/runtime"), 0711, false).
+ Ensure(m("/tmp/hakurei.0/runtime/1"), 0700, true).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/1"), acl.Read|acl.Write|acl.Execute).
+ Ensure(m("/tmp/hakurei.0/tmpdir"), 0711, false).
+ Ensure(m("/tmp/hakurei.0/tmpdir/1"), 01700, true).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/1"), acl.Read|acl.Write|acl.Execute).
+ Ensure(m("/run/user/1971"), 0700, true).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
+ Ensure(m("/run/user/1971/hakurei"), 0700, true).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
+ UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read|acl.Write|acl.Execute).
+ Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711, false).
PipeWire(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire"), "org.chromium.Chromium", "8e2c76b066dabe574cf073bdb46eb5c1").
MustProxyDBus(&hst.BusConfig{
Talk: []string{
@@ -515,8 +511,8 @@ func TestOutcomeRun(t *testing.T) {
"unix:path=/var/run/dbus/system_bus_socket",
"/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket",
}).
- UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), acl.Read, acl.Write).
- UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), acl.Read, acl.Write), &container.Params{
+ UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), acl.Read|acl.Write).
+ UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), acl.Read|acl.Write), &container.Params{
Uid: 1971,
Gid: 100,
@@ -581,7 +577,7 @@ func TestOutcomeRun(t *testing.T) {
t.Fatalf("populateLocal: error = %#v", err)
}
- gotSys = system.New(t.Context(), msg, sPriv.uid.unwrap())
+ gotSys = system.New(t.Context(), msg, sPriv.uid.unwrap(), nil)
if err := sPriv.newSys(tc.config, gotSys).toSystem(); err != nil {
t.Fatalf("toSystem: error = %#v", err)
}
diff --git a/internal/outcome/shim-signal.c b/internal/outcome/shim-signal.c
index f02c0df1..6543d4b8 100644
--- a/internal/outcome/shim-signal.c
+++ b/internal/outcome/shim-signal.c
@@ -9,54 +9,57 @@ static pid_t hakurei_shim_param_ppid = -1;
static int hakurei_shim_fd = -1;
/* see shim.go for handling of the message */
-static inline ssize_t hakurei_shim_write(hakurei_shim_msg msg) {
- int savedErrno = errno;
- unsigned char buf = (unsigned char)msg;
- ssize_t ret = write(hakurei_shim_fd, &buf, 1);
- if (ret == -1 && errno != EAGAIN)
- exit(EXIT_FAILURE);
- errno = savedErrno;
- return ret;
+static inline ssize_t hakurei_shim_write(hakurei_shim_msg msg)
+{
+ int savedErrno = errno;
+ unsigned char buf = (unsigned char)msg;
+ ssize_t ret = write(hakurei_shim_fd, &buf, 1);
+ if (ret == -1 && errno != EAGAIN)
+ exit(EXIT_FAILURE);
+ errno = savedErrno;
+ return ret;
}
-static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
- if (sig != SIGCONT || si == NULL) {
- hakurei_shim_write(HAKUREI_SHIM_INVALID);
- return;
- }
+static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext)
+{
+ if (sig != SIGCONT || si == NULL) {
+ hakurei_shim_write(HAKUREI_SHIM_INVALID);
+ return;
+ }
- if (si->si_pid == hakurei_shim_param_ppid) {
- hakurei_shim_write(HAKUREI_SHIM_EXIT_REQUESTED);
- return;
- }
+ if (si->si_pid == hakurei_shim_param_ppid) {
+ hakurei_shim_write(HAKUREI_SHIM_EXIT_REQUESTED);
+ return;
+ }
- hakurei_shim_write(HAKUREI_SHIM_BAD_PID);
+ hakurei_shim_write(HAKUREI_SHIM_BAD_PID);
- if (getppid() != hakurei_shim_param_ppid)
- hakurei_shim_write(HAKUREI_SHIM_ORPHAN);
+ if (getppid() != hakurei_shim_param_ppid)
+ hakurei_shim_write(HAKUREI_SHIM_ORPHAN);
}
-void hakurei_shim_setup_cont_signal(pid_t ppid, int fd) {
- if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1)
- *(volatile int *)NULL = 0; /* unreachable */
+void hakurei_shim_setup_cont_signal(pid_t ppid, int fd)
+{
+ if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1)
+ *(volatile int *)NULL = 0; /* unreachable */
- struct sigaction new_action = {0}, old_action = {0};
- if (sigaction(SIGCONT, NULL, &old_action) != 0)
- return;
- if (old_action.sa_handler != SIG_DFL) {
- errno = ENOTRECOVERABLE;
- return;
- }
+ struct sigaction new_action = { 0 }, old_action = { 0 };
+ if (sigaction(SIGCONT, NULL, &old_action) != 0)
+ return;
+ if (old_action.sa_handler != SIG_DFL) {
+ errno = ENOTRECOVERABLE;
+ return;
+ }
- new_action.sa_sigaction = hakurei_shim_sigaction;
- if (sigemptyset(&new_action.sa_mask) != 0)
- return;
- new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
+ new_action.sa_sigaction = hakurei_shim_sigaction;
+ if (sigemptyset(&new_action.sa_mask) != 0)
+ return;
+ new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
- if (sigaction(SIGCONT, &new_action, NULL) != 0)
- return;
+ if (sigaction(SIGCONT, &new_action, NULL) != 0)
+ return;
- errno = 0;
- hakurei_shim_param_ppid = ppid;
- hakurei_shim_fd = fd;
+ errno = 0;
+ hakurei_shim_param_ppid = ppid;
+ hakurei_shim_fd = fd;
}
diff --git a/internal/outcome/shim-signal.h b/internal/outcome/shim-signal.h
index f396a915..db43b561 100644
--- a/internal/outcome/shim-signal.h
+++ b/internal/outcome/shim-signal.h
@@ -2,10 +2,10 @@
/* see shim.go for documentation */
typedef enum {
- HAKUREI_SHIM_EXIT_REQUESTED,
- HAKUREI_SHIM_ORPHAN,
- HAKUREI_SHIM_INVALID,
- HAKUREI_SHIM_BAD_PID,
+ HAKUREI_SHIM_EXIT_REQUESTED,
+ HAKUREI_SHIM_ORPHAN,
+ HAKUREI_SHIM_INVALID,
+ HAKUREI_SHIM_BAD_PID,
} hakurei_shim_msg;
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd);
diff --git a/internal/outcome/shim.go b/internal/outcome/shim.go
index 6e039b65..2e7b0910 100644
--- a/internal/outcome/shim.go
+++ b/internal/outcome/shim.go
@@ -72,6 +72,12 @@ type shimParams struct {
// Limits are enforced on the priv side.
WaitDelay time.Duration
+ // Duration to wait after the initial process terminates before all
+ // lingering processes are killed.
+ //
+ // Limits are enforced on the priv side.
+ AdoptWaitDelay time.Duration
+
// Verbosity pass through from [message.Msg].
Verbose bool
@@ -286,6 +292,7 @@ func shimEntrypoint(k syscallDispatcher) {
// bounds and default enforced in finalise.go
z.WaitDelay = state.Shim.WaitDelay
+ z.AdoptWaitDelay = state.Shim.AdoptWaitDelay
if stateParams.pipewirePulsePath != nil {
zpw := container.NewCommand(ctx, msg, stateParams.pipewirePulsePath, pipewirePulseName)
diff --git a/internal/outcome/spcontainer.go b/internal/outcome/spcontainer.go
index bfa5dbdc..4ba3bb2d 100644
--- a/internal/outcome/spcontainer.go
+++ b/internal/outcome/spcontainer.go
@@ -39,7 +39,7 @@ type spParamsOp struct {
func (s *spParamsOp) toSystem(state *outcomeStateSys) error {
s.Term, s.TermSet = state.k.lookupEnv("TERM")
- state.sys.Ensure(state.sc.SharePath, 0711)
+ state.sys.Ensure(state.sc.SharePath, 0711, false)
return nil
}
@@ -351,20 +351,20 @@ func flattenExtraPerms(sys *system.I, extraPerms []hst.ExtraPermConfig) {
}
if p.Ensure {
- sys.Ensure(p.Path, 0700)
+ sys.Ensure(p.Path, 0700, true)
}
- perms := make(acl.Perms, 0, 3)
+ var perms acl.Perm
if p.Read {
- perms = append(perms, acl.Read)
+ perms |= acl.Read
}
if p.Write {
- perms = append(perms, acl.Write)
+ perms |= acl.Write
}
if p.Execute {
- perms = append(perms, acl.Execute)
+ perms |= acl.Execute
}
- sys.UpdatePermType(system.User, p.Path, perms...)
+ sys.UpdatePermType(system.User, p.Path, perms)
}
}
diff --git a/internal/outcome/spcontainer_test.go b/internal/outcome/spcontainer_test.go
index 7dd47735..54c276f9 100644
--- a/internal/outcome/spcontainer_test.go
+++ b/internal/outcome/spcontainer_test.go
@@ -36,7 +36,7 @@ func TestSpParamsOp(t *testing.T) {
}, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
}, newI().
- Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{
+ Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711, false), nil, nil, nil, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, nil, nil, &hst.AppError{
Step: "finalise",
@@ -57,7 +57,7 @@ func TestSpParamsOp(t *testing.T) {
}, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
}, newI().
- Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{
+ Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711, false), nil, nil, nil, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Hostname: config.Container.Hostname,
@@ -94,7 +94,7 @@ func TestSpParamsOp(t *testing.T) {
}, hst.Template, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
}, newI().
- Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{
+ Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711, false), nil, nil, nil, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Hostname: config.Container.Hostname,
@@ -309,11 +309,11 @@ func TestSpFilesystemOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil),
}, newI().
- Ensure(m("/var/lib/hakurei/u0"), 0700).
+ Ensure(m("/var/lib/hakurei/u0"), 0700, true).
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
acl.Execute).
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
- acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
+ acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
state.filesystem = append(configSmall.Container.Filesystem, hst.FilesystemConfigJSON{})
})), []stub.Call{
// this op configures the container state and does not make calls during toContainer
@@ -342,11 +342,11 @@ func TestSpFilesystemOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil),
}, newI().
- Ensure(m("/var/lib/hakurei/u0"), 0700).
+ Ensure(m("/var/lib/hakurei/u0"), 0700, true).
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
acl.Execute).
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
- acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
+ acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
state.filesystem = configSmall.Container.Filesystem
})), []stub.Call{
// this op configures the container state and does not make calls during toContainer
@@ -397,11 +397,11 @@ func TestSpFilesystemOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/usr"}, nePrefix+"/var/lib/hakurei/base/org.debian/usr", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/var"}, nePrefix+"/var/lib/hakurei/base/org.debian/var", nil),
}, newI().
- Ensure(m("/var/lib/hakurei/u0"), 0700).
+ Ensure(m("/var/lib/hakurei/u0"), 0700, true).
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
acl.Execute).
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
- acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
+ acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
state.filesystem = config.Container.Filesystem[1:]
})), []stub.Call{
// this op configures the container state and does not make calls during toContainer
@@ -441,18 +441,18 @@ func TestFlattenExtraPerms(t *testing.T) {
want *system.I
}{
{"path nil check", append(hst.Template().ExtraPerms, hst.ExtraPermConfig{}), newI().
- Ensure(m("/var/lib/hakurei/u0"), 0700).
+ Ensure(m("/var/lib/hakurei/u0"), 0700, true).
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
acl.Execute).
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
- acl.Read, acl.Write, acl.Execute)},
+ acl.Read|acl.Write|acl.Execute)},
{"template", hst.Template().ExtraPerms, newI().
- Ensure(m("/var/lib/hakurei/u0"), 0700).
+ Ensure(m("/var/lib/hakurei/u0"), 0700, true).
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
acl.Execute).
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
- acl.Read, acl.Write, acl.Execute)},
+ acl.Read|acl.Write|acl.Execute)},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
diff --git a/internal/outcome/spdbus.go b/internal/outcome/spdbus.go
index a50f9cd2..c45dde6c 100644
--- a/internal/outcome/spdbus.go
+++ b/internal/outcome/spdbus.go
@@ -38,10 +38,10 @@ func (s *spDBusOp) toSystem(state *outcomeStateSys) error {
return err
}
- state.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
+ state.sys.UpdatePerm(sessionPath, acl.Read|acl.Write)
if state.systemBus != nil {
s.ProxySystem = true
- state.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
+ state.sys.UpdatePerm(systemPath, acl.Read|acl.Write)
}
return nil
}
diff --git a/internal/outcome/spdbus_test.go b/internal/outcome/spdbus_test.go
index 6f357193..3b9f07f7 100644
--- a/internal/outcome/spdbus_test.go
+++ b/internal/outcome/spdbus_test.go
@@ -80,8 +80,8 @@ func TestSpDBusOp(t *testing.T) {
"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
)}}, nil, nil),
}, func() *system.I {
- sys := system.New(panicMsgContext{}, message.New(nil), checkExpectUid)
- sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711)
+ sys := system.New(panicMsgContext{}, message.New(nil), checkExpectUid, nil)
+ sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711, false)
if err := sys.ProxyDBus(
dbus.NewConfig(config.ID, true, true), nil,
dbus.ProxyPair{"unix:path=/run/user/1000/bus", wantInstancePrefix + "/bus"},
@@ -89,7 +89,7 @@ func TestSpDBusOp(t *testing.T) {
); err != nil {
t.Fatalf("cannot prepare sys: %v", err)
}
- sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read, acl.Write)
+ sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read|acl.Write)
return sys
}(), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
// this op configures the container state and does not make calls during toContainer
@@ -162,8 +162,8 @@ func TestSpDBusOp(t *testing.T) {
"--talk=org.freedesktop.UPower",
)}}, nil, nil),
}, func() *system.I {
- sys := system.New(panicMsgContext{}, message.New(nil), checkExpectUid)
- sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711)
+ sys := system.New(panicMsgContext{}, message.New(nil), checkExpectUid, nil)
+ sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711, false)
if err := sys.ProxyDBus(
config.SessionBus, config.SystemBus,
dbus.ProxyPair{"unix:path=/run/user/1000/bus", wantInstancePrefix + "/bus"},
@@ -171,8 +171,8 @@ func TestSpDBusOp(t *testing.T) {
); err != nil {
t.Fatalf("cannot prepare sys: %v", err)
}
- sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read, acl.Write).
- UpdatePerm(m(wantInstancePrefix+"/system_bus_socket"), acl.Read, acl.Write)
+ sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read|acl.Write).
+ UpdatePerm(m(wantInstancePrefix+"/system_bus_socket"), acl.Read|acl.Write)
return sys
}(), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
// this op configures the container state and does not make calls during toContainer
diff --git a/internal/outcome/sppipewire_test.go b/internal/outcome/sppipewire_test.go
index 1e3dab6a..804404dc 100644
--- a/internal/outcome/sppipewire_test.go
+++ b/internal/outcome/sppipewire_test.go
@@ -31,7 +31,7 @@ func TestSpPipeWireOp(t *testing.T) {
return c
}, nil, []stub.Call{}, newI().
// state.instance
- Ephemeral(system.Process, m(wantInstancePrefix), 0711).
+ Ephemeral(system.Process, m(wantInstancePrefix), 0711, false).
// toSystem
PipeWire(
m(wantInstancePrefix+"/pipewire"),
diff --git a/internal/outcome/sppulse_test.go b/internal/outcome/sppulse_test.go
index a1ff0d03..a9995562 100644
--- a/internal/outcome/sppulse_test.go
+++ b/internal/outcome/sppulse_test.go
@@ -143,12 +143,12 @@ func TestSpPulseOp(t *testing.T) {
call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie)}, nil),
}, newI().
// state.ensureRuntimeDir
- Ensure(m(wantRuntimePath), 0700).
+ Ensure(m(wantRuntimePath), 0700, true).
UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
- Ensure(m(wantRunDirPath), 0700).
+ Ensure(m(wantRunDirPath), 0700, true).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
// state.runtime
- Ephemeral(system.Process, m(wantRuntimeSharePath), 0700).
+ Ephemeral(system.Process, m(wantRuntimeSharePath), 0700, true).
UpdatePerm(m(wantRuntimeSharePath), acl.Execute).
// toSystem
Link(m(wantRuntimePath+"/pulse/native"), m(wantRuntimeSharePath+"/pulse")), sysUsesRuntime(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
@@ -175,12 +175,12 @@ func TestSpPulseOp(t *testing.T) {
call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie[:len(sampleCookie)-0xe])}, nil),
}, newI().
// state.ensureRuntimeDir
- Ensure(m(wantRuntimePath), 0700).
+ Ensure(m(wantRuntimePath), 0700, true).
UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
- Ensure(m(wantRunDirPath), 0700).
+ Ensure(m(wantRunDirPath), 0700, true).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
// state.runtime
- Ephemeral(system.Process, m(wantRuntimeSharePath), 0700).
+ Ephemeral(system.Process, m(wantRuntimeSharePath), 0700, true).
UpdatePerm(m(wantRuntimeSharePath), acl.Execute).
// toSystem
Link(m(wantRuntimePath+"/pulse/native"), m(wantRuntimeSharePath+"/pulse")), sysUsesRuntime(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
@@ -208,12 +208,12 @@ func TestSpPulseOp(t *testing.T) {
call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie)}, nil),
}, newI().
// state.ensureRuntimeDir
- Ensure(m(wantRuntimePath), 0700).
+ Ensure(m(wantRuntimePath), 0700, true).
UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
- Ensure(m(wantRunDirPath), 0700).
+ Ensure(m(wantRunDirPath), 0700, true).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
// state.runtime
- Ephemeral(system.Process, m(wantRuntimeSharePath), 0700).
+ Ephemeral(system.Process, m(wantRuntimeSharePath), 0700, true).
UpdatePerm(m(wantRuntimeSharePath), acl.Execute).
// toSystem
Link(m(wantRuntimePath+"/pulse/native"), m(wantRuntimeSharePath+"/pulse")), sysUsesRuntime(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
@@ -238,12 +238,12 @@ func TestSpPulseOp(t *testing.T) {
call("verbose", stub.ExpectArgs{[]any{"cannot locate PulseAudio cookie (tried $PULSE_COOKIE, $XDG_CONFIG_HOME/pulse/cookie, $HOME/.pulse-cookie)"}}, nil, nil),
}, newI().
// state.ensureRuntimeDir
- Ensure(m(wantRuntimePath), 0700).
+ Ensure(m(wantRuntimePath), 0700, true).
UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
- Ensure(m(wantRunDirPath), 0700).
+ Ensure(m(wantRunDirPath), 0700, true).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
// state.runtime
- Ephemeral(system.Process, m(wantRuntimeSharePath), 0700).
+ Ephemeral(system.Process, m(wantRuntimeSharePath), 0700, true).
UpdatePerm(m(wantRuntimeSharePath), acl.Execute).
// toSystem
Link(m(wantRuntimePath+"/pulse/native"), m(wantRuntimeSharePath+"/pulse")), sysUsesRuntime(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
diff --git a/internal/outcome/spruntime.go b/internal/outcome/spruntime.go
index c08bfe81..b8121bfa 100644
--- a/internal/outcome/spruntime.go
+++ b/internal/outcome/spruntime.go
@@ -76,10 +76,9 @@ type spRuntimeOp struct {
func (s *spRuntimeOp) toSystem(state *outcomeStateSys) error {
if state.Container.Flags&hst.FShareRuntime != 0 {
runtimeDir, runtimeDirInst := s.commonPaths(state.outcomeState)
- state.sys.Ensure(runtimeDir, 0700)
- state.sys.UpdatePermType(system.User, runtimeDir, acl.Execute)
- state.sys.Ensure(runtimeDirInst, 0700)
- state.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute)
+ state.sys.Ensure(runtimeDir, 0711, false)
+ state.sys.Ensure(runtimeDirInst, 0700, true)
+ state.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read|acl.Write|acl.Execute)
}
if state.et&hst.EWayland != 0 {
diff --git a/internal/outcome/spruntime_test.go b/internal/outcome/spruntime_test.go
index 25f2684c..c549c857 100644
--- a/internal/outcome/spruntime_test.go
+++ b/internal/outcome/spruntime_test.go
@@ -33,10 +33,9 @@ func TestSpRuntimeOp(t *testing.T) {
}, nil, []stub.Call{
// this op configures the system state and does not make calls during toSystem
}, newI().
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
+ Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0711, false).
+ Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700, true).
+ UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
@@ -61,10 +60,9 @@ func TestSpRuntimeOp(t *testing.T) {
}, nil, []stub.Call{
// this op configures the system state and does not make calls during toSystem
}, newI().
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
+ Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0711, false).
+ Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700, true).
+ UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
@@ -89,10 +87,9 @@ func TestSpRuntimeOp(t *testing.T) {
}, nil, []stub.Call{
// this op configures the system state and does not make calls during toSystem
}, newI().
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
+ Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0711, false).
+ Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700, true).
+ UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
@@ -113,10 +110,9 @@ func TestSpRuntimeOp(t *testing.T) {
}, hst.Template, nil, []stub.Call{
// this op configures the system state and does not make calls during toSystem
}, newI().
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
+ Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0711, false).
+ Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700, true).
+ UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
diff --git a/internal/outcome/sptmpdir.go b/internal/outcome/sptmpdir.go
index daa6f0de..c0c8db7b 100644
--- a/internal/outcome/sptmpdir.go
+++ b/internal/outcome/sptmpdir.go
@@ -19,10 +19,9 @@ type spTmpdirOp struct{}
func (s spTmpdirOp) toSystem(state *outcomeStateSys) error {
if state.Container.Flags&hst.FShareTmpdir != 0 {
tmpdir, tmpdirInst := s.commonPaths(state.outcomeState)
- state.sys.Ensure(tmpdir, 0700)
- state.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
- state.sys.Ensure(tmpdirInst, 01700)
- state.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
+ state.sys.Ensure(tmpdir, 0711, false)
+ state.sys.Ensure(tmpdirInst, 01700, true)
+ state.sys.UpdatePermType(system.User, tmpdirInst, acl.Read|acl.Write|acl.Execute)
}
return nil
}
diff --git a/internal/outcome/sptmpdir_test.go b/internal/outcome/sptmpdir_test.go
index 620f51b0..74aceeee 100644
--- a/internal/outcome/sptmpdir_test.go
+++ b/internal/outcome/sptmpdir_test.go
@@ -21,10 +21,9 @@ func TestSpTmpdirOp(t *testing.T) {
}, hst.Template, nil, []stub.Call{
// this op configures the system state and does not make calls during toSystem
}, newI().
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/tmpdir"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/tmpdir"), acl.Execute).
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), 01700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
+ Ensure(m("/proc/nonexistent/tmp/hakurei.0/tmpdir"), 0711, false).
+ Ensure(m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), 01700, true).
+ UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
diff --git a/internal/outcome/spwayland.go b/internal/outcome/spwayland.go
index 3c60ff10..db79d1d2 100644
--- a/internal/outcome/spwayland.go
+++ b/internal/outcome/spwayland.go
@@ -49,7 +49,7 @@ func (s *spWaylandOp) toSystem(state *outcomeStateSys) error {
state.msg.Verbose("direct wayland access, PROCEED WITH CAUTION")
state.ensureRuntimeDir()
s.SocketPath = socketPath
- state.sys.UpdatePermType(hst.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
+ state.sys.UpdatePermType(hst.EWayland, socketPath, acl.Read|acl.Write|acl.Execute)
}
return nil
}
diff --git a/internal/outcome/spwayland_test.go b/internal/outcome/spwayland_test.go
index 7cc4ba84..eea23136 100644
--- a/internal/outcome/spwayland_test.go
+++ b/internal/outcome/spwayland_test.go
@@ -34,7 +34,7 @@ func TestSpWaylandOp(t *testing.T) {
call("lookupEnv", stub.ExpectArgs{"WAYLAND_DISPLAY"}, "wayland-1", nil),
}, newI().
// state.instance
- Ephemeral(system.Process, m(wantInstancePrefix), 0711).
+ Ephemeral(system.Process, m(wantInstancePrefix), 0711, false).
// toSystem
Wayland(
m(wantInstancePrefix+"/wayland"),
@@ -64,12 +64,12 @@ func TestSpWaylandOp(t *testing.T) {
call("verbose", stub.ExpectArgs{[]any{"direct wayland access, PROCEED WITH CAUTION"}}, nil, nil),
}, newI().
// state.ensureRuntimeDir
- Ensure(m(wantRuntimePath), 0700).
+ Ensure(m(wantRuntimePath), 0700, true).
UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
- Ensure(m(wantRunDirPath), 0700).
+ Ensure(m(wantRunDirPath), 0700, true).
UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
// toSystem
- UpdatePermType(hst.EWayland, m("/proc/nonexistent/wayland"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
+ UpdatePermType(hst.EWayland, m("/proc/nonexistent/wayland"), acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
@@ -85,7 +85,7 @@ func TestSpWaylandOp(t *testing.T) {
call("verbose", stub.ExpectArgs{[]any{"WAYLAND_DISPLAY is not set, assuming wayland-0"}}, nil, nil),
}, newI().
// state.instance
- Ephemeral(system.Process, m(wantInstancePrefix), 0711).
+ Ephemeral(system.Process, m(wantInstancePrefix), 0711, false).
// toSystem
Wayland(
m(wantInstancePrefix+"/wayland"),
diff --git a/internal/outcome/spx11.go b/internal/outcome/spx11.go
index c71ff530..ebd5c77e 100644
--- a/internal/outcome/spx11.go
+++ b/internal/outcome/spx11.go
@@ -56,7 +56,7 @@ func (s *spX11Op) toSystem(state *outcomeStateSys) error {
), Err: err}
}
} else {
- state.sys.UpdatePermType(hst.EX11, socketPath, acl.Read, acl.Write, acl.Execute)
+ state.sys.UpdatePermType(hst.EX11, socketPath, acl.Read|acl.Write|acl.Execute)
if state.Container.Flags&hst.FHostAbstract == 0 {
s.Display = "unix:" + socketPath.String()
}
diff --git a/internal/outcome/spx11_test.go b/internal/outcome/spx11_test.go
index 0ede2d65..d183f798 100644
--- a/internal/outcome/spx11_test.go
+++ b/internal/outcome/spx11_test.go
@@ -83,7 +83,7 @@ func TestSpX11Op(t *testing.T) {
call("lookupEnv", stub.ExpectArgs{"DISPLAY"}, "unix:/tmp/.X11-unix/X0", nil),
call("stat", stub.ExpectArgs{"/tmp/.X11-unix/X0"}, (*stubFi)(nil), nil),
}, newI().
- UpdatePermType(hst.EX11, m("/tmp/.X11-unix/X0"), acl.Read, acl.Write, acl.Execute).
+ UpdatePermType(hst.EX11, m("/tmp/.X11-unix/X0"), acl.Read|acl.Write|acl.Execute).
ChangeHosts("#10009"), nil, nil, insertsOps(nil), []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
@@ -106,7 +106,7 @@ func TestSpX11Op(t *testing.T) {
call("lookupEnv", stub.ExpectArgs{"DISPLAY"}, ":0", nil),
call("stat", stub.ExpectArgs{"/tmp/.X11-unix/X0"}, (*stubFi)(nil), nil),
}, newI().
- UpdatePermType(hst.EX11, m("/tmp/.X11-unix/X0"), acl.Read, acl.Write, acl.Execute).
+ UpdatePermType(hst.EX11, m("/tmp/.X11-unix/X0"), acl.Read|acl.Write|acl.Execute).
ChangeHosts("#10009"), nil, nil, insertsOps(nil), []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
diff --git a/internal/rosa/llvm.go b/internal/rosa/llvm.go
index 6d1dcf0b..08625fcf 100644
--- a/internal/rosa/llvm.go
+++ b/internal/rosa/llvm.go
@@ -145,6 +145,8 @@ func init() {
"global-value-regex",
"global-hex-value-regex",
"check-globals",
+ // clang: expected string not found in input
+ "Interpreter/failed-input-keeps-redecls.cpp",
// cxx: fails on musl
"close.dont-get-rid-of-buffer",
"re/re.traits",
diff --git a/internal/rosa/llvm_test.go b/internal/rosa/llvm_test.go
index 3152107c..3fa8fc9d 100644
--- a/internal/rosa/llvm_test.go
+++ b/internal/rosa/llvm_test.go
@@ -8,7 +8,7 @@ import (
)
func TestLLVMInputs(t *testing.T) {
- const wantInputCount = 466
+ const wantInputCount = 472
_, llvm := rosa.MustLoad(rosa.H("llvm"))
var n int
diff --git a/internal/rosa/mirror.go b/internal/rosa/mirror.go
index 4af931c7..20dd3231 100644
--- a/internal/rosa/mirror.go
+++ b/internal/rosa/mirror.go
@@ -451,7 +451,6 @@ func writeF[B any](
return
}
m.msg.GetLogger().Println(err)
- w.WriteHeader(http.StatusInternalServerError)
}
// Register configures an [http.ServeMux] for servicing mirror requests.
diff --git a/internal/rosa/package/acl/package.az b/internal/rosa/package/acl/package.az
index 0e06f227..06c9abe2 100644
--- a/internal/rosa/package/acl/package.az
+++ b/internal/rosa/package/acl/package.az
@@ -5,7 +5,7 @@ package attr {
version# = "2.6.0";
source = remoteTar {
- url = "https://download.savannah.nongnu.org/releases/attr/"+
+ url = "https://download-mirror.savannah.nongnu.org/releases/attr/"+
"attr-"+version+".tar.gz";
checksum = "pp-NvD1cMIwZycNwZGW2ez-PbTEpHxrRnVH27csj9QdN4oEBkEbJOZX1IIvKnTWS";
compress = gzip;
@@ -28,7 +28,7 @@ package acl {
version# = "2.4.0";
source = remoteTar {
- url = "https://download.savannah.nongnu.org/releases/acl/"+
+ url = "https://download-mirror.savannah.nongnu.org/releases/acl/"+
"acl-"+version+".tar.gz";
checksum = "U2eaAsWrhhzEfhyprwHQQG55bFPZxtsHk1Usnd9Jb_g-H__pJod4H3RTrL4ipXDw";
compress = gzip;
diff --git a/internal/rosa/package/cairo.az b/internal/rosa/package/cairo.az
index 7ffbb304..abe0122c 100644
--- a/internal/rosa/package/cairo.az
+++ b/internal/rosa/package/cairo.az
@@ -3,12 +3,12 @@ package cairo {
website = "https://www.cairographics.org";
anitya = 247;
- version# = "1.18.4";
+ version# = "1.18.6";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "cairo/cairo";
ref = version;
- checksum = "_Ztw0ttmYLIiElqS27x2Iu4EneXOMNHp2or6bJA6suLeF3NRvORzUMPPQy9Wr5gT";
+ checksum = "xkXsQ_D8ZH5YF6tldJLRazKjP04xXu3RgDiTEbCwKP6s5ot_5VDfljj2gwnFOoc6";
};
exec = meson {
diff --git a/internal/rosa/package/cmake/package.az b/internal/rosa/package/cmake/package.az
index 1e6ea8b3..bd67abb8 100644
--- a/internal/rosa/package/cmake/package.az
+++ b/internal/rosa/package/cmake/package.az
@@ -3,12 +3,12 @@ package cmake {
website = "https://cmake.org";
anitya = 306;
- version# = "4.4.3";
+ version# = "4.4.4";
source = remoteGitHubRelease {
suffix = "Kitware/CMake";
tag = "v"+version;
name = "cmake-"+version+".tar.gz";
- checksum = "69yrNbgUPZXY0phavZEMPRgBu_6858LHzLoyHk1aeTuXf8RW5Ec37lr2OBaGZ5PY";
+ checksum = "I7vJROVzty0xtoWvDQzR5JpnEl-gO-1kXd-qIO2lWgj65Hgyd4kA08mM1wk5l8z3";
compress = gzip;
};
patches = [ "disable-broken-tests-musl.patch" ];
diff --git a/internal/rosa/package/cryptsetup.az b/internal/rosa/package/cryptsetup.az
index 0886d222..2c3b679c 100644
--- a/internal/rosa/package/cryptsetup.az
+++ b/internal/rosa/package/cryptsetup.az
@@ -3,12 +3,12 @@ package cryptsetup {
website = "https://gitlab.com/cryptsetup/cryptsetup";
anitya = 13709;
- version# = "2.8.7";
+ version# = "2.8.8";
source = remoteGitLab {
domain = "gitlab.com";
suffix = "cryptsetup/cryptsetup";
ref = "v"+version;
- checksum = "eDi_UaMtwhIzqpsdcA91aVb56zCYDrsDzeMWfWXAnPbuvn2bEgeH5dFs7mouHz8V";
+ checksum = "Q88h-7N-QFJcBe-MbtkFu0fSjrhRUF8WYCH4vhznbtFjniXLXRKMku-JoZAtHQMS";
};
bin = [ "bash" ];
diff --git a/internal/rosa/package/curl.az b/internal/rosa/package/curl.az
index a54deccc..0f10763d 100644
--- a/internal/rosa/package/curl.az
+++ b/internal/rosa/package/curl.az
@@ -3,10 +3,10 @@ package curl {
website = "https://curl.se";
anitya = 381;
- version# = "8.22.0";
+ version# = "8.23.0";
source = remoteTar {
url = "https://curl.se/download/curl-"+version+".tar.bz2";
- checksum = "zeIqlmHElDa3IPLZq86ICRxn1YoiXlNCdx30BuahcUToYqjSuyvu8AtRqbzKH_06";
+ checksum = "FBVGKlIt-kkyHMcu7SsNosovyg3jNiaQ4GrHEFzApJ_OviyYPaUcacOpsHYUGV40";
compress = bzip2;
};
diff --git a/internal/rosa/package/dbus.az b/internal/rosa/package/dbus.az
index 4d2dd6ec..402cc6fb 100644
--- a/internal/rosa/package/dbus.az
+++ b/internal/rosa/package/dbus.az
@@ -36,11 +36,11 @@ package xdg-dbus-proxy {
website = "https://github.com/flatpak/xdg-dbus-proxy";
anitya = 58434;
- version# = "0.1.8";
+ version# = "0.1.9";
source = remoteGitHub {
suffix = "flatpak/xdg-dbus-proxy";
tag = version;
- checksum = "ygdW0EEvZ-_PJfnFTNoobITmRzqJecTinBsJd_iJQrRvzidwqESCjeNkB55smZ2y";
+ checksum = "HfAi2M60v3wE_TbESYZ4eMB4G2-spBt0x0UrKaRp7FNVywDbg9d_L3_kr2QXzlfp";
};
exec = meson {
diff --git a/internal/rosa/package/firmware.az b/internal/rosa/package/firmware.az
index f04b39e9..e87a34c1 100644
--- a/internal/rosa/package/firmware.az
+++ b/internal/rosa/package/firmware.az
@@ -3,12 +3,12 @@ package firmware {
website = "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git";
anitya = 141464;
- version# = "20260810";
+ version# = "20260916";
source = remoteGitLab {
domain = "gitlab.com";
suffix = "kernel-firmware/linux-firmware";
ref = version;
- checksum = "XNUhz9QstaDKNegM3Bl0-TtZzpsvX7-E8WMnDCO7KBhBC6t_TxsdG05-ETdXynry";
+ checksum = "Yv3yaffN6-ARSFSnDm-0gHM3T7m3ZwMqBLIP8UA3Jpe23Jk8MyNLUE44wJMy_DH6";
};
// dedup creates temporary file
diff --git a/internal/rosa/package/freetype.az b/internal/rosa/package/freetype.az
index cfa5933a..6f240bf3 100644
--- a/internal/rosa/package/freetype.az
+++ b/internal/rosa/package/freetype.az
@@ -3,11 +3,11 @@ package freetype {
website = "http://www.freetype.org";
anitya = 854;
- version# = "2.14.3";
+ version# = "2.15.0";
source = remoteTar {
- url = "https://download.savannah.gnu.org/releases/freetype/"+
+ url = "https://download-mirror.savannah.gnu.org/releases/freetype/"+
"freetype-"+version+".tar.gz";
- checksum = "-WfLv8fVJNyCHpP_lriiDzOcVbBL9ajdQ3tl8AzIIUa9-8sVpU9irxOmSMgRHWYz";
+ checksum = "zjiWtj5ry7tnc5libuhSv3Ik3F8rAoK0j7abCmdTxulPdKYEjAsbLXAGkTq-IOJ4";
compress = gzip;
};
diff --git a/internal/rosa/package/fribidi.az b/internal/rosa/package/fribidi.az
index 0bd45693..28df4375 100644
--- a/internal/rosa/package/fribidi.az
+++ b/internal/rosa/package/fribidi.az
@@ -3,11 +3,11 @@ package fribidi {
website = "https://github.com/fribidi/fribidi";
anitya = 857;
- version# = "1.0.16";
+ version# = "1.0.17";
source = remoteGitHub {
suffix = "fribidi/fribidi";
tag = "v"+version;
- checksum = "Sn9hwdgViXW9_MOi8ovAcBgQy6s6o4cc0o260k76nqCRzxmxxD1-B37GVkJ2EoMy";
+ checksum = "yJWcJqYDmzdyFB-kc9J1JDPLiqWHTw2n2bheXvqzP66bk56HYWW9sz_w-TMUqiEV";
};
exec = meson {
diff --git a/internal/rosa/package/fuse/package.az b/internal/rosa/package/fuse/package.az
index 31000e07..3e43c3c6 100644
--- a/internal/rosa/package/fuse/package.az
+++ b/internal/rosa/package/fuse/package.az
@@ -3,12 +3,12 @@ package fuse {
website = "https://github.com/libfuse/libfuse";
anitya = 861;
- version# = "3.18.2";
+ version# = "3.18.3";
source = remoteGitHubRelease {
suffix = "libfuse/libfuse";
tag = "fuse-"+version;
name = "fuse-"+version+".tar.gz";
- checksum = "iL-7b7eUtmlVSf5cSq0dzow3UiqSjBmzV3cI_ENPs1tXcHdktkG45j1V12h-4jZe";
+ checksum = "zQuo1ZLCePjlgxbOO8BuiLPPHwjbU0IItSo3K9_BtcxIid027b7XQWfm07c1T5D4";
compress = gzip;
};
patches = [ "example-include.patch" ];
@@ -20,12 +20,8 @@ package fuse {
"useroot": "false";
"initscriptdir": "/system/etc";
};
-
- postCompile = "python3 -m pytest test/";
- // this project uses pytest
- test = false;
};
- tools = [ python-pytest ];
+ tools = [ bash ];
inputs = [ kernel-headers ];
}
diff --git a/internal/rosa/package/git.az b/internal/rosa/package/git.az
index 07abe88e..42d4665d 100644
--- a/internal/rosa/package/git.az
+++ b/internal/rosa/package/git.az
@@ -3,11 +3,11 @@ package git {
website = "https://www.git-scm.com";
anitya = 5350;
- version# = "2.55.0";
+ version# = "2.56.0";
source = remoteTar {
url = "https://www.kernel.org/pub/software/scm/git/"+
"git-"+version+".tar.gz";
- checksum = "lOer6jb8vZQk6Nd1rLZIgsYys8whLvjJq8XbmCdTATcPFAYuQcK1Rgj1Jj6a00W8";
+ checksum = "9HMTVgQnHmUH9v8ot_DXl5mI8tGMcbilKLUzgFXg1k8Z5YbzxkF6b-ngTL4ZgeYN";
compress = gzip;
};
@@ -63,6 +63,8 @@ disable_test t5515-fetch-merge-logic
disable_test t4124-apply-ws-rule
disable_test t3404-rebase-interactive
disable_test t7527-builtin-fsmonitor
+disable_test t5702-protocol-v2
+disable_test t3600-rm
`;
check = [
diff --git a/internal/rosa/package/glib/loopvar-cast.patch b/internal/rosa/package/glib/loopvar-cast.patch
new file mode 100644
index 00000000..fd4f8c45
--- /dev/null
+++ b/internal/rosa/package/glib/loopvar-cast.patch
@@ -0,0 +1,13 @@
+diff --git a/glib/tests/string.c b/glib/tests/string.c
+index 3d9229616..9553c9217 100644
+--- a/glib/tests/string.c
++++ b/glib/tests/string.c
+@@ -348,7 +348,7 @@ test_string_prepend_c (void)
+ string = g_string_new ("hi pete!");
+
+ for (i = 0; i < 10000; i++)
+- g_string_prepend_c (string, 'a'+(i%26));
++ g_string_prepend_c (string, 'a'+(gchar)(i%26));
+
+ g_assert((strlen("hi pete!") + 10000) == string->len);
+ g_assert((strlen("hi pete!") + 10000) == strlen(string->str));
diff --git a/internal/rosa/package/glib/package.az b/internal/rosa/package/glib/package.az
index 14ff0450..7f3040e0 100644
--- a/internal/rosa/package/glib/package.az
+++ b/internal/rosa/package/glib/package.az
@@ -3,12 +3,13 @@ package glib {
website = "https://developer.gnome.org/glib";
anitya = 10024;
- version# = "2.89.4";
+ version# = "2.90.1";
source = remoteGit {
url = "https://gitlab.gnome.org/GNOME/glib.git";
tag = version;
- checksum = "I_nTEVp4rpOQ4VCmJpesjvq3VbFJ3oLRAUVvsdJEfFiY4XXsvi0ITTqCEWe9Lr47";
+ checksum = "h3Chw60ZesAtUzKQfHKsMvsfkEAK0HaTSV8ZANJK7vkTtb9f7D1P763_vROClV3Z";
};
+ patches = [ "loopvar-cast.patch" ];
files = {
"/etc/machine-id": "stub-machine-id-fhs";
diff --git a/internal/rosa/package/glslang.az b/internal/rosa/package/glslang.az
index 3cbd4fa0..1aca89c6 100644
--- a/internal/rosa/package/glslang.az
+++ b/internal/rosa/package/glslang.az
@@ -3,11 +3,11 @@ package glslang {
website = "https://github.com/KhronosGroup/glslang";
anitya = 334917;
- version# = "1.4.357.0";
+ version# = "1.4.363.0";
source = remoteGitHub {
suffix = "KhronosGroup/glslang";
tag = "vulkan-sdk-"+version;
- checksum = "7Zq12xtSd5z_aEEeYCnsWR5WvcEGVlTsRaV6MqkmY1a3iNIrRbX_rrg-taI3wMe6";
+ checksum = "ATWNapAERPA42c5EKZNnYj37M54wkaOCx5L-QWlZe9EfKVomP-s4FHieiJYg4FQI";
};
// test suite writes to source
@@ -18,6 +18,8 @@ package glslang {
cache = {
"BUILD_SHARED_LIBS": "ON";
"ALLOW_EXTERNAL_SPIRV_TOOLS": "ON";
+
+ "CMAKE_EXE_LINKER_FLAGS": "-Wl,-z,stack-size=1048576";
};
};
diff --git a/internal/rosa/package/gnu.az b/internal/rosa/package/gnu.az
index 3eb48ab5..e117173e 100644
--- a/internal/rosa/package/gnu.az
+++ b/internal/rosa/package/gnu.az
@@ -66,10 +66,10 @@ package automake {
website = "https://www.gnu.org/software/automake";
anitya = 144;
- version# = "1.18.1";
+ version# = "1.19";
source = remoteTar {
url = "https://mirrors.kernel.org/gnu/automake/automake-"+version+".tar.gz";
- checksum = "FjvLG_GdQP7cThTZJLDMxYpRcKdpAVG-YDs1Fj1yaHlSdh_Kx6nRGN14E0r_BjcG";
+ checksum = "p-EeV6uh3lSbLtsnxI-l0HxC_QAnANoEdrQTI2l_VCRtYQpvmbwXIglo8S6DAI7G";
compress = gzip;
};
@@ -125,24 +125,6 @@ package libtool {
];
}
-package gzip {
- description = "a popular data compression program";
- website = "https://www.gnu.org/software/gzip";
- anitya = 1290;
-
- version# = "1.14";
- source = remoteTar {
- url = "https://mirrors.kernel.org/gnu/gzip/gzip-"+version+".tar.gz";
- checksum = "NWhjUavnNfTDFkZJyAUonL9aCOak8GVajWX2OMlzpFnuI0ErpBFyj88mz2xSjz0q";
- compress = gzip;
- };
-
- exec = make {
- // dependency loop
- check = nil;
- };
-}
-
package sed {
description = "a non-interactive command-line text editor";
website = "https://www.gnu.org/software/sed";
@@ -339,10 +321,10 @@ package coreutils {
website = "https://www.gnu.org/software/coreutils";
anitya = 343;
- version# = "9.11";
+ version# = "9.12";
source = remoteTar {
url = "https://mirrors.kernel.org/gnu/coreutils/coreutils-"+version+".tar.gz";
- checksum = "t8UMed5wpFEoC56aa42_yidfOAaRGzOfj7MRtQkkqgGbpXiskNA8bd-EmVSQkZie";
+ checksum = "MYYday_Pwp2YFSgVHeCQWJBhJCBhqkIkfML4_a_uHvM_FsKbYQ4vbXy60FprcUoo";
compress = gzip;
};
@@ -497,10 +479,10 @@ package parallel {
website = "https://www.gnu.org/software/parallel";
anitya = 5448;
- version# = "20260822";
+ version# = "20260922";
source = remoteTar {
url = "https://mirrors.kernel.org/gnu/parallel/parallel-"+version+".tar.bz2";
- checksum = "zf-5ZEpAZ073coMSyOY7NRmXwCRPWvpjbjOMOvirU9fHlU0WWovwss-bAMrh0q_G";
+ checksum = "4M2gBy-esCSvR9ub6SUnoVHz7p3ZDhdeJXm7yv2C4iJQdXyVNkYqgusPFsvH8MCp";
compress = bzip2;
};
@@ -520,10 +502,10 @@ package libunistring {
website = "https://www.gnu.org/software/libunistring";
anitya = 1747;
- version# = "1.4.2";
+ version# = "1.5";
source = remoteTar {
url = "https://mirrors.kernel.org/gnu/libunistring/libunistring-"+version+".tar.gz";
- checksum = "iW9BbfLoVlXjWoLTZ4AekQSu4cFBnLcZ4W8OHWbv0AhJNgD3j65_zqaLMzFKylg2";
+ checksum = "6r3gdRT3T5j1YPjoRBnRMY6uXVJuRdzu8eir1qeHcErEJegjlbMux2IDP6XPJyDX";
compress = gzip;
};
diff --git a/internal/rosa/package/go.az b/internal/rosa/package/go.az
index 89d3732d..08ad847e 100644
--- a/internal/rosa/package/go.az
+++ b/internal/rosa/package/go.az
@@ -3,10 +3,10 @@ package go {
website = "https://go.dev";
anitya = 1227;
- version# = "1.27.1";
+ version# = "1.27.2";
source = remoteTar {
url = "https://go.dev/dl/go"+version+".src.tar.gz";
- checksum = "Jw7OCTZbixLPF0E2WafFBh6DnxbsElvEg_RsMGkrzAmi0pyJTI3eQqLJsvXlVtbK";
+ checksum = "Vxj6mhsDZEPq3AX21AA20zWMaVPX3Ezhdbkm1SK8O__BH8jczdeeLFMasQ-PUIww";
compress = gzip;
};
diff --git a/internal/rosa/package/gzip/late-include.patch b/internal/rosa/package/gzip/late-include.patch
new file mode 100644
index 00000000..4638d578
--- /dev/null
+++ b/internal/rosa/package/gzip/late-include.patch
@@ -0,0 +1,21 @@
+diff --git a/gzip.c b/gzip.c
+index 220f6fc..ef79a89 100644
+--- a/gzip.c
++++ b/gzip.c
+@@ -56,6 +56,8 @@ static char const license_msg[] =
+
+ #include <config.h>
+
++#include <signal.h>
++
+ #include "tailor.h"
+
+ #include "gzip.h"
+@@ -79,7 +81,6 @@ static char const license_msg[] =
+ #include <inttypes.h>
+ #include <limits.h>
+ #include <locale.h>
+-#include <signal.h>
+ #include <stdcountof.h>
+ #include <stddef.h>
+ #include <stdlib.h>
diff --git a/internal/rosa/package/gzip/package.az b/internal/rosa/package/gzip/package.az
new file mode 100644
index 00000000..6c6b3b1f
--- /dev/null
+++ b/internal/rosa/package/gzip/package.az
@@ -0,0 +1,18 @@
+package gzip {
+ description = "a popular data compression program";
+ website = "https://www.gnu.org/software/gzip";
+ anitya = 1290;
+
+ version# = "1.15";
+ source = remoteTar {
+ url = "https://mirrors.kernel.org/gnu/gzip/gzip-"+version+".tar.gz";
+ checksum = "N8v7cH-Vhmxvk1PYaF-B1vH-r-rMT3Z3VyMdCFXhBd74a8v62wT3dKes-u1JCvUG";
+ compress = gzip;
+ };
+ patches = [ "late-include.patch" ];
+
+ exec = make {
+ // dependency loop
+ check = nil;
+ };
+}
diff --git a/internal/rosa/package/hakurei.az b/internal/rosa/package/hakurei.az
index 4e9a8d2c..5585002c 100644
--- a/internal/rosa/package/hakurei.az
+++ b/internal/rosa/package/hakurei.az
@@ -4,7 +4,7 @@ package hakurei-source {
version# = "0.4.8";
output = remoteGit {
- url = "https://git.gensokyo.uk/rosa/hakurei.git";
+ url = "https://src.rosa.moe/hakurei";
tag = "v"+version;
checksum = "B6aoojyXblZ7eYwdc3Ak3wDbBpu63j8S79Gpiul9Obb8diqzBIJ2qNCgWVH6qLsc";
};
@@ -75,6 +75,7 @@ mkdir -p /work/system/bin/
xcb,
wayland,
wayland-protocols,
+ util-linux,
kernel-headers,
];
@@ -121,6 +122,55 @@ DESTDIR=/work "./${NAME}.sh"
xcb,
wayland,
wayland-protocols,
+ util-linux,
+ kernel-headers,
+ ];
+
+ runtime = nil;
+}
+
+package hakurei-dist-race {
+ description = "low-level userspace tooling for Rosa OS (distribution tarball with race instrument)";
+ website = "https://hakurei.app";
+
+ source = hakurei-source;
+ enterSource = true;
+ writable = true;
+ chmod = true;
+
+ env = [
+ "CGO_ENABLED=1",
+ "GOCACHE=/tmp/gocache",
+ "CC=clang -O3 -Werror",
+ "HAKUREI_RACE=1",
+ ];
+
+ exec = generic {
+ chdir = false;
+ build = `
+NAME=make`;
+ check = `
+NAME=all`;
+ install = `
+DESTDIR=/work "./${NAME}.sh"
+`;
+ };
+
+ tools = [
+ go,
+ pkgconf,
+ net-tools,
+ acl,
+ ];
+
+ inputs = [
+ libseccomp,
+ acl,
+ fuse,
+ xcb,
+ wayland,
+ wayland-protocols,
+ util-linux,
kernel-headers,
];
diff --git a/internal/rosa/package/harfbuzz.az b/internal/rosa/package/harfbuzz.az
index 7e10f0f2..2d62d023 100644
--- a/internal/rosa/package/harfbuzz.az
+++ b/internal/rosa/package/harfbuzz.az
@@ -3,11 +3,11 @@ package harfbuzz {
website = "https://harfbuzz.github.io";
anitya = 1299;
- version# = "14.4.0";
+ version# = "14.6.0";
source = remoteGitHub {
suffix = "harfbuzz/harfbuzz";
tag = version;
- checksum = "Avi4h70Src2_E7l8ag1NP64KH2moVvAWb_cCay4b9FB_5zAJzcTTJLCTf_-nnCHw";
+ checksum = "raMMlGfzwy1bEIIO8a93-eEj50ksDDWDdLyeQlO8IM04_woqkIPhJ90QvU3lZJ2L";
};
exec = meson {
diff --git a/internal/rosa/package/hwdata.az b/internal/rosa/package/hwdata.az
index 57bde7e8..5f853a0a 100644
--- a/internal/rosa/package/hwdata.az
+++ b/internal/rosa/package/hwdata.az
@@ -3,11 +3,11 @@ package hwdata {
website = "https://github.com/vcrhonek/hwdata";
anitya = 5387;
- version# = "0.411";
+ version# = "0.412";
source = remoteGitHub {
suffix = "vcrhonek/hwdata";
tag = "v"+version;
- checksum = "5YWsZC8NgZShWWHmC66xqEkTJLAUYJoDt8kmstJyLWfsNqAWAhJZGkVUfgqbTv-6";
+ checksum = "_HkJ6nni1s_D_CEPUM905AaQkE8v-CorQOv9sB8x0EafeTeOV86b8l-SqL6i7DJL";
};
writable = true;
diff --git a/internal/rosa/package/kernel/amd64.config b/internal/rosa/package/kernel/amd64.config
index b52bfbd4..cd8f1132 100644
--- a/internal/rosa/package/kernel/amd64.config
+++ b/internal/rosa/package/kernel/amd64.config
@@ -1,16 +1,16 @@
#
# Automatically generated file; DO NOT EDIT.
-# Linux/x86 6.18.49 Kernel Configuration
+# Linux/x86 6.18.57 Kernel Configuration
#
-CONFIG_CC_VERSION_TEXT="clang version 23.1.0"
+CONFIG_CC_VERSION_TEXT="clang version 23.1.3"
CONFIG_GCC_VERSION=0
CONFIG_CC_IS_CLANG=y
-CONFIG_CLANG_VERSION=230100
+CONFIG_CLANG_VERSION=230103
CONFIG_AS_IS_LLVM=y
-CONFIG_AS_VERSION=230100
+CONFIG_AS_VERSION=230103
CONFIG_LD_VERSION=0
CONFIG_LD_IS_LLD=y
-CONFIG_LLD_VERSION=230100
+CONFIG_LLD_VERSION=230103
CONFIG_RUSTC_VERSION=0
CONFIG_RUSTC_LLVM_VERSION=0
CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y
@@ -21,6 +21,7 @@ CONFIG_CC_HAS_ASSUME=y
CONFIG_CC_HAS_NO_PROFILE_FN_ATTR=y
CONFIG_CC_HAS_COUNTED_BY=y
CONFIG_CC_HAS_MULTIDIMENSIONAL_NONSTRING=y
+CONFIG_CC_OPT_INLINE_MEMSET="-mllvm -max-store-memset=4294967294"
CONFIG_LD_CAN_USE_KEEP_IN_OVERLAY=y
CONFIG_PAHOLE_VERSION=0
CONFIG_IRQ_WORK=y
@@ -658,7 +659,7 @@ CONFIG_ACPI_IPMI=m
CONFIG_ACPI_HOTPLUG_CPU=y
CONFIG_ACPI_PROCESSOR_AGGREGATOR=m
CONFIG_ACPI_THERMAL=m
-CONFIG_ACPI_PLATFORM_PROFILE=m
+CONFIG_ACPI_PLATFORM_PROFILE=y
CONFIG_ACPI_CUSTOM_DSDT_FILE=""
CONFIG_ARCH_HAS_ACPI_TABLE_UPGRADE=y
CONFIG_ACPI_TABLE_UPGRADE=y
@@ -730,6 +731,7 @@ CONFIG_X86_INTEL_PSTATE=y
CONFIG_X86_PCC_CPUFREQ=m
CONFIG_X86_AMD_PSTATE=y
CONFIG_X86_AMD_PSTATE_DEFAULT_MODE=3
+CONFIG_X86_AMD_PSTATE_DYNAMIC_EPP=y
CONFIG_X86_AMD_PSTATE_UT=m
CONFIG_X86_ACPI_CPUFREQ=m
CONFIG_X86_ACPI_CPUFREQ_CPB=y
@@ -1044,6 +1046,7 @@ CONFIG_FUNCTION_ALIGNMENT_16B=y
CONFIG_FUNCTION_ALIGNMENT=16
CONFIG_CC_HAS_SANE_FUNCTION_ALIGNMENT=y
CONFIG_ARCH_HAS_CPU_ATTACK_VECTORS=y
+CONFIG_HAVE_ARCH_GET_SECUREBOOT=y
# end of General architecture-dependent options
CONFIG_RT_MUTEXES=y
diff --git a/internal/rosa/package/kernel/arm64.config b/internal/rosa/package/kernel/arm64.config
index 9c6951b7..8af9d893 100644
--- a/internal/rosa/package/kernel/arm64.config
+++ b/internal/rosa/package/kernel/arm64.config
@@ -1,16 +1,16 @@
#
# Automatically generated file; DO NOT EDIT.
-# Linux/arm64 6.18.49 Kernel Configuration
+# Linux/arm64 6.18.57 Kernel Configuration
#
-CONFIG_CC_VERSION_TEXT="clang version 23.1.0"
+CONFIG_CC_VERSION_TEXT="clang version 23.1.3"
CONFIG_GCC_VERSION=0
CONFIG_CC_IS_CLANG=y
-CONFIG_CLANG_VERSION=230100
+CONFIG_CLANG_VERSION=230103
CONFIG_AS_IS_LLVM=y
-CONFIG_AS_VERSION=230100
+CONFIG_AS_VERSION=230103
CONFIG_LD_VERSION=0
CONFIG_LD_IS_LLD=y
-CONFIG_LLD_VERSION=230100
+CONFIG_LLD_VERSION=230103
CONFIG_RUSTC_VERSION=0
CONFIG_RUSTC_LLVM_VERSION=0
CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y
@@ -21,6 +21,7 @@ CONFIG_CC_HAS_ASSUME=y
CONFIG_CC_HAS_NO_PROFILE_FN_ATTR=y
CONFIG_CC_HAS_COUNTED_BY=y
CONFIG_CC_HAS_MULTIDIMENSIONAL_NONSTRING=y
+CONFIG_CC_OPT_INLINE_MEMSET="-mllvm -max-store-memset=4294967294"
CONFIG_LD_CAN_USE_KEEP_IN_OVERLAY=y
CONFIG_PAHOLE_VERSION=0
CONFIG_IRQ_WORK=y
@@ -456,8 +457,10 @@ CONFIG_ARM64_ERRATUM_2224489=y
CONFIG_ARM64_ERRATUM_2064142=y
CONFIG_ARM64_ERRATUM_2038923=y
CONFIG_ARM64_ERRATUM_1902691=y
+CONFIG_ARM64_WORKAROUND_BROKEN_AMU_CONSTCNT=y
CONFIG_ARM64_ERRATUM_2457168=y
CONFIG_ARM64_ERRATUM_2645198=y
+CONFIG_ARM64_ERRATUM_3821522=y
CONFIG_ARM64_WORKAROUND_SPECULATIVE_UNPRIV_LOAD=y
CONFIG_ARM64_ERRATUM_2966298=y
CONFIG_ARM64_ERRATUM_3117295=y
@@ -575,7 +578,6 @@ CONFIG_ARM64_TLB_RANGE=y
#
CONFIG_AS_HAS_ARMV8_5=y
CONFIG_ARM64_BTI=y
-CONFIG_ARM64_BTI_KERNEL=y
CONFIG_CC_HAS_BRANCH_PROT_PAC_RET_BTI=y
CONFIG_ARM64_E0PD=y
CONFIG_ARM64_AS_HAS_MTE=y
@@ -975,6 +977,7 @@ CONFIG_HAVE_GCC_PLUGINS=y
CONFIG_FUNCTION_ALIGNMENT_4B=y
CONFIG_FUNCTION_ALIGNMENT=4
CONFIG_CC_HAS_SANE_FUNCTION_ALIGNMENT=y
+CONFIG_HAVE_ARCH_GET_SECUREBOOT=y
# end of General architecture-dependent options
CONFIG_RT_MUTEXES=y
@@ -12217,9 +12220,10 @@ CONFIG_PXA1908_PM_DOMAINS=m
#
CONFIG_MTK_SCPSYS=y
CONFIG_MTK_SCPSYS_PM_DOMAINS=y
-CONFIG_AIROHA_CPU_PM_DOMAIN=m
# end of MediaTek PM Domains
+CONFIG_AIROHA_CPU_PM_DOMAIN=m
+
#
# Qualcomm PM Domains
#
@@ -14220,11 +14224,9 @@ CONFIG_CRYPTO_DEV_SUN4I_SS=m
CONFIG_CRYPTO_DEV_SUN8I_CE=m
# CONFIG_CRYPTO_DEV_SUN8I_CE_DEBUG is not set
# CONFIG_CRYPTO_DEV_SUN8I_CE_HASH is not set
-# CONFIG_CRYPTO_DEV_SUN8I_CE_PRNG is not set
# CONFIG_CRYPTO_DEV_SUN8I_CE_TRNG is not set
CONFIG_CRYPTO_DEV_SUN8I_SS=m
# CONFIG_CRYPTO_DEV_SUN8I_SS_DEBUG is not set
-# CONFIG_CRYPTO_DEV_SUN8I_SS_PRNG is not set
# CONFIG_CRYPTO_DEV_SUN8I_SS_HASH is not set
CONFIG_CRYPTO_DEV_FSL_CAAM_COMMON=m
CONFIG_CRYPTO_DEV_FSL_CAAM_CRYPTO_API_DESC=m
diff --git a/internal/rosa/package/kernel/dml-frame_warn_limit.patch b/internal/rosa/package/kernel/dml-frame_warn_limit.patch
deleted file mode 100644
index d3947011..00000000
--- a/internal/rosa/package/kernel/dml-frame_warn_limit.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/drivers/gpu/drm/amd/display/dc/dml/Makefile b/drivers/gpu/drm/amd/display/dc/dml/Makefile
-index 268b5fbdb48b..9f1ef07fdfe5 100644
---- a/drivers/gpu/drm/amd/display/dc/dml/Makefile
-+++ b/drivers/gpu/drm/amd/display/dc/dml/Makefile
-@@ -36,7 +36,7 @@ ifneq ($(CONFIG_FRAME_WARN),0)
- frame_warn_limit := 3072
- endif
- else
-- frame_warn_limit := 2048
-+ frame_warn_limit := 3072
- endif
-
- ifeq ($(call test-lt, $(CONFIG_FRAME_WARN), $(frame_warn_limit)),y)
diff --git a/internal/rosa/package/kernel/package.az b/internal/rosa/package/kernel/package.az
index f2be2890..47bc4323 100644
--- a/internal/rosa/package/kernel/package.az
+++ b/internal/rosa/package/kernel/package.az
@@ -3,11 +3,10 @@ package kernel-source {
website = "https://kernel.org";
exclude = true;
- version# = "6.18.49";
+ version# = "6.18.57";
output = remoteTar {
- url = "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/"+
- "snapshot/linux-"+version+".tar.gz";
- checksum = "J0FT9987a7mlYBGcACXWeTsmfScbS8FM_JAG9LLl917UdlcFRP_UDj73yR82bvGR";
+ url = "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-"+version+".tar.gz";
+ checksum = "CnOD4u2pRjkjmyD6wg4dyQVnXXy8rNlwZ-OmTZvSFZPJNSEC996heziLGbKQNCTL";
compress = gzip;
};
}
@@ -46,8 +45,8 @@ cat \
checksum = arch {
default = "";
- amd64 = "uwTZX3iLp6iPsR0-F0Jz2aUviLMMqv4xGQHoKeemo2HrNLD7fDlIDqqYT--i_5HJ";
- arm64 = "A9EWzTGEChVLJVm5BP1GzpibdUddXUN2hIjAGkD-KQr5sc6RuEeIQle8VAiAYUqp";
+ amd64 = "8BdskuTo4WxcJDFbdHakHmvkeIASmOkDd0LanTjaXwkmoMa5PeMf0gqee0FqYRFd";
+ arm64 = "RQ27XMgxEj5M6cagVHljCE1n-9q_gFayOaakWr0PoAq5yllfLHEz0KQx1fXLwCzp";
riscv64 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
};
@@ -61,7 +60,6 @@ package kernel {
anitya = 389470;
source = kernel-source;
- patches = [ "dml-frame_warn_limit.patch" ];
files = {
".config": arch {
amd64 = "amd64.config";
diff --git a/internal/rosa/package/kernel/version.h b/internal/rosa/package/kernel/version.h
index 4fc45286..8b0cd01b 100644
--- a/internal/rosa/package/kernel/version.h
+++ b/internal/rosa/package/kernel/version.h
@@ -1,5 +1,5 @@
-#define LINUX_VERSION_CODE 397870
+#define LINUX_VERSION_CODE 397881
#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + ((c) > 255 ? 255 : (c)))
#define LINUX_VERSION_MAJOR 6
#define LINUX_VERSION_PATCHLEVEL 18
-#define LINUX_VERSION_SUBLEVEL 46
+#define LINUX_VERSION_SUBLEVEL 57
diff --git a/internal/rosa/package/libarchive/CTestCustom.cmake b/internal/rosa/package/libarchive/CTestCustom.cmake
index cd1bc34a..6ec6d952 100644
--- a/internal/rosa/package/libarchive/CTestCustom.cmake
+++ b/internal/rosa/package/libarchive/CTestCustom.cmake
@@ -61,4 +61,5 @@ list(APPEND CTEST_CUSTOM_TESTS_IGNORE
"libarchive_test_read_format_ustar_filename"
"libarchive_test_read_append_wrong_filter"
"libarchive_test_read_format_zip_ppmd8_aes256_streaming"
+ "libarchive_test_read_format_zip_lzma_alone_large_size"
)
diff --git a/internal/rosa/package/libarchive/package.az b/internal/rosa/package/libarchive/package.az
index b06daaf3..bd57a860 100644
--- a/internal/rosa/package/libarchive/package.az
+++ b/internal/rosa/package/libarchive/package.az
@@ -3,11 +3,11 @@ package libarchive {
website = "https://www.libarchive.org";
anitya = 1558;
- version# = "3.8.9";
+ version# = "3.8.10";
source = remoteGitHub {
suffix = "libarchive/libarchive";
tag = "v"+version;
- checksum = "8C_8Mr22elOAO8Qvxl-B-R-B35M7dpdFdAlph1CeE1L75JdyUnolee-P3KDRFc8T";
+ checksum = "B8gGWvDB24_W4rzlPz1PxQYroCn-xOmUcnhsRVmzYl-L9mV_t6uj-TnpvjGqQjkV";
};
files = { "CTestCustom.cmake"; };
diff --git a/internal/rosa/package/libbpf.az b/internal/rosa/package/libbpf.az
index 33df95e4..8c21b7e9 100644
--- a/internal/rosa/package/libbpf.az
+++ b/internal/rosa/package/libbpf.az
@@ -3,11 +3,11 @@ package libbpf {
website = "https://github.com/libbpf/libbpf";
anitya = 141355;
- version# = "1.7.0";
+ version# = "1.8.0";
source = remoteGitHub {
suffix = "libbpf/libbpf";
tag = "v"+version;
- checksum = "Gsk3TYDTne6behhdiyRB0Y5GTGECRh2265VY7IQ_VB62vIm7U9oqzEW-Va5Tvi_w";
+ checksum = "vpacRQrLgXtttQw0fwv3fcJ-FRHbly38CcLPJyXd2MsKr8gsfp1K6bBVelxZZj52";
};
env = [ "PREFIX=/system" ];
diff --git a/internal/rosa/package/libevdev.az b/internal/rosa/package/libevdev.az
index 85c9b674..cd4cd671 100644
--- a/internal/rosa/package/libevdev.az
+++ b/internal/rosa/package/libevdev.az
@@ -3,12 +3,12 @@ package libevdev {
website = "https://www.freedesktop.org/software/libevdev";
anitya = 20540;
- version# = "1.13.7";
+ version# = "1.14.0";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "libevdev/libevdev";
ref = "libevdev-"+version;
- checksum = "zmN9kkbYkrwr1YdzeMxlZIuiMkRQ9-mZ9DTcBufMckYWUq2IutSt9ZtRE4Agl75D";
+ checksum = "_ybQw43ruWkQQr1uiIfhOKY5HKBKknipWef7EJUhPnTelwkxurGKN2fvEX7Z8EWy";
};
exec = meson {
diff --git a/internal/rosa/package/libexpat.az b/internal/rosa/package/libexpat.az
index acb8445b..2f6386f5 100644
--- a/internal/rosa/package/libexpat.az
+++ b/internal/rosa/package/libexpat.az
@@ -3,7 +3,7 @@ package libexpat {
website = "https://libexpat.github.io";
anitya = 770;
- version# = "2.8.4";
+ version# = "2.9.0";
source = remoteGitHubRelease {
suffix = "libexpat/libexpat";
tag = "R_"+replace {
@@ -12,7 +12,7 @@ package libexpat {
new = "_";
};
name = "expat-"+version+".tar.bz2";
- checksum = "Nbi2wl8U9KHf3LVLczfAb5Kqr7HFv_myReVjxKXUkfhToWfL-pP2o44fDpWHJ4ig";
+ checksum = "dMHFWTPDk1trundt3hRvg6lKbTTcxN-MCRGph2_rfWNA_HYOURLNmlYm9i25c6zZ";
compress = bzip2;
};
diff --git a/internal/rosa/package/libglvnd.az b/internal/rosa/package/libglvnd.az
index dbcf198b..9be9e2a9 100644
--- a/internal/rosa/package/libglvnd.az
+++ b/internal/rosa/package/libglvnd.az
@@ -22,6 +22,7 @@ export DISPLAY=':0'
Xvfb &
XVFB_PID="$!"
trap 'kill $XVFB_PID && wait $XVFB_PID' EXIT
+while [ ! -e /tmp/.X11-unix/X0 ]; do sleep 0.1; done
`;
};
diff --git a/internal/rosa/package/libinput.az b/internal/rosa/package/libinput.az
index aeb54f77..3769f680 100644
--- a/internal/rosa/package/libinput.az
+++ b/internal/rosa/package/libinput.az
@@ -3,12 +3,12 @@ package libinput {
website = "https://www.freedesktop.org/wiki/Software/libinput";
anitya = 5781;
- version# = "1.31.3";
+ version# = "1.32.0";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "libinput/libinput";
ref = version;
- checksum = "GxBGPN6YybQxrD2MDsIL8gdDYImXn4NAJi6EvTx_Hb_1jcbjwCrjeyjY2upUyTMi";
+ checksum = "hjuI9afDPrCORLCCM5koBjyRNmTr_YCUmmF4Tl0bqK_7fz0beC8125wt3lmimtgL";
};
bin = [ "bash" ];
diff --git a/internal/rosa/package/libliftoff/0001-test-Disable-alloc-deadline-on-tests.patch b/internal/rosa/package/libliftoff/0001-test-Disable-alloc-deadline-on-tests.patch
new file mode 100644
index 00000000..6747ac01
--- /dev/null
+++ b/internal/rosa/package/libliftoff/0001-test-Disable-alloc-deadline-on-tests.patch
@@ -0,0 +1,139 @@
+From 343a7d02cf5417e0732d8ba73089f86dd6173253 Mon Sep 17 00:00:00 2001
+From: David Turner <david.turner@raspberrypi.com>
+Date: Wed, 5 Mar 2025 16:02:25 +0000
+Subject: [PATCH] test: Disable alloc deadline on tests
+
+I've been seeing occasional spurious CI failures due to
+liftoff_output_apply() deadline timeouts, presumably when the runner is
+busy. Disable the allocation deadline in all tests to stop this
+happening.
+---
+ test/test_alloc.c | 16 ++++++++++++----
+ test/test_candidate.c | 4 +++-
+ test/test_dynamic.c | 8 ++++++--
+ test/test_priority.c | 4 +++-
+ test/test_prop.c | 4 +++-
+ 5 files changed, 27 insertions(+), 9 deletions(-)
+
+diff --git a/test/test_alloc.c b/test/test_alloc.c
+index 8d0caf7..a002c46 100644
+--- a/test/test_alloc.c
++++ b/test/test_alloc.c
+@@ -803,7 +803,9 @@ run_test(const struct test_case *test)
+ }
+
+ req = drmModeAtomicAlloc();
+- ret = liftoff_output_apply(output, req, 0, NULL);
++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){
++ .timeout_ns = INT64_MAX,
++ });
+ assert(ret == 0);
+ ret = drmModeAtomicCommit(drm_fd, req, 0, NULL);
+ assert(ret == 0);
+@@ -875,7 +877,9 @@ test_basic(void)
+ liftoff_mock_plane_add_compatible_layer(mock_plane, layer);
+
+ req = drmModeAtomicAlloc();
+- ret = liftoff_output_apply(output, req, 0, NULL);
++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){
++ .timeout_ns = INT64_MAX,
++ });
+ assert(ret == 0);
+ ret = drmModeAtomicCommit(drm_fd, req, 0, NULL);
+ assert(ret == 0);
+@@ -913,7 +917,9 @@ test_no_props_fail(void)
+ liftoff_mock_plane_add_compatible_layer(mock_plane, layer);
+
+ req = drmModeAtomicAlloc();
+- ret = liftoff_output_apply(output, req, 0, NULL);
++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){
++ .timeout_ns = INT64_MAX,
++ });
+ assert(ret == 0);
+ ret = drmModeAtomicCommit(drm_fd, req, 0, NULL);
+ assert(ret == 0);
+@@ -959,7 +965,9 @@ test_composition_no_props(void)
+ liftoff_mock_plane_add_compatible_layer(mock_plane, layer_with_fb);
+
+ req = drmModeAtomicAlloc();
+- ret = liftoff_output_apply(output, req, 0, NULL);
++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){
++ .timeout_ns = INT64_MAX,
++ });
+ assert(ret == 0);
+ ret = drmModeAtomicCommit(drm_fd, req, 0, NULL);
+ assert(ret == 0);
+diff --git a/test/test_candidate.c b/test/test_candidate.c
+index 5e7f698..74a0bcb 100644
+--- a/test/test_candidate.c
++++ b/test/test_candidate.c
+@@ -60,7 +60,9 @@ test_basic(void)
+ liftoff_layer_set_property(layer, "COLOR_RANGE", 0);
+
+ req = drmModeAtomicAlloc();
+- ret = liftoff_output_apply(output, req, 0, NULL);
++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){
++ .timeout_ns = INT64_MAX,
++ });
+ assert(ret == 0);
+ ret = drmModeAtomicCommit(drm_fd, req, 0, NULL);
+ assert(ret == 0);
+diff --git a/test/test_dynamic.c b/test/test_dynamic.c
+index 3a95a7f..8c1049d 100644
+--- a/test/test_dynamic.c
++++ b/test/test_dynamic.c
+@@ -50,7 +50,9 @@ first_commit(struct context *ctx)
+ assert(ctx->commit_count == 0);
+
+ req = drmModeAtomicAlloc();
+- ret = liftoff_output_apply(ctx->output, req, 0, NULL);
++ ret = liftoff_output_apply(ctx->output, req, 0, &(struct liftoff_output_apply_options){
++ .timeout_ns = INT64_MAX,
++ });
+ assert(ret == 0);
+ ret = drmModeAtomicCommit(ctx->drm_fd, req, 0, NULL);
+ assert(ret == 0);
+@@ -72,7 +74,9 @@ second_commit(struct context *ctx, bool want_reuse_prev_alloc)
+ int ret;
+
+ req = drmModeAtomicAlloc();
+- ret = liftoff_output_apply(ctx->output, req, 0, NULL);
++ ret = liftoff_output_apply(ctx->output, req, 0, &(struct liftoff_output_apply_options){
++ .timeout_ns = INT64_MAX,
++ });
+ assert(ret == 0);
+ if (want_reuse_prev_alloc) {
+ /* The library should perform only one TEST_ONLY commit with the
+diff --git a/test/test_priority.c b/test/test_priority.c
+index cf2e4c5..41ede18 100644
+--- a/test/test_priority.c
++++ b/test/test_priority.c
+@@ -76,7 +76,9 @@ main(int argc, char *argv[])
+
+ liftoff_layer_set_property(layer, "FB_ID", fbs[j % 2]);
+
+- ret = liftoff_output_apply(output, req, 0, NULL);
++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){
++ .timeout_ns = INT64_MAX,
++ });
+ assert(ret == 0);
+ ret = drmModeAtomicCommit(drm_fd, req, 0, NULL);
+ assert(ret == 0);
+diff --git a/test/test_prop.c b/test/test_prop.c
+index 05193c7..093b581 100644
+--- a/test/test_prop.c
++++ b/test/test_prop.c
+@@ -34,7 +34,9 @@ commit(int drm_fd, struct liftoff_output *output)
+ int ret;
+
+ req = drmModeAtomicAlloc();
+- ret = liftoff_output_apply(output, req, 0, NULL);
++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){
++ .timeout_ns = INT64_MAX,
++ });
+ assert(ret == 0);
+ ret = drmModeAtomicCommit(drm_fd, req, 0, NULL);
+ assert(ret == 0);
+--
+2.55.0
+
diff --git a/internal/rosa/package/libliftoff.az b/internal/rosa/package/libliftoff/package.az
index a1c23fd8..7d8900bc 100644
--- a/internal/rosa/package/libliftoff.az
+++ b/internal/rosa/package/libliftoff/package.az
@@ -10,6 +10,7 @@ package libliftoff {
ref = "v"+version;
checksum = "anxKfrW56iK7l6-bm6RGcaC_uk-Kc8x3_VqKaywmSjv5HLCs6A2sHyjGUbOC9xZv";
};
+ patches = [ "0001-test-Disable-alloc-deadline-on-tests.patch" ];
exec = meson {};
diff --git a/internal/rosa/package/libmd.az b/internal/rosa/package/libmd.az
index e618d11e..f8fafd80 100644
--- a/internal/rosa/package/libmd.az
+++ b/internal/rosa/package/libmd.az
@@ -3,11 +3,11 @@ package libmd {
website = "https://www.hadrons.org/software/libmd";
anitya = 15525;
- version# = "1.2.0";
+ version# = "1.3.0";
source = remoteGit {
url = "https://git.hadrons.org/git/libmd.git";
tag = version;
- checksum = "1rJ6joAO0wwMZvSfnRNkc1MOhywyAq7SM8VmF92NvDtv7Qdl1LRbjm5fg_DFFtGj";
+ checksum = "CuzJwaavZusUsVcQEAJDsQ2mf9C9BjFhYLemSt0y6K_Gkx0niacVjqV7QpjFjhta";
};
exec = make {
diff --git a/internal/rosa/package/libpng.az b/internal/rosa/package/libpng.az
index 4711cbce..431c38a4 100644
--- a/internal/rosa/package/libpng.az
+++ b/internal/rosa/package/libpng.az
@@ -3,7 +3,7 @@ package libpng {
website = "https://www.libpng.org/pub/png/libpng.html";
anitya = 1705;
- version# = "1.6.58";
+ version# = "1.6.59";
source = remoteTar {
url = "https://downloads.sourceforge.net/project/libpng/libpng"+
join {
@@ -16,7 +16,7 @@ package libpng {
end = 2;
};
}+"/"+version+"/libpng-"+version+".tar.gz";
- checksum = "m_a5lROJH7vmF3cMjqwTUqURuQLhV1JQx2ySPzcN3VPdgDB9pG3UINsIx_mtkr-t";
+ checksum = "W-glb0kwxxmfjQepn0bFejBtMZXCiRM0OQ_I-TvyigQr_EmMBEI44WBG-3uCWbIK";
compress = gzip;
};
diff --git a/internal/rosa/package/libtirpc.az b/internal/rosa/package/libtirpc.az
index 0b12a92e..b1a49c79 100644
--- a/internal/rosa/package/libtirpc.az
+++ b/internal/rosa/package/libtirpc.az
@@ -3,7 +3,7 @@ package libtirpc {
website = "https://sourceforge.net/projects/libtirpc";
anitya = 1740;
- version# = "1.3.7";
+ version# = "1.3.8";
source = remoteGit {
url = "git://linux-nfs.org/~steved/libtirpc";
tag = "libtirpc-"+join {
@@ -14,7 +14,7 @@ package libtirpc {
};
sep = "-";
};
- checksum = "nzFfu7LNvnSNiNAryD1vtnNWnU-Xqee8KqfXUKoBf5yjb5-dkeRkYuRijdCoYLof";
+ checksum = "OYM7CljjvlHv3Xzc9bXC2WRfBQY0QcBEZ9ubE8qVDweq9ZvQPrAWxSOFVhFBoToz";
};
exec = make {
@@ -29,6 +29,10 @@ package libtirpc {
automake,
libtool,
pkgconf,
+
+ // version script assignment of 'TIRPC_1.3.7' to symbol 'authdes_getucred' failed: symbol not defined
+ // version script assignment of 'TIRPC_1.3.7' to symbol 'rpc_gss_getcred' failed: symbol not defined
+ binutils,
];
inputs = [
diff --git a/internal/rosa/package/llvm/package.az b/internal/rosa/package/llvm/package.az
index 11210563..05a9d937 100644
--- a/internal/rosa/package/llvm/package.az
+++ b/internal/rosa/package/llvm/package.az
@@ -2,11 +2,11 @@ package llvm-project {
description = "LLVM monorepo with Rosa OS patches";
anitya = 1830;
- version# = "23.1.0";
+ version# = "23.1.3";
output = remoteGitHub {
suffix = "llvm/llvm-project";
tag = "llvmorg-"+version;
- checksum = "wgirRNAhU9451qsZn4FXcaIButgRNZWb4kb8pQwERyF1PGc-iJQVewr6xGk7ctNj";
+ checksum = "bEWWfej4llsLsQ_wKIv57zcfNNgQD1yihufRDeSDYDlTzyrMTUS8J-ve6Bk2kPrJ";
};
patches = [
"increase-stack-size-unconditional.patch",
diff --git a/internal/rosa/package/lvm2/package.az b/internal/rosa/package/lvm2/package.az
index 3a363b54..cc0ccef3 100644
--- a/internal/rosa/package/lvm2/package.az
+++ b/internal/rosa/package/lvm2/package.az
@@ -3,11 +3,11 @@ package lvm2 {
website = "https://sourceware.org/lvm2";
anitya = 5354;
- version# = "2.03.42";
+ version# = "2.03.43";
source = remoteTar {
url = "https://mirrors.kernel.org/sourceware/lvm2/releases/LVM2."+version+".tgz";
compress = gzip;
- checksum = "qzqVAeW9ttt_H5W0Wfg8twvZjFZH7x_RIzSjqYSooMsRpiXQ7yNdOL1MXY5Zntvi";
+ checksum = "W98OpJpTiLc-wO7CHh08UQ8nc-gNEj3kr84IzOv2QDoYfnonVVj-Lt_NZLJi_vi5";
};
patches = [
"fix-stdio-usage.patch",
diff --git a/internal/rosa/package/mesa/include.patch b/internal/rosa/package/mesa/include.patch
index b1942625..4a8a7fe9 100644
--- a/internal/rosa/package/mesa/include.patch
+++ b/internal/rosa/package/mesa/include.patch
@@ -1,15 +1,3 @@
-diff --git a/src/amd/compiler/aco_util.h b/src/amd/compiler/aco_util.h
-index 82d754f155a..9f20e80a554 100644
---- a/src/amd/compiler/aco_util.h
-+++ b/src/amd/compiler/aco_util.h
-@@ -12,6 +12,7 @@
- #include "util/macros.h"
- #include "util/u_math.h"
-
-+#include <algorithm>
- #include <array>
- #include <cassert>
- #include <cstddef>
diff --git a/src/freedreno/common/freedreno_common.h b/src/freedreno/common/freedreno_common.h
index 36dea4e12b5..cfa243c270f 100644
--- a/src/freedreno/common/freedreno_common.h
diff --git a/internal/rosa/package/mesa/package.az b/internal/rosa/package/mesa/package.az
index 8db6462e..b15bb880 100644
--- a/internal/rosa/package/mesa/package.az
+++ b/internal/rosa/package/mesa/package.az
@@ -4,12 +4,12 @@ package mesa {
anitya = 1970;
latest = anityaFallback;
- version# = "26.2.2";
+ version# = "26.2.5";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "mesa/mesa";
ref = "mesa-"+version;
- checksum = "5mIdojhazcdAgbLW1v4d-7P5pIhu9wnGS3ek1QhJIPMJyFWhfKhiZCsd53WAHt1_";
+ checksum = "uGxr-ED_AydefaUMuVWnIFtT6F8COqbKvQbmsVwKn0VKEZL-nbUHuvHvP-YuCr5v";
};
patches = [ "include.patch" ];
diff --git a/internal/rosa/package/meson/package.az b/internal/rosa/package/meson/package.az
index 78fa2035..9b266654 100644
--- a/internal/rosa/package/meson/package.az
+++ b/internal/rosa/package/meson/package.az
@@ -3,11 +3,11 @@ package meson {
website = "https://mesonbuild.com";
anitya = 6472;
- version# = "1.12.0";
+ version# = "1.12.1";
source = remoteGitHub {
suffix = "mesonbuild/meson";
tag = version;
- checksum = "K0hm0h26_6e3-1poYZKwj18IB6bdAtYE-whpROtTCSpVN5GllwSwbXC2xPYcSFAJ";
+ checksum = "P7lvRR-dRtSmNF4CZtVWd6uwGPzjkE76MvJL0s3DDw-CFparOmUK1DNEkCt-cuEF";
};
patches = [
"respect-colour.patch",
diff --git a/internal/rosa/package/muon.az b/internal/rosa/package/muon/package.az
index dc183591..3a5be8bd 100644
--- a/internal/rosa/package/muon.az
+++ b/internal/rosa/package/muon/package.az
@@ -3,13 +3,14 @@ package meson-tests {
website = "https://github.com/muon-build/meson-tests";
exclude = true;
- version# = "0bbaa42e7ba553356fbec038e8b40cf89f90da82";
+ version# = "38483b8cf5eb4f6e9df7a78d786ec111e0d6eb5d";
output = remoteTar {
url = "https://github.com/muon-build/meson-tests/archive/"+
version+".tar.gz";
- checksum = "W_yNC2Id1aGBXLmBhrQ53UARVBl-nZ2bDZJcmZDaE4GyoZyj6tSURojuTyWgG1U8";
+ checksum = "ikrJiczlObaLUMHRbzzWmGgAyZMymxxC3D9VFozD0sV3e5ckiHXk8qk0Zr8bBWvO";
compress = gzip;
};
+ patches = [ "remove-broken-tests.patch" ];
}
package muon {
@@ -17,12 +18,19 @@ package muon {
website = "https://muon.build";
anitya = 391173;
- version# = "0.6.0";
+ version# = "0.7.0";
source = remoteTar {
url = "https://git.sr.ht/~lattis/muon/archive/"+version+".tar.gz";
- checksum = "DiRw1dTaHlL5d3TNK8gykFHI_BAWK7ERooSmVC-kltUkjOubWkN9b-DG21a1ETCe";
+ checksum = "ackLkMhxutd7xvLAVU25qkM5CAztLaYf1dAC2vIvlRIZ8o6LjaNpH-3OyP1xGQTO";
compress = gzip;
};
+ patches = [
+ // for some reason, muon decides to use clang-cl on Rosa OS
+ "remove-cl.patch",
+
+ // equivalent to the meson patches
+ "respect-colour.patch",
+ ];
extra = [ meson-tests ];
writable = true;
@@ -30,7 +38,6 @@ package muon {
chmod +w subprojects
cp -r ../extra/meson-tests subprojects
chmod -R +w subprojects/meson-tests
-sed -i '/common.220 fs module/d' subprojects/meson-tests/meson.build
`;
exec = generic {
diff --git a/internal/rosa/package/muon/remove-broken-tests.patch b/internal/rosa/package/muon/remove-broken-tests.patch
new file mode 100644
index 00000000..d4f868be
--- /dev/null
+++ b/internal/rosa/package/muon/remove-broken-tests.patch
@@ -0,0 +1,30 @@
+diff --git a/meson.build b/meson.build
+index ebcd241..30b4c57 100644
+--- a/meson.build
++++ b/meson.build
+@@ -213,7 +213,6 @@ foreach t : [
+ ['common/202 custom target build by default', {'python': true}],
+ ['common/203 find_library and headers'],
+ ['common/204 line continuation'],
+- ['common/205 native file path override', {'cpp': true}],
+ ['common/206 tap tests'],
+ ['common/207 warning level 0', {'cpp': true}],
+ ['common/208 link custom', {'python': true}],
+@@ -228,9 +227,7 @@ foreach t : [
+ ['common/217 test priorities', {'python': true}],
+ ['common/218 include_dir dot'],
+ ['common/219 include_type dependency', {'cpp': true}],
+- ['common/220 fs module'],
+ ['common/221 zlib'],
+- ['common/222 native prop'],
+ ['common/223 persubproject options', {'cpp': true}],
+ ['common/224 arithmetic operators'],
+ ['common/225 link language', {'cpp': true}],
+@@ -336,7 +333,6 @@ foreach t : [
+ ['common/280 pkgconfig override', {'python': true}],
+ ['common/281 pkgconfig-gen'],
+ ['common/282 -D_FORTIFY_SOURCE=2 and -O0'],
+- ['common/283 subproj options', {'options': {'b_lto': 'true'}}],
+ ['common/284 test args and depends in path'],
+ ['common/285 wrap override'],
+ ['common/286 pkgconfig subproject'],
diff --git a/internal/rosa/package/muon/remove-cl.patch b/internal/rosa/package/muon/remove-cl.patch
new file mode 100644
index 00000000..e421c25d
--- /dev/null
+++ b/internal/rosa/package/muon/remove-cl.patch
@@ -0,0 +1,39 @@
+diff --git a/src/script/runtime/toolchains.meson b/src/script/runtime/toolchains.meson
+index 97682251..bb49b041 100644
+--- a/src/script/runtime/toolchains.meson
++++ b/src/script/runtime/toolchains.meson
+@@ -1059,22 +1059,6 @@ toolchain.register_compiler(
+ },
+ )
+
+-toolchain.register_compiler(
+- 'clang-cl',
+- exe: {'c': 'clang-cl', 'cpp': 'clang-cl'},
+- inherit: 'msvc',
+- linker: 'lld-link',
+- archiver: 'llvm-lib',
+- detect: func(out str) -> int
+- return 'CL.EXE COMPATIBILITY' in out ? 100 : 0
+- endfunc,
+- handlers: {
+- 'force_language': func(_c compiler, s str) -> list[str]
+- return ['-x', s]
+- endfunc,
+- },
+-)
+-
+ toolchain.register_compiler(
+ 'nasm',
+ exe: {'nasm': 'nasm'},
+diff --git a/src/vsenv.c b/src/vsenv.c
+index f55541ec..c92e665a 100644
+--- a/src/vsenv.c
++++ b/src/vsenv.c
+@@ -73,7 +73,6 @@ vsenv_setup(struct workspace *wk, const char *cache_path, enum setup_platform_en
+ "cc",
+ "gcc",
+ "clang",
+- "clang-cl",
+ };
+ uint32_t i;
+ for (i = 0; i < ARRAY_LEN(comps); ++i) {
diff --git a/internal/rosa/package/muon/respect-colour.patch b/internal/rosa/package/muon/respect-colour.patch
new file mode 100644
index 00000000..a4da13d5
--- /dev/null
+++ b/internal/rosa/package/muon/respect-colour.patch
@@ -0,0 +1,14 @@
+diff --git a/src/log.c b/src/log.c
+index 34f11f19..22fe8f7d 100644
+--- a/src/log.c
++++ b/src/log.c
+@@ -506,6 +506,9 @@ log_set_file(struct workspace *wk, FILE *log_file)
+ log_cfg.tstr = 0;
+ log_cfg.tstr_wk = 0;
+ log_cfg.file_is_a_tty = log_file && fs_is_a_tty(wk, log_file);
++ if (getenv("CLICOLOR_FORCE") != NULL) {
++ log_cfg.file_is_a_tty = true;
++ }
+ }
+
+ void
diff --git a/internal/rosa/package/noto.az b/internal/rosa/package/noto.az
index 7481b5e7..6fc8e125 100644
--- a/internal/rosa/package/noto.az
+++ b/internal/rosa/package/noto.az
@@ -3,11 +3,11 @@ package noto {
website = "https://fonts.google.com/noto";
anitya = 10671;
- version# = "2026.09.01";
+ version# = "2026.10.01";
source = remoteGitHub {
suffix = "notofonts/notofonts.github.io";
tag = "noto-monthly-release-"+version;
- checksum = "ZITq5IyZNZovca3t7snPndDgjdzbCeBIoGYpY7bbnR-w9oAiz1vkN9zwRWz5JJw5";
+ checksum = "lEKVFkRavG8j1wNdrVFK6s1k8Tlp3ICN30cIX_tuTv5w6uAnAMDIWSoviJ4oiwu8";
};
enterSource = true;
diff --git a/internal/rosa/package/nss.az b/internal/rosa/package/nss.az
index fb9c1a33..115e5667 100644
--- a/internal/rosa/package/nss.az
+++ b/internal/rosa/package/nss.az
@@ -3,7 +3,7 @@ package nss-cacert {
website = "https://firefox-source-docs.mozilla.org/security/nss/index.html";
anitya = 2503;
- version# = "3.128";
+ version# = "3.131";
source = remoteGitHub {
suffix = "nss-dev/nss";
tag = "NSS_"+join {
@@ -14,7 +14,7 @@ package nss-cacert {
};
sep = "_";
}+"_RTM";
- checksum = "uVP9Tbv17Vo7tUAm1LK16vu-9PA4Mecjc1Kr6y4ptXby9e0zT7I15rzsD2fMApqA";
+ checksum = "fQZ1hbSQyeqc2uK1bx4CeYPb3nb1t4M40D3_JXOJ0WO6a0QvMtddn2jQjKmnmCKD";
};
enterSource = true;
diff --git a/internal/rosa/package/openssl.az b/internal/rosa/package/openssl.az
index cb76d0fa..eb2f28fa 100644
--- a/internal/rosa/package/openssl.az
+++ b/internal/rosa/package/openssl.az
@@ -5,14 +5,12 @@ package openssl {
// strange malformed tags treated as pre-releases in Anitya
latest = anityaFallback;
- block = "python";
-
- version# = "3.6.2";
+ version# = "4.0.3";
source = remoteGitHubRelease {
suffix = "openssl/openssl";
tag = "openssl-"+version;
name = "openssl-"+version+".tar.gz";
- checksum = "jH004dXTiE01Hp0kyShkWXwrSHEksZi4i_3v47D9H9Uz9LQ1aMwF7mrl2Tb4t_XA";
+ checksum = "qRm2EpUnLqKBgUFimF2uqjHZJaUgK-AWTuX9DVHhnowvCdm09p0wIn3mPRjXjGxj";
compress = gzip;
};
diff --git a/internal/rosa/package/pcre2.az b/internal/rosa/package/pcre2.az
index 85f603a9..1df3fdad 100644
--- a/internal/rosa/package/pcre2.az
+++ b/internal/rosa/package/pcre2.az
@@ -3,12 +3,12 @@ package pcre2 {
website = "https://pcre2project.github.io/pcre2";
anitya = 5832;
- version# = "10.48";
+ version# = "10.49";
source = remoteGitHubRelease {
suffix = "PCRE2Project/pcre2";
tag = "pcre2-"+version;
name = "pcre2-"+version+".tar.bz2";
- checksum = "3HorftFDryXBh8JLSbEVWpU3OtSF_wUhtNZNCRvCDiGlBsMU150CxrDFKE8lVo6x";
+ checksum = "3vFs-Cz17RWuneRKbt76ZTGgyqxMWDm9gXV9P7eIh4IaTYgyi2XTvdU3dAeJrB7T";
compress = bzip2;
};
diff --git a/internal/rosa/package/pipewire.az b/internal/rosa/package/pipewire.az
index 06153a9f..230755fd 100644
--- a/internal/rosa/package/pipewire.az
+++ b/internal/rosa/package/pipewire.az
@@ -3,12 +3,12 @@ package wireplumber {
website = "https://gitlab.freedesktop.org/pipewire/wireplumber";
anitya = 235056;
- version# = "0.5.17";
+ version# = "0.5.18";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "pipewire/wireplumber";
ref = version;
- checksum = "2vWxpOqnQmOlgMlNLYMu9ebPbF2y3W_eqDFM_bwAtCwnHhYE0S4kM8MkpFuXyVlu";
+ checksum = "YsKFuMxDJQtwnR0N7h7KA7fNR866k7XhCiSdn3L30Wn1kU7mTV66V8eHHL9dZ5sZ";
};
// OSError: [Errno 30] Read-only file system: '../../usr/src/wireplumber/po/conf.pot'
@@ -37,12 +37,12 @@ package pipewire {
website = "https://pipewire.org";
anitya = 57357;
- version# = "1.6.8";
+ version# = "1.6.9";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "pipewire/pipewire";
ref = version;
- checksum = "KPPJDODFi6RpNpoGztQGNTiINRubQ_7WVIngsboHQi0XzEhMAKVp7iaUG74QX3aE";
+ checksum = "EtqI-7Gwgp_6rHRqVlhuGAt39vOlX_SAoUWzaeeCaUqX4X6feCQvqmSwQvzR88Dr";
};
exec = meson {
diff --git a/internal/rosa/package/pkgconf.az b/internal/rosa/package/pkgconf.az
index 1ff20388..644c916d 100644
--- a/internal/rosa/package/pkgconf.az
+++ b/internal/rosa/package/pkgconf.az
@@ -3,11 +3,11 @@ package pkgconf {
website = "https://github.com/pkgconf/pkgconf";
anitya = 12753;
- version# = "3.0.7";
+ version# = "3.0.9";
source = remoteGitHub {
suffix = "pkgconf/pkgconf";
tag = "pkgconf-"+version;
- checksum = "_IpcXySlbiPfFIS95oAg9C65_wV3BAoLD_QVP2OHx4MNTgdndsgT_WjtJkCbhlEd";
+ checksum = "uGY_4freweuu58B5ftn9b9fmvND_BN1B6zCmmnFh2nO14aGZ98tCa3qAWakLF3q3";
};
exec = meson {
diff --git a/internal/rosa/package/python/package.az b/internal/rosa/package/python/package.az
index aef3ecda..fa12b203 100644
--- a/internal/rosa/package/python/package.az
+++ b/internal/rosa/package/python/package.az
@@ -6,11 +6,11 @@ package python {
// too many inputs; early variant available
bootstrap = false;
- version# = "3.14.7";
+ version# = "3.15.0";
source = remoteTar {
url = "https://www.python.org/ftp/python/"+version+
"/Python-"+version+".tgz";
- checksum = "hXLV7_BGEFZvQ04DNIlk5IdoRZd9DkQt4abKhc8rWsLXHTeSboAWTewJ6l0-LxVz";
+ checksum = "SHy8bsXI1zLONLJHIK6gEJUiPjdUcRLzI9PjQnUfXOdL6Jcy0RQFEOyhMkp9vonP";
compress = gzip;
};
patches = [ "zipfile-no-default-strict_timestamps.patch" ];
@@ -41,6 +41,9 @@ package python {
// breaks on llvm
"test_dbm_gnu",
+
+ // broken on almost everything other than amd64
+ "test_c_stack_unwind",
];
sep = " -x ";
},
@@ -167,6 +170,7 @@ package python-wheel {
tag = version;
checksum = "h-UZQHS-NwB3b9YxYpJePR5cbDGZaKxz4bdhYjilWdQXAE46SV7A1CwZKzZcuZFh";
};
+ patches = [ "wheel-test-colour.patch" ];
exec = pip {
install = true;
@@ -182,12 +186,13 @@ package python-vcs-versioning {
description = "core VCS versioning functionality extracted as a standalone library";
website = "https://setuptools-scm.readthedocs.io/en/latest";
anitya = 389421;
+ block = "frequent ineffectual updates; questionable maintainer decisions";
- version# = "2.3.4";
+ version# = "2.4.0";
source = remoteGitHub {
suffix = "pypa/setuptools-scm";
tag = "vcs-versioning-v"+version;
- checksum = "TljjSQUxo07jMzfaVyieXD8dS_olgwEA2_RndGeIjlsdKWSa2oYZ5laMRjdgnXJS";
+ checksum = "Ziau4BBAwjQVEVe-MlKpA-27nqOm30LlGfHG5SqIrqKbCABWBndCwzpXLxdnZuGn";
};
env = [
@@ -211,12 +216,13 @@ package python-setuptools-scm {
description = "extracts Python package versions from Git or Mercurial metadata";
website = "https://setuptools-scm.readthedocs.io/en/latest";
anitya = 7874;
+ block = "see package python-vcs-versioning";
- version# = "10.2.3";
+ version# = "10.3.2";
source = remoteGitHub {
suffix = "pypa/setuptools-scm";
tag = "setuptools-scm-v"+version;
- checksum = "C76_jiIc_XanEZrpxLb47B3YArtmlt7HcpEOo0wZ5Z4mCFJb2HLIgOSOiRSfttpW";
+ checksum = "wnOU9_VeAiNp881loQiJD3KuWgbufNXhQKqHOGBdt9oEjIMXosHjUseE67fW4ips";
};
env = [
@@ -248,11 +254,11 @@ package python-flit-core {
website = "https://flit.pypa.io";
anitya = 44841;
- version# = "4.0.2";
+ version# = "4.1.0";
source = remoteGitHub {
suffix = "pypa/flit";
tag = version;
- checksum = "LAYWMeQ2wf2fUavUctRHlEVCUUfsVu0so4d0DZh-DAkAvd8kf9ZHQd-o7dXr3tq6";
+ checksum = "c-iUN-XyUB-1jF4CYaQaMNa5W0G4c3UcpjQcwSI9KOZFQ_my3ebpPjBvfVchjywC";
};
exec = pip {
@@ -310,11 +316,11 @@ package python-trove-classifiers {
website = "https://pypi.org/p/trove-classifiers";
anitya = 88298;
- version# = "2026.6.1.19";
+ version# = "2026.9.21.13";
source = remoteGitHub {
suffix = "pypa/trove-classifiers";
tag = version;
- checksum = "zdDI6XTfzuFnlhdhilnhplItbLhZ9f2_vSHZP8u5Zvhu5_rhTB73SFEzxT5gb1bO";
+ checksum = "SUj49aPO71eUQFxBE-G_BuJezwEUfeYCv9RbWFFsyTGmfbLGsZelEvqXGHErX-sZ";
};
exec = pip {
@@ -330,11 +336,11 @@ package python-pluggy {
website = "https://pluggy.readthedocs.io/en/latest";
anitya = 7500;
- version# = "1.6.0";
+ version# = "1.7.0";
source = remoteGitHub {
suffix = "pytest-dev/pluggy";
tag = version;
- checksum = "GiUgDkKjF8Xn1cmq6iMhTGXzcPIYeaJrvQpHBSAJapNVx4UyuiTXqd5eVlxSClJu";
+ checksum = "KukKDsVCm3aO-eMHI0fN2LCN5qU8wVUo-95T7MuRf_DOmXRNDiIuaDeWrIQxd0-h";
};
env = [
@@ -354,11 +360,11 @@ package python-poetry-core {
website = "https://github.com/python-poetry/poetry-core";
anitya = 71155;
- version# = "2.4.1";
+ version# = "2.5.0";
source = remoteGitHub {
suffix = "python-poetry/poetry-core";
tag = version;
- checksum = "tf9OeSUv9TJwKd48fQIU2uGtZ36GN-PDmPDZzjClozzLjv-rQ5nIbOg9gOXpegiH";
+ checksum = "FZCJeScQLZBm2W1JiqgPozV2QKxkU0Dr0tQDsFf-H3PNswQwxSLPEGgEl94va7r3";
};
exec = pip {
@@ -374,11 +380,11 @@ package python-tomlkit {
website = "https://github.com/python-poetry/tomlkit";
anitya = 19609;
- version# = "0.15.1";
+ version# = "0.15.2";
source = remoteGitHub {
suffix = "python-poetry/tomlkit";
tag = version;
- checksum = "6n8h0Yf-OO0ARildxqyB1mB1EhS7uALtqFJqD7t7G1JSFTTb9RGRWSpCg-vamuu8";
+ checksum = "f7Cwnf-mzMPzRoSNkRikLxQ3TXzlzbx69K7oSpDmWfUwD8rlR-hy3ZLOKuFI2G6i";
};
exec = pip {
@@ -394,11 +400,11 @@ package python-hatchling {
website = "https://hatch.pypa.io";
anitya = 16137;
- version# = "1.18.0";
+ version# = "1.18.1";
source = remoteGitHub {
suffix = "pypa/hatch";
tag = "hatch-v"+version;
- checksum = "a_ntndxBlMdqSVW3PkiAY42m-NDgyWJcKLWFIVTB_5PciVeJdYYdbcdF6jsd0U96";
+ checksum = "MQHbzLg0Vo9Wp8oNwFqzyr6Z8xIqI8uxp1r_sue5r_QMI6Ryfx4wXJWpQQgdmUdJ";
};
exec = pip {
@@ -444,11 +450,11 @@ package python-iniconfig {
website = "https://github.com/pytest-dev/iniconfig";
anitya = 114778;
- version# = "2.3.0";
+ version# = "2.3.1";
source = remoteGitHub {
suffix = "pytest-dev/iniconfig";
tag = "v"+version;
- checksum = "mH7VBZaXcYatBPE3RQQZvSzz_Ay8IPPek60NpPHZulPq4ReAFUUsA4EPWfiyMknZ";
+ checksum = "_kAzeTxeje5L7WYtFwMGOx-Qv2pfErrBvoajcz1JovWaeIQ72w94yD5oidld2OxN";
};
env = [
@@ -499,11 +505,11 @@ package python-markupsafe {
website = "https://markupsafe.palletsprojects.com";
anitya = 3918;
- version# = "3.0.3";
+ version# = "3.0.4";
source = remoteGitHub {
suffix = "pallets/markupsafe";
tag = version;
- checksum = "txRGYdWE3his1lHHRI-lZADw0-ILvUg2l5OGdFHtFXIb_QowGxwdxHCUSJIgmjQs";
+ checksum = "V0lPnpENthlPMRq08uHN03BAeqHeZfi_tKgn9oUGGKrz9yMfcgeoUmhP3JT1uhlB";
};
exec = pip {
@@ -519,7 +525,7 @@ package python-mako {
website = "https://www.makotemplates.org";
anitya = 3915;
- version# = "1.4.1";
+ version# = "1.4.3";
source = remoteGitHub {
suffix = "sqlalchemy/mako";
tag = "rel_"+join {
@@ -530,7 +536,7 @@ package python-mako {
};
sep = "_";
};
- checksum = "OYVVx9PxvZLSvA1C-lX-xElq5TvThYRElzDDKUHYU1tZMM7XlCtHSMGmdgqlbYxd";
+ checksum = "-7wYBvuaMaRtWEPlPbIyfrrVSJWsuNnVr1rCwaXnwNNvyap3R9kL282cLYidO_LX";
};
early = "rm test/test_exceptions.py";
@@ -568,11 +574,11 @@ package python-pycparser {
website = "https://github.com/eliben/pycparser";
anitya = 8175;
- version# = "3.00";
+ version# = "3.11";
source = remoteGitHub {
suffix = "eliben/pycparser";
tag = "release_v"+version;
- checksum = "4qfCMFKp0fLsRsloOAF780tXX_Ce_68RwinCmjNGObAX32WpF_iBafIKW1S1bYlA";
+ checksum = "MjkTN3iAR3UglDqTHUjGozSowe7i0QJWBmaRYygYCH1TYlfFYyCaLx1LKG-Zf5_l";
};
// test case hard codes gcc
diff --git a/internal/rosa/package/python/wheel-test-colour.patch b/internal/rosa/package/python/wheel-test-colour.patch
new file mode 100644
index 00000000..db399fdc
--- /dev/null
+++ b/internal/rosa/package/python/wheel-test-colour.patch
@@ -0,0 +1,21 @@
+diff --git a/tests/commands/util.py b/tests/commands/util.py
+index 5d139d9..d18d103 100644
+--- a/tests/commands/util.py
++++ b/tests/commands/util.py
+@@ -2,7 +2,7 @@ from __future__ import annotations
+
+ import sys
+ from io import StringIO
+-from os import PathLike
++from os import PathLike, environ
+ from subprocess import CalledProcessError
+ from unittest.mock import patch
+
+@@ -10,6 +10,7 @@ import pytest
+
+ from wheel._commands import main
+
++environ["NO_COLOR"] = "1"
+
+ def run_command(
+ command: str, *args: str | PathLike, catch_systemexit: bool = True
diff --git a/internal/rosa/package/qemu/package.az b/internal/rosa/package/qemu/package.az
index 3e7b8fcf..7252c4d8 100644
--- a/internal/rosa/package/qemu/package.az
+++ b/internal/rosa/package/qemu/package.az
@@ -3,10 +3,10 @@ package qemu {
website = "https://www.qemu.org";
anitya = 13607;
- version# = "11.1.1";
+ version# = "11.1.2";
source = remoteTar {
url = "https://download.qemu.org/qemu-"+version+".tar.bz2";
- checksum = "zvoYXpZGfnaD1bo2fD44_GfViIVCCH1iSH0xjy9VAB7Ta1X5AzBtnMnbalc_WEmm";
+ checksum = "PA9KeH0ifsIUJDTEq0_zLiysF9nlQXUTQsEx7cRzJiC1P4-u-AKCD-uanbyVASMJ";
compress = bzip2;
};
patches = [
diff --git a/internal/rosa/package/spirv.az b/internal/rosa/package/spirv.az
index 79ac5a8d..8cfb33f7 100644
--- a/internal/rosa/package/spirv.az
+++ b/internal/rosa/package/spirv.az
@@ -5,11 +5,11 @@ package spirv-headers {
// upstream changed version scheme, anitya incapable of filtering them
latest = anityaFilterSPIRV;
- version# = "1.4.357.0";
+ version# = "1.4.363.0";
source = remoteGitHub {
suffix = "KhronosGroup/SPIRV-Headers";
tag = "vulkan-sdk-"+version;
- checksum = "KKhVra3IQ4DcciWO7J7Vphhyw-Lb9lD1ZCjcg1AIIwsY04j1DeSIrXiVfCF67juc";
+ checksum = "cngyFYwADy0Hbe96bJTunvGJh3Xj_Oa3dEfUkKpVxrBLBnpNQY_umcGMbUccP7MC";
};
exec = cmake {
@@ -23,11 +23,11 @@ package spirv-tools {
website = "https://github.com/KhronosGroup/SPIRV-Tools";
anitya = 334920;
- version# = "1.4.357.0";
+ version# = "1.4.363.0";
source = remoteGitHub {
suffix = "KhronosGroup/SPIRV-Tools";
tag = "vulkan-sdk-"+version;
- checksum = "yCZ92UX21pRFjVoySWiKAUermsE3A69dkeqf_8OPVn_1MxbaP5EAcKnHf27W8eYG";
+ checksum = "YxfqYZtOIGgy_SuO6xadbGIUomunSxSVkShFx3qYj-_A4sGyfmbtFtqt6qYVYDeL";
};
exec = cmake {
@@ -45,11 +45,11 @@ package spirv-llvm-translator {
website = "https://github.com/KhronosGroup/SPIRV-LLVM-Translator";
anitya = 227273;
- version# = "23.1.1";
+ version# = "23.1.2";
source = remoteGitHub {
suffix = "KhronosGroup/SPIRV-LLVM-Translator";
tag = "v"+version;
- checksum = "8Sr087sQcaGw1yOodbRQAOv5bBj_QpxbOWGN75xxKqhRcuAUvdPZ9a009nlXZRwK";
+ checksum = "6t7WCbw6xTVHF70ktDFCttrN9IO0UD4SdBiDQHtQShU7-laowtiRmXhVtRaQIhdC";
};
// litArgs emits shell syntax
diff --git a/internal/rosa/package/sway/package.az b/internal/rosa/package/sway/package.az
index cd7f1f7f..20834397 100644
--- a/internal/rosa/package/sway/package.az
+++ b/internal/rosa/package/sway/package.az
@@ -11,6 +11,7 @@ package sway {
};
patches = [ "remove-unused.patch" ];
+ env = [ "CFLAGS=-Wno-switch" ];
exec = meson {};
inputs = [
diff --git a/internal/rosa/package/tamago.az b/internal/rosa/package/tamago.az
index d784c85a..c4393b13 100644
--- a/internal/rosa/package/tamago.az
+++ b/internal/rosa/package/tamago.az
@@ -3,11 +3,11 @@ package tamago {
website = "https://github.com/usbarmory/tamago-go";
anitya = 388872;
- version# = "1.27.1";
+ version# = "1.27.2";
source = remoteGitHub {
suffix = "usbarmory/tamago-go";
tag = "tamago-go"+version;
- checksum = "TaWPzqqBMuhGslUm9CQvShJRjfONbFEr51mcM5zFlopYhYA6Tb58g8wRPc5a5lQ0";
+ checksum = "z-4tuciBpbu4cR4hwkRemUcxWUgfLpfc8shIFDjUAdthxWMv9FYkwCfTta0Ho_4-";
};
env = [
diff --git a/internal/rosa/package/toybox.az b/internal/rosa/package/toybox.az
index bd3dc966..b90e08d2 100644
--- a/internal/rosa/package/toybox.az
+++ b/internal/rosa/package/toybox.az
@@ -2,10 +2,10 @@ package toybox-source {
description = "toybox source tree";
exclude = true;
- version# = "0.8.14";
+ version# = "0.8.15";
output = remoteTar {
url = "https://landley.net/toybox/downloads/toybox-"+version+".tar.gz";
- checksum = "RZQp2CTsLt_y15vsZxwqUb2O1XfK7uvwn-2sTd38O4HAsFKPQpS1UP0brYJ3dRA-";
+ checksum = "-0KgQ7Ogk5Nt6LLTn08b_xPFZXi4aqoBzw7XPnUwP3r2AZcfPOV10rMB0qAEzXcI";
compress = gzip;
};
}
diff --git a/internal/rosa/package/utf8proc.az b/internal/rosa/package/utf8proc.az
index 68e904ec..a881c493 100644
--- a/internal/rosa/package/utf8proc.az
+++ b/internal/rosa/package/utf8proc.az
@@ -3,11 +3,11 @@ package utf8proc {
website = "https://juliastrings.github.io/utf8proc";
anitya = 7455;
- version# = "2.11.3";
+ version# = "2.12.0";
source = remoteGitHub {
suffix = "JuliaStrings/utf8proc";
tag = "v"+version;
- checksum = "LlmejkymQEhaMciSE17JSl3pAYdf6DdXNr0ucwclClsMCeq7l1dcgIJl1JmNTQVq";
+ checksum = "kfzU-vVmmCzyYO9CO-PGiX5Oq63f6o_Zv0G3jO4gxoyCP2qZap20Hh7qAUDMeTKo";
};
exec = cmake {
diff --git a/internal/rosa/package/util-linux/package.az b/internal/rosa/package/util-linux.az
index 1317e50f..8beff173 100644
--- a/internal/rosa/package/util-linux/package.az
+++ b/internal/rosa/package/util-linux.az
@@ -5,7 +5,7 @@ package util-linux {
// release candidates confuse Anitya
latest = anityaFallback;
- version# = "2.42.3";
+ version# = "2.42.4";
source = remoteTar {
url = "https://www.kernel.org/pub/linux/utils/util-linux/"+
"v"+join {
@@ -19,10 +19,9 @@ package util-linux {
};
sep = ".";
}+"/util-linux-"+version+".tar.gz";
- checksum = "VQ3NCM4kt0-TTrdJElBfckXNCGBv6UfXGTRm1poQ0XP0WBND4eG-O7n5Ly_a-Iw_";
+ checksum = "tDzZiC8DCtH3f-QA9uI2EUKMQ4m5nPZfxVMuOsPBB2dHQS9gJ2HqVTJzRbq7p7VC";
compress = gzip;
};
- patches = [ "a323dddbcd1ed05a10e7e870b3e1a48b4ed44a43.patch" ];
bin = [ "bash" ];
exec = make {
diff --git a/internal/rosa/package/util-linux/a323dddbcd1ed05a10e7e870b3e1a48b4ed44a43.patch b/internal/rosa/package/util-linux/a323dddbcd1ed05a10e7e870b3e1a48b4ed44a43.patch
deleted file mode 100644
index 36d6f968..00000000
--- a/internal/rosa/package/util-linux/a323dddbcd1ed05a10e7e870b3e1a48b4ed44a43.patch
+++ /dev/null
@@ -1,35 +0,0 @@
-From a323dddbcd1ed05a10e7e870b3e1a48b4ed44a43 Mon Sep 17 00:00:00 2001
-From: Karel Zak <kzak@redhat.com>
-Date: Wed, 2 Sep 2026 13:32:27 +0200
-Subject: [PATCH] libmount: add missing fileutils.h include to hook_idmap.c
-
-The hook_idmap.c uses RESOLVE_NO_SYMLINKS (added by commit fb8e26535)
-but does not include fileutils.h, which provides the fallback #define
-for this constant.
-
-On Fedora (glibc 2.40+), this is masked because glibc's
-<bits/fcntl-linux.h> transitively includes <linux/openat2.h>, which
-defines RESOLVE_NO_SYMLINKS. On Ubuntu (and other distros with older
-glibc), <fcntl.h> does not pull in openat2.h, so the build fails:
-
- hook_idmap.c:335:33: error: 'RESOLVE_NO_SYMLINKS' undeclared
-
-Fixes: fb8e26535 ("libmount: pin source path with openat2() for restricted users")
-Signed-off-by: Karel Zak <kzak@redhat.com>
-(cherry picked from commit 7e2e010874b10b3aabdc3c4c844c9ffc46a4a374)
----
- libmount/src/hook_idmap.c | 1 +
- 1 file changed, 1 insertion(+)
-
-diff --git a/libmount/src/hook_idmap.c b/libmount/src/hook_idmap.c
-index 77494e29810..2c697b17154 100644
---- a/libmount/src/hook_idmap.c
-+++ b/libmount/src/hook_idmap.c
-@@ -23,6 +23,7 @@
-
- #include "strutils.h"
- #include "all-io.h"
-+#include "fileutils.h"
- #include "namespace.h"
-
- #include "mountP.h"
diff --git a/internal/rosa/package/x.az b/internal/rosa/package/x.az
index 73b82981..44af6d22 100644
--- a/internal/rosa/package/x.az
+++ b/internal/rosa/package/x.az
@@ -48,12 +48,12 @@ package xorgproto {
website = "https://gitlab.freedesktop.org/xorg/proto/xorgproto";
anitya = 17190;
- version# = "2025.1";
+ version# = "2026.1";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "xorg/proto/xorgproto";
ref = "xorgproto-"+version;
- checksum = "pTwJiBJHKA6Rgm3cVDXy1lyvXNIUzTRaukvvYdk1xWoJ_1G-Dfjm9MyewuyIjoHz";
+ checksum = "-inAZyzcTjFH1wfc2Zw_mdfx9K1YG0ieQ8kF5u3ZS1QykAavZ0T238EjljRzEYZ-";
};
exec = make {
@@ -242,12 +242,12 @@ package libX11 {
website = "https://gitlab.freedesktop.org/xorg/lib/libx11";
anitya = 1764;
- version# = "1.8.13";
+ version# = "1.8.14";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "xorg/lib/libx11";
ref = "libX11-"+version;
- checksum = "ARh-cuZY_U2v3DbPS1byc7ybh9NInZc-yav7SJiusk_C7408s058qWV83ocMd2pT";
+ checksum = "9huww9V6fAjKhiG7cWpV-zIQ8aTt9JiuO7qf0yuiLvU4B_5d6XA2y2GvOBQ06XAZ";
};
exec = make {
@@ -337,12 +337,12 @@ package libXrender {
website = "https://gitlab.freedesktop.org/xorg/lib/libxrender";
anitya = 1789;
- version# = "0.9.12";
+ version# = "0.9.13";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "xorg/lib/libxrender";
ref = "libXrender-"+version;
- checksum = "s6nWn-r17X_W-7anaS7wIcq5_Levf9zAigwJDD9Klp-cCJTVDhLo7GufuVOrxVP0";
+ checksum = "RGzBPut1QMqP5nY2cn0tybZAvakvaOXPM_DPRgNox9PT_8ZLycfxHQZzJhilzCI5";
};
exec = make {
@@ -676,12 +676,12 @@ package xserver {
website = "https://gitlab.freedesktop.org/xorg/xserver";
anitya = 5250;
- version# = "26.0.99.901";
+ version# = "26.0.99.903";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "xorg/xserver";
ref = "xorg-server-"+version;
- checksum = "2Liwqj-tu_NuD8BuWZR0PyYVJ_A0nj_sgx-LZWtdVpKAKQHtN-dSWCl-IPVOlcHG";
+ checksum = "YVT4TT4pI-N8cEPLty3MuJsy6w7K-EqrIJ9jp8c6AzV4PjN8ZYr6HdeHO5MnTsjF";
};
exec = meson {
@@ -753,12 +753,12 @@ package xwayland {
website = "https://gitlab.freedesktop.org/xorg/xserver";
anitya = 180949;
- version# = "26.0.99.901";
+ version# = "26.0.99.903";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "xorg/xserver";
ref = "xwayland-"+version;
- checksum = "Vgtnwme2HU3CCeLE_xNScV_uJ55kPMremaOyVWRUFaRpwpaOQxsNB2fde_9Rx5zm";
+ checksum = "TGto_v7iLObFrQ6tsS9OHR2sJlpeYp1akqiwVnn51dZ73kF_sbWZ8Zm0HiGwIfPV";
};
bin = [ "bash" ];
diff --git a/internal/rosa/package/xz.az b/internal/rosa/package/xz.az
index 0a186c28..3bb91b5f 100644
--- a/internal/rosa/package/xz.az
+++ b/internal/rosa/package/xz.az
@@ -3,12 +3,12 @@ package xz {
website = "https://tukaani.org/xz";
anitya = 5277;
- version# = "5.8.3";
+ version# = "5.8.4";
source = remoteGitHubRelease {
suffix = "tukaani-project/xz";
tag = "v"+version;
name = "xz-"+version+".tar.bz2";
- checksum = "nCdayphPGdIdVoAZ2hR4vYlhDG9LeVKho_i7ealTud4Vxy5o5dWe0VwFlN7utuUL";
+ checksum = "sYLUjO5X0QREmCBYceLU3CSj-HUCd3g0kJAHq8F2GO0Y6-LcyKYbESyRq4sr7Bih";
compress = bzip2;
};
diff --git a/internal/rosa/state.go b/internal/rosa/state.go
index f2ba2059..01abf736 100644
--- a/internal/rosa/state.go
+++ b/internal/rosa/state.go
@@ -1339,6 +1339,30 @@ func (s *S) RegisterFS(fsys fs.FS) error {
return nil
}
+// setSource overrides the hakurei-source package with the specified tarball and
+// version string. The resulting IR is curable on the daemon. Must not be used
+// concurrently with any other method.
+func (s *S) setSource(p []byte, version string) {
+ const name = "hakurei-source"
+ s.p.Store(
+ H(name),
+ Artifact(func(t Toolchain) (*Metadata, pkg.Artifact) {
+ return &Metadata{
+ Name: name,
+ Description: "hakurei source tree (current)",
+ Version: version,
+ Exclude: true,
+ }, pkg.NewTar(
+ pkg.NewDecompress(
+ pkg.NewFile("hakurei-src-current.tar.gz", p),
+ pkg.Gzip,
+ ),
+ )
+ }),
+ )
+ s.DropCaches(s.Arch(), s.Flags())
+}
+
// SetSource overrides the hakurei-source package with a cached tarball of fsys.
// The resulting IR is curable on the daemon. Must not be used concurrently with
// any other method.
@@ -1416,20 +1440,7 @@ func (s *S) SetSource(fsys fs.FS) error {
return err
}
- const name = "hakurei-source"
- a := pkg.NewFile("hakurei-src-current.tar.gz", buf.Bytes())
- s.p.Store(
- H(name),
- Artifact(func(t Toolchain) (*Metadata, pkg.Artifact) {
- return &Metadata{
- Name: name,
- Description: "hakurei source tree (current)",
- Version: version,
- Exclude: true,
- }, pkg.NewTar(pkg.NewDecompress(a, pkg.Gzip))
- }),
- )
- s.DropCaches(s.Arch(), s.Flags())
+ s.setSource(buf.Bytes(), version)
return nil
}
diff --git a/internal/system/acl.go b/internal/system/acl.go
index e91f4eb7..7d34fa62 100644
--- a/internal/system/acl.go
+++ b/internal/system/acl.go
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"os"
- "slices"
"hakurei.app/check"
"hakurei.app/hst"
@@ -12,14 +11,14 @@ import (
)
// UpdatePerm calls UpdatePermType with the [Process] criteria.
-func (sys *I) UpdatePerm(path *check.Absolute, perms ...acl.Perm) *I {
- sys.UpdatePermType(Process, path, perms...)
+func (sys *I) UpdatePerm(path *check.Absolute, perms acl.Perm) *I {
+ sys.UpdatePermType(Process, path, perms)
return sys
}
// UpdatePermType maintains [acl.Perms] on a file until its [hst.Enablements] is
// no longer satisfied.
-func (sys *I) UpdatePermType(et hst.Enablements, path *check.Absolute, perms ...acl.Perm) *I {
+func (sys *I) UpdatePermType(et hst.Enablements, path *check.Absolute, perms acl.Perm) *I {
sys.ops = append(sys.ops, &aclUpdateOp{et, path.String(), perms})
return sys
}
@@ -28,20 +27,20 @@ func (sys *I) UpdatePermType(et hst.Enablements, path *check.Absolute, perms ...
type aclUpdateOp struct {
et hst.Enablements
path string
- perms acl.Perms
+ perms acl.Perm
}
func (a *aclUpdateOp) Type() hst.Enablements { return a.et }
func (a *aclUpdateOp) apply(sys *I) error {
sys.msg.Verbose("applying ACL", a)
- return newOpError("acl", sys.aclUpdate(a.path, sys.uid, a.perms...), false)
+ return newOpError("acl", sys.aclUpdate(a.path, sys.uid, a.perms), false)
}
func (a *aclUpdateOp) revert(sys *I, ec *Criteria) error {
if ec.hasType(a.Type()) {
sys.msg.Verbose("stripping ACL", a)
- err := sys.aclUpdate(a.path, sys.uid)
+ err := sys.aclUpdate(a.path, sys.uid, 0)
if errors.Is(err, os.ErrNotExist) {
// the ACL is effectively stripped if the file no longer exists
sys.msg.Verbosef("target of ACL %s no longer exists", a)
@@ -57,9 +56,7 @@ func (a *aclUpdateOp) revert(sys *I, ec *Criteria) error {
func (a *aclUpdateOp) Is(o Op) bool {
target, ok := o.(*aclUpdateOp)
return ok && a != nil && target != nil &&
- a.et == target.et &&
- a.path == target.path &&
- slices.Equal(a.perms, target.perms)
+ *a == *target
}
func (a *aclUpdateOp) Path() string { return a.path }
diff --git a/internal/system/acl_test.go b/internal/system/acl_test.go
index 01f9d9b1..a340e234 100644
--- a/internal/system/acl_test.go
+++ b/internal/system/acl_test.go
@@ -15,45 +15,45 @@ func TestACLUpdateOp(t *testing.T) {
checkOpBehaviour(t, 0, []opBehaviourTestCase{
{"apply aclUpdate", 0xbeef, 0xff,
- &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
- call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(1)),
+ &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil),
+ call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, stub.UniqueError(1)),
}, &OpError{Op: "acl", Err: stub.UniqueError(1)}, nil, nil},
{"revert aclUpdate", 0xbeef, 0xff,
- &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
- call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
+ &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil),
+ call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, nil),
}, nil, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
- call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, ([]acl.Perm)(nil)}, nil, stub.UniqueError(0)),
+ call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil),
+ call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Perm(0)}, nil, stub.UniqueError(0)),
}, &OpError{Op: "acl", Err: stub.UniqueError(0), Revert: true}},
{"success revert skip", 0xbeef, Process,
- &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
- call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
+ &aclUpdateOp{User, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{User, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil),
+ call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, nil),
}, nil, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"skipping ACL", &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"skipping ACL", &aclUpdateOp{User, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil),
}, nil},
{"success revert aclUpdate ENOENT", 0xbeef, 0xff,
- &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
- call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
+ &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil),
+ call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, nil),
}, nil, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
- call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, ([]acl.Perm)(nil)}, nil, &os.PathError{Op: "acl_get_file", Path: "/proc/nonexistent", Err: syscall.ENOENT}),
- call("verbosef", stub.ExpectArgs{"target of ACL %s no longer exists", []any{&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil),
+ call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Perm(0)}, nil, &os.PathError{Op: "acl_get_file", Path: "/proc/nonexistent", Err: syscall.ENOENT}),
+ call("verbosef", stub.ExpectArgs{"target of ACL %s no longer exists", []any{&aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil),
}, nil},
{"success", 0xbeef, 0xff,
- &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
- call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
+ &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil),
+ call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, nil),
}, nil, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
- call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, ([]acl.Perm)(nil)}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil),
+ call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Perm(0)}, nil, nil),
}, nil},
})
@@ -63,28 +63,28 @@ func TestACLUpdateOp(t *testing.T) {
func(_ *testing.T, sys *I) {
sys.
UpdatePerm(m("/run/user/1971/hakurei"), acl.Execute).
- UpdatePerm(m("/tmp/hakurei.0/tmpdir/150"), acl.Read, acl.Write, acl.Execute)
+ UpdatePerm(m("/tmp/hakurei.0/tmpdir/150"), acl.Read|acl.Write|acl.Execute)
}, []Op{
- &aclUpdateOp{Process, "/run/user/1971/hakurei", []acl.Perm{acl.Execute}},
- &aclUpdateOp{Process, "/tmp/hakurei.0/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
+ &aclUpdateOp{Process, "/run/user/1971/hakurei", acl.Execute},
+ &aclUpdateOp{Process, "/tmp/hakurei.0/tmpdir/150", acl.Read | acl.Write | acl.Execute},
}, stub.Expect{}},
{"tmpdirp", 0xbeef, func(_ *testing.T, sys *I) {
sys.UpdatePermType(User, m("/tmp/hakurei.0/tmpdir"), acl.Execute)
}, []Op{
- &aclUpdateOp{User, "/tmp/hakurei.0/tmpdir", []acl.Perm{acl.Execute}},
+ &aclUpdateOp{User, "/tmp/hakurei.0/tmpdir", acl.Execute},
}, stub.Expect{}},
{"tmpdir", 0xbeef, func(_ *testing.T, sys *I) {
- sys.UpdatePermType(User, m("/tmp/hakurei.0/tmpdir/150"), acl.Read, acl.Write, acl.Execute)
+ sys.UpdatePermType(User, m("/tmp/hakurei.0/tmpdir/150"), acl.Read|acl.Write|acl.Execute)
}, []Op{
- &aclUpdateOp{User, "/tmp/hakurei.0/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
+ &aclUpdateOp{User, "/tmp/hakurei.0/tmpdir/150", acl.Read | acl.Write | acl.Execute},
}, stub.Expect{}},
{"share", 0xbeef, func(_ *testing.T, sys *I) {
sys.UpdatePermType(Process, m("/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5"), acl.Execute)
}, []Op{
- &aclUpdateOp{Process, "/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5", []acl.Perm{acl.Execute}},
+ &aclUpdateOp{Process, "/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5", acl.Execute},
}, stub.Expect{}},
{"passwd", 0xbeef, func(_ *testing.T, sys *I) {
@@ -92,14 +92,14 @@ func TestACLUpdateOp(t *testing.T) {
UpdatePermType(Process, m("/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd"), acl.Read).
UpdatePermType(Process, m("/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group"), acl.Read)
}, []Op{
- &aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd", []acl.Perm{acl.Read}},
- &aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group", []acl.Perm{acl.Read}},
+ &aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd", acl.Read},
+ &aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group", acl.Read},
}, stub.Expect{}},
{"wayland", 0xbeef, func(_ *testing.T, sys *I) {
- sys.UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute)
+ sys.UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read|acl.Write|acl.Execute)
}, []Op{
- &aclUpdateOp{hst.EWayland, "/run/user/1971/wayland-0", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
+ &aclUpdateOp{hst.EWayland, "/run/user/1971/wayland-0", acl.Read | acl.Write | acl.Execute},
}, stub.Expect{}},
})
@@ -110,75 +110,75 @@ func TestACLUpdateOp(t *testing.T) {
{"et differs",
&aclUpdateOp{
hst.EWayland, "/run/user/1971/wayland-0",
- []acl.Perm{acl.Read, acl.Write, acl.Execute},
+ acl.Read | acl.Write | acl.Execute,
}, &aclUpdateOp{
hst.EX11, "/run/user/1971/wayland-0",
- []acl.Perm{acl.Read, acl.Write, acl.Execute},
+ acl.Read | acl.Write | acl.Execute,
}, false},
{"path differs", &aclUpdateOp{
hst.EWayland, "/run/user/1971/wayland-0",
- []acl.Perm{acl.Read, acl.Write, acl.Execute},
+ acl.Read | acl.Write | acl.Execute,
}, &aclUpdateOp{
hst.EWayland, "/run/user/1971/wayland-1",
- []acl.Perm{acl.Read, acl.Write, acl.Execute},
+ acl.Read | acl.Write | acl.Execute,
}, false},
{"perms differs", &aclUpdateOp{
hst.EWayland, "/run/user/1971/wayland-0",
- []acl.Perm{acl.Read, acl.Write, acl.Execute},
+ acl.Read | acl.Write | acl.Execute,
}, &aclUpdateOp{
hst.EWayland, "/run/user/1971/wayland-0",
- []acl.Perm{acl.Read, acl.Write},
+ acl.Read | acl.Write,
}, false},
{"equals", &aclUpdateOp{
hst.EWayland, "/run/user/1971/wayland-0",
- []acl.Perm{acl.Read, acl.Write, acl.Execute},
+ acl.Read | acl.Write | acl.Execute,
}, &aclUpdateOp{
hst.EWayland, "/run/user/1971/wayland-0",
- []acl.Perm{acl.Read, acl.Write, acl.Execute},
+ acl.Read | acl.Write | acl.Execute,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"clear",
- &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{}},
+ &aclUpdateOp{Process, "/proc/nonexistent", 0},
Process, "/proc/nonexistent",
`--- type: process path: "/proc/nonexistent"`},
{"read",
- &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0", []acl.Perm{acl.Read}},
+ &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0", acl.Read},
User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0",
`r-- type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0"`},
{"write",
- &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1", []acl.Perm{acl.Write}},
+ &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1", acl.Write},
User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1",
`-w- type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1"`},
{"execute",
- &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2", []acl.Perm{acl.Execute}},
+ &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2", acl.Execute},
User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2",
`--x type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2"`},
{"wayland",
- &aclUpdateOp{hst.EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland", []acl.Perm{acl.Read, acl.Write}},
+ &aclUpdateOp{hst.EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland", acl.Read | acl.Write},
hst.EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland",
`rw- type: wayland path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland"`},
{"x11",
- &aclUpdateOp{hst.EX11, "/tmp/.X11-unix/X0", []acl.Perm{acl.Read, acl.Execute}},
+ &aclUpdateOp{hst.EX11, "/tmp/.X11-unix/X0", acl.Read | acl.Execute},
hst.EX11, "/tmp/.X11-unix/X0",
`r-x type: x11 path: "/tmp/.X11-unix/X0"`},
{"dbus",
- &aclUpdateOp{hst.EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus", []acl.Perm{acl.Write, acl.Execute}},
+ &aclUpdateOp{hst.EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus", acl.Write | acl.Execute},
hst.EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus",
`-wx type: dbus path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus"`},
{"pulseaudio",
- &aclUpdateOp{hst.EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
+ &aclUpdateOp{hst.EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse", acl.Read | acl.Write | acl.Execute},
hst.EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse",
`rwx type: pulseaudio path: "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse"`},
})
diff --git a/internal/system/dispatcher.go b/internal/system/dispatcher.go
index 8f70b0c0..5a222176 100644
--- a/internal/system/dispatcher.go
+++ b/internal/system/dispatcher.go
@@ -50,7 +50,7 @@ type syscallDispatcher interface {
println(v ...any)
// aclUpdate provides [acl.Update].
- aclUpdate(name string, uid int, perms ...acl.Perm) error
+ aclUpdate(name string, uid int, perms acl.Perm) error
// waylandNew provides [wayland.New].
waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (io.Closer, error)
@@ -85,8 +85,8 @@ func (k direct) remove(name string) error { return os.Remove(na
func (k direct) println(v ...any) { log.Println(v...) }
-func (k direct) aclUpdate(name string, uid int, perms ...acl.Perm) error {
- return acl.Update(name, uid, perms...)
+func (k direct) aclUpdate(name string, uid int, perms acl.Perm) error {
+ return acl.Update(name, uid, perms)
}
func (k direct) waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (io.Closer, error) {
diff --git a/internal/system/dispatcher_test.go b/internal/system/dispatcher_test.go
index 3bf14e75..bfb8df72 100644
--- a/internal/system/dispatcher_test.go
+++ b/internal/system/dispatcher_test.go
@@ -2,11 +2,13 @@ package system
import (
"io"
+ "io/fs"
"log"
"os"
"reflect"
"slices"
"testing"
+ "time"
"unsafe"
"hakurei.app/check"
@@ -206,7 +208,7 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
// InternalNew initialises [I] with a stub syscallDispatcher.
func InternalNew(t *testing.T, want stub.Expect, uid int) (*I, *stub.Stub[syscallDispatcher]) {
k := &kstub{stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} }, want)}
- sys := New(t.Context(), k, uid)
+ sys := New(t.Context(), k, uid, nil)
sys.syscallDispatcher = k
return sys, k.Stub
}
@@ -215,6 +217,17 @@ type kstub struct{ *stub.Stub[syscallDispatcher] }
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
+// modeFi implements the Mode method of [os.FileInfo].
+type modeFi os.FileMode
+
+func (modeFi) Name() string { panic("attempting to call Name") }
+func (modeFi) Size() int64 { panic("attempting to call Size") }
+func (modeFi) ModTime() time.Time { panic("attempting to call ModTime") }
+func (modeFi) Sys() any { panic("attempting to call Sys") }
+
+func (fi modeFi) Mode() fs.FileMode { return fs.FileMode(fi) }
+func (fi modeFi) IsDir() bool { return fs.FileMode(fi).IsDir() }
+
func (k *kstub) stat(name string) (fi os.FileInfo, err error) {
k.Helper()
expect := k.Expects("stat")
@@ -280,7 +293,7 @@ func (k *kstub) println(v ...any) {
}
}
-func (k *kstub) aclUpdate(name string, uid int, perms ...acl.Perm) error {
+func (k *kstub) aclUpdate(name string, uid int, perms acl.Perm) error {
k.Helper()
expect := k.Expects("aclUpdate")
@@ -293,7 +306,7 @@ func (k *kstub) aclUpdate(name string, uid int, perms ...acl.Perm) error {
return expect.Error(
stub.CheckArgReflect(k.Stub, "name", nameVal, 0),
stub.CheckArg(k.Stub, "uid", uid, 1),
- stub.CheckArgReflect(k.Stub, "perms", perms, 2))
+ stub.CheckArg(k.Stub, "perms", perms, 2))
}
func (k *kstub) waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (io.Closer, error) {
diff --git a/internal/system/mkdir.go b/internal/system/mkdir.go
index 81b89d58..ecbfc876 100644
--- a/internal/system/mkdir.go
+++ b/internal/system/mkdir.go
@@ -4,21 +4,33 @@ import (
"errors"
"fmt"
"os"
+ "syscall"
"hakurei.app/check"
"hakurei.app/hst"
)
// Ensure ensures the existence of a directory.
-func (sys *I) Ensure(name *check.Absolute, perm os.FileMode) *I {
- sys.ops = append(sys.ops, &mkdirOp{User, name.String(), perm, false})
+func (sys *I) Ensure(name *check.Absolute, perm os.FileMode, skipChmod bool) *I {
+ sys.ops = append(sys.ops, &mkdirOp{
+ et: User,
+ path: name.String(),
+ perm: perm,
+ skipChmod: skipChmod,
+ })
return sys
}
// Ephemeral ensures the existence of a directory until its [hst.Enablements] is
// no longer satisfied.
-func (sys *I) Ephemeral(et hst.Enablements, name *check.Absolute, perm os.FileMode) *I {
- sys.ops = append(sys.ops, &mkdirOp{et, name.String(), perm, true})
+func (sys *I) Ephemeral(et hst.Enablements, name *check.Absolute, perm os.FileMode, skipChmod bool) *I {
+ sys.ops = append(sys.ops, &mkdirOp{
+ et: et,
+ path: name.String(),
+ perm: perm,
+ ephemeral: true,
+ skipChmod: skipChmod,
+ })
return sys
}
@@ -28,6 +40,7 @@ type mkdirOp struct {
path string
perm os.FileMode
ephemeral bool
+ skipChmod bool
}
func (m *mkdirOp) Type() hst.Enablements { return m.et }
@@ -35,12 +48,21 @@ func (m *mkdirOp) Type() hst.Enablements { return m.et }
func (m *mkdirOp) apply(sys *I) error {
sys.msg.Verbose("ensuring directory", m)
- if err := sys.mkdir(m.path, m.perm); err != nil {
- if !errors.Is(err, os.ErrExist) {
- return newOpError("mkdir", err, false)
+ if fi, err := sys.stat(m.path); err == nil {
+ if !fi.IsDir() {
+ return newOpError("mkdir", syscall.ENOTDIR, false)
+ }
+ if !m.skipChmod && fi.Mode().Perm() != m.perm {
+ // directory exists, ensure mode
+ return newOpError("mkdir", sys.chmod(m.path, m.perm), false)
}
- // directory exists, ensure mode
- return newOpError("mkdir", sys.chmod(m.path, m.perm), false)
+ return nil
+ } else if !errors.Is(err, os.ErrNotExist) {
+ return newOpError("mkdir", err, false)
+ }
+
+ if err := sys.mkdir(m.path, m.perm); err != nil {
+ return newOpError("mkdir", err, false)
} else {
return nil
}
diff --git a/internal/system/mkdir_test.go b/internal/system/mkdir_test.go
index f226a48d..ea79d5b0 100644
--- a/internal/system/mkdir_test.go
+++ b/internal/system/mkdir_test.go
@@ -2,6 +2,7 @@ package system
import (
"os"
+ "syscall"
"testing"
"hakurei.app/internal/stub"
@@ -11,63 +12,78 @@ func TestMkdirOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, 0, []opBehaviourTestCase{
- {"mkdir", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil),
+ {"mkdir", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist),
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, stub.UniqueError(2)),
}, &OpError{Op: "mkdir", Err: stub.UniqueError(2)}, nil, nil},
- {"chmod", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil),
- call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, os.ErrExist),
+ {"not directory", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, modeFi(0700), nil),
+ }, &OpError{Op: "mkdir", Err: syscall.ENOTDIR}, nil, nil},
+
+ {"chmod", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, modeFi(os.ModeDir|0700), nil),
call("chmod", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, stub.UniqueError(1)),
}, &OpError{Op: "mkdir", Err: stub.UniqueError(1)}, nil, nil},
- {"remove", 0xbeef, 0xff, &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ {"remove", 0xbeef, 0xff, &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist),
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
}, nil, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, stub.UniqueError(0)),
}, &OpError{Op: "mkdir", Err: stub.UniqueError(0), Revert: true}},
- {"success exist chmod", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil),
- call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, os.ErrExist),
+ {"success exist chmod", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, modeFi(os.ModeDir|0700), nil),
call("chmod", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
}, nil, nil, nil},
- {"success ensure", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil),
+ {"success exist", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, modeFi(os.ModeDir|0711), nil),
+ }, nil, nil, nil},
+
+ {"success ensure", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist),
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
}, nil, nil, nil},
- {"success skip", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ {"success skip", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist),
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
}, nil, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"skipping ephemeral directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"skipping ephemeral directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
}, nil},
- {"success", 0xbeef, 0xff, &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ {"success", 0xbeef, 0xff, &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist),
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
}, nil, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, nil),
}, nil},
})
checkOpsBuilder(t, "EnsureEphemeral", []opsBuilderTestCase{
{"ensure", 0xcafe, func(_ *testing.T, sys *I) {
- sys.Ensure(m("/tmp/hakurei.0"), 0700)
+ sys.Ensure(m("/tmp/hakurei.0"), 0700, false)
}, []Op{
- &mkdirOp{User, "/tmp/hakurei.0", 0700, false},
+ &mkdirOp{User, "/tmp/hakurei.0", 0700, false, false},
}, stub.Expect{}},
{"ephemeral", 0xcafe, func(_ *testing.T, sys *I) {
- sys.Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711)
+ sys.Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711, false)
}, []Op{
- &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true},
+ &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false},
}, stub.Expect{}},
})
@@ -76,37 +92,37 @@ func TestMkdirOp(t *testing.T) {
{"zero", new(mkdirOp), new(mkdirOp), true},
{"ephemeral differs",
- &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, false},
- &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
+ &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, false, false},
+ &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false},
false},
{"perm differs",
- &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0701, true},
- &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
+ &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0701, true, false},
+ &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false},
false},
{"path differs",
- &mkdirOp{Process, "/tmp/hakurei.1/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
- &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
+ &mkdirOp{Process, "/tmp/hakurei.1/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false},
+ &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false},
false},
{"et differs",
- &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
- &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
+ &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false},
+ &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false},
false},
{"equals",
- &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
- &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
+ &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false},
+ &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false},
true},
})
checkOpMeta(t, []opMetaTestCase{
- {"ensure", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, false},
+ {"ensure", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, false, false},
User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9",
`mode: -rwx------ type: ensure path: "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"`},
- {"ephemeral", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
+ {"ephemeral", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false},
User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9",
`mode: -rwx------ type: user path: "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"`},
})
diff --git a/internal/system/pipewire.go b/internal/system/pipewire.go
index c2d7b396..427013a4 100644
--- a/internal/system/pipewire.go
+++ b/internal/system/pipewire.go
@@ -73,7 +73,7 @@ func (p *pipewireOp) apply(sys *I) (err error) {
return newOpError("pipewire", err, false)
}
- if err = sys.aclUpdate(p.dst.String(), sys.uid, acl.Read, acl.Write, acl.Execute); err != nil {
+ if err = sys.aclUpdate(p.dst.String(), sys.uid, acl.Read|acl.Write|acl.Execute); err != nil {
if closeErr := p.scc.Close(); closeErr != nil {
return newOpError("pipewire", errors.Join(err, closeErr), false)
}
diff --git a/internal/system/pipewire_test.go b/internal/system/pipewire_test.go
index f0ca0b34..5953681e 100644
--- a/internal/system/pipewire_test.go
+++ b/internal/system/pipewire_test.go
@@ -423,7 +423,7 @@ func TestPipeWireOp(t *testing.T) {
call("verbosef", stub.ExpectArgs{"pipewire pathname socket on %q", []any{ignoreValue{}}}, nil, nil),
call("chmod", stub.ExpectArgs{ignoreValue{}, os.FileMode(0)}, nil, nil),
- call("aclUpdate", stub.ExpectArgs{ignoreValue{}, 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
+ call("aclUpdate", stub.ExpectArgs{ignoreValue{}, 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, nil),
}, nil, []stub.Call{
call("verbosef", stub.ExpectArgs{"hanging up pipewire socket on %q", []any{ignoreValue{}}}, nil, nil),
}, nil},
diff --git a/internal/system/system.go b/internal/system/system.go
index 1462f818..340e23ec 100644
--- a/internal/system/system.go
+++ b/internal/system/system.go
@@ -6,7 +6,9 @@ import (
"errors"
"strings"
+ "hakurei.app/check"
"hakurei.app/hst"
+ "hakurei.app/internal/lockedfile"
"hakurei.app/message"
)
@@ -67,14 +69,6 @@ func TypeString(e hst.Enablements) string {
}
}
-// New returns the address of a new [I] targeting uid.
-func New(ctx context.Context, msg message.Msg, uid int) (sys *I) {
- if ctx == nil || msg == nil || uid < 0 {
- panic("invalid call to New")
- }
- return &I{ctx: ctx, msg: msg, uid: uid, syscallDispatcher: direct{}}
-}
-
// An I provides deferred operating system interaction. [I] must not be copied.
// Methods of [I] must not be used concurrently.
type I struct {
@@ -84,15 +78,32 @@ type I struct {
ops []Op
ctx context.Context
- // the behaviour of Commit is only defined for up to one call
- committed bool
- // the behaviour of Revert is only defined for up to one call
- reverted bool
+ // The behaviour of Commit and Revert is only defined for at most one call.
+ committed, reverted bool
+ // Optional Commit and Revert big lock.
+ mu *lockedfile.Mutex
msg message.Msg
syscallDispatcher
}
+// New returns the address of a new [I] targeting uid.
+func New(
+ ctx context.Context,
+ msg message.Msg,
+ uid int,
+ lock *check.Absolute,
+) (sys *I) {
+ if ctx == nil || msg == nil || uid < 0 {
+ panic("invalid call to New")
+ }
+ v := I{ctx: ctx, msg: msg, uid: uid, syscallDispatcher: direct{}}
+ if lock != nil {
+ v.mu = lockedfile.MutexAt(lock.String())
+ }
+ return &v
+}
+
func (sys *I) UID() int { return sys.uid }
// Equal returns whether all [Op] instances held by sys matches that of target.
@@ -118,9 +129,18 @@ func (sys *I) Commit() error {
if sys.committed {
panic("attempting to commit twice")
}
+
+ if sys.mu != nil {
+ if unlock, err := sys.mu.Lock(); err != nil {
+ return err
+ } else {
+ defer unlock()
+ }
+ }
sys.committed = true
- sp := New(sys.ctx, sys.msg, sys.uid)
+ sp := New(sys.ctx, sys.msg, sys.uid, nil)
+ // lock is still held on revert
sp.syscallDispatcher = sys.syscallDispatcher
sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits
defer func() {
@@ -153,6 +173,13 @@ func (sys *I) Revert(ec *Criteria) error {
if sys.reverted {
panic("attempting to revert twice")
}
+ if sys.mu != nil {
+ if unlock, err := sys.mu.Lock(); err != nil {
+ return err
+ } else {
+ defer unlock()
+ }
+ }
sys.reverted = true
// collect errors
diff --git a/internal/system/system_test.go b/internal/system/system_test.go
index b5dec992..8d863826 100644
--- a/internal/system/system_test.go
+++ b/internal/system/system_test.go
@@ -82,7 +82,7 @@ func TestNew(t *testing.T) {
t.Errorf("recover: %v, want %v", r, want)
}
}()
- New(nil, message.New(nil), 0)
+ New(nil, message.New(nil), 0, nil)
})
t.Run("msg", func(t *testing.T) {
@@ -92,7 +92,7 @@ func TestNew(t *testing.T) {
t.Errorf("recover: %v, want %v", r, want)
}
}()
- New(t.Context(), nil, 0)
+ New(t.Context(), nil, 0, nil)
})
t.Run("uid", func(t *testing.T) {
@@ -102,11 +102,11 @@ func TestNew(t *testing.T) {
t.Errorf("recover: %v, want %v", r, want)
}
}()
- New(t.Context(), message.New(nil), -1)
+ New(t.Context(), message.New(nil), -1, nil)
})
})
- sys := New(t.Context(), message.New(nil), 0xbeef)
+ sys := New(t.Context(), message.New(nil), 0xbeef, nil)
if sys.ctx == nil {
t.Error("New: ctx = nil")
}
@@ -125,53 +125,53 @@ func TestEqual(t *testing.T) {
want bool
}{
{"simple UID",
- New(t.Context(), message.New(nil), 150),
- New(t.Context(), message.New(nil), 150),
+ New(t.Context(), message.New(nil), 150, nil),
+ New(t.Context(), message.New(nil), 150, nil),
true},
{"simple UID differ",
- New(t.Context(), message.New(nil), 150),
- New(t.Context(), message.New(nil), 151),
+ New(t.Context(), message.New(nil), 150, nil),
+ New(t.Context(), message.New(nil), 151, nil),
false},
{"simple UID nil",
- New(t.Context(), message.New(nil), 150),
+ New(t.Context(), message.New(nil), 150, nil),
nil,
false},
{"op length mismatch",
- New(t.Context(), message.New(nil), 150).
+ New(t.Context(), message.New(nil), 150, nil).
ChangeHosts("chronos"),
- New(t.Context(), message.New(nil), 150).
+ New(t.Context(), message.New(nil), 150, nil).
ChangeHosts("chronos").
- Ensure(m("/run"), 0755),
+ Ensure(m("/run"), 0755, false),
false},
{"op value mismatch",
- New(t.Context(), message.New(nil), 150).
+ New(t.Context(), message.New(nil), 150, nil).
ChangeHosts("chronos").
- Ensure(m("/run"), 0644),
- New(t.Context(), message.New(nil), 150).
+ Ensure(m("/run"), 0644, false),
+ New(t.Context(), message.New(nil), 150, nil).
ChangeHosts("chronos").
- Ensure(m("/run"), 0755),
+ Ensure(m("/run"), 0755, false),
false},
{"op type mismatch",
- New(t.Context(), message.New(nil), 150).
+ New(t.Context(), message.New(nil), 150, nil).
ChangeHosts("chronos").
Wayland(m("/proc/nonexistent/dst"), m("/proc/nonexistent/src"), "\x00", "\x00"),
- New(t.Context(), message.New(nil), 150).
+ New(t.Context(), message.New(nil), 150, nil).
ChangeHosts("chronos").
- Ensure(m("/run"), 0755),
+ Ensure(m("/run"), 0755, false),
false},
{"op equals",
- New(t.Context(), message.New(nil), 150).
+ New(t.Context(), message.New(nil), 150, nil).
ChangeHosts("chronos").
- Ensure(m("/run"), 0755),
- New(t.Context(), message.New(nil), 150).
+ Ensure(m("/run"), 0755, false),
+ New(t.Context(), message.New(nil), 150, nil).
ChangeHosts("chronos").
- Ensure(m("/run"), 0755),
+ Ensure(m("/run"), 0755, false),
true},
}
@@ -200,46 +200,49 @@ func TestCommitRevert(t *testing.T) {
}{
{"apply xhost partial mkdir", func(sys *I) {
sys.
- Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711).
+ Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711, false).
ChangeHosts("chronos")
}, 0xff, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist),
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(2)),
call("verbosef", stub.ExpectArgs{"commit faulted after %d ops, rolling back partial commit", []any{1}}, nil, nil),
- call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, stub.UniqueError(3)),
call("println", stub.ExpectArgs{[]any{"cannot revert mkdir: unique error 3 injected by the test suite"}}, nil, nil),
}, &OpError{Op: "xhost", Err: stub.UniqueError(2)}, nil, nil},
{"apply xhost", func(sys *I) {
sys.
- Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711).
+ Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711, false).
ChangeHosts("chronos")
}, 0xff, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist),
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(2)),
call("verbosef", stub.ExpectArgs{"commit faulted after %d ops, rolling back partial commit", []any{1}}, nil, nil),
- call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, nil),
}, &OpError{Op: "xhost", Err: stub.UniqueError(2)}, nil, nil},
{"revert multi", func(sys *I) {
sys.
- Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711).
+ Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711, false).
ChangeHosts("chronos")
}, 0xff, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist),
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil),
}, nil, []stub.Call{
call("verbosef", stub.ExpectArgs{"deleting entry %s from X11", []any{xhostOp("chronos")}}, nil, nil),
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeDelete), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(1)),
- call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, stub.UniqueError(0)),
}, errors.Join(
&OpError{Op: "xhost", Err: stub.UniqueError(1), Revert: true},
@@ -247,17 +250,18 @@ func TestCommitRevert(t *testing.T) {
{"success", func(sys *I) {
sys.
- Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711).
+ Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711, false).
ChangeHosts("chronos")
}, 0xff, []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist),
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil),
}, nil, []stub.Call{
call("verbosef", stub.ExpectArgs{"deleting entry %s from X11", []any{xhostOp("chronos")}}, nil, nil),
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeDelete), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil),
- call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
+ call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil),
call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, nil),
}, nil},
}
diff --git a/internal/system/wayland.go b/internal/system/wayland.go
index c22c3fdf..e86c3159 100644
--- a/internal/system/wayland.go
+++ b/internal/system/wayland.go
@@ -42,7 +42,7 @@ func (w *waylandOp) apply(sys *I) (err error) {
return newOpError("wayland", err, false)
}
- if err = sys.aclUpdate(w.dst.String(), sys.uid, acl.Read, acl.Write, acl.Execute); err != nil {
+ if err = sys.aclUpdate(w.dst.String(), sys.uid, acl.Read|acl.Write|acl.Execute); err != nil {
if closeErr := w.ctx.Close(); closeErr != nil {
return newOpError("wayland", errors.Join(err, closeErr), false)
}
diff --git a/internal/system/wayland_test.go b/internal/system/wayland_test.go
index 76e6cc93..a9d29378 100644
--- a/internal/system/wayland_test.go
+++ b/internal/system/wayland_test.go
@@ -32,7 +32,7 @@ func TestWaylandOp(t *testing.T) {
call("waylandNew", stub.ExpectArgs{m("/run/user/1971/wayland-0"), m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}, nil, nil),
call("verbosef", stub.ExpectArgs{"wayland pathname socket on %q via %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0")}}, nil, nil),
call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil),
- call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(2)),
+ call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, stub.UniqueError(2)),
}, &OpError{Op: "wayland", Err: stub.UniqueError(2)}, nil, nil},
{"success", 0xbeef, 0xff, &waylandOp{nil,
@@ -44,7 +44,7 @@ func TestWaylandOp(t *testing.T) {
call("waylandNew", stub.ExpectArgs{m("/run/user/1971/wayland-0"), m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}, nil, nil),
call("verbosef", stub.ExpectArgs{"wayland pathname socket on %q via %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0")}}, nil, nil),
call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil),
- call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
+ call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, nil),
}, nil, []stub.Call{
call("verbosef", stub.ExpectArgs{"hanging up wayland socket on %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland")}}, nil, nil),
}, nil},
diff --git a/internal/testsuite/fs.go b/internal/testsuite/fs.go
new file mode 100644
index 00000000..9acd24b6
--- /dev/null
+++ b/internal/testsuite/fs.go
@@ -0,0 +1,129 @@
+package testsuite
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "path/filepath"
+ "strings"
+)
+
+var (
+ // ErrFSBadLength is returned by [FS.Compare] for a directory with an
+ // unexpected amount of dents.
+ ErrFSBadLength = errors.New("bad dir length")
+ // ErrFSBadData is returned by [FS.Compare] for a file with unexpected
+ // contents.
+ ErrFSBadData = errors.New("data differs")
+ // ErrFSBadMode is returned by [FS.Compare] for an entry with unexpected
+ // mode.
+ ErrFSBadMode = errors.New("mode differs")
+ // ErrFSInvalidEnt is returned by [FS.Compare] if an invalid [FS] is visited.
+ ErrFSInvalidEnt = errors.New("invalid entry condition")
+)
+
+// FS represents part of a filesystem hierarchy.
+type FS struct {
+ // Expected mode of corresponding entry.
+ Mode fs.FileMode `json:"mode"`
+ // Expected directory contents. The directory is not descended if Dir is nil.
+ Dir map[string]*FS `json:"dir"`
+ // Expected file contents. The file is not read if Data is nil.
+ Data *string `json:"data"`
+}
+
+// dprintf calls printf if it is non-nil.
+func dprintf(printf func(format string, a ...any), format string, a ...any) {
+ if printf == nil {
+ return
+ }
+ printf(format, a...)
+}
+
+// printDir prints a failed [FS.Compare] directory.
+func printDir(
+ printf func(format string, a ...any),
+ prefix string,
+ dir []fs.DirEntry,
+) {
+ names := make([]string, len(dir))
+ for i, ent := range dir {
+ name := ent.Name()
+ if ent.IsDir() {
+ name += "/"
+ }
+ names[i] = fmt.Sprintf("%q", name)
+ }
+ dprintf(printf, "[FAIL] d %s: %s", prefix, strings.Join(names, " "))
+}
+
+// Compare compares the contents of prefix against the hierarchy described by s.
+func (s *FS) Compare(
+ printf func(format string, a ...any),
+ prefix string,
+ e fs.FS,
+) error {
+ if s.Data != nil {
+ if s.Dir != nil {
+ panic("invalid state")
+ }
+ panic("invalid compare call")
+ }
+
+ if s.Dir == nil {
+ dprintf(printf, "[ OK ] s %s", prefix)
+ return nil
+ }
+
+ var dir []fs.DirEntry
+ if d, err := fs.ReadDir(e, prefix); err != nil {
+ return err
+ } else if len(d) != len(s.Dir) {
+ printDir(printf, prefix, d)
+ return ErrFSBadLength
+ } else {
+ dir = d
+ }
+
+ for _, got := range dir {
+ name := got.Name()
+
+ if want, ok := s.Dir[name]; !ok {
+ printDir(printf, prefix, dir)
+ return fs.ErrNotExist
+ } else if want.Dir != nil && !got.IsDir() {
+ printDir(printf, prefix, dir)
+ return ErrFSInvalidEnt
+ } else {
+ name = filepath.Join(prefix, name)
+
+ if fi, err := got.Info(); err != nil {
+ return err
+ } else if fi.Mode() != want.Mode {
+ dprintf(printf, "[FAIL] m %s: %#o, want %#o",
+ name, uint32(fi.Mode()), uint32(want.Mode))
+ return ErrFSBadMode
+ }
+
+ if want.Data != nil {
+ if want.Dir != nil {
+ panic("invalid state")
+ }
+ if v, err := fs.ReadFile(e, name); err != nil {
+ return err
+ } else if string(v) != *want.Data {
+ dprintf(printf,
+ "[FAIL] f %s\n\t got: %s\n\twant: %s",
+ name, v, *want.Data,
+ )
+ return ErrFSBadData
+ }
+ dprintf(printf, "[ OK ] f %s", name)
+ } else if err := want.Compare(printf, name, e); err != nil {
+ return err
+ }
+ }
+ }
+ dprintf(printf, "[ OK ] d %s", prefix)
+ return nil
+}
diff --git a/internal/testsuite/fs_test.go b/internal/testsuite/fs_test.go
new file mode 100644
index 00000000..4eea866d
--- /dev/null
+++ b/internal/testsuite/fs_test.go
@@ -0,0 +1,85 @@
+package testsuite_test
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io/fs"
+ "testing"
+ "testing/fstest"
+
+ "hakurei.app/internal/testsuite"
+)
+
+func TestCompare(t *testing.T) {
+ var (
+ fsPasswdSample = "u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh"
+ fsGroupSample = "hakurei:x:65534:"
+ )
+
+ testCases := []struct {
+ name string
+
+ sample fstest.MapFS
+ want *testsuite.FS
+ wantOut string
+ wantErr error
+ }{
+ {"skip", fstest.MapFS{}, &testsuite.FS{}, "[ OK ] s .\x00", nil},
+ {"simple pass", fstest.MapFS{".hakurei": {Mode: 0x800001ed}},
+ &testsuite.FS{Dir: map[string]*testsuite.FS{".hakurei": {Mode: 0x800001ed}}},
+ "[ OK ] s .hakurei\x00[ OK ] d .\x00", nil},
+ {"bad length", fstest.MapFS{".hakurei": {Mode: 0x800001ed}},
+ &testsuite.FS{Dir: make(map[string]*testsuite.FS)},
+ "[FAIL] d .: \".hakurei/\"\x00", testsuite.ErrFSBadLength},
+ {"top level bad mode", fstest.MapFS{".hakurei": {Mode: 0x800001ed}},
+ &testsuite.FS{Dir: map[string]*testsuite.FS{".hakurei": {Mode: 0xdeadbeef}}},
+ "[FAIL] m .hakurei: 020000000755, want 033653337357\x00", testsuite.ErrFSBadMode},
+ {"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
+ &testsuite.FS{Dir: map[string]*testsuite.FS{"test": {Dir: make(map[string]*testsuite.FS)}}},
+ "[FAIL] d .: \"test\"\x00", testsuite.ErrFSInvalidEnt},
+ {"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
+ &testsuite.FS{Dir: map[string]*testsuite.FS{".test": {}}},
+ "[FAIL] d .: \"test\"\x00", fs.ErrNotExist},
+ {"file", fstest.MapFS{"etc": {Mode: 0x800001c0},
+ "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
+ "etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
+ }, &testsuite.FS{Dir: map[string]*testsuite.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*testsuite.FS{
+ "passwd": {Mode: 0x1a4, Data: &fsPasswdSample},
+ "group": {Mode: 0x1a4, Data: &fsGroupSample},
+ }}}}, "[ OK ] f etc/group\x00[ OK ] f etc/passwd\x00[ OK ] d etc\x00[ OK ] d .\x00", nil},
+ {"file differ", fstest.MapFS{"etc": {Mode: 0x800001c0},
+ "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
+ "etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
+ }, &testsuite.FS{Dir: map[string]*testsuite.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*testsuite.FS{
+ "passwd": {Mode: 0x1a4, Data: &fsGroupSample},
+ "group": {Mode: 0x1a4, Data: &fsGroupSample},
+ }}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\n\t got: u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh\n\twant: hakurei:x:65534:\x00", testsuite.ErrFSBadData},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ var buf bytes.Buffer
+
+ err := tc.want.Compare(
+ func(format string, a ...any) {
+ _, _ = fmt.Fprintf(&buf, format+"\x00", a...)
+ },
+ ".", tc.sample,
+ )
+ if !errors.Is(err, tc.wantErr) {
+ t.Errorf(
+ "Compare: error = %v; wantErr %v",
+ err, tc.wantErr,
+ )
+ }
+
+ if buf.String() != tc.wantOut {
+ t.Errorf(
+ "Compare: output %q; want %q",
+ &buf, tc.wantOut,
+ )
+ }
+ })
+ }
+}
diff --git a/internal/testsuite/mountinfo/mountinfo.go b/internal/testsuite/mountinfo/mountinfo.go
new file mode 100644
index 00000000..19892cd2
--- /dev/null
+++ b/internal/testsuite/mountinfo/mountinfo.go
@@ -0,0 +1,177 @@
+// Package mountinfo provides util-linux bindings for parsing
+// proc_pid_mountinfo(5).
+//
+// This package must never be used outside integration tests, a much better
+// implementation can be found in package vfs.
+//
+// Attempting to import this package outside testing causes the resulting
+// program to panic.
+package mountinfo
+
+/*
+#cgo linux pkg-config: --static mount
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <libmount.h>
+
+const char *HAKUREI_MOUNTINFO_PATH = "/proc/self/mountinfo";
+*/
+import "C"
+
+import (
+ "errors"
+ "fmt"
+ "runtime"
+ "unsafe"
+)
+
+var (
+ // ErrParse is returned by [Open] when encountering a bad record.
+ ErrParse = errors.New("invalid mountinfo record")
+ // ErrIter is returned by [Open] if an iterator cannot be allocated.
+ ErrIter = errors.New("cannot allocate iterator")
+ // ErrIterAdvance is stored when the iterator is unable to advance.
+ ErrIterAdvance = errors.New("unable to advance iterator")
+)
+
+type (
+ // Iter refers to libmnt iterator state.
+ Iter struct {
+ // Last stored error.
+ err error
+ // Whether iteration has concluded.
+ ok bool
+ // Whether Close had already been called.
+ closed bool
+
+ tb *C.struct_libmnt_table
+ itr *C.struct_libmnt_iter
+
+ fs *C.struct_libmnt_fs
+ }
+
+ // Entry represents deterministic mountinfo parts of a libmnt_fs entry.
+ Entry struct {
+ // mount ID: a unique ID for the mount (may be reused after umount(2)).
+ ID int `json:"id"`
+ // parent ID: the ID of the parent mount (or of self for the root of
+ // this mount namespace's mount tree).
+ Parent int `json:"parent"`
+ // root: the pathname of the directory in the filesystem which forms the
+ // root of this mount.
+ Root string `json:"root"`
+ // mount point: the pathname of the mount point relative to the
+ // process's root directory.
+ Target string `json:"target"`
+ // mount options: per-mount options (see mount(2)).
+ VfsOptstr string `json:"vfs_optstr"`
+ // filesystem type: the filesystem type in the form "type[.subtype]".
+ FsType string `json:"fstype"`
+ // mount source: filesystem-specific information or "none".
+ Source string `json:"source"`
+ // super options: per-superblock options (see mount(2)).
+ FsOptstr string `json:"fs_optstr"`
+ }
+)
+
+// Copy populates v with the current record.
+func (m *Iter) Copy(v *Entry) {
+ if m.fs == nil {
+ panic("invalid entry")
+ }
+ v.ID = int(C.mnt_fs_get_id(m.fs))
+ v.Parent = int(C.mnt_fs_get_parent_id(m.fs))
+ v.Root = C.GoString(C.mnt_fs_get_root(m.fs))
+ v.Target = C.GoString(C.mnt_fs_get_target(m.fs))
+ v.VfsOptstr = C.GoString(C.mnt_fs_get_vfs_options(m.fs))
+ v.FsType = C.GoString(C.mnt_fs_get_fstype(m.fs))
+ v.Source = C.GoString(C.mnt_fs_get_source(m.fs))
+ v.FsOptstr = C.GoString(C.mnt_fs_get_fs_options(m.fs))
+}
+
+// Err returns the saved iterator error.
+func (m *Iter) Err() error { return m.err }
+
+// Open opens a mountinfo document. If name is an empty string, the mountinfo
+// document of the current process is opened instead.
+func Open(name string) (*Iter, error) {
+ var m Iter
+ if name == "" {
+ m.tb = C.mnt_new_table_from_file(C.HAKUREI_MOUNTINFO_PATH)
+ } else {
+ _name := C.CString(name)
+ m.tb = C.mnt_new_table_from_file(_name)
+ C.free(unsafe.Pointer(_name))
+ }
+ if m.tb == nil {
+ return nil, ErrParse
+ }
+ m.itr = C.mnt_new_iter(C.MNT_ITER_FORWARD)
+ if m.itr == nil {
+ C.mnt_unref_table(m.tb)
+ return nil, ErrIter
+ }
+ m.ok = true
+
+ runtime.SetFinalizer(&m, (*Iter).Close)
+ return &m, nil
+}
+
+// Close frees the iterator.
+func (m *Iter) Close() {
+ if m.closed {
+ return
+ }
+ if m.tb == nil {
+ panic("unref called before open")
+ }
+
+ C.mnt_unref_table(m.tb)
+ C.mnt_free_iter(m.itr)
+ m.closed = true
+ runtime.SetFinalizer(m, nil)
+}
+
+// Reset resets the iterator to the first record for reuse.
+func (m *Iter) Reset() {
+ if m.err != nil {
+ panic("attempting to reset a faulted iterator")
+ }
+ m.ok = true
+ C.mnt_reset_iter(m.itr, -1)
+}
+
+// Next advances the iterator to the next record. The record may be copied if
+// Next returns true.
+func (m *Iter) Next() bool {
+ if !m.ok || m.err != nil {
+ return false
+ }
+
+ r := C.mnt_table_next_fs(m.tb, m.itr, &m.fs)
+ if r < 0 {
+ m.err = ErrIterAdvance
+ }
+ m.ok = r == 0
+ return m.ok
+}
+
+// EqualWithIgnore compares e with want, ignoring fields with the specified
+// ignore value.
+func (e *Entry) EqualWithIgnore(want *Entry, ignore string) bool {
+ return (e.ID == want.ID || want.ID == -1) &&
+ (e.Parent == want.Parent || want.Parent == -1) &&
+ (e.Root == want.Root || want.Root == ignore) &&
+ (e.Target == want.Target || want.Target == ignore) &&
+ (e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) &&
+ (e.FsType == want.FsType || want.FsType == ignore) &&
+ (e.Source == want.Source || want.Source == ignore) &&
+ (e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore)
+}
+
+// String returns a text representation of e loosely following the kernel format.
+func (e *Entry) String() string {
+ return fmt.Sprintf("%d %d %s %s %s %s %s %s",
+ e.ID, e.Parent, e.Root, e.Target, e.VfsOptstr, e.FsType, e.Source, e.FsOptstr)
+}
diff --git a/internal/testsuite/mountinfo/mountinfo_guard.go b/internal/testsuite/mountinfo/mountinfo_guard.go
new file mode 100644
index 00000000..aaf63ac4
--- /dev/null
+++ b/internal/testsuite/mountinfo/mountinfo_guard.go
@@ -0,0 +1,15 @@
+//go:build !testsuite && !tester
+
+package mountinfo
+
+import (
+ "os"
+ "testing"
+)
+
+func init() {
+ if !testing.Testing() {
+ println("package mountinfo imported in non-testsuite program")
+ os.Exit(1)
+ }
+}
diff --git a/internal/testsuite/mountinfo/mountinfo_test.go b/internal/testsuite/mountinfo/mountinfo_test.go
new file mode 100644
index 00000000..93be3880
--- /dev/null
+++ b/internal/testsuite/mountinfo/mountinfo_test.go
@@ -0,0 +1,146 @@
+package mountinfo_test
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "hakurei.app/internal/testsuite/mountinfo"
+)
+
+func TestMountinfo(t *testing.T) {
+ testCases := []struct {
+ name string
+
+ sample string
+ want []*mountinfo.Entry
+ }{
+ {"util-linux", `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
+18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
+19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
+20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered
+21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
+22 21 0:18 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd
+23 21 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset
+24 21 0:20 / /sys/fs/cgroup/ns rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,ns
+25 21 0:21 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu
+26 21 0:22 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct
+27 21 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory
+28 21 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices
+29 21 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer
+30 21 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls
+31 21 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio
+32 16 0:28 / /sys/kernel/security rw,relatime - autofs systemd-1 rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
+33 17 0:29 / /dev/hugepages rw,relatime - autofs systemd-1 rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
+34 16 0:30 / /sys/kernel/debug rw,relatime - autofs systemd-1 rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
+35 15 0:31 / /proc/sys/fs/binfmt_misc rw,relatime - autofs systemd-1 rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
+36 17 0:32 / /dev/mqueue rw,relatime - autofs systemd-1 rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
+37 15 0:14 / /proc/bus/usb rw,relatime - usbfs /proc/bus/usb rw
+38 33 0:33 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw
+39 36 0:12 / /dev/mqueue rw,relatime - mqueue mqueue rw
+40 20 8:6 / /boot rw,noatime - ext3 /dev/sda6 rw,errors=continue,barrier=0,data=ordered
+41 20 253:0 / /home/kzak rw,noatime - ext4 /dev/mapper/kzak-home rw,barrier=1,data=ordered
+42 35 0:34 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc none rw
+43 16 0:35 / /sys/fs/fuse/connections rw,relatime - fusectl fusectl rw
+44 41 0:36 / /home/kzak/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=500,group_id=500
+45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw
+47 20 0:38 / /mnt/sounds rw,relatime - cifs //foo.home/bar/ rw,unc=\\foo.home\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344
+49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw`, []*mountinfo.Entry{
+ e(15, 20, "/", "/proc", "rw,relatime", "proc", "/proc", "rw"),
+ e(16, 20, "/", "/sys", "rw,relatime", "sysfs", "/sys", "rw"),
+ e(17, 20, "/", "/dev", "rw,relatime", "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755"),
+ e(18, 17, "/", "/dev/pts", "rw,relatime", "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000"),
+ e(19, 17, "/", "/dev/shm", "rw,relatime", "tmpfs", "tmpfs", "rw"),
+ e(20, 1, "/", "/", "rw,noatime", "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered"),
+ e(21, 16, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", "tmpfs", "tmpfs", "rw,mode=755"),
+ e(22, 21, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd"),
+ e(23, 21, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuset"),
+ e(24, 21, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,ns"),
+ e(25, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpu"),
+ e(26, 21, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuacct"),
+ e(27, 21, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,memory"),
+ e(28, 21, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,devices"),
+ e(29, 21, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,freezer"),
+ e(30, 21, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,net_cls"),
+ e(31, 21, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,blkio"),
+ e(32, 16, "/", "/sys/kernel/security", "rw,relatime", "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
+ e(33, 17, "/", "/dev/hugepages", "rw,relatime", "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
+ e(34, 16, "/", "/sys/kernel/debug", "rw,relatime", "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
+ e(35, 15, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "autofs", "systemd-1", "rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
+ e(36, 17, "/", "/dev/mqueue", "rw,relatime", "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
+ e(37, 15, "/", "/proc/bus/usb", "rw,relatime", "usbfs", "/proc/bus/usb", "rw"),
+ e(38, 33, "/", "/dev/hugepages", "rw,relatime", "hugetlbfs", "hugetlbfs", "rw"),
+ e(39, 36, "/", "/dev/mqueue", "rw,relatime", "mqueue", "mqueue", "rw"),
+ e(40, 20, "/", "/boot", "rw,noatime", "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered"),
+ e(41, 20, "/", "/home/kzak", "rw,noatime", "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered"),
+ e(42, 35, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "binfmt_misc", "none", "rw"),
+ e(43, 16, "/", "/sys/fs/fuse/connections", "rw,relatime", "fusectl", "fusectl", "rw"),
+ e(44, 41, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500"),
+ e(45, 20, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", "rpc_pipefs", "sunrpc", "rw"),
+ e(47, 20, "/", "/mnt/sounds", "rw,relatime", "cifs", "//foo.home/bar/", "rw,unc=\\\\foo.home\\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344"),
+ e(49, 20, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", "tmpfs", "tmpfs", "rw"),
+ }},
+ }
+
+ for _, tc := range testCases {
+ name := filepath.Join(t.TempDir(), "sample")
+ if err := os.WriteFile(name, []byte(tc.sample), 0400); err != nil {
+ t.Fatalf("cannot write sample: %v", err)
+ }
+
+ t.Run(tc.name, func(t *testing.T) {
+ m, err := mountinfo.Open(name)
+ if err != nil {
+ t.Fatalf("Open: error = %v", err)
+ }
+ t.Cleanup(m.Close)
+
+ i := 0
+ var ent mountinfo.Entry
+ for m.Next() {
+ m.Copy(&ent)
+
+ if i == len(tc.want) {
+ t.Errorf("Next: got more than %d entries", i)
+ t.FailNow()
+ }
+ if !ent.EqualWithIgnore(tc.want[i], "\x00") {
+ t.Errorf("Next: entry %d\n got: %#v\nwant: %#v", i,
+ ent, &tc.want[i])
+ t.FailNow()
+ } else {
+ t.Logf("%s", &ent)
+ }
+
+ i++
+ }
+
+ if err = m.Err(); err != nil {
+ t.Fatalf("Err: %v", err)
+ }
+ })
+
+ if err := os.Remove(name); err != nil {
+ t.Fatalf("cannot remove %q: %v", name, err)
+ }
+ }
+}
+
+func e(
+ id, parent int,
+ root, target, vfsOptstr string,
+ fsType, source, fsOptstr string,
+) *mountinfo.Entry {
+ return &mountinfo.Entry{
+ ID: id,
+ Parent: parent,
+ Root: root,
+ Target: target,
+ VfsOptstr: vfsOptstr,
+ FsType: fsType,
+ Source: source,
+ FsOptstr: fsOptstr,
+ }
+}
diff --git a/internal/testsuite/proc.go b/internal/testsuite/proc.go
new file mode 100644
index 00000000..e7ef1aed
--- /dev/null
+++ b/internal/testsuite/proc.go
@@ -0,0 +1,353 @@
+package testsuite
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "syscall"
+ "unsafe"
+
+ "hakurei.app/fhs"
+)
+
+// Stat represents status information read from /proc/pid/stat.
+type Stat struct {
+ // The process ID.
+ PID int
+ // The filename of the executable, with parenthesis stripped.
+ Comm string
+ // One of the following characters, indicating process state:
+ //
+ // R Running
+ //
+ // S Sleeping in an interruptible wait
+ //
+ // D Waiting in uninterruptible disk sleep
+ //
+ // Z Zombie
+ //
+ // T Stopped (on a signal) or (before Linux
+ // 2.6.33) trace stopped
+ //
+ // t Tracing stop (Linux 2.6.33 onward)
+ //
+ // W Paging (only before Linux 2.6.0)
+ //
+ // X Dead (from Linux 2.6.0 onward)
+ //
+ // x Dead (Linux 2.6.33 to 3.13 only)
+ //
+ // K Wakekill (Linux 2.6.33 to 3.13 only)
+ //
+ // W Waking (Linux 2.6.33 to 3.13 only)
+ //
+ // P Parked (Linux 3.9 to 3.13 only)
+ //
+ // I Idle (Linux 4.14 onward)
+ State byte
+ // The process ID of the parent of this process.
+ PPID int
+ // The process group ID of the process.
+ PGRP int
+ // The session ID of the process.
+ Session int
+ // The controlling terminal of the process.
+ TTYNR int
+ // The ID of the foreground process group of the controlling terminal of the
+ // process.
+ TPGID int
+ // The kernel flags word of the process. For bit meanings, see the PF_*
+ // defines in the Linux kernel source file include/linux/sched.h.
+ Flags uint
+ // The number of minor faults the process has made which have not required
+ // loading a memory page from disk.
+ MinFlt uint
+ // The number of minor faults that the process's waited-for children have
+ // made.
+ CMinFlt uint
+ // The number of major faults the process has made which have required
+ // loading a memory page from disk.
+ MajFlt uint
+ // The number of major faults that the process's waited-for children have
+ // made.
+ CMajFlt uint
+ // Amount of time that this process has been scheduled in user mode,
+ // measured in clock ticks.
+ UTime uint
+ // Amount of time that this process has been scheduled in kernel mode,
+ // measured in clock ticks.
+ STime uint
+ // Amount of time that this process's waited-for children have been
+ // scheduled in user mode, measured in clock ticks.
+ CUTime int
+ // Amount of time that this process's waited-for children have been
+ // scheduled in kernel mode, measured in clock ticks.
+ CSTime int
+ // For processes running a real-time scheduling policy, this is the negated
+ // scheduling priority, minus one.
+ Priority int
+ // The nice value, a value in the range 19 (low priority) to -20 (high
+ // priority).
+ Nice int
+ // Number of threads in this process.
+ NumThreads int
+
+ // unmaintained field: itrealvalue
+
+ // The time the process started after system boot. Since Linux 2.6, the
+ // value is expressed in clock ticks.
+ StartTime uint64
+ // Virtual memory size in bytes.
+ VSize uint
+ // Resident set size in pages.
+ RSS int
+ // Soft limit in bytes on the rss of the process.
+ RSSLim uint64
+ // The address above which program text can run.
+ StartCode uint64
+ // The address below which program text can run.
+ EndCode uint64
+ // The address of the start (i.e., bottom) of the stack.
+ StartStack uint64
+ // The current value of ESP (stack pointer), as found in the kernel stack
+ // page for the process.
+ KSTKESP uint64
+ // The current EIP (instruction pointer).
+ KSTKEIP uint64
+
+ // obsolete fields: signal, blocked, sigignore, sigcatch
+
+ // This is the "channel" in which the process is waiting. It is the address
+ // of a location in the kernel where the process is sleeping.
+ WChan uint64
+
+ // unmaintained fields: nswap, cnswap
+
+ // Signal to be sent to parent when we die.
+ ExitSignal int
+ // CPU number last executed on.
+ Processor int
+ // Real-time scheduling priority, a number in the range 1 to 99 for processes
+ // scheduled under a real-time policy, or 0, for non-real-time processes.
+ RTPriority uint
+ // Scheduling policy (see sched_setscheduler(2)). Decode using the SCHED_*
+ // constants in linux/sched.h.
+ Policy uint
+ // Aggregated block I/O delays, measured in clock ticks (centiseconds).
+ DelayAcctBlkIOTicks uint64
+ // Guest time of the process (time spent running a virtual CPU for a guest
+ // operating system), measured in clock ticks.
+ GuestTime int
+ // Guest time of the process's children, measured in clock ticks.
+ CGuestTime int
+}
+
+// Executable is like [os.Executable], but for the process referred to by s.
+func (s *Stat) Executable() (string, error) {
+ path, err := os.Readlink(filepath.Join(fhs.Proc, strconv.Itoa(s.PID), "exe"))
+
+ // When the executable has been deleted then Readlink returns a
+ // path appended with " (deleted)".
+ return strings.TrimSuffix(path, " (deleted)"), err
+}
+
+// Stat populates stat with the proc filesystem entry referred to by s.
+func (s *Stat) Stat(stat *syscall.Stat_t) (err error) {
+ err = syscall.Stat(filepath.Join(fhs.Proc, strconv.Itoa(s.PID)), stat)
+ if err != nil {
+ err = os.NewSyscallError("stat", err)
+ }
+ return
+}
+
+// Args reads arguments of the process referred to by s.
+func (s *Stat) Args() ([]string, error) {
+ p, err := os.ReadFile(filepath.Join(fhs.Proc, strconv.Itoa(s.PID), "cmdline"))
+ if err != nil {
+ return nil, err
+ }
+ a := bytes.Split(p, []byte{0})
+ if len(a) > 0 && len(a[len(a)-1]) == 0 {
+ a = a[:len(a)-1]
+ }
+
+ args := make([]string, len(a))
+ for i, arg := range a {
+ args[i] = unsafe.String(unsafe.SliceData(arg), len(arg))
+ }
+ return args, nil
+}
+
+// ErrBadDelimiters is returned by [Stat.UnmarshalText] if one or both bytes of
+// the comm delimiter pair were missing or misplaced.
+var ErrBadDelimiters = errors.New("missing comm delimiters")
+
+// UnmarshalText populates the structure pointed to by s from text.
+func (s *Stat) UnmarshalText(text []byte) (err error) {
+ var (
+ discard uint64
+ _uint64 = &discard
+ _int64 = (*int64)(unsafe.Pointer(&discard))
+
+ ld = bytes.Index(text, []byte("("))
+ rd = bytes.LastIndex(text, []byte(")"))
+ )
+
+ if ld <= 0 || rd < 0 {
+ return ErrBadDelimiters
+ }
+
+ if s.PID, err = strconv.Atoi(
+ unsafe.String(unsafe.SliceData(text), ld-1),
+ ); err != nil {
+ return
+ }
+
+ s.Comm = string(text[ld+1 : rd])
+
+ var (
+ n int
+
+ state string
+ )
+ n, err = fmt.Fscan(
+ bytes.NewBuffer(text[rd+2:]),
+ &state,
+ &s.PPID,
+ &s.PGRP,
+ &s.Session,
+ &s.TTYNR,
+ &s.TPGID,
+ &s.Flags,
+ &s.MinFlt,
+ &s.CMinFlt,
+ &s.MajFlt,
+ &s.CMajFlt,
+ &s.UTime,
+ &s.STime,
+ &s.CUTime,
+ &s.CSTime,
+ &s.Priority,
+ &s.Nice,
+ &s.NumThreads,
+ _int64,
+ &s.StartTime,
+ &s.VSize,
+ &s.RSS,
+ &s.RSSLim,
+ &s.StartCode,
+ &s.EndCode,
+ &s.StartStack,
+ &s.KSTKESP,
+ &s.KSTKEIP,
+ _uint64,
+ _uint64,
+ _uint64,
+ _uint64,
+ &s.WChan,
+ _uint64,
+ _uint64,
+ &s.ExitSignal,
+ &s.Processor,
+ &s.RTPriority,
+ &s.Policy,
+ &s.DelayAcctBlkIOTicks,
+ &s.GuestTime,
+ &s.CGuestTime,
+ )
+ if err != nil {
+ err = fmt.Errorf("field %d: %w", n, err)
+ } else if len(state) != 1 {
+ err = fmt.Errorf("invalid state %q", state)
+ } else {
+ s.State = state[0]
+ }
+ return
+}
+
+// A StatScanner continuously scans the proc filesystem for process status
+// information in /proc/pid/stat.
+type StatScanner struct {
+ // Current entry.
+ stat Stat
+ // Cached top-level /proc entries.
+ dents []os.DirEntry
+ // Current progress through dents.
+ i int
+ // Whether the previous call to Scan had repopulated dents.
+ wrapped bool
+ // First stored error: a non-nil err disables the scanner.
+ err error
+}
+
+// IsNotExist returns whether an error is [os.ErrNotExist] or ESRCH.
+func IsNotExist(err error) bool {
+ return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ESRCH)
+}
+
+// Scan reads a process status information entry. It returns false if an
+// unrecoverable error is encountered, after which Scan no longer scans new
+// entries.
+func (s *StatScanner) Scan() bool {
+ if s.err != nil {
+ return false
+ }
+
+ if s.wrapped = s.i == len(s.dents); s.wrapped {
+ if s.dents, s.err = os.ReadDir(fhs.Proc); s.err != nil {
+ return false
+ }
+ s.i = 0
+ if len(s.dents) == 0 {
+ s.err = syscall.ENOTRECOVERABLE
+ return false
+ }
+ }
+
+ for s.i < len(s.dents) {
+ dent := s.dents[s.i]
+ s.i++
+ if !dent.IsDir() {
+ continue
+ }
+
+ pid, err := strconv.Atoi(dent.Name())
+ if err != nil {
+ continue
+ }
+
+ var p []byte
+ p, err = os.ReadFile(filepath.Join(fhs.Proc, dent.Name(), "stat"))
+ if err != nil {
+ if IsNotExist(err) {
+ continue
+ }
+ s.err = err
+ return false
+ }
+
+ s.err = s.stat.UnmarshalText(p)
+ if s.err == nil && pid != s.stat.PID {
+ s.err = fmt.Errorf(
+ "bad status information: dent=%d, stat=%d",
+ pid, s.stat.PID,
+ )
+ }
+ return s.err == nil
+ }
+ return s.Scan()
+}
+
+// Stat returns the address of the [Stat] structure populated by the last call
+// to Scan.
+func (s *StatScanner) Stat() *Stat { return &s.stat }
+
+// Err returns the stored error value.
+func (s *StatScanner) Err() error { return s.err }
+
+// Repopulated returns whether the last Scan call had re-read the proc filesystem.
+func (s *StatScanner) Repopulated() bool { return s.wrapped }
diff --git a/internal/testsuite/proc_test.go b/internal/testsuite/proc_test.go
new file mode 100644
index 00000000..73693972
--- /dev/null
+++ b/internal/testsuite/proc_test.go
@@ -0,0 +1,33 @@
+package testsuite_test
+
+import (
+ "testing"
+
+ "hakurei.app/internal/testsuite"
+)
+
+func BenchmarkStatScanner(b *testing.B) {
+ var s testsuite.StatScanner
+
+ for b.Loop() {
+ if !s.Scan() {
+ b.Fatal(s.Err())
+ }
+ }
+}
+
+func BenchmarkStatScannerFull(b *testing.B) {
+ var s testsuite.StatScanner
+
+ for b.Loop() {
+ for s.Scan() {
+ if s.Repopulated() {
+ break
+ }
+ }
+
+ if err := s.Err(); err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/internal/testsuite/ptrace.go b/internal/testsuite/ptrace.go
new file mode 100644
index 00000000..d7abd0aa
--- /dev/null
+++ b/internal/testsuite/ptrace.go
@@ -0,0 +1,151 @@
+package testsuite
+
+import (
+ "crypto/sha512"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "os"
+ "runtime"
+ "syscall"
+ "unsafe"
+)
+
+const (
+ // _PTRACE_ATTACH attaches to the process specified in pid.
+ _PTRACE_ATTACH = 16
+ // _PTRACE_DETACH restarts the stopped tracee as for PTRACE_CONT, but first
+ // detaches from it.
+ _PTRACE_DETACH = 17
+
+ // _PTRACE_SECCOMP_GET_FILTER allows the tracer to dump the tracee's classic
+ // BPF filters.
+ _PTRACE_SECCOMP_GET_FILTER = 0x420c
+)
+
+// ptrace wraps the ptrace syscall.
+func ptrace(
+ op uintptr,
+ pid, addr int,
+ data unsafe.Pointer,
+) (r uintptr, errno syscall.Errno) {
+ r, _, errno = syscall.Syscall6(
+ syscall.SYS_PTRACE,
+ op,
+ uintptr(pid),
+ uintptr(addr),
+ uintptr(data),
+ 0, 0,
+ )
+ return
+}
+
+// ptraceAttach attaches to the process referred to by pid.
+func ptraceAttach(pid int) error {
+ if _, errno := ptrace(_PTRACE_ATTACH, pid, 0, nil); errno != 0 {
+ return os.NewSyscallError("PTRACE_ATTACH", errno)
+ }
+
+ var status syscall.WaitStatus
+ for {
+ if _, err := syscall.Wait4(
+ pid,
+ &status,
+ syscall.WALL,
+ nil,
+ ); err != nil {
+ if errors.Is(err, syscall.EINTR) {
+ continue
+ }
+ return os.NewSyscallError("wait4", err)
+ }
+ switch {
+ case status.Stopped():
+ return nil
+
+ case status.Continued():
+ continue
+
+ case status.Signaled():
+ return fmt.Errorf(
+ "tracee terminated by signal %s",
+ status.Signal(),
+ )
+
+ case status.Exited():
+ return fmt.Errorf(
+ "tracee terminated unexpectedly with code %d",
+ status.ExitStatus(),
+ )
+ }
+ }
+}
+
+// ptraceDetach detaches from the attached process referred to by pid.
+func ptraceDetach(pid int) error {
+ if _, errno := ptrace(_PTRACE_DETACH, pid, 0, nil); errno != 0 {
+ return os.NewSyscallError("PTRACE_DETACH", errno)
+ }
+ return nil
+}
+
+// getFilter dumps the specified tracee's cBPF filter at the specified index
+// and returns the resulting payload. T must be eight bytes long and must not
+// contain pointers.
+func getFilter(pid, index int) ([]syscall.SockFilter, error) {
+ var buf []syscall.SockFilter
+ if n, errno := ptrace(
+ _PTRACE_SECCOMP_GET_FILTER,
+ pid, index, nil,
+ ); errno != 0 {
+ return nil, os.NewSyscallError("PTRACE_SECCOMP_GET_FILTER", errno)
+ } else {
+ buf = make([]syscall.SockFilter, n)
+ }
+ if _, errno := ptrace(
+ _PTRACE_SECCOMP_GET_FILTER,
+ pid, index, unsafe.Pointer(&buf[0]),
+ ); errno != 0 {
+ return nil, os.NewSyscallError("PTRACE_SECCOMP_GET_FILTER", errno)
+ }
+ return buf, nil
+}
+
+// CheckFilter checks the process at pid to have its first filter's contents
+// match the specified sha512 checksum.
+func CheckFilter(pid, index int, sum [sha512.Size]byte) (err error) {
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ if err = ptraceAttach(pid); err != nil {
+ return
+ }
+ defer func() {
+ if detachErr := ptraceDetach(pid); err == nil {
+ err = detachErr
+ if errors.Is(err, syscall.ESRCH) {
+ err = nil
+ }
+ }
+ }()
+
+ var buf []syscall.SockFilter
+ h := sha512.New()
+ if buf, err = getFilter(pid, index); err != nil {
+ return
+ } else {
+ h.Write(unsafe.Slice(
+ (*byte)(unsafe.Pointer(&buf[0])),
+ uintptr(len(buf))*unsafe.Sizeof(buf[0]),
+ ))
+ }
+
+ if got := h.Sum(nil); string(got) != string(sum[:]) {
+ return fmt.Errorf(
+ "bad filter\n\t got: %s\n\twant: %s",
+ base64.StdEncoding.EncodeToString(got),
+ base64.StdEncoding.EncodeToString(sum[:]),
+ )
+ }
+ return
+}
diff --git a/internal/testsuite/ptrace_test.go b/internal/testsuite/ptrace_test.go
new file mode 100644
index 00000000..18ad3f31
--- /dev/null
+++ b/internal/testsuite/ptrace_test.go
@@ -0,0 +1,13 @@
+package testsuite_test
+
+import (
+ "syscall"
+ "testing"
+ "unsafe"
+)
+
+func TestBlockSize(t *testing.T) {
+ if sz := unsafe.Sizeof(syscall.SockFilter{}); sz != 8 {
+ t.Fatalf("invalid filter block size %d", sz)
+ }
+}
diff --git a/internal/testsuite/testsuite.go b/internal/testsuite/testsuite.go
new file mode 100644
index 00000000..395bf8f2
--- /dev/null
+++ b/internal/testsuite/testsuite.go
@@ -0,0 +1,910 @@
+// Package testsuite provides many quick-and-dirty integration testing utilities.
+//
+// Attempting to import this package outside testing causes the resulting
+// program to panic.
+package testsuite
+
+import (
+ "bufio"
+ "context"
+ "crypto/sha512"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "iter"
+ "log"
+ "os"
+ "os/exec"
+ "os/signal"
+ "os/user"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "slices"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "syscall"
+ "time"
+
+ "hakurei.app/check"
+ "hakurei.app/fhs"
+ "hakurei.app/hst"
+ "hakurei.app/internal/store"
+)
+
+// serial is the next value returned by [NewSerial].
+var serial atomic.Uint64
+
+// NewSerial returns a serial number.
+func NewSerial() uint64 { return serial.Add(1) - 1 }
+
+// ReceiveSignals blocks until a termination signal arrives, and terminates.
+func ReceiveSignals() {
+ s := make(chan os.Signal, 3)
+ signal.Notify(s, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
+ log.Fatalf("terminating on signal %s", <-s)
+}
+
+var (
+ // users caches [user.LookupId] calls.
+ users = make(map[string]*user.User)
+ // usersMu synchronises access to users.
+ usersMu sync.RWMutex
+)
+
+// mustLookupId is like [user.LookupId], but the first result is cached.
+func mustLookupId(uid string) *user.User {
+ usersMu.RLock()
+ v, ok := users[uid]
+ usersMu.RUnlock()
+ if ok {
+ return v
+ }
+
+ u, err := user.LookupId(uid)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ usersMu.Lock()
+ users[uid] = u
+ usersMu.Unlock()
+ return u
+}
+
+// MustAppendEnv adds extra environment variables to cmd.
+func MustAppendEnv(cmd *exec.Cmd, env ...string) {
+ if len(cmd.Env) == 0 {
+ var cred syscall.Credential
+ if cmd.SysProcAttr != nil && cmd.SysProcAttr.Credential != nil {
+ cred = *cmd.SysProcAttr.Credential
+ }
+ u := mustLookupId(strconv.Itoa(int(cred.Uid)))
+
+ cmd.Env = append(cmd.Env,
+ "PATH="+os.Getenv("PATH"),
+ "HOME="+u.HomeDir,
+ "USER="+u.Username,
+ "USERNAME="+u.Username,
+ )
+ }
+ cmd.Env = append(cmd.Env, env...)
+}
+
+// MustRun runs command and terminates the testsuite on error.
+func MustRun(cred *syscall.Credential, extraEnv []string, command ...string) {
+ cmd := exec.Command(command[0], command[1:]...)
+ cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Pdeathsig: syscall.SIGKILL,
+ Credential: cred,
+ }
+ MustAppendEnv(cmd, extraEnv...)
+ if err := cmd.Run(); err != nil {
+ log.Fatalf("must run %s: %v", strings.Join(command, " "), err)
+ }
+}
+
+// ErrUnexpectedSuccess is returned for processes expected to exit with a
+// non-zero code, but failed to do so.
+var ErrUnexpectedSuccess = errors.New("process unexpectedly exited with code 0")
+
+// MustFail runs command and terminates the testsuite if the program fails to
+// start or exits with code 0.
+func MustFail(
+ cred *syscall.Credential,
+ extraEnv []string,
+ quiet bool,
+ command ...string,
+) {
+ cmd := exec.Command(command[0], command[1:]...)
+ if !quiet {
+ cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
+ }
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Pdeathsig: syscall.SIGKILL,
+ Credential: cred,
+ }
+ MustAppendEnv(cmd, extraEnv...)
+ if err := cmd.Run(); err == nil {
+ log.Fatal(ErrUnexpectedSuccess)
+ } else if e, ok := errors.AsType[*exec.ExitError](err); !ok {
+ log.Fatalf("must fail %s: %v", strings.Join(command, " "), err)
+ } else if !e.Exited() {
+ log.Fatalf("must fail %s: %v", strings.Join(command, " "), e)
+ }
+}
+
+// MustStart starts cmd and returns a channel delivering its wait error.
+func MustStart(cmd *exec.Cmd) (done <-chan error) {
+ if err := cmd.Start(); err != nil {
+ log.Fatal(err)
+ }
+ d := make(chan error, 1)
+ go func() { d <- cmd.Wait() }()
+ return d
+}
+
+// MustStartWith wraps [MustStart] and creates the [exec.Cmd] object internally.
+func MustStartWith(
+ ctx context.Context,
+ cred *syscall.Credential,
+ quiet bool,
+ extraEnv []string,
+ files []*os.File,
+ command ...string,
+) (proc *os.Process, done <-chan error) {
+ cmd := exec.CommandContext(ctx, command[0], command[1:]...)
+ if !quiet {
+ cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
+ }
+ cmd.ExtraFiles = files
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Pdeathsig: syscall.SIGTERM,
+ Credential: cred,
+ }
+ MustAppendEnv(cmd, extraEnv...)
+ return cmd.Process, MustStart(cmd)
+}
+
+// MustOutput is like [MustRun] and [MustFail], but collects the entire output
+// in memory and returns them as strings.
+func MustOutput(
+ ctx context.Context,
+ cred *syscall.Credential,
+ extraEnv []string,
+ exitCode int,
+ name string,
+ command ...string,
+) (string, string) {
+ var stdout, stderr strings.Builder
+ cmd := exec.CommandContext(ctx, command[0], command[1:]...)
+ cmd.Stdout, cmd.Stderr = &stdout, &stderr
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Pdeathsig: syscall.SIGTERM,
+ Credential: cred,
+ }
+ MustAppendEnv(cmd, extraEnv...)
+
+ err := cmd.Run()
+ if err == nil {
+ if exitCode == 0 {
+ return stdout.String(), stderr.String()
+ }
+ log.Fatalf(
+ "%s succeeded unexpectedly"+
+ "\nstdout = %q\nstderr = %q",
+ name,
+ stdout.String(),
+ stderr.String(),
+ )
+ }
+
+ if e, ok := errors.AsType[*exec.ExitError](err); !ok {
+ log.Fatal(err)
+ } else if e.ExitCode() != exitCode {
+ log.Fatalf(
+ "%s: %v"+
+ "\nstdout = %q\nstderr = %q",
+ name, e,
+ stdout.String(),
+ stderr.String(),
+ )
+ }
+ return stdout.String(), stderr.String()
+}
+
+// AssertOutput returns a function that terminates the program if called with
+// arguments differing from the ones it is initialised with.
+func AssertOutput(action, stdout, stderr string) (
+ assert func(stdout, stderr string),
+) {
+ return func(gotStdout, gotStderr string) {
+ if stdout != gotStdout || stderr != gotStderr {
+ log.Fatalf(
+ "unexpected output %s"+
+ "\nstdout = %q\nstderr = %q",
+ action,
+ gotStdout,
+ gotStderr,
+ )
+ }
+ }
+}
+
+// MustCheckFilter is like [CheckFilter], but terminates the test suite if a
+// non-nil error is returned. Otherwise, the tracee is terminated after it
+// resumes.
+func MustCheckFilter(name string, pid int, sum [sha512.Size]byte) {
+ if err := CheckFilter(
+ pid,
+ 1, /* podman installs its own filter */
+ sum,
+ ); err != nil {
+ log.Fatalln(name, err)
+ } else if err = syscall.Kill(pid, syscall.SIGTERM); err != nil {
+ log.Fatalf("cannot terminate tracee: %v", err)
+ }
+}
+
+// FilterTerminated returns a non-nil error if err is not an [exec.ExitError]
+// describing a process terminated by a syscall.SIGTERM signal.
+func FilterTerminated(err error) error {
+ if err == nil {
+ return ErrUnexpectedSuccess
+ }
+
+ e, ok := errors.AsType[*exec.ExitError](err)
+ if !ok {
+ return err
+ }
+
+ if e.ExitCode() == 0x80+int(syscall.SIGTERM) {
+ return nil
+ }
+ return e
+}
+
+// MustDecodeFrom runs the specified command and decodes its JSON output,
+// storing the result in the value pointed to by v.
+func MustDecodeFrom[T any](
+ ctx context.Context,
+ cred *syscall.Credential,
+ env []string,
+ v *T,
+ name string,
+ arg ...string,
+) {
+ cmd := exec.CommandContext(ctx, name, arg...)
+ cmd.Env = env
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Pdeathsig: syscall.SIGKILL,
+ Credential: cred,
+ }
+ cmd.Stderr = os.Stderr
+
+ r, err := cmd.StdoutPipe()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ if err = cmd.Start(); err != nil {
+ log.Fatalf("start %s: %v", name, err)
+ }
+
+ if err = json.NewDecoder(r).Decode(v); err != nil {
+ log.Fatalf("decode %s: %v", name, err)
+ }
+
+ if err = cmd.Wait(); err != nil {
+ log.Fatalf("wait for %s: %v", name, err)
+ }
+}
+
+// Poll repeatedly runs command until it succeeds.
+func Poll(
+ d time.Duration,
+ cred *syscall.Credential,
+ extraEnv []string,
+ command ...string,
+) {
+ for range time.NewTicker(d).C {
+ cmd := exec.Command(command[0], command[1:]...)
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Pdeathsig: syscall.SIGKILL,
+ Credential: cred,
+ }
+ MustAppendEnv(cmd, extraEnv...)
+ if err := cmd.Run(); err != nil {
+ if e, ok := errors.AsType[*exec.ExitError](err); ok && e.Exited() {
+ continue
+ }
+ log.Fatalf("poll %s: %v", strings.Join(command, " "), err)
+ }
+ break
+ }
+}
+
+// PollPID polls until the process referred to by pid no longer exists.
+func PollPID(name string, pid int) {
+ for {
+ time.Sleep(10 * time.Millisecond)
+ if err := syscall.Kill(pid, 0); err == nil {
+ continue
+ } else if errors.Is(err, syscall.ESRCH) {
+ break
+ } else {
+ log.Fatalf("cannot poll for %s: %v", name, err)
+ }
+ }
+}
+
+const (
+ // XDGRuntimeDir is the hardcoded XDG runtime directory of the only user
+ // mapped in hsurc.
+ XDGRuntimeDir = "/var/run/user/1000"
+
+ // XDGRuntimeEnv is the environment variable string for XDG_RUNTIME_DIR.
+ XDGRuntimeEnv = "XDG_RUNTIME_DIR=" + XDGRuntimeDir
+
+ // HakureiPrefix is the hakurei work directory of the only user mapped in
+ // hsurc.
+ HakureiPrefix = "/tmp/hakurei.0/"
+)
+
+// MustPrepareUserDirs creates user-specific directories.
+func MustPrepareUserDirs(cred *syscall.Credential) {
+ if err := os.MkdirAll(XDGRuntimeDir, 0700); err != nil {
+ log.Fatal(err)
+ } else if err = os.Chown(
+ XDGRuntimeDir,
+ int(cred.Uid),
+ int(cred.Gid),
+ ); err != nil {
+ log.Fatal(err)
+ }
+}
+
+// SessionEnv returns environment variables referring to the hosted session.
+func SessionEnv(dbusEnv string) []string {
+ return []string{
+ "PATH=" + os.Getenv("PATH"),
+ "TERM=xterm",
+ XDGRuntimeEnv,
+ WaylandEnv,
+ "DISPLAY=:0",
+ dbusEnv,
+ }
+}
+
+// quietServices is initialised from the environment and controls whether
+// session services should have their outputs connected.
+var quietServices = os.Getenv("HAKUREI_VERBOSE_SERVICES") == ""
+
+// MustStartSessionBus starts a session bus that is never explicitly terminated.
+// The test suite is terminated if the session bus daemon terminates.
+func MustStartSessionBus(cred *syscall.Credential) (dbusEnv string) {
+ r, w, err := os.Pipe()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // this is never explicitly terminated
+ _, done := MustStartWith(
+ context.Background(), cred, quietServices, nil, []*os.File{w},
+ "dbus-daemon",
+ "--print-address=3",
+ "--address=unix:path="+XDGRuntimeDir+"/dbus",
+ "--session",
+ "--nofork",
+ "--nopidfile",
+ )
+
+ go func() {
+ if _err := <-done; _err != nil {
+ log.Fatalf("session bus terminated unexpectedly: %v", _err)
+ }
+ log.Fatal("session bus terminated unexpectedly")
+ }()
+
+ dbusEnv, err = bufio.NewReader(r).ReadString('\n')
+ if err != nil {
+ log.Fatal(err)
+ }
+ dbusEnv = dbusEnv[:len(dbusEnv)-1]
+ log.Printf("dbus listening on %s", dbusEnv)
+ dbusEnv = "DBUS_SESSION_BUS_ADDRESS=" + dbusEnv
+
+ if err = r.Close(); err != nil {
+ log.Fatal(err)
+ }
+ return
+}
+
+const (
+ // SwayEnv is the environment variable string for the sway IPC socket.
+ SwayEnv = "SWAYSOCK=" + XDGRuntimeDir + "/sway"
+ // WaylandEnv is the environment variable string for the wayland display.
+ WaylandEnv = "WAYLAND_DISPLAY=wayland-1"
+)
+
+// MustStartSway starts the sway wayland display server which must be terminated
+// by calling [TerminateSway].
+func MustStartSway(
+ wg *sync.WaitGroup,
+ cred *syscall.Credential,
+ dbusEnv string,
+) {
+ wg.Go(func() {
+ // this is terminated via swaymsg
+ _, done := MustStartWith(
+ context.Background(), cred, quietServices, []string{
+ "WLR_BACKENDS=headless",
+ XDGRuntimeEnv,
+ SwayEnv,
+ dbusEnv,
+ }, nil,
+ "sway",
+ )
+ if err := <-done; err != nil {
+ log.Fatalf("sway terminated unexpectedly: %v", err)
+ }
+ })
+
+ Poll(
+ 50*time.Millisecond,
+ cred,
+ []string{SwayEnv},
+ "swaymsg",
+ )
+ Poll(
+ 50*time.Millisecond,
+ cred,
+ []string{"DISPLAY=:0"},
+ "xset", "q",
+ )
+ log.Printf("sway available via %s", SwayEnv)
+}
+
+// TerminateSway requests for the sway server to terminate via sway IPC.
+func TerminateSway(cred *syscall.Credential) {
+ MustFail(cred, []string{SwayEnv}, quietServices, "swaymsg", "exit")
+}
+
+// SwayNode represents a Sway window with hakurei-specific fields.
+type SwayNode struct {
+ // Window title.
+ Name string `json:"name"`
+ // Descendents.
+ Nodes []SwayNode `json:"nodes"`
+ // Wayland app id.
+ AppID string `json:"app_id,omitempty"`
+
+ // Should always be app.hakurei for cmd/hakurei containers.
+ Sandbox string `json:"sandbox_engine,omitempty"`
+ // Configured via [hst].
+ ID string `json:"sandbox_app_id,omitempty"`
+ // Hakurei instance identifier.
+ Instance hst.ID `json:"sandbox_instance_id,omitempty"`
+}
+
+// All returns an iterator over n and all descendents of n.
+func (n *SwayNode) All() iter.Seq[*SwayNode] {
+ return func(yield func(*SwayNode) bool) {
+ if !yield(n) {
+ return
+ }
+
+ for _, d := range n.Nodes {
+ for v := range d.All() {
+ if !yield(v) {
+ return
+ }
+ }
+ }
+ }
+}
+
+// PollSwayWindow continuously polls sway for a Wayland client to appear.
+func PollSwayWindow(
+ ctx context.Context,
+ cred *syscall.Credential,
+ f func(n *SwayNode) bool,
+) *SwayNode {
+ var t SwayNode
+ for {
+ time.Sleep(25 * time.Millisecond)
+ MustDecodeFrom(
+ ctx, cred, []string{SwayEnv}, &t,
+ "swaymsg", "-t", "get_tree",
+ )
+ for n := range t.All() {
+ if f(n) {
+ return n
+ }
+ }
+ }
+}
+
+// MustStartPipeWire starts a PipeWire server that is never explicitly
+// terminated. The test suite is terminated if the PipeWire server terminates.
+func MustStartPipeWire(cred *syscall.Credential, dbusEnv string) (version string) {
+ version, _ = MustOutput(
+ context.Background(), nil, nil, 0,
+ "pipewire version",
+ "pipewire", "--version",
+ )
+ _, version, _ = strings.Cut(version, "Compiled with libpipewire ")
+ _, version, _ = strings.Cut(version, "\nLinked with libpipewire")
+ version = strings.TrimSpace(version)
+
+ // this is never explicitly terminated
+ _, done := MustStartWith(
+ context.Background(), cred, quietServices, []string{
+ XDGRuntimeEnv,
+ dbusEnv,
+ }, nil,
+ "pipewire",
+ )
+
+ go func() {
+ if _err := <-done; _err != nil {
+ log.Fatalf("pipewire terminated unexpectedly: %v", _err)
+ }
+ log.Fatal("pipewire terminated unexpectedly")
+ }()
+
+ Poll(50*time.Millisecond, cred, []string{
+ XDGRuntimeEnv,
+ dbusEnv,
+ },
+ "wpctl",
+ "status",
+ )
+
+ Poll(50*time.Millisecond, cred, []string{
+ XDGRuntimeEnv,
+ dbusEnv,
+ },
+ "pw-cli",
+ "load-module",
+ "libpipewire-module-null-sink",
+ )
+
+ _, _done := MustStartWith(
+ context.Background(), cred, quietServices, []string{
+ XDGRuntimeEnv,
+ dbusEnv,
+ }, nil,
+ "wireplumber",
+ )
+
+ go func() {
+ if _err := <-_done; _err != nil {
+ log.Fatalf("wireplumber terminated unexpectedly: %v", _err)
+ }
+ log.Fatal("wireplumber terminated unexpectedly")
+ }()
+ return
+}
+
+// MustScanFor continuously scans the proc filesystem and calls f for each entry
+// visited.
+func MustScanFor(f func(ps *StatScanner) bool) int {
+ var ps StatScanner
+
+ for ps.Scan() {
+ if f(&ps) {
+ break
+ }
+ }
+ if err := ps.Err(); err != nil {
+ log.Fatal(err)
+ }
+ return ps.Stat().PID
+}
+
+// storeLookup describes a store lookup request.
+type storeLookup struct {
+ // Identifier to look up.
+ id hst.ID
+ // Address of value to write result to.
+ p *hst.State
+ // Error encountered while fulfilling the request.
+ done chan<- error
+}
+
+var (
+ // storeThreads holds channels for dispatching requests to goroutines
+ // locked to threads configured with specific credentials.
+ storeThreads = make(map[[2]uint32]chan<- storeLookup)
+ // storeThreadsMu synchronises access to storeThreads.
+ storeThreadsMu sync.Mutex
+)
+
+// LookupStore looks up the specified identifier from the hakurei state store
+// and stores its contents in the value pointed to by p.
+func LookupStore(cred *syscall.Credential, id hst.ID, p *hst.State) error {
+ var key [2]uint32
+ if cred != nil {
+ key[0], key[1] = cred.Uid, cred.Gid
+ }
+
+ storeThreadsMu.Lock()
+ c, ok := storeThreads[key]
+ if !ok {
+ s := store.New(check.MustAbs(HakureiPrefix + "state"))
+ r := make(chan storeLookup)
+ go func() {
+ if cred != nil {
+ runtime.LockOSThread()
+ if err := syscall.Setfsuid(int(cred.Uid)); err != nil {
+ log.Fatalf("setfsuid: %v", err)
+ } else if err = syscall.Setfsgid(int(cred.Gid)); err != nil {
+ log.Fatalf("setfsgid: %v", err)
+ }
+ }
+
+ req:
+ for req := range r {
+ ok = false
+ entries, copyError := s.All()
+ for entry := range entries {
+ if entry.ID == req.id {
+ ok = true
+ if _, err := entry.Load(req.p, nil); err != nil {
+ req.done <- err
+ continue req
+ }
+ break
+ }
+ }
+ if err := copyError(); err != nil {
+ req.done <- err
+ continue
+ }
+ if !ok {
+ req.done <- fmt.Errorf("instance %s is not present in store", id.String())
+ continue
+ }
+ req.done <- nil
+ }
+ }()
+ c = r
+ storeThreads[key] = c
+ }
+ storeThreadsMu.Unlock()
+
+ done := make(chan error, 1)
+ c <- storeLookup{id, p, done}
+ return <-done
+}
+
+// StartHakurei starts a hakurei container configured by c.
+func StartHakurei(
+ ctx context.Context,
+ wg *sync.WaitGroup,
+ cred *syscall.Credential,
+ dbusEnv string,
+ processCommand func(cmd *exec.Cmd),
+ onDone func(err error, output string),
+ scan, wait, verbose, insecure bool,
+ c *hst.Config,
+) (p hst.State, pid int, err error) {
+ var hakureiFlags string
+ if verbose {
+ hakureiFlags += " -v"
+ }
+ if insecure {
+ hakureiFlags += " --insecure"
+ }
+
+ var notifySuffix string
+ if wait {
+ notifySuffix += " 1>&3"
+ }
+
+ cmd := exec.CommandContext(ctx,
+ "script", "/dev/null",
+ "-E", "always",
+ "-qec",
+ "hakurei"+hakureiFlags+" run "+
+ "--identifier-fd=5"+
+ " 4"+notifySuffix,
+ )
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Pdeathsig: syscall.SIGTERM,
+ Credential: cred,
+ }
+ var output strings.Builder
+ cmd.Stdout, cmd.Stderr = &output, &output
+ cmd.Env = SessionEnv(dbusEnv)
+
+ var notify, _notify, _conf, conf, ident, _ident *os.File
+ if notify, _notify, err = os.Pipe(); err != nil {
+ return
+ }
+ cmd.ExtraFiles = append(cmd.ExtraFiles, _notify)
+ if _conf, conf, err = os.Pipe(); err != nil {
+ _, _ = notify.Close(), _notify.Close()
+ return
+ }
+ cmd.ExtraFiles = append(cmd.ExtraFiles, _conf)
+ if ident, _ident, err = os.Pipe(); err != nil {
+ _, _ = notify.Close(), _notify.Close()
+ _, _ = conf.Close(), _conf.Close()
+ return
+ }
+ cmd.ExtraFiles = append(cmd.ExtraFiles, _ident)
+ if processCommand != nil {
+ processCommand(cmd)
+ }
+
+ done := MustStart(cmd)
+ wg.Go(func() { _err := <-done; onDone(_err, output.String()) })
+
+ if err = errors.Join(
+ _notify.Close(),
+ _conf.Close(),
+ _ident.Close(),
+ ); err != nil {
+ _, _, _ = notify.Close(), conf.Close(), ident.Close()
+ return
+ }
+
+ if err = json.NewEncoder(conf).Encode(c); err != nil {
+ _, _, _ = notify.Close(), conf.Close(), ident.Close()
+ return
+ } else if err = conf.Close(); err != nil {
+ _, _ = notify.Close(), ident.Close()
+ return
+ }
+
+ if !scan && len(cmd.ExtraFiles) != 3 {
+ err = errors.Join(notify.Close(), ident.Close())
+ return
+ }
+
+ var id hst.ID
+ if _, err = io.ReadFull(ident, id[:]); err != nil {
+ _, _ = notify.Close(), ident.Close()
+ if !scan && errors.Is(err, io.EOF) {
+ err = nil
+ }
+ return
+ } else if err = ident.Close(); err != nil {
+ _ = notify.Close()
+ return
+ }
+
+ if !scan {
+ err = notify.Close()
+ return
+ }
+
+ if wait {
+ if _, err = io.ReadFull(notify, make([]byte, 8)); err != nil {
+ _ = notify.Close()
+ return
+ }
+ }
+ if err = notify.Close(); err != nil {
+ return
+ }
+
+ var identifiers []hst.ID
+ MustDecodeFrom(
+ ctx, cred, nil, &identifiers,
+ "hakurei", "--json", "ps", "--short",
+ )
+ if !slices.Contains(identifiers, id) {
+ log.Fatalf("(%s) instance not present in ps output", c.Container.Hostname)
+ }
+
+ var got hst.State
+ MustDecodeFrom(
+ ctx, cred, nil, &got,
+ "hakurei", "--json", "show", id.String(),
+ )
+
+ if err = LookupStore(cred, id, &p); err != nil {
+ return
+ }
+
+ if !reflect.DeepEqual(got, p) {
+ log.Fatalf("show: %#v, want %#v", got, p)
+ }
+
+ var stat syscall.Stat_t
+ pid = MustScanFor(func(ps *StatScanner) bool {
+ select {
+ case err = <-done:
+ if err == nil {
+ log.Fatal("test process terminated unexpectedly")
+ }
+ log.Fatal(err)
+ default:
+ break
+ }
+
+ var args []string
+ if args, err = ps.Stat().Args(); err != nil {
+ if IsNotExist(err) {
+ return false
+ }
+ log.Fatal(err)
+ } else if !slices.Equal(args, c.Container.Args) {
+ return false
+ }
+
+ if err = ps.Stat().Stat(&stat); err != nil {
+ if IsNotExist(err) {
+ return false
+ }
+ log.Fatal(err)
+ }
+
+ uid := hst.ToUser[uint32](0, uint32(c.Identity))
+ if stat.Uid != uid || stat.Gid != uid {
+ return false
+ }
+
+ var t []byte
+ if t, err = os.ReadFile(filepath.Join(
+ fhs.Proc,
+ strconv.Itoa(ps.Stat().PPID),
+ "stat",
+ )); err != nil {
+ if IsNotExist(err) {
+ return false
+ }
+ log.Fatal(err)
+ }
+
+ var _stat Stat
+ if err = _stat.UnmarshalText(t); err != nil {
+ log.Fatal(err)
+ }
+ if _stat.PPID != p.ShimPID {
+ return false
+ }
+
+ return true
+ })
+ return
+}
+
+// MustStartHakurei is like [StartHakurei], but terminates the test suite if
+// an error is encountered.
+func MustStartHakurei(
+ ctx context.Context,
+ wg *sync.WaitGroup,
+ cred *syscall.Credential,
+ dbusEnv string,
+ onDone func(err error, output string),
+ scan, wait, verbose, insecure bool,
+ c *hst.Config,
+) (p hst.State, pid int) {
+ _ctx, cancel := context.WithCancel(ctx)
+
+ var err error
+ p, pid, err = StartHakurei(
+ _ctx, wg, cred, dbusEnv,
+ nil, func(err error, output string) {
+ defer cancel()
+ onDone(err, output)
+ }, scan, wait, verbose, insecure, c,
+ )
+ if err != nil {
+ log.Printf(
+ "(%s) cannot start hakurei container: %v",
+ c.Container.Hostname,
+ err,
+ )
+ cancel()
+ }
+ return
+}
diff --git a/internal/testsuite/testsuite_guard.go b/internal/testsuite/testsuite_guard.go
new file mode 100644
index 00000000..0859d17f
--- /dev/null
+++ b/internal/testsuite/testsuite_guard.go
@@ -0,0 +1,15 @@
+//go:build !testsuite && !tester
+
+package testsuite
+
+import (
+ "os"
+ "testing"
+)
+
+func init() {
+ if !testing.Testing() {
+ println("package testsuite imported in non-testsuite program")
+ os.Exit(1)
+ }
+}
diff --git a/internal/testsuite/testsuite_root.go b/internal/testsuite/testsuite_root.go
new file mode 100644
index 00000000..11f503ef
--- /dev/null
+++ b/internal/testsuite/testsuite_root.go
@@ -0,0 +1,17 @@
+//go:build testsuite
+
+package testsuite
+
+import (
+ "log"
+ "os"
+)
+
+func init() {
+ if os.Geteuid() != 0 {
+ log.Fatal("this program must run as root")
+ }
+
+ log.SetFlags(0)
+ log.SetPrefix("testsuite: ")
+}
diff --git a/internal/wayland/security-context-v1-protocol.c b/internal/wayland/security-context-v1-protocol.c
index 5b15b1b4..54cd69d2 100644
--- a/internal/wayland/security-context-v1-protocol.c
+++ b/internal/wayland/security-context-v1-protocol.c
@@ -1,4 +1,4 @@
-/* Generated by wayland-scanner 1.23.1 */
+/* Generated by wayland-scanner 1.26.0 */
/*
* Copyright © 2021 Simon Ser
diff --git a/internal/wayland/security-context-v1-protocol.h b/internal/wayland/security-context-v1-protocol.h
index f973619e..0b5bc69e 100644
--- a/internal/wayland/security-context-v1-protocol.h
+++ b/internal/wayland/security-context-v1-protocol.h
@@ -1,4 +1,4 @@
-/* Generated by wayland-scanner 1.23.1 */
+/* Generated by wayland-scanner 1.26.0 */
#ifndef SECURITY_CONTEXT_V1_CLIENT_PROTOCOL_H
#define SECURITY_CONTEXT_V1_CLIENT_PROTOCOL_H
diff --git a/internal/wayland/wayland-client-helper.c b/internal/wayland/wayland-client-helper.c
index 25af847b..0af69403 100644
--- a/internal/wayland/wayland-client-helper.c
+++ b/internal/wayland/wayland-client-helper.c
@@ -8,110 +8,119 @@
#include "security-context-v1-protocol.h"
#include <wayland-client.h>
-static void registry_handle_global(
- void *data,
- struct wl_registry *registry,
- uint32_t name,
- const char *interface,
- uint32_t version) {
- struct wp_security_context_manager_v1 **out = data;
+static void registry_handle_global(void *data, struct wl_registry *registry,
+ uint32_t name, const char *interface,
+ uint32_t version)
+{
+ struct wp_security_context_manager_v1 **out = data;
- if (strcmp(interface, wp_security_context_manager_v1_interface.name) == 0)
- *out = wl_registry_bind(registry, name, &wp_security_context_manager_v1_interface, 1);
+ if (strcmp(interface, wp_security_context_manager_v1_interface.name) ==
+ 0)
+ *out = wl_registry_bind(
+ registry, name,
+ &wp_security_context_manager_v1_interface, 1);
}
-static void registry_handle_global_remove(
- void *data,
- struct wl_registry *registry,
- uint32_t name) {} /* no-op */
+static void registry_handle_global_remove(void *data,
+ struct wl_registry *registry,
+ uint32_t name)
+{
+} /* no-op */
static const struct wl_registry_listener registry_listener = {
- .global = registry_handle_global,
- .global_remove = registry_handle_global_remove,
+ .global = registry_handle_global,
+ .global_remove = registry_handle_global_remove,
};
-hakurei_wayland_res hakurei_security_context_bind(
- char *socket_path,
- int server_fd,
- const char *app_id,
- const char *instance_id,
- int close_fd) {
- hakurei_wayland_res res = HAKUREI_WAYLAND_SUCCESS; /* see wayland.go for handling */
+hakurei_wayland_res hakurei_security_context_bind(char *socket_path,
+ int server_fd,
+ const char *app_id,
+ const char *instance_id,
+ int close_fd)
+{
+ hakurei_wayland_res res =
+ HAKUREI_WAYLAND_SUCCESS; /* see wayland.go for handling */
- struct wl_display *display = NULL;
- struct wl_registry *registry;
- struct wp_security_context_manager_v1 *security_context_manager = NULL;
- int event_cnt;
- int listen_fd = -1;
- struct sockaddr_un sockaddr = {0};
- struct wp_security_context_v1 *security_context;
+ struct wl_display *display = NULL;
+ struct wl_registry *registry;
+ struct wp_security_context_manager_v1 *security_context_manager = NULL;
+ int event_cnt;
+ int listen_fd = -1;
+ struct sockaddr_un sockaddr = { 0 };
+ struct wp_security_context_v1 *security_context;
- display = wl_display_connect_to_fd(server_fd);
- if (display == NULL) {
- res = HAKUREI_WAYLAND_CONNECT;
- goto out;
- };
+ display = wl_display_connect_to_fd(server_fd);
+ if (display == NULL) {
+ res = HAKUREI_WAYLAND_CONNECT;
+ goto out;
+ };
- registry = wl_display_get_registry(display);
- if (wl_registry_add_listener(registry, &registry_listener, &security_context_manager) < 0) {
- res = HAKUREI_WAYLAND_LISTENER;
- goto out;
- }
- event_cnt = wl_display_roundtrip(display);
- wl_registry_destroy(registry);
- if (event_cnt < 0) {
- res = HAKUREI_WAYLAND_ROUNDTRIP;
- goto out;
- }
+ registry = wl_display_get_registry(display);
+ if (wl_registry_add_listener(registry, &registry_listener,
+ &security_context_manager) < 0) {
+ res = HAKUREI_WAYLAND_LISTENER;
+ goto out;
+ }
+ event_cnt = wl_display_roundtrip(display);
+ wl_registry_destroy(registry);
+ if (event_cnt < 0) {
+ res = HAKUREI_WAYLAND_ROUNDTRIP;
+ goto out;
+ }
- if (security_context_manager == NULL) {
- res = HAKUREI_WAYLAND_NOT_AVAIL;
- goto out;
- }
+ if (security_context_manager == NULL) {
+ res = HAKUREI_WAYLAND_NOT_AVAIL;
+ goto out;
+ }
- listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
- if (listen_fd < 0) {
- res = HAKUREI_WAYLAND_SOCKET;
- goto out;
- }
+ listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (listen_fd < 0) {
+ res = HAKUREI_WAYLAND_SOCKET;
+ goto out;
+ }
- sockaddr.sun_family = AF_UNIX;
- snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", socket_path);
- if (bind(listen_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != 0) {
- res = HAKUREI_WAYLAND_BIND;
- goto out;
- }
+ sockaddr.sun_family = AF_UNIX;
+ snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s",
+ socket_path);
+ if (bind(listen_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) !=
+ 0) {
+ res = HAKUREI_WAYLAND_BIND;
+ goto out;
+ }
- if (listen(listen_fd, 0) != 0) {
- res = HAKUREI_WAYLAND_LISTEN;
- goto out;
- }
+ if (listen(listen_fd, 0) != 0) {
+ res = HAKUREI_WAYLAND_LISTEN;
+ goto out;
+ }
- security_context = wp_security_context_manager_v1_create_listener(security_context_manager, listen_fd, close_fd);
- if (security_context == NULL) { /* not reached */
- res = HAKUREI_WAYLAND_NOT_AVAIL;
- goto out;
- }
- wp_security_context_v1_set_sandbox_engine(security_context, "app.hakurei");
- wp_security_context_v1_set_app_id(security_context, app_id);
- wp_security_context_v1_set_instance_id(security_context, instance_id);
- wp_security_context_v1_commit(security_context);
- wp_security_context_v1_destroy(security_context);
- if (wl_display_roundtrip(display) < 0) {
- res = HAKUREI_WAYLAND_ROUNDTRIP;
- goto out;
- }
+ security_context = wp_security_context_manager_v1_create_listener(
+ security_context_manager, listen_fd, close_fd);
+ if (security_context == NULL) { /* not reached */
+ res = HAKUREI_WAYLAND_NOT_AVAIL;
+ goto out;
+ }
+ wp_security_context_v1_set_sandbox_engine(security_context,
+ "app.hakurei");
+ wp_security_context_v1_set_app_id(security_context, app_id);
+ wp_security_context_v1_set_instance_id(security_context, instance_id);
+ wp_security_context_v1_commit(security_context);
+ wp_security_context_v1_destroy(security_context);
+ if (wl_display_roundtrip(display) < 0) {
+ res = HAKUREI_WAYLAND_ROUNDTRIP;
+ goto out;
+ }
out:
- if (listen_fd >= 0)
- close(listen_fd);
- if (security_context_manager != NULL)
- wp_security_context_manager_v1_destroy(security_context_manager);
- if (display != NULL)
- wl_display_disconnect(display);
+ if (listen_fd >= 0)
+ close(listen_fd);
+ if (security_context_manager != NULL)
+ wp_security_context_manager_v1_destroy(
+ security_context_manager);
+ if (display != NULL)
+ wl_display_disconnect(display);
- free((void *)socket_path);
- free((void *)app_id);
- free((void *)instance_id);
- return res;
+ free((void *)socket_path);
+ free((void *)app_id);
+ free((void *)instance_id);
+ return res;
}
diff --git a/internal/wayland/wayland-client-helper.h b/internal/wayland/wayland-client-helper.h
index ce67f6f6..09348be4 100644
--- a/internal/wayland/wayland-client-helper.h
+++ b/internal/wayland/wayland-client-helper.h
@@ -2,41 +2,41 @@
#include <sys/un.h>
typedef enum {
- HAKUREI_WAYLAND_SUCCESS,
- /* wl_display_connect_to_fd failed, errno */
- HAKUREI_WAYLAND_CONNECT,
- /* wl_registry_add_listener failed, errno */
- HAKUREI_WAYLAND_LISTENER,
- /* wl_display_roundtrip failed, errno */
- HAKUREI_WAYLAND_ROUNDTRIP,
- /* compositor does not implement wp_security_context_v1 */
- HAKUREI_WAYLAND_NOT_AVAIL,
- /* socket failed, errno */
- HAKUREI_WAYLAND_SOCKET,
- /* bind failed, errno */
- HAKUREI_WAYLAND_BIND,
- /* listen failed, errno */
- HAKUREI_WAYLAND_LISTEN,
+ HAKUREI_WAYLAND_SUCCESS,
+ /* wl_display_connect_to_fd failed, errno */
+ HAKUREI_WAYLAND_CONNECT,
+ /* wl_registry_add_listener failed, errno */
+ HAKUREI_WAYLAND_LISTENER,
+ /* wl_display_roundtrip failed, errno */
+ HAKUREI_WAYLAND_ROUNDTRIP,
+ /* compositor does not implement wp_security_context_v1 */
+ HAKUREI_WAYLAND_NOT_AVAIL,
+ /* socket failed, errno */
+ HAKUREI_WAYLAND_SOCKET,
+ /* bind failed, errno */
+ HAKUREI_WAYLAND_BIND,
+ /* listen failed, errno */
+ HAKUREI_WAYLAND_LISTEN,
- /* ensure pathname failed, implemented in conn.go */
- HAKUREI_WAYLAND_CREAT,
- /* socket for host server failed, implemented in conn.go */
- HAKUREI_WAYLAND_HOST_SOCKET,
- /* connect for host server failed, implemented in conn.go */
- HAKUREI_WAYLAND_HOST_CONNECT,
- /* cleanup failed, implemented in conn.go */
- HAKUREI_WAYLAND_CLEANUP,
+ /* ensure pathname failed, implemented in conn.go */
+ HAKUREI_WAYLAND_CREAT,
+ /* socket for host server failed, implemented in conn.go */
+ HAKUREI_WAYLAND_HOST_SOCKET,
+ /* connect for host server failed, implemented in conn.go */
+ HAKUREI_WAYLAND_HOST_CONNECT,
+ /* cleanup failed, implemented in conn.go */
+ HAKUREI_WAYLAND_CLEANUP,
} hakurei_wayland_res;
-hakurei_wayland_res hakurei_security_context_bind(
- char *socket_path,
- int server_fd,
- const char *app_id,
- const char *instance_id,
- int close_fd);
+hakurei_wayland_res hakurei_security_context_bind(char *socket_path,
+ int server_fd,
+ const char *app_id,
+ const char *instance_id,
+ int close_fd);
/* returns whether the specified size fits in the sun_path field of sockaddr_un */
-static inline bool hakurei_is_valid_size_sun_path(size_t sz) {
- struct sockaddr_un sockaddr;
- return sz <= sizeof(sockaddr.sun_path);
+static inline bool hakurei_is_valid_size_sun_path(size_t sz)
+{
+ struct sockaddr_un sockaddr;
+ return sz <= sizeof(sockaddr.sun_path);
};
diff --git a/internal/workflows/Containerfile b/internal/workflows/Containerfile
new file mode 100644
index 00000000..82ce8820
--- /dev/null
+++ b/internal/workflows/Containerfile
@@ -0,0 +1,93 @@
+FROM docker.gitea.com/runner-images:ubuntu-latest
+
+RUN rm /var/run && ln -sf ../run /var && mkdir /var/empty
+RUN add-apt-repository -y ppa:savoury1/pipewire && \
+ apt-get update && apt-get install -y \
+ msft-golang \
+ libmount-dev \
+ acl \
+ sway \
+ xwayland \
+ x11-xserver-utils \
+ xdg-dbus-proxy \
+ libnotify-bin \
+ pipewire \
+ pipewire-pulse \
+ wireplumber- \
+ pulseaudio-utils \
+ foot \
+ xterm \
+ grim \
+ fuse3 \
+ fsmark && \
+ apt-get clean && rm -rf /var/lib/apt/lists/*
+
+RUN PACKAGES=" \
+ build-essential \
+ ninja-build \
+ bison \
+ libxcb-composite0-dev \
+ libxcb-ewmh-dev \
+ libxcb-icccm4-dev \
+ libxcb-res0-dev \
+ libjson-c-dev \
+ libpango1.0-dev \
+ libevdev-dev \
+ libudev-dev \
+ libpipewire-0.3-dev" && \
+ apt-get update && apt-get install -y $PACKAGES && \
+ VERSION="1.12.1" && \
+ curl -L "https://github.com/mesonbuild/meson/releases/download/$VERSION/meson-$VERSION.tar.gz" | \
+ tar -C /tmp -zx && \
+ MESON_DIR="/tmp/meson-$VERSION" && \
+ MESON="$MESON_DIR/meson.py" && \
+ VERSION="0.20.2" && \
+ curl "https://gitlab.freedesktop.org/wlroots/wlroots/-/archive/$VERSION/wlroots-${VERSION}.tar.bz2?ref_type=tags" | \
+ tar -C /tmp -jx && \
+ "$MESON" setup \
+ --wrap-mode=default \
+ -Dprefix=/usr \
+ -Dbuildtype=release \
+ -Dc_args='-w' \
+ -Dxwayland=enabled \
+ "/tmp/wlroots-$VERSION" build && \
+ "$MESON" compile -C build && \
+ "$MESON" install -C build && \
+ rm -rf build "/tmp/wlroots-$VERSION" && \
+ VERSION="1.32.0" && \
+ curl "https://gitlab.freedesktop.org/libinput/libinput/-/archive/$VERSION/libinput-$VERSION.tar.bz2?ref_type=tags" | \
+ tar -C /tmp -jx && \
+ "$MESON" setup \
+ -Dprefix=/usr \
+ -Dbuildtype=release \
+ -Dmtdev=false \
+ -Dlibwacom=false \
+ -Ddebug-gui=false \
+ "/tmp/libinput-$VERSION" build && \
+ "$MESON" compile -C build && \
+ "$MESON" install -C build && \
+ rm -rf build "/tmp/libinput-$VERSION" && \
+ VERSION="1.12" && \
+ curl -L "https://github.com/swaywm/sway/releases/download/$VERSION/sway-$VERSION.tar.gz" | \
+ tar -C /tmp -zx && \
+ "$MESON" setup \
+ -Dprefix=/usr \
+ -Dbuildtype=release \
+ "/tmp/sway-$VERSION" build && \
+ "$MESON" compile -C build && \
+ "$MESON" install -C build && \
+ rm -rf build "/tmp/sway-$VERSION" && \
+ VERSION='0.5.14' && \
+ curl "https://gitlab.freedesktop.org/pipewire/wireplumber/-/archive/${VERSION}/wireplumber-${VERSION}.tar.bz2" | \
+ tar -C /tmp -jx && \
+ "$MESON" setup \
+ -Dprefix=/usr \
+ -Dbuildtype=release \
+ "/tmp/wireplumber-${VERSION}" build && \
+ "$MESON" compile -C build && \
+ "$MESON" install -C build && \
+ rm -rf build "/tmp/wireplumber-${VERSION}" && \
+ apt-get remove -y $PACKAGES && \
+ apt-get autoremove -y && \
+ apt-get clean && rm -rf /var/lib/apt/lists/* && \
+ rm -rf "$MESON_DIR"
diff --git a/internal/workflows/build.sh b/internal/workflows/build.sh
new file mode 100755
index 00000000..2df8656e
--- /dev/null
+++ b/internal/workflows/build.sh
@@ -0,0 +1,5 @@
+#!/bin/sh -e
+
+TAG="git.gensokyo.uk/rosa/runner:latest"
+podman build -t "$TAG" --format docker "$(dirname -- "$0")"
+podman push "$TAG"
diff --git a/internal/workflows/doc.go b/internal/workflows/doc.go
new file mode 100644
index 00000000..e220ed67
--- /dev/null
+++ b/internal/workflows/doc.go
@@ -0,0 +1,162 @@
+//go:build !workflows
+
+/*
+The workflows program manages the .gitea directory to avoid having to write
+yaml. It is never invoked directly or built for the distribution tarball.
+
+This program is an internal detail of the hakurei project and is not usable
+on its own. It is not covered by the compatibility promise.
+
+# Configuring act_runner
+
+It is unreasonable and impractical to bootstrap Rosa OS each time a job runs.
+This section documents an ideal configuration for exposing the cmd/mbf CI
+service to all job containers, while avoiding some issues with the container
+setup documented upstream.
+
+Microsoft Github workflows expect a docker socket in the job container. This is
+problematic because docker has a hard time running inside a docker container.
+The Gitea act_runner simply bind mounts whatever socket it sees into the
+container. With a regular docker daemon, this allows not only a simple container
+escape, but also privilege escalation as unconstrained root in the init
+namespace. To mitigate this, set up an unprivileged podman daemon and expose its
+socket to the container instead. Since mountinfo always use credentials from the
+init user namespace, subordinate user and group ID must always be 100000.
+
+On Alpine Linux, this is achieved by:
+
+ # Configuration for /etc/init.d/podman
+
+ # See podman-system-service(1) for service description
+ # and available options.
+ #podman_opts="--time 0"
+
+ # API endpoint in URI form. Leave empty to use defaults.
+ podman_uri="unix:///srv/act_runner/podman"
+
+ # Setting root user will start rootful service.
+ # Use any other user for rootless mode.
+ podman_user="act_runner"
+
+ # Required by cmd/hakurei test suite.
+ rc_ulimit="-r 49"
+
+A symlink must then be created at /var/run/docker.sock pointing to the socket
+owned by the unprivileged daemon. This should be done in a /etc/local.d script.
+
+The container can be specified via docker-compose:
+
+ version: "3.8"
+ services:
+ runner:
+ restart: always
+ image: docker.io/gitea/act_runner:nightly
+ networks:
+ - act_runner
+ environment:
+ CONFIG_FILE: /config.yaml
+ GITEA_INSTANCE_URL: "https://git.gensokyo.uk"
+
+ GITEA_RUNNER_REGISTRATION_TOKEN: "${REGISTRATION_TOKEN}"
+ GITEA_RUNNER_NAME: "${RUNNER_NAME}"
+ volumes:
+ - ./config.yaml:/config.yaml
+ - ./data:/data
+ - ./podman:/var/run/docker.sock
+
+ networks:
+ act_runner:
+ name: act_runner
+
+Before starting the container, configure act_runner via config.yaml:
+
+ container:
+ network: act_runner
+ options: -e PAGER=cat -e MBF_CACHE_DIR=/rosa -e MBF_POISON_OPEN=1
+ -v /var/lib/rosa:/rosa
+ --security-opt='unmask=/proc/*'
+ --cap-add=SYS_ADMIN
+ --cap-add=SYS_PTRACE
+ --device=/dev/kvm
+ --device=/dev/fuse
+ valid_volumes:
+ - /var/lib/rosa
+
+ runner:
+ labels:
+ - 'rosa:docker://git.gensokyo.uk/rosa/runner:latest'
+
+where /var/lib/rosa is the absolute pathname of the cache directory in the init
+namespace. Setting MBF_POISON_OPEN enables cmd/mbf to run as root. It is also
+a good idea here to set runner.capacity to reflect the capacity of the guest, so
+jobs can be consumed quicker. Removing mount points covering /proc enables
+testing of cmd/hakurei. Exposing the fuse device and adding capability
+CAP_SYS_ADMIN enables testing of cmd/sharefs. Adding capability CAP_SYS_PTRACE
+enables dumping seccomp filters via ptrace on the patched kernel.
+
+Build a statically-linked cmd/mbf:
+
+ go build -trimpath \
+ -ldflags='-s -w
+ -buildid=
+ -linkmode external
+ -extldflags=-static' \
+ ./cmd/mbf
+
+Create a directory named "bin" in the cache directory and install the resulting
+binary.
+
+The cmd/mbf CI service should generally run as a separate user. To achieve this,
+change the socket permission to 0777 after the service starts. On Alpine Linux,
+this can be achieved by the init script:
+
+ #!/sbin/openrc-run
+ supervisor=supervise-daemon
+
+ name="Rosa OS cmd/mbf CI backend"
+ description="Listening service that services CI workloads from act_runner"
+
+ command=/var/lib/rosa/bin/mbf
+ command_args="-d /var/lib/rosa ci daemon"
+ command_user="${mbf_user:=mbf}"
+
+ output_logger="logger"
+ error_logger="logger"
+
+ depend() {
+ need sysfs
+ }
+
+ start_post() {
+ while [ ! -e /var/lib/rosa/ci ]; do sleep 0.1; done
+ chmod 0777 /var/lib/rosa/ci
+ }
+
+It is often a good idea to populate the cache from a mirror service before the
+first workflow job is started and re-populate it after every cmd/mbf update.
+
+# Configuring the kernel
+
+In order to attach to the container process, the sysctl kernel.yama.ptrace_scope
+must be set to 0. After which, apply the patch internal/workflows/seccomp.patch
+to your kernel sources, compile and install the new kernel. Refer to
+https://wiki.alpinelinux.org/wiki/Custom_Kernel if the guest runs Alpine Linux.
+If running podman or docker as root, the patch is not required. Do not apply
+this patch on a system meant to be secure.
+
+# Security
+
+The design of Microsoft Github workflows is inherently insecure: it requires
+internet access just to fetch actions, and these actions all expect internet
+access as well. Gitea, being a drop-in replacement, retains the same problems.
+
+The setup documented above already avoids the obvious container escape via the
+bind-mounted docker socket, but it is still easily possible to escape as the
+context of the unconstrained runner user. For this reason, the runner must
+reside in a hardened guest. Since internet access cannot be disabled, extra care
+must be taken when setting up the host firewall to prevent access to internal
+systems from the untrusted guest.
+*/
+package workflows
+
+//go:generate go run -tags=workflows .
diff --git a/internal/workflows/main.go b/internal/workflows/main.go
new file mode 100644
index 00000000..81b14a12
--- /dev/null
+++ b/internal/workflows/main.go
@@ -0,0 +1,151 @@
+//go:build workflows
+
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+)
+
+// KV represents a string to value pair.
+type KV[T any] struct {
+ K string
+ V T
+}
+
+// A Map represents an ordered set of key-value pairs.
+type Map[T any] []KV[T]
+
+// MarshalJSON encodes m as a key-value pair in JSON.
+func (m Map[T]) MarshalJSON() ([]byte, error) {
+ if m == nil || len(m) == 0 {
+ return []byte("{}"), nil
+ }
+
+ buf := bytes.NewBuffer(make([]byte, 0, len(m)<<5))
+ buf.WriteByte('{')
+
+ last := len(m) - 1
+ for i, pair := range m {
+ p, err := json.Marshal(pair.K)
+ if err != nil {
+ return nil, fmt.Errorf("key %q: %w", pair.K, err)
+ }
+ buf.Write(p)
+ buf.WriteByte(':')
+
+ p, err = json.Marshal(pair.V)
+ if err != nil {
+ return nil, fmt.Errorf("value of key %q: %w", pair.K, err)
+ }
+ buf.Write(p)
+
+ if i < last {
+ buf.WriteByte(',')
+ }
+ }
+
+ buf.WriteByte('}')
+ return buf.Bytes(), nil
+}
+
+// A Step is a task ran as part of a [Job].
+type Step struct {
+ // A name for the step.
+ Name string `json:"name,omitempty"`
+ // Selects an action to run as part of a step in the job.
+ Uses string `json:"uses,omitempty"`
+ // A unique identifier for the step.
+ ID string `json:"id,omitempty"`
+ // Prevent a step from running unless a condition is met.
+ If string `json:"if,omitempty"`
+ // Runs command-line programs that do not exceed 21,000 characters using the
+ // operating system's shell.
+ Run string `json:"run,omitempty"`
+ // Key-value pairs of input parameters defined by the action.
+ With Map[any] `json:"with,omitempty"`
+}
+
+// A Job is an entry in a [Workflow] document.
+type Job struct {
+ // Set a name for the job.
+ Name string `json:"name,omitempty"`
+ // Identifies any jobs that must complete successfully before this job will
+ // run.
+ Needs []string `json:"needs,omitempty"`
+ // Define the type of machine to run the job on.
+ On string `json:"runs-on,omitempty"`
+ // Create a map of outputs for a job.
+ Outputs Map[string] `json:"outputs,omitempty"`
+ // A sequence of tasks.
+ Steps []Step `json:"steps,omitempty"`
+}
+
+// Workflow describes a workflow document.
+type Workflow struct {
+ // The name of the workflow.
+ Name string `json:"name,omitempty"`
+ // The name for workflow runs generated from the workflow.
+ RunName string `json:"run-name,omitempty"`
+ // Define which events can cause the workflow to run.
+ On any `json:"on"`
+
+ // Named jobs part of the workflow run.
+ Jobs Map[Job] `json:"jobs,omitempty"`
+}
+
+// workflowsDir is the relative pathname of the workflows directory.
+const workflowsDir = "../../.gitea/workflows"
+
+// writeFile writes the action to the .gitea directory.
+func (w *Workflow) writeFile(name string) error {
+ f, err := os.OpenFile(
+ filepath.Join(workflowsDir, name+".yml"),
+ os.O_CREATE|os.O_EXCL|os.O_WRONLY,
+ 0444,
+ )
+ if err != nil {
+ return err
+ }
+
+ err = json.NewEncoder(f).Encode(w)
+ if closeErr := f.Close(); err == nil {
+ err = closeErr
+ }
+ return err
+}
+
+// removeOnce synchronises removal of stale workflow files.
+var removeOnce sync.Once
+
+// removeWorkflows removes existing workflow files once.
+func removeWorkflows() {
+ removeOnce.Do(func() {
+ if err := os.RemoveAll(workflowsDir); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ if err := os.Mkdir(workflowsDir, 0755); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ })
+}
+
+// mustWriteFile is like writeFile, but terminates the program if a non-nil
+// error is returned.
+func (w *Workflow) mustWriteFile(name string) (_ struct{}) {
+ removeWorkflows()
+
+ if err := w.writeFile(name); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ return
+}
+
+func main() {}
diff --git a/internal/workflows/release.go b/internal/workflows/release.go
new file mode 100644
index 00000000..a2f4c687
--- /dev/null
+++ b/internal/workflows/release.go
@@ -0,0 +1,50 @@
+//go:build workflows
+
+package main
+
+var _ = (&Workflow{
+ Name: "Release",
+ On: Map[any]{{"push", Map[[]string]{{"tags", []string{"v*"}}}}},
+
+ Jobs: []KV[Job]{
+ {"release", Job{
+ Name: "Create release",
+ On: "rosa",
+
+ Steps: []Step{
+ {
+ Name: "Checkout",
+ Uses: "actions/checkout@v4",
+
+ With: []KV[any]{
+ {"fetch-depth", 0},
+ {"fetch-tags", true},
+ },
+ },
+
+ {
+ Name: "Request distribution",
+ ID: "dist",
+ Run: "/rosa/bin/mbf ci dist -o result " +
+ ". \"$(cat cmd/dist/VERSION)-act\" && " +
+ "git tag " +
+ "--list " +
+ "--format='%(contents:body)' " +
+ "\"$(cat cmd/dist/VERSION)\" >> " +
+ "\"/tmp/release-body\"",
+ },
+
+ {
+ Name: "Create release",
+ Uses: "https://gitea.com/actions/gitea-release-action@v1.3.7",
+
+ With: []KV[any]{
+ {"files", "result/hakurei-**"},
+ {"body_path", "/tmp/release-body"},
+ {"draft", true},
+ },
+ },
+ },
+ }},
+ },
+}).mustWriteFile("release")
diff --git a/internal/workflows/seccomp.patch b/internal/workflows/seccomp.patch
new file mode 100644
index 00000000..ddabc71e
--- /dev/null
+++ b/internal/workflows/seccomp.patch
@@ -0,0 +1,18 @@
+diff --git a/kernel/seccomp.c b/kernel/seccomp.c
+index 25f62867a16d..7b63ccc8daf4 100644
+--- a/kernel/seccomp.c
++++ b/kernel/seccomp.c
+@@ -2216,8 +2216,12 @@ long seccomp_get_filter(struct task_struct *task, unsigned long filter_off,
+ struct seccomp_filter *filter;
+ struct sock_fprog_kern *fprog;
+ long ret;
++ struct user_namespace *user_ns = current_user_ns();
+
+- if (!capable(CAP_SYS_ADMIN) ||
++ if (in_userns(user_ns, task_cred_xxx(task, user_ns))) {
++ if (!ns_capable(user_ns, CAP_SYS_ADMIN))
++ return -EACCES;
++ } else if (!capable(CAP_SYS_ADMIN) ||
+ current->seccomp.mode != SECCOMP_MODE_DISABLED) {
+ return -EACCES;
+ }
diff --git a/internal/workflows/step.go b/internal/workflows/step.go
new file mode 100644
index 00000000..3458c0ea
--- /dev/null
+++ b/internal/workflows/step.go
@@ -0,0 +1,60 @@
+//go:build workflows
+
+package main
+
+// checkout is the standard actions/checkout step.
+var checkout = Step{Name: "Checkout", Uses: "actions/checkout@v4"}
+
+// newUploadArtifact returns an actions/upload-artifact step uploading
+// everything in the result directory as the specified name.
+func newUploadArtifact(display, name string) Step {
+ return Step{
+ Name: "Upload " + display,
+ Uses: "actions/upload-artifact@v3",
+
+ With: Map[any]{
+ {"name", name},
+ {"path", "result/*"},
+
+ {"retention-days", 1},
+ },
+ }
+}
+
+// newCIRequest returns a step for requesting a mbf CI task.
+func newCIRequest(display, name, id string) Step {
+ return Step{
+ Name: "Request " + display,
+ ID: id,
+ Run: "HAKUREI_REV=\"$(git rev-parse --short HEAD)\" && " +
+ "/rosa/bin/mbf ci " + name + " " +
+ ". \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" && " +
+ "echo \"rev=$HAKUREI_REV\" >> \"$GITHUB_OUTPUT\"",
+ }
+}
+
+var (
+ // dist is a step to obtain a hakurei distribution.
+ dist = newCIRequest("distribution", "dist -o /tmp/dist", "dist")
+ // _dist is like dist, but with race instrument.
+ _dist = newCIRequest("distribution", "race -o /tmp/dist", "dist")
+
+ // install is a step to unpack and deploy a hakurei distribution in /tmp/dist.
+ install = Step{
+ Name: "Install hakurei",
+ Run: "HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" && " +
+ "tar -C /tmp/dist " +
+ "-xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz && " +
+ "/tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh && " +
+ "rm -rf /tmp/dist && " +
+ "sudo -u ubuntu hakurei version",
+ }
+)
+
+// newTestsuite returns a step for running an integration test suite.
+func newTestsuite(name, prefix string) Step {
+ return Step{
+ Name: "Compile and run test suite",
+ Run: prefix + "go run -tags=testsuite ./cmd/" + name,
+ }
+}
diff --git a/internal/workflows/test.go b/internal/workflows/test.go
new file mode 100644
index 00000000..b0819916
--- /dev/null
+++ b/internal/workflows/test.go
@@ -0,0 +1,86 @@
+//go:build workflows
+
+package main
+
+var _ = (&Workflow{
+ Name: "Test",
+ On: []any{"push"},
+
+ Jobs: Map[Job]{
+ {"hakurei", Job{
+ Name: "Hakurei",
+ On: "rosa",
+
+ Steps: []Step{
+ checkout,
+ dist,
+ install,
+ newTestsuite("hakurei/testsuite", "unshare -n "),
+ },
+ }},
+
+ {"hakurei-race", Job{
+ Name: "Hakurei (with race instrument)",
+ On: "rosa",
+
+ Steps: []Step{
+ checkout,
+ _dist,
+ install,
+ newTestsuite("hakurei/testsuite", "unshare -n "),
+ },
+ }},
+
+ {"sandbox", Job{
+ Name: "Sandbox",
+ On: "rosa",
+
+ Steps: []Step{
+ checkout,
+ dist,
+ install,
+ newTestsuite("hakurei/testsuite/sandbox", "unshare -n "),
+ },
+ }},
+
+ {"sandbox-race", Job{
+ Name: "Sandbox (with race instrument)",
+ On: "rosa",
+
+ Steps: []Step{
+ checkout,
+ _dist,
+ install,
+ newTestsuite("hakurei/testsuite/sandbox", "unshare -n "),
+ },
+ }},
+
+ {"sharefs", Job{
+ Name: "ShareFS",
+ On: "rosa",
+
+ Steps: []Step{
+ checkout,
+ dist,
+ install,
+
+ {
+ Name: "Mount sharefs",
+ Run: "useradd " +
+ "-ru 1023 " +
+ "-md /var/lib/sdcard " +
+ "-k /var/empty " +
+ "-s /sbin/nologin " +
+ "media_rw && install -dm0 /sdcard && " +
+ "sharefs " +
+ "-o rw,noexec,nosuid,nodev,noatime," +
+ "allow_other,mkdir,source=/var/lib/sdcard," +
+ "setuid=1023,setgid=1023 /sdcard",
+ },
+
+ newTestsuite("sharefs/testsuite", "sharefs -V && "),
+ newUploadArtifact("test output", "fs_mark"),
+ },
+ }},
+ },
+}).mustWriteFile("test")