diff options
Diffstat (limited to 'internal')
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, ®istry_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, ®istry_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") |
