16 Commits

Author SHA1 Message Date
0b1009786f release: 0.4.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-11 10:49:43 +09:00
b390640376 internal/landlock: relocate from package container
This is not possible to use directly, so remove it from the public API.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 23:56:45 +09:00
ad2c9f36cd container: unexport PR_SET_NO_NEW_PRIVS wrapper
This is subtle to use correctly. It also does not make sense as part of the container API.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 23:45:51 +09:00
67db3fbb8d check: use encoding interfaces
This turned out not to require specific treatment, so the shared interfaces are cleaner.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 22:11:53 +09:00
560cb626a1 hst: remove enablement json adapter
The go116 behaviour of built-in new function makes this cleaner.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 20:47:30 +09:00
c33a6a5b7e hst: optionally reject insecure options
This prevents inadvertent use of insecure compatibility features.

Closes #30.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 19:34:02 +09:00
952082bd9b internal/rosa/python: 3.14.3 to 3.14.4
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 02:38:22 +09:00
24a9b24823 internal/rosa/openssl: 3.6.1 to 3.6.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 02:38:02 +09:00
c2e61e7987 internal/rosa/libcap: 2.77 to 2.78
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 02:37:04 +09:00
86787b3bc5 internal/rosa/tamago: 1.26.1 to 1.26.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 02:31:57 +09:00
cdfcfe6ce0 internal/rosa/go: 1.26.1 to 1.26.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 02:18:27 +09:00
68a2f0c240 internal/rosa/llvm: remove unused field
This change also renames confusingly named flags field and corrects its doc comment.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 02:13:26 +09:00
7319c7adf9 internal/rosa/llvm: use latest version on arm64
This also removes arch-specific patches because they were not useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 01:07:25 +09:00
e9c890cbb2 internal/rosa/llvm: enable cross compilation
This now passes the test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 00:59:14 +09:00
6f924336fc internal/rosa/llvm: increase stack size
Some aarch64 regression tests fail intermittently on the default size.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 00:56:51 +09:00
bd88f10524 internal/rosa/llvm: 22.1.2 to 22.1.3
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-09 17:36:23 +09:00
71 changed files with 504 additions and 1389 deletions

4
.gitignore vendored
View File

@@ -7,10 +7,6 @@
# go generate # go generate
/cmd/hakurei/LICENSE /cmd/hakurei/LICENSE
/cmd/pkgserver/.sass-cache
/cmd/pkgserver/ui/static/*.js
/cmd/pkgserver/ui/static/*.css*
/cmd/pkgserver/ui/static/*.css.map
/internal/pkg/testdata/testtool /internal/pkg/testdata/testtool
/internal/rosa/hakurei_current.tar.gz /internal/rosa/hakurei_current.tar.gz

View File

@@ -2,7 +2,7 @@
package check package check
import ( import (
"encoding/json" "encoding"
"errors" "errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
@@ -30,6 +30,16 @@ func (e AbsoluteError) Is(target error) bool {
// Absolute holds a pathname checked to be absolute. // Absolute holds a pathname checked to be absolute.
type Absolute struct{ pathname unique.Handle[string] } type Absolute struct{ pathname unique.Handle[string] }
var (
_ encoding.TextAppender = new(Absolute)
_ encoding.TextMarshaler = new(Absolute)
_ encoding.TextUnmarshaler = new(Absolute)
_ encoding.BinaryAppender = new(Absolute)
_ encoding.BinaryMarshaler = new(Absolute)
_ encoding.BinaryUnmarshaler = new(Absolute)
)
// ok returns whether [Absolute] is not the zero value. // ok returns whether [Absolute] is not the zero value.
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) } func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
@@ -84,13 +94,16 @@ func (a *Absolute) Append(elem ...string) *Absolute {
// Dir calls [filepath.Dir] with [Absolute] as its argument. // Dir calls [filepath.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) } func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }
// GobEncode returns the checked pathname. // AppendText appends the checked pathname.
func (a *Absolute) GobEncode() ([]byte, error) { func (a *Absolute) AppendText(data []byte) ([]byte, error) {
return []byte(a.String()), nil return append(data, a.String()...), nil
} }
// GobDecode stores data if it represents an absolute pathname. // MarshalText returns the checked pathname.
func (a *Absolute) GobDecode(data []byte) error { func (a *Absolute) MarshalText() ([]byte, error) { return a.AppendText(nil) }
// UnmarshalText stores data if it represents an absolute pathname.
func (a *Absolute) UnmarshalText(data []byte) error {
pathname := string(data) pathname := string(data)
if !filepath.IsAbs(pathname) { if !filepath.IsAbs(pathname) {
return AbsoluteError(pathname) return AbsoluteError(pathname)
@@ -99,23 +112,9 @@ func (a *Absolute) GobDecode(data []byte) error {
return nil return nil
} }
// MarshalJSON returns a JSON representation of the checked pathname. func (a *Absolute) AppendBinary(data []byte) ([]byte, error) { return a.AppendText(data) }
func (a *Absolute) MarshalJSON() ([]byte, error) { func (a *Absolute) MarshalBinary() ([]byte, error) { return a.MarshalText() }
return json.Marshal(a.String()) func (a *Absolute) UnmarshalBinary(data []byte) error { return a.UnmarshalText(data) }
}
// UnmarshalJSON stores data if it represents an absolute pathname.
func (a *Absolute) UnmarshalJSON(data []byte) error {
var pathname string
if err := json.Unmarshal(data, &pathname); err != nil {
return err
}
if !filepath.IsAbs(pathname) {
return AbsoluteError(pathname)
}
a.pathname = unique.Make(pathname)
return nil
}
// SortAbs calls [slices.SortFunc] for a slice of [Absolute]. // SortAbs calls [slices.SortFunc] for a slice of [Absolute].
func SortAbs(x []*Absolute) { func SortAbs(x []*Absolute) {

View File

@@ -170,20 +170,20 @@ func TestCodecAbsolute(t *testing.T) {
{"good", MustAbs("/etc"), {"good", MustAbs("/etc"),
nil, nil,
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc", "\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00", ",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00",
`"/etc"`, `{"val":"/etc","magic":3236757504}`}, `"/etc"`, `{"val":"/etc","magic":3236757504}`},
{"not absolute", nil, {"not absolute", nil,
AbsoluteError("etc"), AbsoluteError("etc"),
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc", "\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00", ",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
`"etc"`, `{"val":"etc","magic":3236757504}`}, `"etc"`, `{"val":"etc","magic":3236757504}`},
{"zero", nil, {"zero", nil,
new(AbsoluteError), new(AbsoluteError),
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00", "\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00", ",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
`""`, `{"val":"","magic":3236757504}`}, `""`, `{"val":"","magic":3236757504}`},
} }
@@ -347,15 +347,6 @@ func TestCodecAbsolute(t *testing.T) {
}) })
}) })
} }
t.Run("json passthrough", func(t *testing.T) {
t.Parallel()
wantErr := "invalid character ':' looking for beginning of value"
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
}
})
} }
func TestAbsoluteWrap(t *testing.T) { func TestAbsoluteWrap(t *testing.T) {

View File

@@ -39,6 +39,7 @@ var errSuccess = errors.New("success")
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command { func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var ( var (
flagVerbose bool flagVerbose bool
flagInsecure bool
flagJSON bool flagJSON bool
) )
c := command.New(out, log.Printf, "hakurei", func([]string) error { c := command.New(out, log.Printf, "hakurei", func([]string) error {
@@ -57,6 +58,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
return nil return nil
}). }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity"). Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagInsecure, "insecure", command.BoolFlag(false), "Allow use of insecure compatibility options").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable") Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess }) c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
@@ -75,7 +77,12 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
config.Container.Args = append(config.Container.Args, args[1:]...) config.Container.Args = append(config.Container.Args, args[1:]...)
} }
outcome.Main(ctx, msg, config, flagIdentifierFile) var flags int
if flagInsecure {
flags |= hst.VAllowInsecure
}
outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
panic("unreachable") panic("unreachable")
}). }).
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1), Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
@@ -145,7 +152,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
} }
} }
var et hst.Enablement var et hst.Enablements
if flagWayland { if flagWayland {
et |= hst.EWayland et |= hst.EWayland
} }
@@ -163,7 +170,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
ID: flagID, ID: flagID,
Identity: flagIdentity, Identity: flagIdentity,
Groups: flagGroups, Groups: flagGroups,
Enablements: hst.NewEnablements(et), Enablements: &et,
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
@@ -282,7 +289,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
} }
} }
outcome.Main(ctx, msg, &config, -1) outcome.Main(ctx, msg, &config, 0, -1)
panic("unreachable") panic("unreachable")
}). }).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"), Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),

View File

@@ -20,7 +20,7 @@ func TestHelp(t *testing.T) {
}{ }{
{ {
"main", []string{}, ` "main", []string{}, `
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS] Usage: hakurei [-h | --help] [-v] [--insecure] [--json] COMMAND [OPTIONS]
Commands: Commands:
run Load and start container from configuration file run Load and start container from configuration file

View File

@@ -56,7 +56,7 @@ func printShowInstance(
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
if err := config.Validate(); err != nil { if err := config.Validate(hst.VAllowInsecure); err != nil {
valid = false valid = false
if m, ok := message.GetMessage(err); ok { if m, ok := message.GetMessage(err); ok {
mustPrint(output, "Error: "+m+"!\n\n") mustPrint(output, "Error: "+m+"!\n\n")

View File

@@ -32,7 +32,7 @@ var (
PID: 0xbeef, PID: 0xbeef,
ShimPID: 0xcafe, ShimPID: 0xcafe,
Config: &hst.Config{ Config: &hst.Config{
Enablements: hst.NewEnablements(hst.EWayland | hst.EPipeWire), Enablements: new(hst.EWayland | hst.EPipeWire),
Identity: 1, Identity: 1,
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Shell: check.MustAbs("/bin/sh"), Shell: check.MustAbs("/bin/sh"),

View File

@@ -1,176 +0,0 @@
package main
import (
"encoding/json"
"log"
"net/http"
"net/url"
"path"
"strconv"
"sync"
"hakurei.app/internal/info"
"hakurei.app/internal/rosa"
)
// for lazy initialisation of serveInfo
var (
infoPayload struct {
// Current package count.
Count int `json:"count"`
// Hakurei version, set at link time.
HakureiVersion string `json:"hakurei_version"`
}
infoPayloadOnce sync.Once
)
// handleInfo writes constant system information.
func handleInfo(w http.ResponseWriter, _ *http.Request) {
infoPayloadOnce.Do(func() {
infoPayload.Count = int(rosa.PresetUnexportedStart)
infoPayload.HakureiVersion = info.Version()
})
// TODO(mae): cache entire response if no additional fields are planned
writeAPIPayload(w, infoPayload)
}
// newStatusHandler returns a [http.HandlerFunc] that offers status files for
// viewing or download, if available.
func (index *packageIndex) newStatusHandler(disposition bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
m, ok := index.names[path.Base(r.URL.Path)]
if !ok || !m.HasReport {
http.NotFound(w, r)
return
}
contentType := "text/plain; charset=utf-8"
if disposition {
contentType = "application/octet-stream"
// quoting like this is unsound, but okay, because metadata is hardcoded
contentDisposition := `attachment; filename="`
contentDisposition += m.Name + "-"
if m.Version != "" {
contentDisposition += m.Version + "-"
}
contentDisposition += m.ids + `.log"`
w.Header().Set("Content-Disposition", contentDisposition)
}
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := func() (err error) {
defer index.handleAccess(&err)()
_, err = w.Write(m.status)
return
}(); err != nil {
log.Println(err)
http.Error(
w, "cannot deliver status, contact maintainers",
http.StatusInternalServerError,
)
}
}
}
// handleGet writes a slice of metadata with specified order.
func (index *packageIndex) handleGet(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
limit, err := strconv.Atoi(q.Get("limit"))
if err != nil || limit > 100 || limit < 1 {
http.Error(
w, "limit must be an integer between 1 and 100",
http.StatusBadRequest,
)
return
}
i, err := strconv.Atoi(q.Get("index"))
if err != nil || i >= len(index.sorts[0]) || i < 0 {
http.Error(
w, "index must be an integer between 0 and "+
strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
http.StatusBadRequest,
)
return
}
sort, err := strconv.Atoi(q.Get("sort"))
if err != nil || sort >= len(index.sorts) || sort < 0 {
http.Error(
w, "sort must be an integer between 0 and "+
strconv.Itoa(sortOrderEnd),
http.StatusBadRequest,
)
return
}
values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
writeAPIPayload(w, &struct {
Values []*metadata `json:"values"`
}{values})
}
func (index *packageIndex) handleSearch(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
limit, err := strconv.Atoi(q.Get("limit"))
if err != nil || limit > 100 || limit < 1 {
http.Error(
w, "limit must be an integer between 1 and 100",
http.StatusBadRequest,
)
return
}
i, err := strconv.Atoi(q.Get("index"))
if err != nil || i >= len(index.sorts[0]) || i < 0 {
http.Error(
w, "index must be an integer between 0 and "+
strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
http.StatusBadRequest,
)
return
}
search, err := url.PathUnescape(q.Get("search"))
if len(search) > 100 || err != nil {
http.Error(
w, "search must be a string between 0 and 100 characters long",
http.StatusBadRequest,
)
return
}
desc := q.Get("desc") == "true"
n, res, err := index.performSearchQuery(limit, i, search, desc)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
writeAPIPayload(w, &struct {
Count int `json:"count"`
Results []searchResult `json:"results"`
}{n, res})
}
// apiVersion is the name of the current API revision, as part of the pattern.
const apiVersion = "v1"
// registerAPI registers API handler functions.
func (index *packageIndex) registerAPI(mux *http.ServeMux) {
mux.HandleFunc("GET /api/"+apiVersion+"/info", handleInfo)
mux.HandleFunc("GET /api/"+apiVersion+"/get", index.handleGet)
mux.HandleFunc("GET /api/"+apiVersion+"/search", index.handleSearch)
mux.HandleFunc("GET /api/"+apiVersion+"/status/", index.newStatusHandler(false))
mux.HandleFunc("GET /status/", index.newStatusHandler(true))
}
// writeAPIPayload sets headers common to API responses and encodes payload as
// JSON for the response body.
func writeAPIPayload(w http.ResponseWriter, payload any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
if err := json.NewEncoder(w).Encode(payload); err != nil {
log.Println(err)
http.Error(
w, "cannot encode payload, contact maintainers",
http.StatusInternalServerError,
)
}
}

View File

@@ -1,183 +0,0 @@
package main
import (
"net/http"
"net/http/httptest"
"slices"
"strconv"
"testing"
"hakurei.app/internal/info"
"hakurei.app/internal/rosa"
)
// prefix is prepended to every API path.
const prefix = "/api/" + apiVersion + "/"
func TestAPIInfo(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
handleInfo(w, httptest.NewRequestWithContext(
t.Context(),
http.MethodGet,
prefix+"info",
nil,
))
resp := w.Result()
checkStatus(t, resp, http.StatusOK)
checkAPIHeader(t, w.Header())
checkPayload(t, resp, struct {
Count int `json:"count"`
HakureiVersion string `json:"hakurei_version"`
}{int(rosa.PresetUnexportedStart), info.Version()})
}
func TestAPIGet(t *testing.T) {
t.Parallel()
const target = prefix + "get"
index := newIndex(t)
newRequest := func(suffix string) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
index.handleGet(w, httptest.NewRequestWithContext(
t.Context(),
http.MethodGet,
target+suffix,
nil,
))
return w
}
checkValidate := func(t *testing.T, suffix string, vmin, vmax int, wantErr string) {
t.Run("invalid", func(t *testing.T) {
t.Parallel()
w := newRequest("?" + suffix + "=invalid")
resp := w.Result()
checkError(t, resp, wantErr, http.StatusBadRequest)
})
t.Run("min", func(t *testing.T) {
t.Parallel()
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmin-1))
resp := w.Result()
checkError(t, resp, wantErr, http.StatusBadRequest)
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmin))
resp = w.Result()
checkStatus(t, resp, http.StatusOK)
})
t.Run("max", func(t *testing.T) {
t.Parallel()
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmax+1))
resp := w.Result()
checkError(t, resp, wantErr, http.StatusBadRequest)
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmax))
resp = w.Result()
checkStatus(t, resp, http.StatusOK)
})
}
t.Run("limit", func(t *testing.T) {
t.Parallel()
checkValidate(
t, "index=0&sort=0&limit", 1, 100,
"limit must be an integer between 1 and 100",
)
})
t.Run("index", func(t *testing.T) {
t.Parallel()
checkValidate(
t, "limit=1&sort=0&index", 0, int(rosa.PresetUnexportedStart-1),
"index must be an integer between 0 and "+strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
)
})
t.Run("sort", func(t *testing.T) {
t.Parallel()
checkValidate(
t, "index=0&limit=1&sort", 0, int(sortOrderEnd),
"sort must be an integer between 0 and "+strconv.Itoa(int(sortOrderEnd)),
)
})
checkWithSuffix := func(name, suffix string, want []*metadata) {
t.Run(name, func(t *testing.T) {
t.Parallel()
w := newRequest(suffix)
resp := w.Result()
checkStatus(t, resp, http.StatusOK)
checkAPIHeader(t, w.Header())
checkPayloadFunc(t, resp, func(got *struct {
Count int `json:"count"`
Values []*metadata `json:"values"`
}) bool {
return got.Count == len(want) &&
slices.EqualFunc(got.Values, want, func(a, b *metadata) bool {
return (a.Version == b.Version ||
a.Version == rosa.Unversioned ||
b.Version == rosa.Unversioned) &&
a.HasReport == b.HasReport &&
a.Name == b.Name &&
a.Description == b.Description &&
a.Website == b.Website
})
})
})
}
checkWithSuffix("declarationAscending", "?limit=2&index=0&sort=0", []*metadata{
{
Metadata: rosa.GetMetadata(0),
Version: rosa.Std.Version(0),
},
{
Metadata: rosa.GetMetadata(1),
Version: rosa.Std.Version(1),
},
})
checkWithSuffix("declarationAscending offset", "?limit=3&index=5&sort=0", []*metadata{
{
Metadata: rosa.GetMetadata(5),
Version: rosa.Std.Version(5),
},
{
Metadata: rosa.GetMetadata(6),
Version: rosa.Std.Version(6),
},
{
Metadata: rosa.GetMetadata(7),
Version: rosa.Std.Version(7),
},
})
checkWithSuffix("declarationDescending", "?limit=3&index=0&sort=1", []*metadata{
{
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 1),
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 1),
},
{
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 2),
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 2),
},
{
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 3),
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 3),
},
})
checkWithSuffix("declarationDescending offset", "?limit=1&index=37&sort=1", []*metadata{
{
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 38),
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 38),
},
})
}

View File

@@ -1,105 +0,0 @@
package main
import (
"cmp"
"errors"
"slices"
"strings"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
)
const (
declarationAscending = iota
declarationDescending
nameAscending
nameDescending
sizeAscending
sizeDescending
sortOrderEnd = iota - 1
)
// packageIndex refers to metadata by name and various sort orders.
type packageIndex struct {
sorts [sortOrderEnd + 1][rosa.PresetUnexportedStart]*metadata
names map[string]*metadata
search searchCache
// Taken from [rosa.Report] if available.
handleAccess func(*error) func()
}
// metadata holds [rosa.Metadata] extended with additional information.
type metadata struct {
p rosa.PArtifact
*rosa.Metadata
// Populated via [rosa.Toolchain.Version], [rosa.Unversioned] is equivalent
// to the zero value. Otherwise, the zero value is invalid.
Version string `json:"version,omitempty"`
// Output data size, available if present in report.
Size int64 `json:"size,omitempty"`
// Whether the underlying [pkg.Artifact] is present in the report.
HasReport bool `json:"report"`
// Ident string encoded ahead of time.
ids string
// Backed by [rosa.Report], access must be prepared by HandleAccess.
status []byte
}
// populate deterministically populates packageIndex, optionally with a report.
func (index *packageIndex) populate(cache *pkg.Cache, report *rosa.Report) (err error) {
if report != nil {
defer report.HandleAccess(&err)()
index.handleAccess = report.HandleAccess
}
var work [rosa.PresetUnexportedStart]*metadata
index.names = make(map[string]*metadata)
for p := range rosa.PresetUnexportedStart {
m := metadata{
p: p,
Metadata: rosa.GetMetadata(p),
Version: rosa.Std.Version(p),
}
if m.Version == "" {
return errors.New("invalid version from " + m.Name)
}
if m.Version == rosa.Unversioned {
m.Version = ""
}
if cache != nil && report != nil {
id := cache.Ident(rosa.Std.Load(p))
m.ids = pkg.Encode(id.Value())
m.status, m.Size = report.ArtifactOf(id)
m.HasReport = m.Size >= 0
}
work[p] = &m
index.names[m.Name] = &m
}
index.sorts[declarationAscending] = work
index.sorts[declarationDescending] = work
slices.Reverse(index.sorts[declarationDescending][:])
index.sorts[nameAscending] = work
slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
return strings.Compare(a.Name, b.Name)
})
index.sorts[nameDescending] = index.sorts[nameAscending]
slices.Reverse(index.sorts[nameDescending][:])
index.sorts[sizeAscending] = work
slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int {
return cmp.Compare(a.Size, b.Size)
})
index.sorts[sizeDescending] = index.sorts[sizeAscending]
slices.Reverse(index.sorts[sizeDescending][:])
return
}

View File

@@ -1,114 +0,0 @@
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"hakurei.app/command"
"hakurei.app/container/check"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
"hakurei.app/message"
)
const shutdownTimeout = 15 * time.Second
func main() {
log.SetFlags(0)
log.SetPrefix("pkgserver: ")
var (
flagBaseDir string
flagAddr string
)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer stop()
msg := message.New(log.Default())
c := command.New(os.Stderr, log.Printf, "pkgserver", func(args []string) error {
var (
cache *pkg.Cache
report *rosa.Report
)
switch len(args) {
case 0:
break
case 1:
baseDir, err := check.NewAbs(flagBaseDir)
if err != nil {
return err
}
cache, err = pkg.Open(ctx, msg, 0, baseDir)
if err != nil {
return err
}
defer cache.Close()
report, err = rosa.OpenReport(args[0])
if err != nil {
return err
}
default:
return errors.New("pkgserver requires 1 argument")
}
var index packageIndex
index.search = make(searchCache)
if err := index.populate(cache, report); err != nil {
return err
}
ticker := time.NewTicker(1 * time.Minute)
go func() {
for {
select {
case <-ctx.Done():
ticker.Stop()
return
case <-ticker.C:
index.search.clean()
}
}
}()
var mux http.ServeMux
uiRoutes(&mux)
index.registerAPI(&mux)
server := http.Server{
Addr: flagAddr,
Handler: &mux,
}
go func() {
<-ctx.Done()
c, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := server.Shutdown(c); err != nil {
log.Fatal(err)
}
}()
return server.ListenAndServe()
}).Flag(
&flagBaseDir,
"b", command.StringFlag(""),
"base directory for cache",
).Flag(
&flagAddr,
"addr", command.StringFlag(":8067"),
"TCP network address to listen on",
)
c.MustParse(os.Args[1:], func(err error) {
if errors.Is(err, http.ErrServerClosed) {
os.Exit(0)
}
log.Fatal(err)
})
}

View File

@@ -1,96 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"testing"
)
// newIndex returns the address of a newly populated packageIndex.
func newIndex(t *testing.T) *packageIndex {
t.Helper()
var index packageIndex
if err := index.populate(nil, nil); err != nil {
t.Fatalf("populate: error = %v", err)
}
return &index
}
// checkStatus checks response status code.
func checkStatus(t *testing.T, resp *http.Response, want int) {
t.Helper()
if resp.StatusCode != want {
t.Errorf(
"StatusCode: %s, want %s",
http.StatusText(resp.StatusCode),
http.StatusText(want),
)
}
}
// checkHeader checks the value of a header entry.
func checkHeader(t *testing.T, h http.Header, key, want string) {
t.Helper()
if got := h.Get(key); got != want {
t.Errorf("%s: %q, want %q", key, got, want)
}
}
// checkAPIHeader checks common entries set for API endpoints.
func checkAPIHeader(t *testing.T, h http.Header) {
t.Helper()
checkHeader(t, h, "Content-Type", "application/json; charset=utf-8")
checkHeader(t, h, "Cache-Control", "no-cache, no-store, must-revalidate")
checkHeader(t, h, "Pragma", "no-cache")
checkHeader(t, h, "Expires", "0")
}
// checkPayloadFunc checks the JSON response of an API endpoint by passing it to f.
func checkPayloadFunc[T any](
t *testing.T,
resp *http.Response,
f func(got *T) bool,
) {
t.Helper()
var got T
r := io.Reader(resp.Body)
if testing.Verbose() {
var buf bytes.Buffer
r = io.TeeReader(r, &buf)
defer func() { t.Helper(); t.Log(buf.String()) }()
}
if err := json.NewDecoder(r).Decode(&got); err != nil {
t.Fatalf("Decode: error = %v", err)
}
if !f(&got) {
t.Errorf("Body: %#v", got)
}
}
// checkPayload checks the JSON response of an API endpoint.
func checkPayload[T any](t *testing.T, resp *http.Response, want T) {
t.Helper()
checkPayloadFunc(t, resp, func(got *T) bool {
return reflect.DeepEqual(got, &want)
})
}
func checkError(t *testing.T, resp *http.Response, error string, code int) {
t.Helper()
checkStatus(t, resp, code)
if got, _ := io.ReadAll(resp.Body); string(got) != fmt.Sprintln(error) {
t.Errorf("Body: %q, want %q", string(got), error)
}
}

View File

@@ -1,77 +0,0 @@
package main
import (
"cmp"
"maps"
"regexp"
"slices"
"time"
)
type searchCache map[string]searchCacheEntry
type searchResult struct {
NameIndices [][]int `json:"name_matches"`
DescIndices [][]int `json:"desc_matches,omitempty"`
Score float64 `json:"score"`
*metadata
}
type searchCacheEntry struct {
query string
results []searchResult
expiry time.Time
}
func (index *packageIndex) performSearchQuery(limit int, i int, search string, desc bool) (int, []searchResult, error) {
entry, ok := index.search[search]
if ok {
return len(entry.results), entry.results[i:min(i+limit, len(entry.results))], nil
}
regex, err := regexp.Compile(search)
if err != nil {
return 0, make([]searchResult, 0), err
}
res := make([]searchResult, 0)
for p := range maps.Values(index.names) {
nameIndices := regex.FindAllIndex([]byte(p.Name), -1)
var descIndices [][]int = nil
if desc {
descIndices = regex.FindAllIndex([]byte(p.Description), -1)
}
if nameIndices == nil && descIndices == nil {
continue
}
score := float64(indexsum(nameIndices)) / (float64(len(nameIndices)) + 1)
if desc {
score += float64(indexsum(descIndices)) / (float64(len(descIndices)) + 1) / 10.0
}
res = append(res, searchResult{
NameIndices: nameIndices,
DescIndices: descIndices,
Score: score,
metadata: p,
})
}
slices.SortFunc(res[:], func(a, b searchResult) int { return -cmp.Compare(a.Score, b.Score) })
expiry := time.Now().Add(1 * time.Minute)
entry = searchCacheEntry{
query: search,
results: res,
expiry: expiry,
}
index.search[search] = entry
return len(res), res[i:min(i+limit, len(entry.results))], nil
}
func (s *searchCache) clean() {
maps.DeleteFunc(*s, func(_ string, v searchCacheEntry) bool {
return v.expiry.Before(time.Now())
})
}
func indexsum(in [][]int) int {
sum := 0
for i := 0; i < len(in); i++ {
sum += in[i][1] - in[i][0]
}
return sum
}

View File

@@ -1,38 +0,0 @@
package main
import "net/http"
func serveWebUI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-XSS-Protection", "1")
w.Header().Set("X-Frame-Options", "DENY")
http.ServeFileFS(w, r, content, "ui/index.html")
}
func serveStaticContent(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/static/style.css":
darkTheme := r.CookiesNamed("dark_theme")
if len(darkTheme) > 0 && darkTheme[0].Value == "true" {
http.ServeFileFS(w, r, content, "ui/static/dark.css")
} else {
http.ServeFileFS(w, r, content, "ui/static/light.css")
}
case "/favicon.ico":
http.ServeFileFS(w, r, content, "ui/static/favicon.ico")
case "/static/index.js":
http.ServeFileFS(w, r, content, "ui/static/index.js")
default:
http.NotFound(w, r)
}
}
func uiRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /{$}", serveWebUI)
mux.HandleFunc("GET /favicon.ico", serveStaticContent)
mux.HandleFunc("GET /static/", serveStaticContent)
}

View File

@@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="static/style.css">
<title>Hakurei PkgServer</title>
<script src="static/index.js"></script>
</head>
<body>
<h1>Hakurei PkgServer</h1>
<table id="pkg-list">
<tr><td>Loading...</td></tr>
</table>
<p>Showing entries <span id="entry-counter"></span>.</p>
<span class="bottom-nav"><a href="javascript:prevPage()">&laquo; Previous</a> <span id="page-number">1</span> <a href="javascript:nextPage()">Next &raquo;</a></span>
<span><label for="count">Entries per page: </label><select name="count" id="count">
<option value="10">10</option>
<option value="20">20</option>
<option value="30">30</option>
<option value="50">50</option>
</select></span>
<span><label for="sort">Sort by: </label><select name="sort" id="sort">
<option value="0">Definition (ascending)</option>
<option value="1">Definition (descending)</option>
<option value="2">Name (ascending)</option>
<option value="3">Name (descending)</option>
<option value="4">Size (ascending)</option>
<option value="5">Size (descending)</option>
</select></span>
</body>
<footer>
<p>&copy;<a href="https://hakurei.app/">Hakurei</a> (<span id="hakurei-version">unknown</span>). Licensed under the MIT license.</p>
</footer>
</html>

View File

@@ -1,6 +0,0 @@
@use 'common';
html {
background-color: #2c2c2c;
color: ghostwhite;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,155 +0,0 @@
class PackageIndexEntry {
name: string
size: number | null
description: string | null
website: string | null
version: string | null
report: boolean
}
function toHTML(entry: PackageIndexEntry): HTMLTableRowElement {
let v = entry.version != null ? `<span>${escapeHtml(entry.version)}</span>` : ""
let s = entry.size != null ? `<p>Size: ${toByteSizeString(entry.size)} (${entry.size})</p>` : ""
let d = entry.description != null ? `<p>${escapeHtml(entry.description)}</p>` : ""
let w = entry.website != null ? `<a href="${encodeURI(entry.website)}">Website</a>` : ""
let r = entry.report ? `Log (<a href=\"${encodeURI('/api/v1/status/' + entry.name)}\">View</a> | <a href=\"${encodeURI('/status/' + entry.name)}\">Download</a>)` : ""
let row = <HTMLTableRowElement>(document.createElement('tr'))
row.innerHTML = `<td>
<h2>${escapeHtml(entry.name)} ${v}</h2>
${d}
${s}
${w}
${r}
</td>`
return row
}
function toByteSizeString(bytes: number): string {
if(bytes == null || bytes < 1024) return `${bytes}B`
if(bytes < Math.pow(1024, 2)) return `${(bytes/1024).toFixed(2)}kiB`
if(bytes < Math.pow(1024, 3)) return `${(bytes/Math.pow(1024, 2)).toFixed(2)}MiB`
if(bytes < Math.pow(1024, 4)) return `${(bytes/Math.pow(1024, 3)).toFixed(2)}GiB`
if(bytes < Math.pow(1024, 5)) return `${(bytes/Math.pow(1024, 4)).toFixed(2)}TiB`
return "not only is it big, it's large"
}
const API_VERSION = 1
const ENDPOINT = `/api/v${API_VERSION}`
class InfoPayload {
count: number
hakurei_version: string
}
async function infoRequest(): Promise<InfoPayload> {
const res = await fetch(`${ENDPOINT}/info`)
const payload = await res.json()
return payload as InfoPayload
}
class GetPayload {
values: PackageIndexEntry[]
}
enum SortOrders {
DeclarationAscending,
DeclarationDescending,
NameAscending,
NameDescending
}
async function getRequest(limit: number, index: number, sort: SortOrders): Promise<GetPayload> {
const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`)
const payload = await res.json()
return payload as GetPayload
}
class State {
entriesPerPage: number = 10
entryIndex: number = 0
maxEntries: number = 0
sort: SortOrders = SortOrders.DeclarationAscending
getEntriesPerPage(): number {
return this.entriesPerPage
}
setEntriesPerPage(entriesPerPage: number) {
this.entriesPerPage = entriesPerPage
this.setEntryIndex(Math.floor(this.getEntryIndex() / entriesPerPage) * entriesPerPage)
}
getEntryIndex(): number {
return this.entryIndex
}
setEntryIndex(entryIndex: number) {
this.entryIndex = entryIndex
this.updatePage()
this.updateRange()
this.updateListings()
}
getMaxEntries(): number {
return this.maxEntries
}
setMaxEntries(max: number) {
this.maxEntries = max
}
getSortOrder(): SortOrders {
return this.sort
}
setSortOrder(sortOrder: SortOrders) {
this.sort = sortOrder
this.setEntryIndex(0)
}
updatePage() {
let page = Math.ceil(((this.getEntryIndex() + this.getEntriesPerPage()) - 1) / this.getEntriesPerPage())
document.getElementById("page-number").innerText = String(page)
}
updateRange() {
let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxEntries())
document.getElementById("entry-counter").innerText = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxEntries()}`
}
updateListings() {
getRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSortOrder())
.then(res => {
let table = document.getElementById("pkg-list")
table.innerHTML = ''
res.values.forEach((row) => {
table.appendChild(toHTML(row))
})
})
}
}
let STATE: State
function prevPage() {
let index = STATE.getEntryIndex()
STATE.setEntryIndex(Math.max(0, index - STATE.getEntriesPerPage()))
}
function nextPage() {
let index = STATE.getEntryIndex()
STATE.setEntryIndex(Math.min((Math.ceil(STATE.getMaxEntries() / STATE.getEntriesPerPage()) * STATE.getEntriesPerPage()) - STATE.getEntriesPerPage(), index + STATE.getEntriesPerPage()))
}
function escapeHtml(str: string): string {
if(str === undefined) return ""
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
document.addEventListener("DOMContentLoaded", () => {
STATE = new State()
infoRequest()
.then(res => {
STATE.setMaxEntries(res.count)
document.getElementById("hakurei-version").innerText = res.hakurei_version
STATE.updateRange()
STATE.updateListings()
})
document.getElementById("count").addEventListener("change", (event) => {
STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value))
})
document.getElementById("sort").addEventListener("change", (event) => {
STATE.setSortOrder(parseInt((event.target as HTMLSelectElement).value))
})
})

View File

@@ -1,6 +0,0 @@
@use 'common';
html {
background-color: #d3d3d3;
color: black;
}

View File

@@ -1,5 +0,0 @@
{
"compilerOptions": {
"target": "ES2024"
}
}

View File

@@ -1,9 +0,0 @@
//go:build frontend
package main
import "embed"
//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc -p ui/static"
//go:embed ui/*
var content embed.FS

View File

@@ -1,7 +0,0 @@
//go:build !frontend
package main
import "testing/fstest"
var content fstest.MapFS

View File

@@ -21,6 +21,7 @@ import (
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/internal/landlock"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -307,7 +308,7 @@ func (p *Container) Start() error {
done <- func() error { done <- func() error {
// PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes // PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes
// created from the calling thread // created from the calling thread
if err := SetNoNewPrivs(); err != nil { if err := setNoNewPrivs(); err != nil {
return &StartError{ return &StartError{
Fatal: true, Fatal: true,
Step: "prctl(PR_SET_NO_NEW_PRIVS)", Step: "prctl(PR_SET_NO_NEW_PRIVS)",
@@ -317,12 +318,14 @@ func (p *Container) Start() error {
// landlock: depends on per-thread state but acts on a process group // landlock: depends on per-thread state but acts on a process group
{ {
rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL} rulesetAttr := &landlock.RulesetAttr{
Scoped: landlock.LANDLOCK_SCOPE_SIGNAL,
}
if !p.HostAbstract { if !p.HostAbstract {
rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET rulesetAttr.Scoped |= landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
} }
if abi, err := LandlockGetABI(); err != nil { if abi, err := landlock.GetABI(); err != nil {
if p.HostAbstract || !p.HostNet { if p.HostAbstract || !p.HostNet {
// landlock can be skipped here as it restricts access // landlock can be skipped here as it restricts access
// to resources already covered by namespaces (pid, net) // to resources already covered by namespaces (pid, net)
@@ -351,7 +354,7 @@ func (p *Container) Start() error {
} }
} else { } else {
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr) p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil { if err = landlock.RestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd) _ = Close(rulesetFd)
return &StartError{ return &StartError{
Fatal: true, Fatal: true,

View File

@@ -26,6 +26,7 @@ import (
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/info" "hakurei.app/internal/info"
"hakurei.app/internal/landlock"
"hakurei.app/internal/params" "hakurei.app/internal/params"
"hakurei.app/ldd" "hakurei.app/ldd"
"hakurei.app/message" "hakurei.app/message"
@@ -456,7 +457,7 @@ func TestContainer(t *testing.T) {
c.RetainSession = tc.session c.RetainSession = tc.session
c.HostNet = tc.net c.HostNet = tc.net
if info.CanDegrade { if info.CanDegrade {
if _, err := container.LandlockGetABI(); err != nil { if _, err := landlock.GetABI(); err != nil {
if !errors.Is(err, syscall.ENOSYS) { if !errors.Is(err, syscall.ENOSYS) {
t.Fatalf("LandlockGetABI: error = %v", err) t.Fatalf("LandlockGetABI: error = %v", err)
} }

View File

@@ -148,7 +148,7 @@ func (direct) lockOSThread() { runtime.LockOSThread() }
func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) } func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) } func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() } func (direct) setNoNewPrivs() error { return setNoNewPrivs() }
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) } func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) } func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }

View File

@@ -1,65 +0,0 @@
package container_test
import (
"testing"
"unsafe"
"hakurei.app/container"
)
func TestLandlockString(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
rulesetAttr *container.RulesetAttr
want string
}{
{"nil", nil, "NULL"},
{"zero", new(container.RulesetAttr), "0"},
{"some", &container.RulesetAttr{Scoped: container.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
{"set", &container.RulesetAttr{
HandledAccessFS: container.LANDLOCK_ACCESS_FS_MAKE_SYM | container.LANDLOCK_ACCESS_FS_IOCTL_DEV | container.LANDLOCK_ACCESS_FS_WRITE_FILE,
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP,
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | container.LANDLOCK_SCOPE_SIGNAL,
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
{"all", &container.RulesetAttr{
HandledAccessFS: container.LANDLOCK_ACCESS_FS_EXECUTE |
container.LANDLOCK_ACCESS_FS_WRITE_FILE |
container.LANDLOCK_ACCESS_FS_READ_FILE |
container.LANDLOCK_ACCESS_FS_READ_DIR |
container.LANDLOCK_ACCESS_FS_REMOVE_DIR |
container.LANDLOCK_ACCESS_FS_REMOVE_FILE |
container.LANDLOCK_ACCESS_FS_MAKE_CHAR |
container.LANDLOCK_ACCESS_FS_MAKE_DIR |
container.LANDLOCK_ACCESS_FS_MAKE_REG |
container.LANDLOCK_ACCESS_FS_MAKE_SOCK |
container.LANDLOCK_ACCESS_FS_MAKE_FIFO |
container.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
container.LANDLOCK_ACCESS_FS_MAKE_SYM |
container.LANDLOCK_ACCESS_FS_REFER |
container.LANDLOCK_ACCESS_FS_TRUNCATE |
container.LANDLOCK_ACCESS_FS_IOCTL_DEV,
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP |
container.LANDLOCK_ACCESS_NET_CONNECT_TCP,
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
container.LANDLOCK_SCOPE_SIGNAL,
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.rulesetAttr.String(); got != tc.want {
t.Errorf("String: %s, want %s", got, tc.want)
}
})
}
}
func TestLandlockAttrSize(t *testing.T) {
t.Parallel()
want := 24
if got := unsafe.Sizeof(container.RulesetAttr{}); got != uintptr(want) {
t.Errorf("Sizeof: %d, want %d", got, want)
}
}

View File

@@ -7,8 +7,8 @@ import (
"hakurei.app/ext" "hakurei.app/ext"
) )
// SetNoNewPrivs sets the calling thread's no_new_privs attribute. // setNoNewPrivs sets the calling thread's no_new_privs attribute.
func SetNoNewPrivs() error { func setNoNewPrivs() error {
return ext.Prctl(PR_SET_NO_NEW_PRIVS, 1, 0) return ext.Prctl(PR_SET_NO_NEW_PRIVS, 1, 0)
} }

View File

@@ -140,21 +140,29 @@ var (
ErrInsecure = errors.New("configuration is insecure") ErrInsecure = errors.New("configuration is insecure")
) )
const (
// VAllowInsecure allows use of compatibility options considered insecure
// under any configuration, to work around ecosystem-wide flaws.
VAllowInsecure = 1 << iota
)
// Validate checks [Config] and returns [AppError] if an invalid value is encountered. // Validate checks [Config] and returns [AppError] if an invalid value is encountered.
func (config *Config) Validate() error { func (config *Config) Validate(flags int) error {
const step = "validate configuration"
if config == nil { if config == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull, return &AppError{Step: step, Err: ErrConfigNull,
Msg: "invalid configuration"} Msg: "invalid configuration"}
} }
// this is checked again in hsu // this is checked again in hsu
if config.Identity < IdentityStart || config.Identity > IdentityEnd { if config.Identity < IdentityStart || config.Identity > IdentityEnd {
return &AppError{Step: "validate configuration", Err: ErrIdentityBounds, return &AppError{Step: step, Err: ErrIdentityBounds,
Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"} Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
} }
if config.SchedPolicy < 0 || config.SchedPolicy > ext.SCHED_LAST { if config.SchedPolicy < 0 || config.SchedPolicy > ext.SCHED_LAST {
return &AppError{Step: "validate configuration", Err: ErrSchedPolicyBounds, return &AppError{Step: step, Err: ErrSchedPolicyBounds,
Msg: "scheduling policy " + Msg: "scheduling policy " +
strconv.Itoa(int(config.SchedPolicy)) + strconv.Itoa(int(config.SchedPolicy)) +
" out of range"} " out of range"}
@@ -168,34 +176,51 @@ func (config *Config) Validate() error {
} }
if config.Container == nil { if config.Container == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull, return &AppError{Step: step, Err: ErrConfigNull,
Msg: "configuration missing container state"} Msg: "configuration missing container state"}
} }
if config.Container.Home == nil { if config.Container.Home == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull, return &AppError{Step: step, Err: ErrConfigNull,
Msg: "container configuration missing path to home directory"} Msg: "container configuration missing path to home directory"}
} }
if config.Container.Shell == nil { if config.Container.Shell == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull, return &AppError{Step: step, Err: ErrConfigNull,
Msg: "container configuration missing path to shell"} Msg: "container configuration missing path to shell"}
} }
if config.Container.Path == nil { if config.Container.Path == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull, return &AppError{Step: step, Err: ErrConfigNull,
Msg: "container configuration missing path to initial program"} Msg: "container configuration missing path to initial program"}
} }
for key := range config.Container.Env { for key := range config.Container.Env {
if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 { if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 {
return &AppError{Step: "validate configuration", Err: ErrEnviron, return &AppError{Step: step, Err: ErrEnviron,
Msg: "invalid environment variable " + strconv.Quote(key)} Msg: "invalid environment variable " + strconv.Quote(key)}
} }
} }
if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 { et := config.Enablements.Unwrap()
return &AppError{Step: "validate configuration", Err: ErrInsecure, if !config.DirectPulse && et&EPulse != 0 {
return &AppError{Step: step, Err: ErrInsecure,
Msg: "enablement PulseAudio is insecure and no longer supported"} Msg: "enablement PulseAudio is insecure and no longer supported"}
} }
if flags&VAllowInsecure == 0 {
switch {
case et&EWayland != 0 && config.DirectWayland:
return &AppError{Step: step, Err: ErrInsecure,
Msg: "direct_wayland is insecure and no longer supported"}
case et&EPipeWire != 0 && config.DirectPipeWire:
return &AppError{Step: step, Err: ErrInsecure,
Msg: "direct_pipewire is insecure and no longer supported"}
case et&EPulse != 0 && config.DirectPulse:
return &AppError{Step: step, Err: ErrInsecure,
Msg: "direct_pulse is insecure and no longer supported"}
}
}
return nil return nil
} }

View File

@@ -14,65 +14,109 @@ func TestConfigValidate(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
config *hst.Config config *hst.Config
flags int
wantErr error wantErr error
}{ }{
{"nil", nil, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, {"nil", nil, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "invalid configuration"}}, Msg: "invalid configuration"}},
{"identity lower", &hst.Config{Identity: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
{"identity lower", &hst.Config{Identity: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
Msg: "identity -1 out of range"}}, Msg: "identity -1 out of range"}},
{"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds, {"identity upper", &hst.Config{Identity: 10000}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
Msg: "identity 10000 out of range"}}, Msg: "identity 10000 out of range"}},
{"sched lower", &hst.Config{SchedPolicy: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
{"sched lower", &hst.Config{SchedPolicy: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
Msg: "scheduling policy -1 out of range"}}, Msg: "scheduling policy -1 out of range"}},
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds, {"sched upper", &hst.Config{SchedPolicy: 0xcafe}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
Msg: "scheduling policy 51966 out of range"}}, Msg: "scheduling policy 51966 out of range"}},
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}},
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}}, 0,
&hst.BadInterfaceError{Interface: "", Segment: "session"}}, &hst.BadInterfaceError{Interface: "", Segment: "session"}},
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}}, {"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}}, 0,
&hst.BadInterfaceError{Interface: "", Segment: "system"}}, &hst.BadInterfaceError{Interface: "", Segment: "system"}},
{"container", &hst.Config{}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
{"container", &hst.Config{}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "configuration missing container state"}}, Msg: "configuration missing container state"}},
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, {"home", &hst.Config{Container: &hst.ContainerConfig{}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to home directory"}}, Msg: "container configuration missing path to home directory"}},
{"shell", &hst.Config{Container: &hst.ContainerConfig{ {"shell", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to shell"}}, Msg: "container configuration missing path to shell"}},
{"path", &hst.Config{Container: &hst.ContainerConfig{ {"path", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to initial program"}}, Msg: "container configuration missing path to initial program"}},
{"env equals", &hst.Config{Container: &hst.ContainerConfig{ {"env equals", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
Env: map[string]string{"TERM=": ""}, Env: map[string]string{"TERM=": ""},
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron, }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
Msg: `invalid environment variable "TERM="`}}, Msg: `invalid environment variable "TERM="`}},
{"env NUL", &hst.Config{Container: &hst.ContainerConfig{ {"env NUL", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
Env: map[string]string{"TERM\x00": ""}, Env: map[string]string{"TERM\x00": ""},
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron, }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
Msg: `invalid environment variable "TERM\x00"`}}, Msg: `invalid environment variable "TERM\x00"`}},
{"insecure pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), Container: &hst.ContainerConfig{
{"insecure pulse", &hst.Config{Enablements: new(hst.EPulse), Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure, }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "enablement PulseAudio is insecure and no longer supported"}}, Msg: "enablement PulseAudio is insecure and no longer supported"}},
{"direct wayland", &hst.Config{Enablements: new(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "direct_wayland is insecure and no longer supported"}},
{"direct wayland allow", &hst.Config{Enablements: new(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, hst.VAllowInsecure, nil},
{"direct pipewire", &hst.Config{Enablements: new(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "direct_pipewire is insecure and no longer supported"}},
{"direct pipewire allow", &hst.Config{Enablements: new(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, hst.VAllowInsecure, nil},
{"direct pulse", &hst.Config{Enablements: new(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "direct_pulse is insecure and no longer supported"}},
{"direct pulse allow", &hst.Config{Enablements: new(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, hst.VAllowInsecure, nil},
{"valid", &hst.Config{Container: &hst.ContainerConfig{ {"valid", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
}}, nil}, }}, 0, nil},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
if err := tc.config.Validate(); !reflect.DeepEqual(err, tc.wantErr) { if err := tc.config.Validate(tc.flags); !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr) t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr)
} }
}) })

View File

@@ -7,12 +7,12 @@ import (
"syscall" "syscall"
) )
// Enablement represents an optional host service to export to the target user. // Enablements denotes optional host service to export to the target user.
type Enablement byte type Enablements byte
const ( const (
// EWayland exposes a Wayland pathname socket via security-context-v1. // EWayland exposes a Wayland pathname socket via security-context-v1.
EWayland Enablement = 1 << iota EWayland Enablements = 1 << iota
// EX11 adds the target user via X11 ChangeHosts and exposes the X11 // EX11 adds the target user via X11 ChangeHosts and exposes the X11
// pathname socket. // pathname socket.
EX11 EX11
@@ -28,8 +28,8 @@ const (
EM EM
) )
// String returns a string representation of the flags set on [Enablement]. // String returns a string representation of the flags set on [Enablements].
func (e Enablement) String() string { func (e Enablements) String() string {
switch e { switch e {
case 0: case 0:
return "(no enablements)" return "(no enablements)"
@@ -47,7 +47,7 @@ func (e Enablement) String() string {
buf := new(strings.Builder) buf := new(strings.Builder)
buf.Grow(32) buf.Grow(32)
for i := Enablement(1); i < EM; i <<= 1 { for i := Enablements(1); i < EM; i <<= 1 {
if e&i != 0 { if e&i != 0 {
buf.WriteString(", " + i.String()) buf.WriteString(", " + i.String())
} }
@@ -60,12 +60,6 @@ func (e Enablement) String() string {
} }
} }
// NewEnablements returns the address of [Enablement] as [Enablements].
func NewEnablements(e Enablement) *Enablements { return (*Enablements)(&e) }
// Enablements is the [json] adapter for [Enablement].
type Enablements Enablement
// enablementsJSON is the [json] representation of [Enablements]. // enablementsJSON is the [json] representation of [Enablements].
type enablementsJSON = struct { type enablementsJSON = struct {
Wayland bool `json:"wayland,omitempty"` Wayland bool `json:"wayland,omitempty"`
@@ -75,24 +69,21 @@ type enablementsJSON = struct {
Pulse bool `json:"pulse,omitempty"` Pulse bool `json:"pulse,omitempty"`
} }
// Unwrap returns the underlying [Enablement]. // Unwrap returns the value pointed to by e.
func (e *Enablements) Unwrap() Enablement { func (e *Enablements) Unwrap() Enablements {
if e == nil { if e == nil {
return 0 return 0
} }
return Enablement(*e) return *e
} }
func (e *Enablements) MarshalJSON() ([]byte, error) { func (e Enablements) MarshalJSON() ([]byte, error) {
if e == nil {
return nil, syscall.EINVAL
}
return json.Marshal(&enablementsJSON{ return json.Marshal(&enablementsJSON{
Wayland: Enablement(*e)&EWayland != 0, Wayland: e&EWayland != 0,
X11: Enablement(*e)&EX11 != 0, X11: e&EX11 != 0,
DBus: Enablement(*e)&EDBus != 0, DBus: e&EDBus != 0,
PipeWire: Enablement(*e)&EPipeWire != 0, PipeWire: e&EPipeWire != 0,
Pulse: Enablement(*e)&EPulse != 0, Pulse: e&EPulse != 0,
}) })
} }
@@ -106,22 +97,21 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
return err return err
} }
var ve Enablement *e = 0
if v.Wayland { if v.Wayland {
ve |= EWayland *e |= EWayland
} }
if v.X11 { if v.X11 {
ve |= EX11 *e |= EX11
} }
if v.DBus { if v.DBus {
ve |= EDBus *e |= EDBus
} }
if v.PipeWire { if v.PipeWire {
ve |= EPipeWire *e |= EPipeWire
} }
if v.Pulse { if v.Pulse {
ve |= EPulse *e |= EPulse
} }
*e = Enablements(ve)
return nil return nil
} }

View File

@@ -13,7 +13,7 @@ func TestEnablementString(t *testing.T) {
t.Parallel() t.Parallel()
testCases := []struct { testCases := []struct {
flags hst.Enablement flags hst.Enablements
want string want string
}{ }{
{0, "(no enablements)"}, {0, "(no enablements)"},
@@ -59,13 +59,13 @@ func TestEnablements(t *testing.T) {
sData string sData string
}{ }{
{"nil", nil, "null", `{"value":null,"magic":3236757504}`}, {"nil", nil, "null", `{"value":null,"magic":3236757504}`},
{"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`}, {"zero", new(hst.Enablements(0)), `{}`, `{"value":{},"magic":3236757504}`},
{"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`}, {"wayland", new(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
{"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`}, {"x11", new(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
{"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`}, {"dbus", new(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
{"pipewire", hst.NewEnablements(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`}, {"pipewire", new(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
{"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`}, {"pulse", new(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
{"all", hst.NewEnablements(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`}, {"all", new(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`},
} }
for _, tc := range testCases { for _, tc := range testCases {
@@ -137,7 +137,7 @@ func TestEnablements(t *testing.T) {
}) })
t.Run("val", func(t *testing.T) { t.Run("val", func(t *testing.T) {
if got := hst.NewEnablements(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse { if got := new(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
t.Errorf("Unwrap: %v", got) t.Errorf("Unwrap: %v", got)
} }
}) })
@@ -146,9 +146,6 @@ func TestEnablements(t *testing.T) {
t.Run("passthrough", func(t *testing.T) { t.Run("passthrough", func(t *testing.T) {
t.Parallel() t.Parallel()
if _, err := (*hst.Enablements)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("MarshalJSON: error = %v", err)
}
if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) { if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) {
t.Errorf("UnmarshalJSON: error = %v", err) t.Errorf("UnmarshalJSON: error = %v", err)
} }

View File

@@ -72,7 +72,7 @@ func Template() *Config {
return &Config{ return &Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Enablements: NewEnablements(EWayland | EDBus | EPipeWire), Enablements: new(EWayland | EDBus | EPipeWire),
SessionBus: &BusConfig{ SessionBus: &BusConfig{
See: nil, See: nil,

View File

@@ -1,4 +1,4 @@
package container package landlock
import ( import (
"strings" "strings"
@@ -14,11 +14,11 @@ const (
LANDLOCK_CREATE_RULESET_VERSION = 1 << iota LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
) )
// LandlockAccessFS is bitmask of handled filesystem actions. // AccessFS is bitmask of handled filesystem actions.
type LandlockAccessFS uint64 type AccessFS uint64
const ( const (
LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 1 << iota LANDLOCK_ACCESS_FS_EXECUTE AccessFS = 1 << iota
LANDLOCK_ACCESS_FS_WRITE_FILE LANDLOCK_ACCESS_FS_WRITE_FILE
LANDLOCK_ACCESS_FS_READ_FILE LANDLOCK_ACCESS_FS_READ_FILE
LANDLOCK_ACCESS_FS_READ_DIR LANDLOCK_ACCESS_FS_READ_DIR
@@ -38,8 +38,8 @@ const (
_LANDLOCK_ACCESS_FS_DELIM _LANDLOCK_ACCESS_FS_DELIM
) )
// String returns a space-separated string of [LandlockAccessFS] flags. // String returns a space-separated string of [AccessFS] flags.
func (f LandlockAccessFS) String() string { func (f AccessFS) String() string {
switch f { switch f {
case LANDLOCK_ACCESS_FS_EXECUTE: case LANDLOCK_ACCESS_FS_EXECUTE:
return "execute" return "execute"
@@ -90,8 +90,8 @@ func (f LandlockAccessFS) String() string {
return "fs_ioctl_dev" return "fs_ioctl_dev"
default: default:
var c []LandlockAccessFS var c []AccessFS
for i := LandlockAccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 { for i := AccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
if f&i != 0 { if f&i != 0 {
c = append(c, i) c = append(c, i)
} }
@@ -107,18 +107,18 @@ func (f LandlockAccessFS) String() string {
} }
} }
// LandlockAccessNet is bitmask of handled network actions. // AccessNet is bitmask of handled network actions.
type LandlockAccessNet uint64 type AccessNet uint64
const ( const (
LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota LANDLOCK_ACCESS_NET_BIND_TCP AccessNet = 1 << iota
LANDLOCK_ACCESS_NET_CONNECT_TCP LANDLOCK_ACCESS_NET_CONNECT_TCP
_LANDLOCK_ACCESS_NET_DELIM _LANDLOCK_ACCESS_NET_DELIM
) )
// String returns a space-separated string of [LandlockAccessNet] flags. // String returns a space-separated string of [AccessNet] flags.
func (f LandlockAccessNet) String() string { func (f AccessNet) String() string {
switch f { switch f {
case LANDLOCK_ACCESS_NET_BIND_TCP: case LANDLOCK_ACCESS_NET_BIND_TCP:
return "bind_tcp" return "bind_tcp"
@@ -127,8 +127,8 @@ func (f LandlockAccessNet) String() string {
return "connect_tcp" return "connect_tcp"
default: default:
var c []LandlockAccessNet var c []AccessNet
for i := LandlockAccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 { for i := AccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
if f&i != 0 { if f&i != 0 {
c = append(c, i) c = append(c, i)
} }
@@ -144,18 +144,18 @@ func (f LandlockAccessNet) String() string {
} }
} }
// LandlockScope is bitmask of scopes restricting a Landlock domain from accessing outside resources. // Scope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
type LandlockScope uint64 type Scope uint64
const ( const (
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET Scope = 1 << iota
LANDLOCK_SCOPE_SIGNAL LANDLOCK_SCOPE_SIGNAL
_LANDLOCK_SCOPE_DELIM _LANDLOCK_SCOPE_DELIM
) )
// String returns a space-separated string of [LandlockScope] flags. // String returns a space-separated string of [Scope] flags.
func (f LandlockScope) String() string { func (f Scope) String() string {
switch f { switch f {
case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET: case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET:
return "abstract_unix_socket" return "abstract_unix_socket"
@@ -164,8 +164,8 @@ func (f LandlockScope) String() string {
return "signal" return "signal"
default: default:
var c []LandlockScope var c []Scope
for i := LandlockScope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 { for i := Scope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
if f&i != 0 { if f&i != 0 {
c = append(c, i) c = append(c, i)
} }
@@ -184,12 +184,12 @@ func (f LandlockScope) String() string {
// RulesetAttr is equivalent to struct landlock_ruleset_attr. // RulesetAttr is equivalent to struct landlock_ruleset_attr.
type RulesetAttr struct { type RulesetAttr struct {
// Bitmask of handled filesystem actions. // Bitmask of handled filesystem actions.
HandledAccessFS LandlockAccessFS HandledAccessFS AccessFS
// Bitmask of handled network actions. // Bitmask of handled network actions.
HandledAccessNet LandlockAccessNet HandledAccessNet AccessNet
// Bitmask of scopes restricting a Landlock domain from accessing outside // Bitmask of scopes restricting a Landlock domain from accessing outside
// resources (e.g. IPCs). // resources (e.g. IPCs).
Scoped LandlockScope Scoped Scope
} }
// String returns a user-facing description of [RulesetAttr]. // String returns a user-facing description of [RulesetAttr].
@@ -239,13 +239,13 @@ func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
return fd, nil return fd, nil
} }
// LandlockGetABI returns the ABI version supported by the kernel. // GetABI returns the ABI version supported by the kernel.
func LandlockGetABI() (int, error) { func GetABI() (int, error) {
return (*RulesetAttr)(nil).Create(LANDLOCK_CREATE_RULESET_VERSION) return (*RulesetAttr)(nil).Create(LANDLOCK_CREATE_RULESET_VERSION)
} }
// LandlockRestrictSelf applies a loaded ruleset to the calling thread. // RestrictSelf applies a loaded ruleset to the calling thread.
func LandlockRestrictSelf(rulesetFd int, flags uintptr) error { func RestrictSelf(rulesetFd int, flags uintptr) error {
r, _, errno := syscall.Syscall( r, _, errno := syscall.Syscall(
ext.SYS_LANDLOCK_RESTRICT_SELF, ext.SYS_LANDLOCK_RESTRICT_SELF,
uintptr(rulesetFd), uintptr(rulesetFd),

View File

@@ -0,0 +1,65 @@
package landlock_test
import (
"testing"
"unsafe"
"hakurei.app/internal/landlock"
)
func TestLandlockString(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
rulesetAttr *landlock.RulesetAttr
want string
}{
{"nil", nil, "NULL"},
{"zero", new(landlock.RulesetAttr), "0"},
{"some", &landlock.RulesetAttr{Scoped: landlock.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
{"set", &landlock.RulesetAttr{
HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_MAKE_SYM | landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV | landlock.LANDLOCK_ACCESS_FS_WRITE_FILE,
HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP,
Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | landlock.LANDLOCK_SCOPE_SIGNAL,
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
{"all", &landlock.RulesetAttr{
HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_EXECUTE |
landlock.LANDLOCK_ACCESS_FS_WRITE_FILE |
landlock.LANDLOCK_ACCESS_FS_READ_FILE |
landlock.LANDLOCK_ACCESS_FS_READ_DIR |
landlock.LANDLOCK_ACCESS_FS_REMOVE_DIR |
landlock.LANDLOCK_ACCESS_FS_REMOVE_FILE |
landlock.LANDLOCK_ACCESS_FS_MAKE_CHAR |
landlock.LANDLOCK_ACCESS_FS_MAKE_DIR |
landlock.LANDLOCK_ACCESS_FS_MAKE_REG |
landlock.LANDLOCK_ACCESS_FS_MAKE_SOCK |
landlock.LANDLOCK_ACCESS_FS_MAKE_FIFO |
landlock.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
landlock.LANDLOCK_ACCESS_FS_MAKE_SYM |
landlock.LANDLOCK_ACCESS_FS_REFER |
landlock.LANDLOCK_ACCESS_FS_TRUNCATE |
landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV,
HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP |
landlock.LANDLOCK_ACCESS_NET_CONNECT_TCP,
Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
landlock.LANDLOCK_SCOPE_SIGNAL,
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.rulesetAttr.String(); got != tc.want {
t.Errorf("String: %s, want %s", got, tc.want)
}
})
}
}
func TestLandlockAttrSize(t *testing.T) {
t.Parallel()
want := 24
if got := unsafe.Sizeof(landlock.RulesetAttr{}); got != uintptr(want) {
t.Errorf("Sizeof: %d, want %d", got, want)
}
}

View File

@@ -32,7 +32,14 @@ type outcome struct {
syscallDispatcher syscallDispatcher
} }
func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, config *hst.Config) error { // finalise prepares an outcome for main.
func (k *outcome) finalise(
ctx context.Context,
msg message.Msg,
id *hst.ID,
config *hst.Config,
flags int,
) error {
if ctx == nil || id == nil { if ctx == nil || id == nil {
// unreachable // unreachable
panic("invalid call to finalise") panic("invalid call to finalise")
@@ -43,7 +50,7 @@ func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, con
} }
k.ctx = ctx k.ctx = ctx
if err := config.Validate(); err != nil { if err := config.Validate(flags); err != nil {
return err return err
} }

View File

@@ -194,7 +194,7 @@ type outcomeStateSys struct {
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem. // Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
appId string appId string
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem. // Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
et hst.Enablement et hst.Enablements
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only. // Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
directWayland bool directWayland bool

View File

@@ -297,12 +297,12 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
// accumulate enablements of remaining instances // accumulate enablements of remaining instances
var ( var (
// alive enablement bits // alive enablement bits
rt hst.Enablement rt hst.Enablements
// alive instance count // alive instance count
n int n int
) )
for eh := range entries { for eh := range entries {
var et hst.Enablement var et hst.Enablements
if et, err = eh.Load(nil); err != nil { if et, err = eh.Load(nil); err != nil {
perror(err, "read state header of instance "+eh.ID.String()) perror(err, "read state header of instance "+eh.ID.String())
} else { } else {

View File

@@ -18,7 +18,13 @@ import (
func IsPollDescriptor(fd uintptr) bool func IsPollDescriptor(fd uintptr) bool
// Main runs an app according to [hst.Config] and terminates. Main does not return. // Main runs an app according to [hst.Config] and terminates. Main does not return.
func Main(ctx context.Context, msg message.Msg, config *hst.Config, fd int) { func Main(
ctx context.Context,
msg message.Msg,
config *hst.Config,
flags int,
fd int,
) {
// avoids runtime internals or standard streams // avoids runtime internals or standard streams
if fd >= 0 { if fd >= 0 {
if IsPollDescriptor(uintptr(fd)) || fd < 3 { if IsPollDescriptor(uintptr(fd)) || fd < 3 {
@@ -34,7 +40,7 @@ func Main(ctx context.Context, msg message.Msg, config *hst.Config, fd int) {
k := outcome{syscallDispatcher: direct{msg}} k := outcome{syscallDispatcher: direct{msg}}
finaliseTime := time.Now() finaliseTime := time.Now()
if err := k.finalise(ctx, msg, &id, config); err != nil { if err := k.finalise(ctx, msg, &id, config, flags); err != nil {
printMessageError(msg.GetLogger().Fatalln, "cannot seal app:", err) printMessageError(msg.GetLogger().Fatalln, "cannot seal app:", err)
panic("unreachable") panic("unreachable")
} }

View File

@@ -288,7 +288,7 @@ func TestOutcomeRun(t *testing.T) {
}, },
Filter: true, Filter: true,
}, },
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse), Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
@@ -427,7 +427,7 @@ func TestOutcomeRun(t *testing.T) {
DirectPipeWire: true, DirectPipeWire: true,
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse), Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Env: nil, Env: nil,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{

View File

@@ -21,7 +21,7 @@ func TestSpPulseOp(t *testing.T) {
newConfig := func() *hst.Config { newConfig := func() *hst.Config {
config := hst.Template() config := hst.Template()
config.DirectPulse = true config.DirectPulse = true
config.Enablements = hst.NewEnablements(hst.EPulse) config.Enablements = new(hst.EPulse)
return config return config
} }

View File

@@ -25,6 +25,7 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/internal/info" "hakurei.app/internal/info"
"hakurei.app/internal/landlock"
"hakurei.app/internal/pkg" "hakurei.app/internal/pkg"
"hakurei.app/internal/stub" "hakurei.app/internal/stub"
"hakurei.app/message" "hakurei.app/message"
@@ -293,7 +294,7 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
flags := tc.flags flags := tc.flags
if info.CanDegrade { if info.CanDegrade {
if _, err := container.LandlockGetABI(); err != nil { if _, err := landlock.GetABI(); err != nil {
if !errors.Is(err, syscall.ENOSYS) { if !errors.Is(err, syscall.ENOSYS) {
t.Fatalf("LandlockGetABI: error = %v", err) t.Fatalf("LandlockGetABI: error = %v", err)
} }

View File

@@ -141,8 +141,8 @@ rm \
) )
const ( const (
version = "1.26.1" version = "1.26.2"
checksum = "DdC5Ea-aCYPUHNObQh_09uWU0vn4e-8Ben850Vq-5OoamDRrXhuYI4YQ_BOFgaT0" checksum = "v-6BE89_1g3xYf-9oIYpJKFXlo3xKHYJj2_VGkaUq8ZVkIVQmLwrto-xGG03OISH"
) )
return t.newGo( return t.newGo(
version, version,

View File

@@ -4,8 +4,8 @@ import "hakurei.app/internal/pkg"
func (t Toolchain) newLibcap() (pkg.Artifact, string) { func (t Toolchain) newLibcap() (pkg.Artifact, string) {
const ( const (
version = "2.77" version = "2.78"
checksum = "2GOTFU4cl2QoS7Dv5wh0c9-hxsQwIzMB9Y_gfAo5xKHqcM13fiHt1RbPkfemzjmB" checksum = "wFdUkBhFMD9InPnrBZyegWrlPSAg_9JiTBC-eSFyWWlmbzL2qjh2mKxr9Kx2a8ut"
) )
return t.NewPackage("libcap", version, pkg.NewHTTPGetTar( return t.NewPackage("libcap", version, pkg.NewHTTPGetTar(
nil, "https://git.kernel.org/pub/scm/libs/libcap/libcap.git/"+ nil, "https://git.kernel.org/pub/scm/libs/libcap/libcap.git/"+

View File

@@ -13,8 +13,8 @@ import (
// llvmAttr holds the attributes that will be applied to a new [pkg.Artifact] // llvmAttr holds the attributes that will be applied to a new [pkg.Artifact]
// containing a LLVM variant. // containing a LLVM variant.
type llvmAttr struct { type llvmAttr struct {
// Passed through to PackageAttr.Flag. // Enabled projects and runtimes.
flags int pr int
// Concatenated with default environment for PackageAttr.Env. // Concatenated with default environment for PackageAttr.Env.
env []string env []string
@@ -24,8 +24,6 @@ type llvmAttr struct {
append []string append []string
// Passed through to PackageAttr.NonStage0. // Passed through to PackageAttr.NonStage0.
nonStage0 []pkg.Artifact nonStage0 []pkg.Artifact
// Passed through to PackageAttr.Paths.
paths []pkg.ExecPath
// Concatenated with default fixup for CMakeHelper.Script. // Concatenated with default fixup for CMakeHelper.Script.
script string script string
@@ -75,19 +73,18 @@ func llvmFlagName(flag int) string {
// newLLVMVariant returns a [pkg.Artifact] containing a LLVM variant. // newLLVMVariant returns a [pkg.Artifact] containing a LLVM variant.
func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact { func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
if attr == nil { if attr == nil {
panic("LLVM attr must be non-nil") panic("LLVM attr must be non-nil")
} }
var projects, runtimes []string var projects, runtimes []string
for i := 1; i < llvmProjectAll; i <<= 1 { for i := 1; i < llvmProjectAll; i <<= 1 {
if attr.flags&i != 0 { if attr.pr&i != 0 {
projects = append(projects, llvmFlagName(i)) projects = append(projects, llvmFlagName(i))
} }
} }
for i := (llvmProjectAll + 1) << 1; i < llvmRuntimeAll; i <<= 1 { for i := (llvmProjectAll + 1) << 1; i < llvmRuntimeAll; i <<= 1 {
if attr.flags&i != 0 { if attr.pr&i != 0 {
runtimes = append(runtimes, llvmFlagName(i)) runtimes = append(runtimes, llvmFlagName(i))
} }
} }
@@ -126,7 +123,7 @@ func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
}...) }...)
} }
if attr.flags&llvmProjectClang != 0 { if attr.pr&llvmProjectClang != 0 {
cache = append(cache, []KV{ cache = append(cache, []KV{
{"CLANG_DEFAULT_LINKER", "lld"}, {"CLANG_DEFAULT_LINKER", "lld"},
{"CLANG_DEFAULT_CXX_STDLIB", "libc++"}, {"CLANG_DEFAULT_CXX_STDLIB", "libc++"},
@@ -134,30 +131,30 @@ func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
{"CLANG_DEFAULT_UNWINDLIB", "libunwind"}, {"CLANG_DEFAULT_UNWINDLIB", "libunwind"},
}...) }...)
} }
if attr.flags&llvmProjectLld != 0 { if attr.pr&llvmProjectLld != 0 {
script += ` script += `
ln -s ld.lld /work/system/bin/ld ln -s ld.lld /work/system/bin/ld
` `
} }
if attr.flags&llvmRuntimeCompilerRT != 0 { if attr.pr&llvmRuntimeCompilerRT != 0 {
if attr.append == nil { if attr.append == nil {
cache = append(cache, []KV{ cache = append(cache, []KV{
{"COMPILER_RT_USE_LLVM_UNWINDER", "ON"}, {"COMPILER_RT_USE_LLVM_UNWINDER", "ON"},
}...) }...)
} }
} }
if attr.flags&llvmRuntimeLibunwind != 0 { if attr.pr&llvmRuntimeLibunwind != 0 {
cache = append(cache, []KV{ cache = append(cache, []KV{
{"LIBUNWIND_USE_COMPILER_RT", "ON"}, {"LIBUNWIND_USE_COMPILER_RT", "ON"},
}...) }...)
} }
if attr.flags&llvmRuntimeLibcxx != 0 { if attr.pr&llvmRuntimeLibcxx != 0 {
cache = append(cache, []KV{ cache = append(cache, []KV{
{"LIBCXX_HAS_MUSL_LIBC", "ON"}, {"LIBCXX_HAS_MUSL_LIBC", "ON"},
{"LIBCXX_USE_COMPILER_RT", "ON"}, {"LIBCXX_USE_COMPILER_RT", "ON"},
}...) }...)
} }
if attr.flags&llvmRuntimeLibcxxABI != 0 { if attr.pr&llvmRuntimeLibcxxABI != 0 {
cache = append(cache, []KV{ cache = append(cache, []KV{
{"LIBCXXABI_USE_COMPILER_RT", "ON"}, {"LIBCXXABI_USE_COMPILER_RT", "ON"},
{"LIBCXXABI_USE_LLVM_UNWINDER", "ON"}, {"LIBCXXABI_USE_LLVM_UNWINDER", "ON"},
@@ -170,7 +167,22 @@ ln -s ld.lld /work/system/bin/ld
mustDecode(llvmChecksum), mustDecode(llvmChecksum),
pkg.TarGzip, pkg.TarGzip,
), &PackageAttr{ ), &PackageAttr{
Patches: attr.patches, Patches: slices.Concat(attr.patches, []KV{
{"increase-stack-size-unconditional", `diff --git a/llvm/lib/Support/Threading.cpp b/llvm/lib/Support/Threading.cpp
index 9da357a7ebb9..b2931510c1ae 100644
--- a/llvm/lib/Support/Threading.cpp
+++ b/llvm/lib/Support/Threading.cpp
@@ -80,7 +80,7 @@ unsigned llvm::ThreadPoolStrategy::compute_thread_count() const {
// keyword.
#include "llvm/Support/thread.h"
-#if defined(__APPLE__)
+#if defined(__APPLE__) || 1
// Darwin's default stack size for threads except the main one is only 512KB,
// which is not enough for some/many normal LLVM compilations. This implements
// the same interface as std::thread but requests the same stack size as the
`},
}),
NonStage0: attr.nonStage0, NonStage0: attr.nonStage0,
Env: slices.Concat([]string{ Env: slices.Concat([]string{
@@ -178,7 +190,6 @@ ln -s ld.lld /work/system/bin/ld
"ROSA_LLVM_RUNTIMES=" + strings.Join(runtimes, ";"), "ROSA_LLVM_RUNTIMES=" + strings.Join(runtimes, ";"),
}, attr.env), }, attr.env),
Paths: attr.paths,
Flag: TExclusive, Flag: TExclusive,
}, &CMakeHelper{ }, &CMakeHelper{
Variant: variant, Variant: variant,
@@ -201,7 +212,8 @@ ln -s ld.lld /work/system/bin/ld
// newLLVM returns LLVM toolchain across multiple [pkg.Artifact]. // newLLVM returns LLVM toolchain across multiple [pkg.Artifact].
func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) { func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) {
var target string target := "'AArch64;RISCV;X86'"
if t.isStage0() {
switch runtime.GOARCH { switch runtime.GOARCH {
case "386", "amd64": case "386", "amd64":
target = "X86" target = "X86"
@@ -213,6 +225,7 @@ func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) {
default: default:
panic("unsupported target " + runtime.GOARCH) panic("unsupported target " + runtime.GOARCH)
} }
}
minimalDeps := []KV{ minimalDeps := []KV{
{"LLVM_ENABLE_ZLIB", "OFF"}, {"LLVM_ENABLE_ZLIB", "OFF"},
@@ -233,7 +246,7 @@ func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) {
{"CMAKE_CXX_COMPILER_TARGET", ""}, {"CMAKE_CXX_COMPILER_TARGET", ""},
{"COMPILER_RT_BUILD_BUILTINS", "ON"}, {"COMPILER_RT_BUILD_BUILTINS", "ON"},
{"COMPILER_RT_DEFAULT_TARGET_ONLY", "ON"}, {"COMPILER_RT_DEFAULT_TARGET_ONLY", "OFF"},
{"COMPILER_RT_SANITIZERS_TO_BUILD", "asan"}, {"COMPILER_RT_SANITIZERS_TO_BUILD", "asan"},
{"LLVM_ENABLE_PER_TARGET_RUNTIME_DIR", "ON"}, {"LLVM_ENABLE_PER_TARGET_RUNTIME_DIR", "ON"},
@@ -277,7 +290,7 @@ ln -s \
env: stage0ExclConcat(t, []string{}, env: stage0ExclConcat(t, []string{},
"LDFLAGS="+earlyLDFLAGS(false), "LDFLAGS="+earlyLDFLAGS(false),
), ),
flags: llvmRuntimeLibunwind | llvmRuntimeLibcxx | llvmRuntimeLibcxxABI, pr: llvmRuntimeLibunwind | llvmRuntimeLibcxx | llvmRuntimeLibcxxABI,
cmake: slices.Concat([]KV{ cmake: slices.Concat([]KV{
// libc++ not yet available // libc++ not yet available
{"CMAKE_CXX_COMPILER_WORKS", "ON"}, {"CMAKE_CXX_COMPILER_WORKS", "ON"},
@@ -293,7 +306,7 @@ ln -s \
}) })
clang = t.newLLVMVariant("clang", &llvmAttr{ clang = t.newLLVMVariant("clang", &llvmAttr{
flags: llvmProjectClang | llvmProjectLld, pr: llvmProjectClang | llvmProjectLld,
env: stage0ExclConcat(t, []string{}, env: stage0ExclConcat(t, []string{},
"CFLAGS="+earlyCFLAGS, "CFLAGS="+earlyCFLAGS,
"CXXFLAGS="+earlyCXXFLAGS(), "CXXFLAGS="+earlyCXXFLAGS(),
@@ -316,7 +329,7 @@ ln -s clang++ /work/system/bin/c++
ninja check-all ninja check-all
`, `,
patches: slices.Concat([]KV{ patches: []KV{
{"add-rosa-vendor", `diff --git a/llvm/include/llvm/TargetParser/Triple.h b/llvm/include/llvm/TargetParser/Triple.h {"add-rosa-vendor", `diff --git a/llvm/include/llvm/TargetParser/Triple.h b/llvm/include/llvm/TargetParser/Triple.h
index 9c83abeeb3b1..5acfe5836a23 100644 index 9c83abeeb3b1..5acfe5836a23 100644
--- a/llvm/include/llvm/TargetParser/Triple.h --- a/llvm/include/llvm/TargetParser/Triple.h
@@ -488,7 +501,7 @@ index 64324a3f8b01..15ce70b68217 100644
"/System/Library/Frameworks"}; "/System/Library/Frameworks"};
`}, `},
}, clangPatches), },
}) })
return return

View File

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

View File

@@ -1,12 +0,0 @@
package rosa
// clangPatches are patches applied to the LLVM source tree for building clang.
var clangPatches []KV
// one version behind, latest fails 5 tests with 2 flaky on arm64
const (
llvmVersionMajor = "21"
llvmVersion = llvmVersionMajor + ".1.8"
llvmChecksum = "8SUpqDkcgwOPsqHVtmf9kXfFeVmjVxl4LMn-qSE1AI_Xoeju-9HaoPNGtidyxyka"
)

View File

@@ -1,11 +1,9 @@
//go:build !arm64
package rosa package rosa
// latest version of LLVM, conditional to temporarily avoid broken new releases // latest version of LLVM, conditional to temporarily avoid broken new releases
const ( const (
llvmVersionMajor = "22" llvmVersionMajor = "22"
llvmVersion = llvmVersionMajor + ".1.2" llvmVersion = llvmVersionMajor + ".1.3"
llvmChecksum = "FwsmurWDVyYYQlOowowFjekwIGSB5__aKTpW_VGP3eWoZGXvBny-bOn1DuQ1U5xE" llvmChecksum = "CUwnpzua_y28HZ9oI0NmcKL2wClsSjFpgY9do5-7cCZJHI5KNF64vfwGvY0TYyR3"
) )

View File

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

View File

@@ -4,8 +4,8 @@ import "hakurei.app/internal/pkg"
func (t Toolchain) newOpenSSL() (pkg.Artifact, string) { func (t Toolchain) newOpenSSL() (pkg.Artifact, string) {
const ( const (
version = "3.6.1" version = "3.6.2"
checksum = "boMAj2SIVIFXHswZva3qHJuFEpc32rxCCu07wjMPsVe9nn_976BGMmW_5P1zthgg" checksum = "jH004dXTiE01Hp0kyShkWXwrSHEksZi4i_3v47D9H9Uz9LQ1aMwF7mrl2Tb4t_XA"
) )
return t.NewPackage("openssl", version, pkg.NewHTTPGetTar( return t.NewPackage("openssl", version, pkg.NewHTTPGetTar(
nil, "https://github.com/openssl/openssl/releases/download/"+ nil, "https://github.com/openssl/openssl/releases/download/"+

View File

@@ -9,8 +9,8 @@ import (
func (t Toolchain) newPython() (pkg.Artifact, string) { func (t Toolchain) newPython() (pkg.Artifact, string) {
const ( const (
version = "3.14.3" version = "3.14.4"
checksum = "ajEC32WPmn9Jvll0n4gGvlTvhMPUHb2H_j5_h9jf_esHmkZBRfAumDcKY7nTTsCH" checksum = "X0VRAAGOlCVldh4J9tRAE-YrJtDvqfQTJaqxKPXNX6YTPlwpR9GwA5WRIZDO-63s"
) )
return t.NewPackage("python", version, pkg.NewHTTPGetTar( return t.NewPackage("python", version, pkg.NewHTTPGetTar(
nil, "https://www.python.org/ftp/python/"+version+ nil, "https://www.python.org/ftp/python/"+version+

View File

@@ -8,8 +8,8 @@ import (
func (t Toolchain) newTamaGo() (pkg.Artifact, string) { func (t Toolchain) newTamaGo() (pkg.Artifact, string) {
const ( const (
version = "1.26.1" version = "1.26.2"
checksum = "fimZnklQcYWGsTQU8KepLn-yCYaTfNdMI9DCg6NJVQv-3gOJnUEO9mqRCMAHnEXZ" checksum = "5xlhWq2NGhYCjt0y73QkydJ386lxg6-HkiO84ne6ByQSJBDat7-HSVzNA6jy7Laz"
) )
return t.New("tamago-go"+version, 0, t.AppendPresets(nil, return t.New("tamago-go"+version, 0, t.AppendPresets(nil,
Bash, Bash,

View File

@@ -24,7 +24,7 @@ func entryEncode(w io.Writer, s *hst.State) error {
} }
// entryDecodeHeader calls entryReadHeader, returning [hst.AppError] for a non-nil error. // entryDecodeHeader calls entryReadHeader, returning [hst.AppError] for a non-nil error.
func entryDecodeHeader(r io.Reader) (hst.Enablement, error) { func entryDecodeHeader(r io.Reader) (hst.Enablements, error) {
if et, err := entryReadHeader(r); err != nil { if et, err := entryReadHeader(r); err != nil {
return 0, &hst.AppError{Step: "decode state header", Err: err} return 0, &hst.AppError{Step: "decode state header", Err: err}
} else { } else {
@@ -32,20 +32,23 @@ func entryDecodeHeader(r io.Reader) (hst.Enablement, error) {
} }
} }
// entryDecode decodes [hst.State] from [io.Reader] and stores the result in the value pointed to by p. // entryDecode decodes [hst.State] from [io.Reader] and stores the result in the
// entryDecode validates the embedded [hst.Config] value. // value pointed to by p. entryDecode validates the embedded [hst.Config] value.
// //
// A non-nil error returned by entryDecode is of type [hst.AppError]. // A non-nil error returned by entryDecode is of type [hst.AppError].
func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error) { func entryDecode(r io.Reader, p *hst.State) (hst.Enablements, error) {
if et, err := entryDecodeHeader(r); err != nil { if et, err := entryDecodeHeader(r); err != nil {
return et, err return et, err
} else if err = gob.NewDecoder(r).Decode(&p); err != nil { } else if err = gob.NewDecoder(r).Decode(&p); err != nil {
return et, &hst.AppError{Step: "decode state body", Err: err} return et, &hst.AppError{Step: "decode state body", Err: err}
} else if err = p.Config.Validate(); err != nil { } else if err = p.Config.Validate(hst.VAllowInsecure); err != nil {
return et, err return et, err
} else if p.Enablements.Unwrap() != et { } else if p.Enablements.Unwrap() != et {
return et, &hst.AppError{Step: "validate state enablement", Err: os.ErrInvalid, return et, &hst.AppError{Step: "validate state enablement", Err: os.ErrInvalid,
Msg: fmt.Sprintf("state entry %s has unexpected enablement byte %#x, %#x", p.ID.String(), byte(p.Enablements.Unwrap()), byte(et))} Msg: fmt.Sprintf(
"state entry %s has unexpected enablement byte %#x, %#x",
p.ID.String(), byte(p.Enablements.Unwrap()), byte(et),
)}
} else { } else {
return et, nil return et, nil
} }

View File

@@ -14,22 +14,25 @@ import (
const ( const (
// entryHeaderMagic are magic bytes at the beginning of the state entry file. // entryHeaderMagic are magic bytes at the beginning of the state entry file.
entryHeaderMagic = "\x00\xff\xca\xfe" entryHeaderMagic = "\x00\xff\xca\xfe"
// entryHeaderRevision follows entryHeaderMagic and is incremented for revisions of the format. // entryHeaderRevision follows entryHeaderMagic and is incremented for
// revisions of the format.
entryHeaderRevision = "\x00\x00" entryHeaderRevision = "\x00\x00"
// entryHeaderSize is the fixed size of the header in bytes, including the enablement byte and its complement. // entryHeaderSize is the fixed size of the header in bytes, including the
// enablement byte and its complement.
entryHeaderSize = len(entryHeaderMagic+entryHeaderRevision) + 2 entryHeaderSize = len(entryHeaderMagic+entryHeaderRevision) + 2
) )
// entryHeaderEncode encodes a state entry header for a [hst.Enablement] byte. // entryHeaderEncode encodes a state entry header for a [hst.Enablements] byte.
func entryHeaderEncode(et hst.Enablement) *[entryHeaderSize]byte { func entryHeaderEncode(et hst.Enablements) *[entryHeaderSize]byte {
data := [entryHeaderSize]byte([]byte( data := [entryHeaderSize]byte([]byte(
entryHeaderMagic + entryHeaderRevision + string([]hst.Enablement{et, ^et}), entryHeaderMagic + entryHeaderRevision + string([]hst.Enablements{et, ^et}),
)) ))
return &data return &data
} }
// entryHeaderDecode validates a state entry header and returns the [hst.Enablement] byte. // entryHeaderDecode validates a state entry header and returns the
func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablement, error) { // [hst.Enablements] byte.
func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablements, error) {
if magic := data[:len(entryHeaderMagic)]; string(magic) != entryHeaderMagic { if magic := data[:len(entryHeaderMagic)]; string(magic) != entryHeaderMagic {
return 0, errors.New("invalid header " + hex.EncodeToString(magic)) return 0, errors.New("invalid header " + hex.EncodeToString(magic))
} }
@@ -41,7 +44,7 @@ func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablement, error) {
if et != ^data[len(entryHeaderMagic+entryHeaderRevision)+1] { if et != ^data[len(entryHeaderMagic+entryHeaderRevision)+1] {
return 0, errors.New("header enablement value is inconsistent") return 0, errors.New("header enablement value is inconsistent")
} }
return hst.Enablement(et), nil return hst.Enablements(et), nil
} }
// EntrySizeError is returned for a file too small to hold a state entry header. // EntrySizeError is returned for a file too small to hold a state entry header.
@@ -68,8 +71,8 @@ func entryCheckFile(fi os.FileInfo) error {
return nil return nil
} }
// entryReadHeader reads [hst.Enablement] from an [io.Reader]. // entryReadHeader reads [hst.Enablements] from an [io.Reader].
func entryReadHeader(r io.Reader) (hst.Enablement, error) { func entryReadHeader(r io.Reader) (hst.Enablements, error) {
var data [entryHeaderSize]byte var data [entryHeaderSize]byte
if n, err := r.Read(data[:]); err != nil { if n, err := r.Read(data[:]); err != nil {
return 0, err return 0, err
@@ -79,8 +82,8 @@ func entryReadHeader(r io.Reader) (hst.Enablement, error) {
return entryHeaderDecode(&data) return entryHeaderDecode(&data)
} }
// entryWriteHeader writes [hst.Enablement] header to an [io.Writer]. // entryWriteHeader writes [hst.Enablements] header to an [io.Writer].
func entryWriteHeader(w io.Writer, et hst.Enablement) error { func entryWriteHeader(w io.Writer, et hst.Enablements) error {
_, err := w.Write(entryHeaderEncode(et)[:]) _, err := w.Write(entryHeaderEncode(et)[:])
return err return err
} }

View File

@@ -20,7 +20,7 @@ func TestEntryHeader(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
data [entryHeaderSize]byte data [entryHeaderSize]byte
et hst.Enablement et hst.Enablements
err error err error
}{ }{
{"complement mismatch", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00, {"complement mismatch", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,

View File

@@ -14,6 +14,7 @@ import (
) )
// EntryHandle is a handle on a state entry retrieved from a [Handle]. // EntryHandle is a handle on a state entry retrieved from a [Handle].
//
// Must only be used while its parent [Handle.Lock] is held. // Must only be used while its parent [Handle.Lock] is held.
type EntryHandle struct { type EntryHandle struct {
// Error returned while decoding pathname. // Error returned while decoding pathname.
@@ -27,6 +28,7 @@ type EntryHandle struct {
} }
// open opens the underlying state entry file. // open opens the underlying state entry file.
//
// A non-nil error returned by open is of type [hst.AppError]. // A non-nil error returned by open is of type [hst.AppError].
func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) { func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
if eh.DecodeErr != nil { if eh.DecodeErr != nil {
@@ -41,6 +43,7 @@ func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
} }
// Destroy removes the underlying state entry. // Destroy removes the underlying state entry.
//
// A non-nil error returned by Destroy is of type [hst.AppError]. // A non-nil error returned by Destroy is of type [hst.AppError].
func (eh *EntryHandle) Destroy() error { func (eh *EntryHandle) Destroy() error {
// destroy does not go through open // destroy does not go through open
@@ -55,8 +58,10 @@ func (eh *EntryHandle) Destroy() error {
} }
// save encodes [hst.State] and writes it to the underlying file. // save encodes [hst.State] and writes it to the underlying file.
//
// An error is returned if a file already exists with the same identifier. // An error is returned if a file already exists with the same identifier.
// save does not validate the embedded [hst.Config]. // save does not validate the embedded [hst.Config].
//
// A non-nil error returned by save is of type [hst.AppError]. // A non-nil error returned by save is of type [hst.AppError].
func (eh *EntryHandle) save(state *hst.State) error { func (eh *EntryHandle) save(state *hst.State) error {
f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
@@ -71,17 +76,19 @@ func (eh *EntryHandle) save(state *hst.State) error {
return err return err
} }
// Load loads and validates the state entry header, and returns the [hst.Enablement] byte. // Load loads and validates the state entry header, and returns the
// for a non-nil v, the full state payload is decoded and stored in the value pointed to by v. // [hst.Enablements] byte. For a non-nil v, the full state payload is decoded
// Load validates the embedded [hst.Config] value. // and stored in the value pointed to by v.
// A non-nil error returned by Load is of type [hst.AppError]. //
func (eh *EntryHandle) Load(v *hst.State) (hst.Enablement, error) { // Load validates the embedded [hst.Config] value. A non-nil error returned by
// Load is of type [hst.AppError].
func (eh *EntryHandle) Load(v *hst.State) (hst.Enablements, error) {
f, err := eh.open(os.O_RDONLY, 0) f, err := eh.open(os.O_RDONLY, 0)
if err != nil { if err != nil {
return 0, err return 0, err
} }
var et hst.Enablement var et hst.Enablements
if v != nil { if v != nil {
et, err = entryDecode(f, v) et, err = entryDecode(f, v)
if err == nil && v.ID != eh.ID { if err == nil && v.ID != eh.ID {
@@ -99,6 +106,7 @@ func (eh *EntryHandle) Load(v *hst.State) (hst.Enablement, error) {
} }
// Handle is a handle on a [Store] segment. // Handle is a handle on a [Store] segment.
//
// Initialised by [Store.Handle]. // Initialised by [Store.Handle].
type Handle struct { type Handle struct {
// Identity of instances tracked by this segment. // Identity of instances tracked by this segment.
@@ -113,8 +121,9 @@ type Handle struct {
mu sync.Mutex mu sync.Mutex
} }
// Lock attempts to acquire a lock on [Handle]. // Lock attempts to acquire a lock on [Handle]. If successful, Lock returns a
// If successful, Lock returns a non-nil unlock function. // non-nil unlock function.
//
// A non-nil error returned by Lock is of type [hst.AppError]. // A non-nil error returned by Lock is of type [hst.AppError].
func (h *Handle) Lock() (unlock func(), err error) { func (h *Handle) Lock() (unlock func(), err error) {
if unlock, err = h.fileMu.Lock(); err != nil { if unlock, err = h.fileMu.Lock(); err != nil {
@@ -123,20 +132,24 @@ func (h *Handle) Lock() (unlock func(), err error) {
return return
} }
// Save attempts to save [hst.State] as a segment entry, and returns its [EntryHandle]. // Save attempts to save [hst.State] as a segment entry, and returns its
// Must be called while holding [Handle.Lock]. // [EntryHandle]. Must be called while holding [Handle.Lock].
//
// An error is returned if an entry already exists with the same identifier. // An error is returned if an entry already exists with the same identifier.
// Save does not validate the embedded [hst.Config]. // Save does not validate the embedded [hst.Config].
//
// A non-nil error returned by Save is of type [hst.AppError]. // A non-nil error returned by Save is of type [hst.AppError].
func (h *Handle) Save(state *hst.State) (*EntryHandle, error) { func (h *Handle) Save(state *hst.State) (*EntryHandle, error) {
eh := EntryHandle{nil, h.Path.Append(state.ID.String()), state.ID} eh := EntryHandle{nil, h.Path.Append(state.ID.String()), state.ID}
return &eh, eh.save(state) return &eh, eh.save(state)
} }
// Entries returns an iterator over all [EntryHandle] held in this segment. // Entries returns an iterator over all [EntryHandle] held in this segment. Must
// Must be called while holding [Handle.Lock]. // be called while holding [Handle.Lock].
// A non-nil error attached to a [EntryHandle] indicates a malformed identifier and is of type [hst.AppError]. //
// A non-nil error returned by Entries is of type [hst.AppError]. // A non-nil error attached to a [EntryHandle] indicates a malformed identifier
// and is of type [hst.AppError]. A non-nil error returned by Entries is of type
// [hst.AppError].
func (h *Handle) Entries() (iter.Seq[*EntryHandle], int, error) { func (h *Handle) Entries() (iter.Seq[*EntryHandle], int, error) {
// for error reporting // for error reporting
const step = "read store segment entries" const step = "read store segment entries"

View File

@@ -21,7 +21,7 @@ import (
// Made available here for direct validation of state entry files. // Made available here for direct validation of state entry files.
// //
//go:linkname entryDecode hakurei.app/internal/store.entryDecode //go:linkname entryDecode hakurei.app/internal/store.entryDecode
func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error) func entryDecode(r io.Reader, p *hst.State) (hst.Enablements, error)
// Made available here for direct access to known segment handles. // Made available here for direct access to known segment handles.
// //

View File

@@ -17,20 +17,21 @@ func (sys *I) UpdatePerm(path *check.Absolute, perms ...acl.Perm) *I {
return sys return sys
} }
// UpdatePermType maintains [acl.Perms] on a file until its [Enablement] is no longer satisfied. // UpdatePermType maintains [acl.Perms] on a file until its [hst.Enablements] is
func (sys *I) UpdatePermType(et hst.Enablement, path *check.Absolute, perms ...acl.Perm) *I { // no longer satisfied.
func (sys *I) UpdatePermType(et hst.Enablements, path *check.Absolute, perms ...acl.Perm) *I {
sys.ops = append(sys.ops, &aclUpdateOp{et, path.String(), perms}) sys.ops = append(sys.ops, &aclUpdateOp{et, path.String(), perms})
return sys return sys
} }
// aclUpdateOp implements [I.UpdatePermType]. // aclUpdateOp implements [I.UpdatePermType].
type aclUpdateOp struct { type aclUpdateOp struct {
et hst.Enablement et hst.Enablements
path string path string
perms acl.Perms perms acl.Perms
} }
func (a *aclUpdateOp) Type() hst.Enablement { return a.et } func (a *aclUpdateOp) Type() hst.Enablements { return a.et }
func (a *aclUpdateOp) apply(sys *I) error { func (a *aclUpdateOp) apply(sys *I) error {
sys.msg.Verbose("applying ACL", a) sys.msg.Verbose("applying ACL", a)

View File

@@ -31,7 +31,9 @@ func (sys *I) MustProxyDBus(
} }
} }
// ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via [dbus] and terminates it on revert. // ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via
// [dbus] and terminates it on revert.
//
// This [Op] is always [Process] scoped. // This [Op] is always [Process] scoped.
func (sys *I) ProxyDBus( func (sys *I) ProxyDBus(
session, system *hst.BusConfig, session, system *hst.BusConfig,
@@ -84,7 +86,7 @@ type dbusProxyOp struct {
system bool system bool
} }
func (d *dbusProxyOp) Type() hst.Enablement { return Process } func (d *dbusProxyOp) Type() hst.Enablements { return Process }
func (d *dbusProxyOp) apply(sys *I) error { func (d *dbusProxyOp) apply(sys *I) error {
sys.msg.Verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0]) sys.msg.Verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0])

View File

@@ -21,12 +21,16 @@ type osFile interface {
fs.File fs.File
} }
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour. // syscallDispatcher provides methods that make state-dependent system calls as
// part of their behaviour.
//
// syscallDispatcher is embedded in [I], so all methods must be unexported. // syscallDispatcher is embedded in [I], so all methods must be unexported.
type syscallDispatcher interface { type syscallDispatcher interface {
// new starts a goroutine with a new instance of syscallDispatcher. // new starts a goroutine with a new instance of syscallDispatcher.
// A syscallDispatcher must never be used in any goroutine other than the one owning it, //
// just synchronising access is not enough, as this is for test instrumentation. // A syscallDispatcher must never be used in any goroutine other than the
// one owning it, just synchronising access is not enough, as this is for
// test instrumentation.
new(f func(k syscallDispatcher)) new(f func(k syscallDispatcher))
// stat provides os.Stat. // stat provides os.Stat.

View File

@@ -27,7 +27,7 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
type opBehaviourTestCase struct { type opBehaviourTestCase struct {
name string name string
uid int uid int
ec hst.Enablement ec hst.Enablements
op Op op Op
apply []stub.Call apply []stub.Call
@@ -158,7 +158,7 @@ type opMetaTestCase struct {
name string name string
op Op op Op
wantType hst.Enablement wantType hst.Enablements
wantPath string wantPath string
wantString string wantString string
} }

View File

@@ -12,19 +12,19 @@ func (sys *I) Link(oldname, newname *check.Absolute) *I {
return sys.LinkFileType(Process, oldname, newname) return sys.LinkFileType(Process, oldname, newname)
} }
// LinkFileType maintains a hardlink until its [Enablement] is no longer satisfied. // LinkFileType maintains a hardlink until its [hst.Enablements] is no longer satisfied.
func (sys *I) LinkFileType(et hst.Enablement, oldname, newname *check.Absolute) *I { func (sys *I) LinkFileType(et hst.Enablements, oldname, newname *check.Absolute) *I {
sys.ops = append(sys.ops, &hardlinkOp{et, newname.String(), oldname.String()}) sys.ops = append(sys.ops, &hardlinkOp{et, newname.String(), oldname.String()})
return sys return sys
} }
// hardlinkOp implements [I.LinkFileType]. // hardlinkOp implements [I.LinkFileType].
type hardlinkOp struct { type hardlinkOp struct {
et hst.Enablement et hst.Enablements
dst, src string dst, src string
} }
func (l *hardlinkOp) Type() hst.Enablement { return l.et } func (l *hardlinkOp) Type() hst.Enablements { return l.et }
func (l *hardlinkOp) apply(sys *I) error { func (l *hardlinkOp) apply(sys *I) error {
sys.msg.Verbose("linking", l) sys.msg.Verbose("linking", l)

View File

@@ -15,21 +15,22 @@ func (sys *I) Ensure(name *check.Absolute, perm os.FileMode) *I {
return sys return sys
} }
// Ephemeral ensures the existence of a directory until its [Enablement] is no longer satisfied. // Ephemeral ensures the existence of a directory until its [hst.Enablements] is
func (sys *I) Ephemeral(et hst.Enablement, name *check.Absolute, perm os.FileMode) *I { // 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}) sys.ops = append(sys.ops, &mkdirOp{et, name.String(), perm, true})
return sys return sys
} }
// mkdirOp implements [I.Ensure] and [I.Ephemeral]. // mkdirOp implements [I.Ensure] and [I.Ephemeral].
type mkdirOp struct { type mkdirOp struct {
et hst.Enablement et hst.Enablements
path string path string
perm os.FileMode perm os.FileMode
ephemeral bool ephemeral bool
} }
func (m *mkdirOp) Type() hst.Enablement { return m.et } func (m *mkdirOp) Type() hst.Enablements { return m.et }
func (m *mkdirOp) apply(sys *I) error { func (m *mkdirOp) apply(sys *I) error {
sys.msg.Verbose("ensuring directory", m) sys.msg.Verbose("ensuring directory", m)

View File

@@ -12,8 +12,9 @@ import (
) )
// PipeWire maintains a pipewire socket with SecurityContext attached via [pipewire]. // PipeWire maintains a pipewire socket with SecurityContext attached via [pipewire].
// The socket stops accepting connections once the pipe referred to by sync is closed. //
// The socket is pathname only and is destroyed on revert. // The socket stops accepting connections once the pipe referred to by sync is
// closed. The socket is pathname only and is destroyed on revert.
func (sys *I) PipeWire(dst *check.Absolute, appID, instanceID string) *I { func (sys *I) PipeWire(dst *check.Absolute, appID, instanceID string) *I {
sys.ops = append(sys.ops, &pipewireOp{nil, dst, appID, instanceID}) sys.ops = append(sys.ops, &pipewireOp{nil, dst, appID, instanceID})
return sys return sys
@@ -27,7 +28,7 @@ type pipewireOp struct {
appID, instanceID string appID, instanceID string
} }
func (p *pipewireOp) Type() hst.Enablement { return Process } func (p *pipewireOp) Type() hst.Enablements { return Process }
func (p *pipewireOp) apply(sys *I) (err error) { func (p *pipewireOp) apply(sys *I) (err error) {
var ctx *pipewire.Context var ctx *pipewire.Context

View File

@@ -20,21 +20,21 @@ const (
) )
// Criteria specifies types of Op to revert. // Criteria specifies types of Op to revert.
type Criteria hst.Enablement type Criteria hst.Enablements
func (ec *Criteria) hasType(t hst.Enablement) bool { func (ec *Criteria) hasType(t hst.Enablements) bool {
// nil criteria: revert everything except User // nil criteria: revert everything except User
if ec == nil { if ec == nil {
return t != User return t != User
} }
return hst.Enablement(*ec)&t != 0 return hst.Enablements(*ec)&t != 0
} }
// Op is a reversible system operation. // Op is a reversible system operation.
type Op interface { type Op interface {
// Type returns [Op]'s enablement type, for matching a revert criteria. // Type returns [Op]'s enablement type, for matching a revert criteria.
Type() hst.Enablement Type() hst.Enablements
apply(sys *I) error apply(sys *I) error
revert(sys *I, ec *Criteria) error revert(sys *I, ec *Criteria) error
@@ -44,8 +44,8 @@ type Op interface {
String() string String() string
} }
// TypeString extends [hst.Enablement.String] to support [User] and [Process]. // TypeString extends [hst.Enablements] to support [User] and [Process].
func TypeString(e hst.Enablement) string { func TypeString(e hst.Enablements) string {
switch e { switch e {
case User: case User:
return "user" return "user"
@@ -110,7 +110,9 @@ func (sys *I) Equal(target *I) bool {
return true return true
} }
// Commit applies all [Op] held by [I] and reverts all successful [Op] on first error encountered. // Commit applies all [Op] held by [I] and reverts all successful [Op] on first
// error encountered.
//
// Commit must not be called more than once. // Commit must not be called more than once.
func (sys *I) Commit() error { func (sys *I) Commit() error {
if sys.committed { if sys.committed {

View File

@@ -20,7 +20,7 @@ func TestCriteria(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
ec, t hst.Enablement ec, t hst.Enablements
want bool want bool
}{ }{
{"nil", 0xff, hst.EWayland, true}, {"nil", 0xff, hst.EWayland, true},
@@ -47,7 +47,7 @@ func TestTypeString(t *testing.T) {
t.Parallel() t.Parallel()
testCases := []struct { testCases := []struct {
e hst.Enablement e hst.Enablements
want string want string
}{ }{
{hst.EWayland, hst.EWayland.String()}, {hst.EWayland, hst.EWayland.String()},
@@ -190,7 +190,7 @@ func TestCommitRevert(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
f func(sys *I) f func(sys *I)
ec hst.Enablement ec hst.Enablements
commit []stub.Call commit []stub.Call
wantErrCommit error wantErrCommit error

View File

@@ -11,8 +11,9 @@ import (
) )
// Wayland maintains a wayland socket with security-context-v1 attached via [wayland]. // Wayland maintains a wayland socket with security-context-v1 attached via [wayland].
// The socket stops accepting connections once the pipe referred to by sync is closed. //
// The socket is pathname only and is destroyed on revert. // The socket stops accepting connections once the pipe referred to by sync is
// closed. The socket is pathname only and is destroyed on revert.
func (sys *I) Wayland(dst, src *check.Absolute, appID, instanceID string) *I { func (sys *I) Wayland(dst, src *check.Absolute, appID, instanceID string) *I {
sys.ops = append(sys.ops, &waylandOp{nil, sys.ops = append(sys.ops, &waylandOp{nil,
dst, src, appID, instanceID}) dst, src, appID, instanceID})
@@ -26,7 +27,7 @@ type waylandOp struct {
appID, instanceID string appID, instanceID string
} }
func (w *waylandOp) Type() hst.Enablement { return Process } func (w *waylandOp) Type() hst.Enablements { return Process }
func (w *waylandOp) apply(sys *I) (err error) { func (w *waylandOp) apply(sys *I) (err error) {
if w.ctx, err = sys.waylandNew(w.src, w.dst, w.appID, w.instanceID); err != nil { if w.ctx, err = sys.waylandNew(w.src, w.dst, w.appID, w.instanceID); err != nil {

View File

@@ -5,7 +5,8 @@ import (
"hakurei.app/internal/xcb" "hakurei.app/internal/xcb"
) )
// ChangeHosts inserts the target user into X11 hosts and deletes it once its [Enablement] is no longer satisfied. // ChangeHosts inserts the target user into X11 hosts and deletes it once its
// [hst.Enablements] is no longer satisfied.
func (sys *I) ChangeHosts(username string) *I { func (sys *I) ChangeHosts(username string) *I {
sys.ops = append(sys.ops, xhostOp(username)) sys.ops = append(sys.ops, xhostOp(username))
return sys return sys
@@ -14,7 +15,7 @@ func (sys *I) ChangeHosts(username string) *I {
// xhostOp implements [I.ChangeHosts]. // xhostOp implements [I.ChangeHosts].
type xhostOp string type xhostOp string
func (x xhostOp) Type() hst.Enablement { return hst.EX11 } func (x xhostOp) Type() hst.Enablements { return hst.EX11 }
func (x xhostOp) apply(sys *I) error { func (x xhostOp) apply(sys *I) error {
sys.msg.Verbosef("inserting entry %s to X11", x) sys.msg.Verbosef("inserting entry %s to X11", x)

View File

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

View File

@@ -35,7 +35,7 @@ package
*Default:* *Default:*
` <derivation hakurei-static-x86_64-unknown-linux-musl-0.3.6> ` ` <derivation hakurei-static-x86_64-unknown-linux-musl-0.4.0> `
@@ -542,6 +542,43 @@ null or string
## environment\.hakurei\.apps\.\<name>\.schedPolicy
Scheduling policy to set for the container\.
The zero value retains the current scheduling policy\.
*Type:*
null or one of “fifo”, “rr”, “batch”, “idle”, “deadline”, “ext”
*Default:*
` null `
## environment\.hakurei\.apps\.\<name>\.schedPriority
Scheduling priority to set for the container\.
*Type:*
null or integer between 1 and 99 (both inclusive)
*Default:*
` null `
## environment\.hakurei\.apps\.\<name>\.script ## environment\.hakurei\.apps\.\<name>\.script
@@ -805,7 +842,7 @@ package
*Default:* *Default:*
` <derivation hakurei-hsu-0.3.6> ` ` <derivation hakurei-hsu-0.4.0> `

View File

@@ -30,7 +30,7 @@
buildGo126Module rec { buildGo126Module rec {
pname = "hakurei"; pname = "hakurei";
version = "0.3.7"; version = "0.4.0";
srcFiltered = builtins.path { srcFiltered = builtins.path {
name = "${pname}-src"; name = "${pname}-src";