1
0
forked from rosa/hakurei

29 Commits

Author SHA1 Message Date
620062cca9 hst: expose scheduling priority
This is useful when limits are configured to allow it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-12 02:15:14 +09:00
196b200d0f container: expose priority and SCHED_OTHER policy
The more explicit API removes the arbitrary limit preventing use of SCHED_OTHER (referred to as SCHED_NORMAL in the kernel). This change also exposes priority value to set.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-12 01:14:03 +09:00
04e6bc3c5c hst: expose scheduling policy
This is primarily useful for poorly written music players for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-12 00:52:18 +09:00
5c540f90aa internal/outcome: improve doc comments
This improves readability on smaller displays.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-11 21:04:02 +09:00
1e8ac5f68e container: use policy name in log message
This is more helpful than having the user resolve the integer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-11 20:20:34 +09:00
fd515badff container: move scheduler policy constants to std
This avoids depending on cgo.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-11 20:03:08 +09:00
330a344845 hst: improve doc comments
These now read a lot better both in source and on pkgsite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-11 19:21:55 +09:00
48cdf8bf85 go: 1.26
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-10 03:29:19 +09:00
7fb42ba49d internal/rosa/llvm: set LLVM_LIT_ARGS
This replaces the progress bar, which was worse than useless.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-10 02:05:11 +09:00
19a2737148 container: sched policy string representation
This also uses priority obtained via sched_get_priority_min, and improves bounds checking.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-09 18:38:31 +09:00
baf2def9cc internal/rosa/kmod: prefix moduledir
This change also works around the kernel build system being unaware of this option.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-09 16:40:55 +09:00
242e042cb9 internal/rosa/nss: rename from ssl
The SSL name came from earlier on and is counterintuitive.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-09 14:58:31 +09:00
6988c9c4db internal/rosa: firmware artifact
Required for generic hardware.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 22:50:36 +09:00
d6e0ed8c76 internal/rosa/python: various pypi artifacts
These are dependencies of pre-commit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 22:25:16 +09:00
53be3309c5 internal/rosa: rdfind artifact
Required by linux firmware.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 20:26:15 +09:00
644dd18a52 internal/rosa: nettle artifact
Required by rdfind, which is required by linux firmware.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 20:22:09 +09:00
27c6f976df internal/rosa/gnu: parallel artifact
Used by linux firmware.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 19:56:40 +09:00
279a973633 internal/rosa: build independent earlyinit
This avoids unnecessarily rebuilding hakurei during development.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 18:29:04 +09:00
9c1b522689 internal/rosa/hakurei: optional hostname tool
This makes it more efficient to reuse the helper for partial builds.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 18:26:03 +09:00
5c8cd46c02 internal/rosa: update arm64 kernel config
This was not feasible during the bump, now there is a viable toolchain.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 03:17:53 +09:00
2dba550a2b internal/rosa/zlib: 1.3.1 to 1.3.2
This also switches to the CMake build system because upstream broke their old build system.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 02:36:59 +09:00
8c64812b34 internal/rosa: add zlib runtime dependency
For transitioning to dynamically linking zlib.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 02:36:09 +09:00
d1423d980d internal/rosa/cmake: bake in CMAKE_INSTALL_LIBDIR
There is never a good reason to set this to anything else, and the default value of lib64 breaks everything. This did not manifest on LLVM (which the CMake helper was initially written for) because it did not use this value.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 01:20:41 +09:00
104da0f66a internal/rosa/cmake: pass correct prefix
This can change build output similar to autotools --prefix and DESTDIR, but was not clearly indicated to do so.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 01:04:02 +09:00
d996d9fbb7 internal/rosa/cmake: pass parallel argument for make
This uses the default value for each build system, which is parallel for ninja but not for make.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 00:55:58 +09:00
469f97ccc1 internal/rosa/gnu: libiconv 1.18 to 1.19
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 00:36:38 +09:00
af7a6180a1 internal/rosa/cmake: optionally use makefile
This breaks the dependency loop in zlib.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-07 22:47:30 +09:00
03b5c0e20a internal/rosa/tamago: populate Anitya project id
This had to wait quite a while due to Microsoft Github rate-limiting.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-07 19:37:03 +09:00
6a31fb4fa3 internal/rosa: hakurei 0.3.5 to 0.3.6
This also removes the backport patch.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-07 18:53:48 +09:00
59 changed files with 1209 additions and 335 deletions

View File

@@ -16,6 +16,7 @@ import (
"hakurei.app/command"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/std"
"hakurei.app/hst"
"hakurei.app/internal/dbus"
"hakurei.app/internal/env"
@@ -89,6 +90,9 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagHomeDir string
flagUserName string
flagSchedPolicy string
flagSchedPriority int
flagPrivateRuntime, flagPrivateTmpdir bool
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
@@ -131,7 +135,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
log.Fatal(optionalErrorUnwrap(err))
return err
} else if progPath, err = check.NewAbs(p); err != nil {
log.Fatal(err.Error())
log.Fatal(err)
return err
}
}
@@ -150,7 +154,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
et |= hst.EPipeWire
}
config := &hst.Config{
config := hst.Config{
ID: flagID,
Identity: flagIdentity,
Groups: flagGroups,
@@ -177,6 +181,13 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
},
}
if err := config.SchedPolicy.UnmarshalText(
[]byte(flagSchedPolicy),
); err != nil {
log.Fatal(err)
}
config.SchedPriority = std.Int(flagSchedPriority)
// bind GPU stuff
if et&(hst.EX11|hst.EWayland) != 0 {
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
@@ -214,7 +225,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
homeDir = passwd.HomeDir
}
if a, err := check.NewAbs(homeDir); err != nil {
log.Fatal(err.Error())
log.Fatal(err)
return err
} else {
config.Container.Home = a
@@ -234,11 +245,11 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
} else {
if f, err := os.Open(flagDBusConfigSession); err != nil {
log.Fatal(err.Error())
log.Fatal(err)
} else {
decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus)
if err = f.Close(); err != nil {
log.Fatal(err.Error())
log.Fatal(err)
}
}
}
@@ -246,11 +257,11 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
// system bus proxy is optional
if flagDBusConfigSystem != "nil" {
if f, err := os.Open(flagDBusConfigSystem); err != nil {
log.Fatal(err.Error())
log.Fatal(err)
} else {
decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus)
if err = f.Close(); err != nil {
log.Fatal(err.Error())
log.Fatal(err)
}
}
}
@@ -266,7 +277,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
}
}
outcome.Main(ctx, msg, config, -1)
outcome.Main(ctx, msg, &config, -1)
panic("unreachable")
}).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
@@ -287,6 +298,10 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
"Container home directory").
Flag(&flagUserName, "u", command.StringFlag("chronos"),
"Passwd user name within sandbox").
Flag(&flagSchedPolicy, "policy", command.StringFlag(""),
"Scheduling policy to set for the container").
Flag(&flagSchedPriority, "priority", command.IntFlag(0),
"Scheduling priority to set for the container").
Flag(&flagPrivateRuntime, "private-runtime", command.BoolFlag(false),
"Do not share XDG_RUNTIME_DIR between containers under the same identity").
Flag(&flagPrivateTmpdir, "private-tmpdir", command.BoolFlag(false),

View File

@@ -36,7 +36,7 @@ Commands:
},
{
"run", []string{"run", "-h"}, `
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS]
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--policy <value>] [--priority <int>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS]
Flags:
-X Enable direct connection to X11
@@ -60,6 +60,10 @@ Flags:
Allow owning MPRIS D-Bus path, has no effect if custom config is available
-pipewire
Enable connection to PipeWire via SecurityContext
-policy string
Scheduling policy to set for the container
-priority int
Scheduling priority to set for the container
-private-runtime
Do not share XDG_RUNTIME_DIR between containers under the same identity
-private-tmpdir

View File

@@ -87,7 +87,7 @@ func main() {
}
if flagIdle {
pkg.SchedPolicy = container.SCHED_IDLE
pkg.SetSchedIdle = true
}
return

View File

@@ -38,9 +38,13 @@ type (
Container struct {
// Whether the container init should stay alive after its parent terminates.
AllowOrphan bool
// Scheduling policy to set via sched_setscheduler(2). The zero value
// skips this call. Supported policies are [SCHED_BATCH], [SCHED_IDLE].
SchedPolicy int
// Whether to set SchedPolicy and SchedPriority via sched_setscheduler(2).
SetScheduler bool
// Scheduling policy to set via sched_setscheduler(2).
SchedPolicy std.SchedPolicy
// Scheduling priority to set via sched_setscheduler(2). The zero value
// implies the minimum value supported by the current SchedPolicy.
SchedPriority std.Int
// Cgroup fd, nil to disable.
Cgroup *int
// ExtraFiles passed through to initial process in the container, with
@@ -373,16 +377,38 @@ func (p *Container) Start() error {
// sched_setscheduler: thread-directed but acts on all processes
// created from the calling thread
if p.SchedPolicy > 0 {
p.msg.Verbosef("setting scheduling policy %d", p.SchedPolicy)
if p.SetScheduler {
if p.SchedPolicy < 0 || p.SchedPolicy > std.SCHED_LAST {
return &StartError{
Fatal: false,
Step: "set scheduling policy",
Err: EINVAL,
}
}
var param schedParam
if priority, err := p.SchedPolicy.GetPriorityMin(); err != nil {
return &StartError{
Fatal: true,
Step: "get minimum priority",
Err: err,
}
} else {
param.priority = max(priority, p.SchedPriority)
}
p.msg.Verbosef(
"setting scheduling policy %s priority %d",
p.SchedPolicy, param.priority,
)
if err := schedSetscheduler(
0, // calling thread
p.SchedPolicy,
&schedParam{0},
&param,
); err != nil {
return &StartError{
Fatal: true,
Step: "enforce landlock ruleset",
Step: "set scheduling policy",
Err: err,
}
}

View File

@@ -1,6 +1,12 @@
package std
import "iter"
import (
"encoding"
"iter"
"strconv"
"sync"
"syscall"
)
// Syscalls returns an iterator over all wired syscalls.
func Syscalls() iter.Seq2[string, ScmpSyscall] {
@@ -26,3 +32,128 @@ func SyscallResolveName(name string) (num ScmpSyscall, ok bool) {
num, ok = syscallNumExtra[name]
return
}
// SchedPolicy denotes a scheduling policy defined in include/uapi/linux/sched.h.
type SchedPolicy int
// include/uapi/linux/sched.h
const (
SCHED_NORMAL SchedPolicy = iota
SCHED_FIFO
SCHED_RR
SCHED_BATCH
_SCHED_ISO // SCHED_ISO: reserved but not implemented yet
SCHED_IDLE
SCHED_DEADLINE
SCHED_EXT
SCHED_LAST SchedPolicy = iota - 1
)
var _ encoding.TextMarshaler = SCHED_LAST
var _ encoding.TextUnmarshaler = new(SCHED_LAST)
// String returns a unique representation of policy, also used in encoding.
func (policy SchedPolicy) String() string {
switch policy {
case SCHED_NORMAL:
return ""
case SCHED_FIFO:
return "fifo"
case SCHED_RR:
return "rr"
case SCHED_BATCH:
return "batch"
case SCHED_IDLE:
return "idle"
case SCHED_DEADLINE:
return "deadline"
case SCHED_EXT:
return "ext"
default:
return "invalid policy " + strconv.Itoa(int(policy))
}
}
// MarshalText performs bounds checking and returns the result of String.
func (policy SchedPolicy) MarshalText() ([]byte, error) {
if policy == _SCHED_ISO || policy < 0 || policy > SCHED_LAST {
return nil, syscall.EINVAL
}
return []byte(policy.String()), nil
}
// InvalidSchedPolicyError is an invalid string representation of a [SchedPolicy].
type InvalidSchedPolicyError string
func (InvalidSchedPolicyError) Unwrap() error { return syscall.EINVAL }
func (e InvalidSchedPolicyError) Error() string {
return "invalid scheduling policy " + strconv.Quote(string(e))
}
// UnmarshalText is the inverse of MarshalText.
func (policy *SchedPolicy) UnmarshalText(text []byte) error {
switch string(text) {
case "fifo":
*policy = SCHED_FIFO
case "rr":
*policy = SCHED_RR
case "batch":
*policy = SCHED_BATCH
case "idle":
*policy = SCHED_IDLE
case "deadline":
*policy = SCHED_DEADLINE
case "ext":
*policy = SCHED_EXT
case "":
*policy = 0
return nil
default:
return InvalidSchedPolicyError(text)
}
return nil
}
// for sched_get_priority_max and sched_get_priority_min
var (
schedPriority [SCHED_LAST + 1][2]Int
schedPriorityErr [SCHED_LAST + 1][2]error
schedPriorityOnce [SCHED_LAST + 1][2]sync.Once
)
// GetPriorityMax returns the maximum priority value that can be used with the
// scheduling algorithm identified by policy.
func (policy SchedPolicy) GetPriorityMax() (Int, error) {
schedPriorityOnce[policy][0].Do(func() {
priority, _, errno := syscall.Syscall(
syscall.SYS_SCHED_GET_PRIORITY_MAX,
uintptr(policy),
0, 0,
)
schedPriority[policy][0] = Int(priority)
if errno != 0 {
schedPriorityErr[policy][0] = errno
}
})
return schedPriority[policy][0], schedPriorityErr[policy][0]
}
// GetPriorityMin returns the minimum priority value that can be used with the
// scheduling algorithm identified by policy.
func (policy SchedPolicy) GetPriorityMin() (Int, error) {
schedPriorityOnce[policy][1].Do(func() {
priority, _, errno := syscall.Syscall(
syscall.SYS_SCHED_GET_PRIORITY_MIN,
uintptr(policy),
0, 0,
)
schedPriority[policy][1] = Int(priority)
if errno != 0 {
schedPriorityErr[policy][1] = errno
}
})
return schedPriority[policy][1], schedPriorityErr[policy][1]
}

View File

@@ -1,6 +1,11 @@
package std_test
import (
"encoding/json"
"errors"
"math"
"reflect"
"syscall"
"testing"
"hakurei.app/container/std"
@@ -19,3 +24,90 @@ func TestSyscallResolveName(t *testing.T) {
})
}
}
func TestSchedPolicyJSON(t *testing.T) {
t.Parallel()
testCases := []struct {
policy std.SchedPolicy
want string
encodeErr error
decodeErr error
}{
{std.SCHED_NORMAL, `""`, nil, nil},
{std.SCHED_FIFO, `"fifo"`, nil, nil},
{std.SCHED_RR, `"rr"`, nil, nil},
{std.SCHED_BATCH, `"batch"`, nil, nil},
{4, `"invalid policy 4"`, syscall.EINVAL, std.InvalidSchedPolicyError("invalid policy 4")},
{std.SCHED_IDLE, `"idle"`, nil, nil},
{std.SCHED_DEADLINE, `"deadline"`, nil, nil},
{std.SCHED_EXT, `"ext"`, nil, nil},
{math.MaxInt, `"iso"`, syscall.EINVAL, std.InvalidSchedPolicyError("iso")},
}
for _, tc := range testCases {
name := tc.policy.String()
if tc.policy == std.SCHED_NORMAL {
name = "normal"
}
t.Run(name, func(t *testing.T) {
t.Parallel()
got, err := json.Marshal(tc.policy)
if !errors.Is(err, tc.encodeErr) {
t.Fatalf("Marshal: error = %v, want %v", err, tc.encodeErr)
}
if err == nil && string(got) != tc.want {
t.Fatalf("Marshal: %s, want %s", string(got), tc.want)
}
var v std.SchedPolicy
if err = json.Unmarshal([]byte(tc.want), &v); !reflect.DeepEqual(err, tc.decodeErr) {
t.Fatalf("Unmarshal: error = %v, want %v", err, tc.decodeErr)
}
if err == nil && v != tc.policy {
t.Fatalf("Unmarshal: %d, want %d", v, tc.policy)
}
})
}
}
func TestSchedPolicyMinMax(t *testing.T) {
t.Parallel()
testCases := []struct {
policy std.SchedPolicy
min, max std.Int
err error
}{
{std.SCHED_NORMAL, 0, 0, nil},
{std.SCHED_FIFO, 1, 99, nil},
{std.SCHED_RR, 1, 99, nil},
{std.SCHED_BATCH, 0, 0, nil},
{4, -1, -1, syscall.EINVAL},
{std.SCHED_IDLE, 0, 0, nil},
{std.SCHED_DEADLINE, 0, 0, nil},
{std.SCHED_EXT, 0, 0, nil},
}
for _, tc := range testCases {
name := tc.policy.String()
if tc.policy == std.SCHED_NORMAL {
name = "normal"
}
t.Run(name, func(t *testing.T) {
t.Parallel()
if priority, err := tc.policy.GetPriorityMax(); !reflect.DeepEqual(err, tc.err) {
t.Fatalf("GetPriorityMax: error = %v, want %v", err, tc.err)
} else if priority != tc.max {
t.Fatalf("GetPriorityMax: %d, want %d", priority, tc.max)
}
if priority, err := tc.policy.GetPriorityMin(); !reflect.DeepEqual(err, tc.err) {
t.Fatalf("GetPriorityMin: error = %v, want %v", err, tc.err)
} else if priority != tc.min {
t.Fatalf("GetPriorityMin: %d, want %d", priority, tc.min)
}
})
}
}

View File

@@ -43,18 +43,6 @@ func Isatty(fd int) bool {
return r == 0
}
// include/uapi/linux/sched.h
const (
SCHED_NORMAL = iota
SCHED_FIFO
SCHED_RR
SCHED_BATCH
_ // SCHED_ISO: reserved but not implemented yet
SCHED_IDLE
SCHED_DEADLINE
SCHED_EXT
)
// schedParam is equivalent to struct sched_param from include/linux/sched.h.
type schedParam struct {
// sched_priority
@@ -74,13 +62,13 @@ type schedParam struct {
// this if you do not have something similar in place!
//
// [very subtle to use correctly]: https://www.openwall.com/lists/musl/2016/03/01/4
func schedSetscheduler(tid, policy int, param *schedParam) error {
if r, _, errno := Syscall(
func schedSetscheduler(tid int, policy std.SchedPolicy, param *schedParam) error {
if _, _, errno := Syscall(
SYS_SCHED_SETSCHEDULER,
uintptr(tid),
uintptr(policy),
uintptr(unsafe.Pointer(param)),
); r < 0 {
); errno != 0 {
return errno
}
return nil

12
flake.lock generated
View File

@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1765384171,
"narHash": "sha256-FuFtkJrW1Z7u+3lhzPRau69E0CNjADku1mLQQflUORo=",
"lastModified": 1772985280,
"narHash": "sha256-FdrNykOoY9VStevU4zjSUdvsL9SzJTcXt4omdEDZDLk=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "44777152652bc9eacf8876976fa72cc77ca8b9d8",
"rev": "8f736f007139d7f70752657dff6a401a585d6cbc",
"type": "github"
},
"original": {
@@ -23,11 +23,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1765311797,
"narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
"lastModified": 1772822230,
"narHash": "sha256-yf3iYLGbGVlIthlQIk5/4/EQDZNNEmuqKZkQssMljuw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
"rev": "71caefce12ba78d84fe618cf61644dce01cf3a96",
"type": "github"
},
"original": {

View File

@@ -99,7 +99,7 @@
hakurei = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs)
# passthru.buildInputs
go
go_1_26
clang
# nativeBuildInputs
@@ -182,7 +182,7 @@
let
# this is used for interactive vm testing during development, where tests might be broken
package = self.packages.${pkgs.stdenv.hostPlatform.system}.hakurei.override {
buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; });
buildGo126Module = previousArgs: pkgs.pkgsStatic.buildGo126Module (previousArgs // { doCheck = false; });
};
in
{

2
go.mod
View File

@@ -1,3 +1,3 @@
module hakurei.app
go 1.25
go 1.26

View File

@@ -6,96 +6,137 @@ import (
"strings"
"hakurei.app/container/check"
"hakurei.app/container/std"
)
// Config configures an application container, implemented in internal/app.
// Config configures an application container.
type Config struct {
// Reverse-DNS style configured arbitrary identifier string.
// Passed to wayland security-context-v1 and used as part of defaults in dbus session proxy.
//
// This value is passed as is to Wayland security-context-v1 and used as
// part of defaults in D-Bus session proxy. The zero value causes a default
// value to be derived from the container instance.
ID string `json:"id,omitempty"`
// System services to make available in the container.
Enablements *Enablements `json:"enablements,omitempty"`
// Session D-Bus proxy configuration.
// If set to nil, session bus proxy assume built-in defaults.
//
// Has no effect if [EDBus] but is not set in Enablements. The zero value
// assumes built-in defaults derived from ID.
SessionBus *BusConfig `json:"session_bus,omitempty"`
// System D-Bus proxy configuration.
// If set to nil, system bus proxy is disabled.
//
// Has no effect if [EDBus] but is not set in Enablements. The zero value
// disables system bus proxy.
SystemBus *BusConfig `json:"system_bus,omitempty"`
// Direct access to wayland socket, no attempt is made to attach security-context-v1
// and the bare socket is made available to the container.
// Direct access to Wayland socket, no attempt is made to attach
// security-context-v1 and the bare socket is made available to the
// container.
//
// This option is unsupported and most likely enables full control over the Wayland
// session. Do not set this to true unless you are sure you know what you are doing.
// This option is unsupported and will most likely enable full control over
// the Wayland session from within the container. Do not set this to true
// unless you are sure you know what you are doing.
DirectWayland bool `json:"direct_wayland,omitempty"`
// Direct access to the PipeWire socket established via SecurityContext::Create, no
// attempt is made to start the pipewire-pulse server.
// Direct access to the PipeWire socket established via SecurityContext::Create,
// no attempt is made to start the pipewire-pulse server.
//
// The SecurityContext machinery is fatally flawed, it blindly sets read and execute
// bits on all objects for clients with the lowest achievable privilege level (by
// setting PW_KEY_ACCESS to "restricted"). This enables them to call any method
// targeting any object, and since Registry::Destroy checks for the read and execute bit,
// allows the destruction of any object other than PW_ID_CORE as well. This behaviour
// is implemented separately in media-session and wireplumber, with the wireplumber
// implementation in Lua via an embedded Lua vm. In all known setups, wireplumber is
// in use, and there is no known way to change its behaviour and set permissions
// differently without replacing the Lua script. Also, since PipeWire relies on these
// permissions to work, reducing them is not possible.
// The SecurityContext machinery is fatally flawed, it unconditionally sets
// read and execute bits on all objects for clients with the lowest achievable
// privilege level (by setting PW_KEY_ACCESS to "restricted" or by satisfying
// all conditions of [the /.flatpak-info hack]). This enables them to call
// any method targeting any object, and since Registry::Destroy checks for
// the read and execute bit, allows the destruction of any object other than
// PW_ID_CORE as well.
//
// Currently, the only other sandboxed use case is flatpak, which is not aware of
// PipeWire and blindly exposes the bare PulseAudio socket to the container (behaves
// like DirectPulse). This socket is backed by the pipewire-pulse compatibility daemon,
// which obtains client pid via the SO_PEERCRED option. The PipeWire daemon, pipewire-pulse
// daemon and the session manager daemon then separately performs the /.flatpak-info hack
// described in https://git.gensokyo.uk/security/hakurei/issues/21. Under such use case,
// since the client has no direct access to PipeWire, insecure parts of the protocol are
// obscured by pipewire-pulse simply not implementing them, and thus hiding the flaws
// described above.
// This behaviour is implemented separately in media-session and wireplumber,
// with the wireplumber implementation in Lua via an embedded Lua vm. In all
// known setups, wireplumber is in use, and in that case, no option for
// configuring this behaviour exists, without replacing the Lua script.
// Also, since PipeWire relies on these permissions to work, reducing them
// was never possible in the first place.
//
// Hakurei does not rely on the /.flatpak-info hack. Instead, a socket is sets up via
// SecurityContext. A pipewire-pulse server connected through it achieves the same
// permissions as flatpak does via the /.flatpak-info hack and is maintained for the
// life of the container.
// Currently, the only other sandboxed use case is flatpak, which is not
// aware of PipeWire and blindly exposes the bare PulseAudio socket to the
// container (behaves like DirectPulse). This socket is backed by the
// pipewire-pulse compatibility daemon, which obtains client pid via the
// SO_PEERCRED option. The PipeWire daemon, pipewire-pulse daemon and the
// session manager daemon then separately performs [the /.flatpak-info hack].
// Under such use case, since the client has no direct access to PipeWire,
// insecure parts of the protocol are obscured by the absence of an
// equivalent API in PulseAudio, or pipewire-pulse simply not implementing
// them.
//
// Hakurei does not rely on [the /.flatpak-info hack]. Instead, a socket is
// sets up via SecurityContext. A pipewire-pulse server connected through it
// achieves the same permissions as flatpak does via [the /.flatpak-info hack]
// and is maintained for the life of the container.
//
// This option is unsupported and enables a denial-of-service attack as the
// sandboxed client is able to destroy any client object and thus
// disconnecting them from PipeWire, or destroy the SecurityContext object,
// preventing any further container creation.
//
// This option is unsupported and enables a denial-of-service attack as the sandboxed
// client is able to destroy any client object and thus disconnecting them from PipeWire,
// or destroy the SecurityContext object preventing any further container creation.
// Do not set this to true, it is insecure under any configuration.
DirectPipeWire bool `json:"direct_pipewire,omitempty"`
// Direct access to PulseAudio socket, no attempt is made to establish pipewire-pulse
// server via a PipeWire socket with a SecurityContext attached and the bare socket
// is made available to the container.
//
// This option is unsupported and enables arbitrary code execution as the PulseAudio
// server. Do not set this to true, it is insecure under any configuration.
// [the /.flatpak-info hack]: https://git.gensokyo.uk/security/hakurei/issues/21
DirectPipeWire bool `json:"direct_pipewire,omitempty"`
// Direct access to PulseAudio socket, no attempt is made to establish
// pipewire-pulse server via a PipeWire socket with a SecurityContext
// attached, and the bare socket is made available to the container.
//
// This option is unsupported and enables arbitrary code execution as the
// PulseAudio server.
//
// Do not set this to true, it is insecure under any configuration.
DirectPulse bool `json:"direct_pulse,omitempty"`
// Extra acl updates to perform before setuid.
ExtraPerms []ExtraPermConfig `json:"extra_perms,omitempty"`
// Numerical application id, passed to hsu, used to derive init user namespace credentials.
// Numerical application id, passed to hsu, used to derive init user
// namespace credentials.
Identity int `json:"identity"`
// Init user namespace supplementary groups inherited by all container processes.
Groups []string `json:"groups"`
// Scheduling policy to set for the container.
//
// The zero value retains the current scheduling policy.
SchedPolicy std.SchedPolicy `json:"sched_policy,omitempty"`
// Scheduling priority to set for the container.
//
// The zero value implies the minimum priority of the current SchedPolicy.
// Has no effect if SchedPolicy is zero.
SchedPriority std.Int `json:"sched_priority,omitempty"`
// High level configuration applied to the underlying [container].
Container *ContainerConfig `json:"container"`
}
var (
// ErrConfigNull is returned by [Config.Validate] for an invalid configuration that contains a null value for any
// field that must not be null.
// ErrConfigNull is returned by [Config.Validate] for an invalid configuration
// that contains a null value for any field that must not be null.
ErrConfigNull = errors.New("unexpected null in config")
// ErrIdentityBounds is returned by [Config.Validate] for an out of bounds [Config.Identity] value.
// ErrIdentityBounds is returned by [Config.Validate] for an out of bounds
// [Config.Identity] value.
ErrIdentityBounds = errors.New("identity out of bounds")
// ErrEnviron is returned by [Config.Validate] if an environment variable name contains '=' or NUL.
// ErrSchedPolicyBounds is returned by [Config.Validate] for an out of bounds
// [Config.SchedPolicy] value.
ErrSchedPolicyBounds = errors.New("scheduling policy out of bounds")
// ErrEnviron is returned by [Config.Validate] if an environment variable
// name contains '=' or NUL.
ErrEnviron = errors.New("invalid environment variable name")
// ErrInsecure is returned by [Config.Validate] if the configuration is considered insecure.
// ErrInsecure is returned by [Config.Validate] if the configuration is
// considered insecure.
ErrInsecure = errors.New("configuration is insecure")
)
@@ -112,6 +153,13 @@ func (config *Config) Validate() error {
Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
}
if config.SchedPolicy < 0 || config.SchedPolicy > std.SCHED_LAST {
return &AppError{Step: "validate configuration", Err: ErrSchedPolicyBounds,
Msg: "scheduling policy " +
strconv.Itoa(int(config.SchedPolicy)) +
" out of range"}
}
if err := config.SessionBus.CheckInterfaces("session"); err != nil {
return err
}

View File

@@ -22,6 +22,10 @@ func TestConfigValidate(t *testing.T) {
Msg: "identity -1 out of range"}},
{"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
Msg: "identity 10000 out of range"}},
{"sched lower", &hst.Config{SchedPolicy: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
Msg: "scheduling policy -1 out of range"}},
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
Msg: "scheduling policy 51966 out of range"}},
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}},
&hst.BadInterfaceError{Interface: "", Segment: "session"}},
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}},

View File

@@ -16,18 +16,20 @@ const PrivateTmp = "/.hakurei"
var AbsPrivateTmp = check.MustAbs(PrivateTmp)
const (
// WaitDelayDefault is used when WaitDelay has its zero value.
// WaitDelayDefault is used when WaitDelay has the zero value.
WaitDelayDefault = 5 * time.Second
// WaitDelayMax is used if WaitDelay exceeds its value.
// WaitDelayMax is used when WaitDelay exceeds its value.
WaitDelayMax = 30 * time.Second
)
const (
// ExitFailure is returned if the container fails to start.
ExitFailure = iota + 1
// ExitCancel is returned if the container is terminated by a shim-directed signal which cancels its context.
// ExitCancel is returned if the container is terminated by a shim-directed
// signal which cancels its context.
ExitCancel
// ExitOrphan is returned when the shim is orphaned before priv side delivers a signal.
// ExitOrphan is returned when the shim is orphaned before priv side process
// delivers a signal.
ExitOrphan
// ExitRequest is returned when the priv side process requests shim exit.
@@ -38,10 +40,12 @@ const (
type Flags uintptr
const (
// FMultiarch unblocks syscalls required for multiarch to work on applicable targets.
// FMultiarch unblocks system calls required for multiarch to work on
// multiarch-enabled targets (amd64, arm64).
FMultiarch Flags = 1 << iota
// FSeccompCompat changes emitted seccomp filter programs to be identical to that of Flatpak.
// FSeccompCompat changes emitted seccomp filter programs to be identical to
// that of Flatpak in enabled rulesets.
FSeccompCompat
// FDevel unblocks ptrace and friends.
FDevel
@@ -54,12 +58,15 @@ const (
// FTty unblocks dangerous terminal I/O (faking input).
FTty
// FMapRealUID maps the target user uid to the privileged user uid in the container user namespace.
// Some programs fail to connect to dbus session running as a different uid,
// this option works around it by mapping priv-side caller uid in container.
// FMapRealUID maps the target user uid to the privileged user uid in the
// container user namespace.
//
// Some programs fail to connect to dbus session running as a different uid,
// this option works around it by mapping priv-side caller uid in container.
FMapRealUID
// FDevice mount /dev/ from the init mount namespace as-is in the container mount namespace.
// FDevice mount /dev/ from the init mount namespace as is in the container
// mount namespace.
FDevice
// FShareRuntime shares XDG_RUNTIME_DIR between containers under the same identity.
@@ -112,30 +119,37 @@ func (flags Flags) String() string {
}
}
// ContainerConfig describes the container configuration to be applied to an underlying [container].
// ContainerConfig describes the container configuration to be applied to an
// underlying [container]. It is validated by [Config.Validate].
type ContainerConfig struct {
// Container UTS namespace hostname.
Hostname string `json:"hostname,omitempty"`
// Duration in nanoseconds to wait for after interrupting the initial process.
// Defaults to [WaitDelayDefault] if zero, or [WaitDelayMax] if greater than [WaitDelayMax].
// Values lesser than zero is equivalent to zero, bypassing [WaitDelayDefault].
//
// Defaults to [WaitDelayDefault] if zero, or [WaitDelayMax] if greater than
// [WaitDelayMax]. Values lesser than zero is equivalent to zero, bypassing
// [WaitDelayDefault].
WaitDelay time.Duration `json:"wait_delay,omitempty"`
// Initial process environment variables.
Env map[string]string `json:"env"`
/* Container mount points.
If the first element targets /, it is inserted early and excluded from path hiding. */
// Container mount points.
//
// If the first element targets /, it is inserted early and excluded from
// path hiding. Otherwise, an anonymous instance of tmpfs is set up on /.
Filesystem []FilesystemConfigJSON `json:"filesystem"`
// String used as the username of the emulated user, validated against the default NAME_REGEX from adduser.
// String used as the username of the emulated user, validated against the
// default NAME_REGEX from adduser.
//
// Defaults to passwd name of target uid or chronos.
Username string `json:"username,omitempty"`
// Pathname of shell in the container filesystem to use for the emulated user.
Shell *check.Absolute `json:"shell"`
// Directory in the container filesystem to enter and use as the home directory of the emulated user.
// Directory in the container filesystem to enter and use as the home
// directory of the emulated user.
Home *check.Absolute `json:"home"`
// Pathname to executable file in the container filesystem.
@@ -148,6 +162,7 @@ type ContainerConfig struct {
}
// ContainerConfigF is [ContainerConfig] stripped of its methods.
//
// The [ContainerConfig.Flags] field does not survive a [json] round trip.
type ContainerConfigF ContainerConfig

View File

@@ -5,8 +5,26 @@ import (
"strings"
)
// BadInterfaceError is returned when Interface fails an undocumented check in xdg-dbus-proxy,
// which would have cause a silent failure.
// BadInterfaceError is returned when Interface fails an undocumented check in
// xdg-dbus-proxy, which would have cause a silent failure.
//
// xdg-dbus-proxy fails without output when this condition is not met:
//
// char *dot = strrchr (filter->interface, '.');
// if (dot != NULL)
// {
// *dot = 0;
// if (strcmp (dot + 1, "*") != 0)
// filter->member = g_strdup (dot + 1);
// }
//
// trim ".*" since they are removed before searching for '.':
//
// if (g_str_has_suffix (name, ".*"))
// {
// name[strlen (name) - 2] = 0;
// wildcard = TRUE;
// }
type BadInterfaceError struct {
// Interface is the offending interface string.
Interface string
@@ -19,7 +37,8 @@ func (e *BadInterfaceError) Error() string {
if e == nil {
return "<nil>"
}
return "bad interface string " + strconv.Quote(e.Interface) + " in " + e.Segment + " bus configuration"
return "bad interface string " + strconv.Quote(e.Interface) +
" in " + e.Segment + " bus configuration"
}
// BusConfig configures the xdg-dbus-proxy process.
@@ -76,31 +95,14 @@ func (c *BusConfig) Interfaces(yield func(string) bool) {
}
}
// CheckInterfaces checks for invalid interface strings based on an undocumented check in xdg-dbus-error,
// returning [BadInterfaceError] if one is encountered.
// CheckInterfaces checks for invalid interface strings based on an undocumented
// check in xdg-dbus-error, returning [BadInterfaceError] if one is encountered.
func (c *BusConfig) CheckInterfaces(segment string) error {
if c == nil {
return nil
}
for iface := range c.Interfaces {
/*
xdg-dbus-proxy fails without output when this condition is not met:
char *dot = strrchr (filter->interface, '.');
if (dot != NULL)
{
*dot = 0;
if (strcmp (dot + 1, "*") != 0)
filter->member = g_strdup (dot + 1);
}
trim ".*" since they are removed before searching for '.':
if (g_str_has_suffix (name, ".*"))
{
name[strlen (name) - 2] = 0;
wildcard = TRUE;
}
*/
if strings.IndexByte(strings.TrimSuffix(iface, ".*"), '.') == -1 {
return &BadInterfaceError{iface, segment}
}

View File

@@ -11,15 +11,17 @@ import (
type Enablement byte
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
// EX11 adds the target user via X11 ChangeHosts and exposes the X11 pathname socket.
// EX11 adds the target user via X11 ChangeHosts and exposes the X11
// pathname socket.
EX11
// EDBus enables the per-container xdg-dbus-proxy daemon.
EDBus
// EPipeWire exposes a pipewire pathname socket via SecurityContext.
EPipeWire
// EPulse copies the PulseAudio cookie to [hst.PrivateTmp] and exposes the PulseAudio socket.
// EPulse copies the PulseAudio cookie to [hst.PrivateTmp] and exposes the
// PulseAudio socket.
EPulse
// EM is a noop.

View File

@@ -24,7 +24,8 @@ type FilesystemConfig interface {
fmt.Stringer
}
// The Ops interface enables [FilesystemConfig] to queue container ops without depending on the container package.
// The Ops interface enables [FilesystemConfig] to queue container ops without
// depending on the container package.
type Ops interface {
// Tmpfs appends an op that mounts tmpfs on a container path.
Tmpfs(target *check.Absolute, size int, perm os.FileMode) Ops
@@ -41,12 +42,15 @@ type Ops interface {
// Link appends an op that creates a symlink in the container filesystem.
Link(target *check.Absolute, linkName string, dereference bool) Ops
// Root appends an op that expands a directory into a toplevel bind mount mirror on container root.
// Root appends an op that expands a directory into a toplevel bind mount
// mirror on container root.
Root(host *check.Absolute, flags int) Ops
// Etc appends an op that expands host /etc into a toplevel symlink mirror with /etc semantics.
// Etc appends an op that expands host /etc into a toplevel symlink mirror
// with /etc semantics.
Etc(host *check.Absolute, prefix string) Ops
// Daemon appends an op that starts a daemon in the container and blocks until target appears.
// Daemon appends an op that starts a daemon in the container and blocks
// until target appears.
Daemon(target, path *check.Absolute, args ...string) Ops
}
@@ -61,7 +65,8 @@ type ApplyState struct {
// ErrFSNull is returned by [json] on encountering a null [FilesystemConfig] value.
var ErrFSNull = errors.New("unexpected null in mount point")
// FSTypeError is returned when [ContainerConfig.Filesystem] contains an entry with invalid type.
// FSTypeError is returned when [ContainerConfig.Filesystem] contains an entry
// with invalid type.
type FSTypeError string
func (f FSTypeError) Error() string { return fmt.Sprintf("invalid filesystem type %q", string(f)) }

View File

@@ -18,7 +18,9 @@ type FSLink struct {
Target *check.Absolute `json:"dst"`
// Arbitrary linkname value store in the symlink.
Linkname string `json:"linkname"`
// Whether to treat Linkname as an absolute pathname and dereference before creating the link.
// Whether to treat Linkname as an absolute pathname and dereference before
// creating the link.
Dereference bool `json:"dereference,omitempty"`
}

View File

@@ -19,9 +19,11 @@ type FSOverlay struct {
// Any filesystem, does not need to be on a writable filesystem, must not be nil.
Lower []*check.Absolute `json:"lower"`
// The upperdir is normally on a writable filesystem, leave as nil to mount Lower readonly.
// The upperdir is normally on a writable filesystem, leave as nil to mount
// Lower readonly.
Upper *check.Absolute `json:"upper,omitempty"`
// The workdir needs to be an empty directory on the same filesystem as Upper, must not be nil if Upper is populated.
// The workdir needs to be an empty directory on the same filesystem as
// Upper, must not be nil if Upper is populated.
Work *check.Absolute `json:"work,omitempty"`
}

View File

@@ -44,11 +44,13 @@ func (e *AppError) Message() string {
type Paths struct {
// Temporary directory returned by [os.TempDir], usually equivalent to [fhs.AbsTmp].
TempDir *check.Absolute `json:"temp_dir"`
// Shared directory specific to the hsu userid, usually (`/tmp/hakurei.%d`, [Info.User]).
// Shared directory specific to the hsu userid, usually
// (`/tmp/hakurei.%d`, [Info.User]).
SharePath *check.Absolute `json:"share_path"`
// Checked XDG_RUNTIME_DIR value, usually (`/run/user/%d`, uid).
RuntimePath *check.Absolute `json:"runtime_path"`
// Shared directory specific to the hsu userid located in RuntimePath, usually (`/run/user/%d/hakurei`, uid).
// Shared directory specific to the hsu userid located in RuntimePath,
// usually (`/run/user/%d/hakurei`, uid).
RunDirPath *check.Absolute `json:"run_dir_path"`
}
@@ -74,10 +76,23 @@ func Template() *Config {
SessionBus: &BusConfig{
See: nil,
Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver",
"org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"},
Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"},
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{"org.freedesktop.portal.*": "*"},
Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
Log: false,
@@ -112,7 +127,12 @@ func Template() *Config {
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
},
Filesystem: []FilesystemConfigJSON{
{&FSBind{Target: fhs.AbsRoot, Source: fhs.AbsVarLib.Append("hakurei/base/org.debian"), Write: true, Special: true}},
{&FSBind{
Target: fhs.AbsRoot,
Source: fhs.AbsVarLib.Append("hakurei/base/org.debian"),
Write: true,
Special: true,
}},
{&FSBind{Target: fhs.AbsEtc, Source: fhs.AbsEtc, Special: true}},
{&FSEphemeral{Target: fhs.AbsTmp, Write: true, Perm: 0755}},
{&FSOverlay{
@@ -121,11 +141,27 @@ func Template() *Config {
Upper: fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"),
Work: fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/work"),
}},
{&FSLink{Target: fhs.AbsRun.Append("current-system"), Linkname: "/run/current-system", Dereference: true}},
{&FSLink{Target: fhs.AbsRun.Append("opengl-driver"), Linkname: "/run/opengl-driver", Dereference: true}},
{&FSBind{Source: fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"),
Target: check.MustAbs("/data/data/org.chromium.Chromium"), Write: true, Ensure: true}},
{&FSBind{Source: fhs.AbsDev.Append("dri"), Device: true, Optional: true}},
{&FSLink{
Target: fhs.AbsRun.Append("current-system"),
Linkname: "/run/current-system",
Dereference: true,
}},
{&FSLink{
Target: fhs.AbsRun.Append("opengl-driver"),
Linkname: "/run/opengl-driver",
Dereference: true,
}},
{&FSBind{
Source: fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"),
Target: check.MustAbs("/data/data/org.chromium.Chromium"),
Write: true,
Ensure: true,
}},
{&FSBind{
Source: fhs.AbsDev.Append("dri"),
Device: true,
Optional: true,
}},
},
Username: "chronos",

View File

@@ -12,10 +12,12 @@ import (
// An ID is a unique identifier held by a running hakurei container.
type ID [16]byte
// ErrIdentifierLength is returned when encountering a [hex] representation of [ID] with unexpected length.
// ErrIdentifierLength is returned when encountering a [hex] representation of
// [ID] with unexpected length.
var ErrIdentifierLength = errors.New("identifier string has unexpected length")
// IdentifierDecodeError is returned by [ID.UnmarshalText] to provide relevant error descriptions.
// IdentifierDecodeError is returned by [ID.UnmarshalText] to provide relevant
// error descriptions.
type IdentifierDecodeError struct{ Err error }
func (e IdentifierDecodeError) Unwrap() error { return e.Err }
@@ -23,7 +25,10 @@ func (e IdentifierDecodeError) Error() string {
var invalidByteError hex.InvalidByteError
switch {
case errors.As(e.Err, &invalidByteError):
return fmt.Sprintf("got invalid byte %#U in identifier", rune(invalidByteError))
return fmt.Sprintf(
"got invalid byte %#U in identifier",
rune(invalidByteError),
)
case errors.Is(e.Err, hex.ErrLength):
return "odd length identifier hex string"
@@ -41,7 +46,9 @@ func (a *ID) CreationTime() time.Time {
}
// NewInstanceID creates a new unique [ID].
func NewInstanceID(id *ID) error { return newInstanceID(id, uint64(time.Now().UnixNano())) }
func NewInstanceID(id *ID) error {
return newInstanceID(id, uint64(time.Now().UnixNano()))
}
// newInstanceID creates a new unique [ID] with the specified timestamp.
func newInstanceID(id *ID, p uint64) error {

View File

@@ -38,6 +38,7 @@ func (h *Hsu) ensureDispatcher() {
}
// ID returns the current user hsurc identifier.
//
// [ErrHsuAccess] is returned if the current user is not in hsurc.
func (h *Hsu) ID() (int, error) {
h.ensureDispatcher()

View File

@@ -1,4 +1,5 @@
// Package outcome implements the outcome of the privileged and container sides of a hakurei container.
// Package outcome implements the outcome of the privileged and container sides
// of a hakurei container.
package outcome
import (
@@ -27,8 +28,9 @@ func Info() *hst.Info {
return &hi
}
// envAllocSize is the initial size of the env map pre-allocated when the configured env map is nil.
// It should be large enough to fit all insertions by outcomeOp.toContainer.
// envAllocSize is the initial size of the env map pre-allocated when the
// configured env map is nil. It should be large enough to fit all insertions by
// outcomeOp.toContainer.
const envAllocSize = 1 << 6
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
@@ -43,7 +45,8 @@ func (s *stringPair[T]) unwrap() T { return s.v }
func (s *stringPair[T]) String() string { return s.s }
// outcomeState is copied to the shim process and available while applying outcomeOp.
// This is transmitted from the priv side to the shim, so exported fields should be kept to a minimum.
// This is transmitted from the priv side to the shim, so exported fields should
// be kept to a minimum.
type outcomeState struct {
// Params only used by the shim process. Populated by populateEarly.
Shim *shimParams
@@ -89,14 +92,25 @@ func (s *outcomeState) valid() bool {
s.Paths != nil
}
// newOutcomeState returns the address of a new outcomeState with its exported fields populated via syscallDispatcher.
// newOutcomeState returns the address of a new outcomeState with its exported
// fields populated via syscallDispatcher.
func newOutcomeState(k syscallDispatcher, msg message.Msg, id *hst.ID, config *hst.Config, hsu *Hsu) *outcomeState {
s := outcomeState{
Shim: &shimParams{PrivPID: k.getpid(), Verbose: msg.IsVerbose()},
ID: id,
Identity: config.Identity,
UserID: hsu.MustID(msg),
Paths: env.CopyPathsFunc(k.fatalf, k.tempdir, func(key string) string { v, _ := k.lookupEnv(key); return v }),
Shim: &shimParams{
PrivPID: k.getpid(),
Verbose: msg.IsVerbose(),
SchedPolicy: config.SchedPolicy,
SchedPriority: config.SchedPriority,
},
ID: id,
Identity: config.Identity,
UserID: hsu.MustID(msg),
Paths: env.CopyPathsFunc(k.fatalf, k.tempdir, func(key string) string {
v, _ := k.lookupEnv(key)
return v
}),
Container: config.Container,
}
@@ -121,6 +135,7 @@ func newOutcomeState(k syscallDispatcher, msg message.Msg, id *hst.ID, config *h
}
// populateLocal populates unexported fields from transmitted exported fields.
//
// These fields are cheaper to recompute per-process.
func (s *outcomeState) populateLocal(k syscallDispatcher, msg message.Msg) error {
if !s.valid() || k == nil || msg == nil {
@@ -136,7 +151,10 @@ func (s *outcomeState) populateLocal(k syscallDispatcher, msg message.Msg) error
s.id = &stringPair[hst.ID]{*s.ID, s.ID.String()}
s.Copy(&s.sc, s.UserID)
msg.Verbosef("process share directory at %q, runtime directory at %q", s.sc.SharePath, s.sc.RunDirPath)
msg.Verbosef(
"process share directory at %q, runtime directory at %q",
s.sc.SharePath, s.sc.RunDirPath,
)
s.identity = newInt(s.Identity)
s.mapuid, s.mapgid = newInt(s.Mapuid), newInt(s.Mapgid)
@@ -146,17 +164,25 @@ func (s *outcomeState) populateLocal(k syscallDispatcher, msg message.Msg) error
}
// instancePath returns a path formatted for outcomeStateSys.instance.
//
// This method must only be called from outcomeOp.toContainer if
// outcomeOp.toSystem has already called outcomeStateSys.instance.
func (s *outcomeState) instancePath() *check.Absolute { return s.sc.SharePath.Append(s.id.String()) }
func (s *outcomeState) instancePath() *check.Absolute {
return s.sc.SharePath.Append(s.id.String())
}
// runtimePath returns a path formatted for outcomeStateSys.runtime.
//
// This method must only be called from outcomeOp.toContainer if
// outcomeOp.toSystem has already called outcomeStateSys.runtime.
func (s *outcomeState) runtimePath() *check.Absolute { return s.sc.RunDirPath.Append(s.id.String()) }
func (s *outcomeState) runtimePath() *check.Absolute {
return s.sc.RunDirPath.Append(s.id.String())
}
// outcomeStateSys wraps outcomeState and [system.I]. Used on the priv side only.
// Implementations of outcomeOp must not access fields other than sys unless explicitly stated.
//
// Implementations of outcomeOp must not access fields other than sys unless
// explicitly stated.
type outcomeStateSys struct {
// Whether XDG_RUNTIME_DIR is used post hsu.
useRuntimeDir bool
@@ -219,6 +245,7 @@ func (state *outcomeStateSys) ensureRuntimeDir() {
}
// instance returns the pathname to a process-specific directory within TMPDIR.
//
// This directory must only hold entries bound to [system.Process].
func (state *outcomeStateSys) instance() *check.Absolute {
if state.sharePath != nil {
@@ -230,6 +257,7 @@ func (state *outcomeStateSys) instance() *check.Absolute {
}
// runtime returns the pathname to a process-specific directory within XDG_RUNTIME_DIR.
//
// This directory must only hold entries bound to [system.Process].
func (state *outcomeStateSys) runtime() *check.Absolute {
if state.runtimeSharePath != nil {
@@ -242,22 +270,29 @@ func (state *outcomeStateSys) runtime() *check.Absolute {
return state.runtimeSharePath
}
// outcomeStateParams wraps outcomeState and [container.Params]. Used on the shim side only.
// outcomeStateParams wraps outcomeState and [container.Params].
//
// Used on the shim side only.
type outcomeStateParams struct {
// Overrides the embedded [container.Params] in [container.Container]. The Env field must not be used.
// Overrides the embedded [container.Params] in [container.Container].
//
// The Env field must not be used.
params *container.Params
// Collapsed into the Env slice in [container.Params] by the final outcomeOp.
env map[string]string
// Filesystems with the optional root sliced off if present. Populated by spParamsOp.
// Safe for use by spFilesystemOp.
// Filesystems with the optional root sliced off if present.
//
// Populated by spParamsOp. Safe for use by spFilesystemOp.
filesystem []hst.FilesystemConfigJSON
// Inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` via mapped uid.
//
// Populated by spRuntimeOp.
runtimeDir *check.Absolute
// Path to pipewire-pulse server.
//
// Populated by spPipeWireOp if DirectPipeWire is false.
pipewirePulsePath *check.Absolute
@@ -265,25 +300,32 @@ type outcomeStateParams struct {
*outcomeState
}
// errNotEnabled is returned by outcomeOp.toSystem and used internally to exclude an outcomeOp from transmission.
// errNotEnabled is returned by outcomeOp.toSystem and used internally to
// exclude an outcomeOp from transmission.
var errNotEnabled = errors.New("op not enabled in the configuration")
// An outcomeOp inflicts an outcome on [system.I] and contains enough information to
// inflict it on [container.Params] in a separate process.
// An implementation of outcomeOp must store cross-process states in exported fields only.
// An outcomeOp inflicts an outcome on [system.I] and contains enough
// information to inflict it on [container.Params] in a separate process.
//
// An implementation of outcomeOp must store cross-process states in exported
// fields only.
type outcomeOp interface {
// toSystem inflicts the current outcome on [system.I] in the priv side process.
toSystem(state *outcomeStateSys) error
// toContainer inflicts the current outcome on [container.Params] in the shim process.
// The implementation must not write to the Env field of [container.Params] as it will be overwritten
// by flattened env map.
// toContainer inflicts the current outcome on [container.Params] in the
// shim process.
//
// Implementations must not write to the Env field of [container.Params]
// as it will be overwritten by flattened env map.
toContainer(state *outcomeStateParams) error
}
// toSystem calls the outcomeOp.toSystem method on all outcomeOp implementations and populates shimParams.Ops.
// This function assumes the caller has already called the Validate method on [hst.Config]
// and checked that it returns nil.
// toSystem calls the outcomeOp.toSystem method on all outcomeOp implementations
// and populates shimParams.Ops.
//
// This function assumes the caller has already called the Validate method on
// [hst.Config] and checked that it returns nil.
func (state *outcomeStateSys) toSystem() error {
if state.Shim == nil || state.Shim.Ops != nil {
return newWithMessage("invalid ops state reached")

View File

@@ -30,7 +30,9 @@ const (
)
// NewStore returns the address of a new instance of [store.Store].
func NewStore(sc *hst.Paths) *store.Store { return store.New(sc.SharePath.Append("state")) }
func NewStore(sc *hst.Paths) *store.Store {
return store.New(sc.SharePath.Append("state"))
}
// main carries out outcome and terminates. main does not return.
func (k *outcome) main(msg message.Msg, identifierFd int) {
@@ -116,7 +118,11 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
processStatePrev, processStateCur = processStateCur, processState
if !processTime.IsZero() && processStatePrev != processLifecycle {
msg.Verbosef("state %d took %.2f ms", processStatePrev, float64(time.Since(processTime).Nanoseconds())/1e6)
msg.Verbosef(
"state %d took %.2f ms",
processStatePrev,
float64(time.Since(processTime).Nanoseconds())/1e6,
)
}
processTime = time.Now()
@@ -141,7 +147,10 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
case processCommit:
if isBeforeRevert {
perrorFatal(newWithMessage("invalid transition to commit state"), "commit", processLifecycle)
perrorFatal(
newWithMessage("invalid transition to commit state"),
"commit", processLifecycle,
)
continue
}
@@ -238,15 +247,26 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
case <-func() chan struct{} {
w := make(chan struct{})
// this ties processLifecycle to ctx with the additional compensated timeout duration
// to allow transition to the next state on a locked up shim
go func() { <-ctx.Done(); time.Sleep(k.state.Shim.WaitDelay + shimWaitTimeout); close(w) }()
// This ties processLifecycle to ctx with the additional
// compensated timeout duration to allow transition to the next
// state on a locked up shim.
go func() {
<-ctx.Done()
time.Sleep(k.state.Shim.WaitDelay + shimWaitTimeout)
close(w)
}()
return w
}():
// this is only reachable when wait did not return within shimWaitTimeout, after its WaitDelay has elapsed.
// This is different from the container failing to terminate within its timeout period, as that is enforced
// by the shim. This path is instead reached when there is a lockup in shim preventing it from completing.
msg.GetLogger().Printf("process %d did not terminate", shimCmd.Process.Pid)
// This is only reachable when wait did not return within
// shimWaitTimeout, after its WaitDelay has elapsed. This is
// different from the container failing to terminate within its
// timeout period, as that is enforced by the shim. This path is
// instead reached when there is a lockup in shim preventing it
// from completing.
msg.GetLogger().Printf(
"process %d did not terminate",
shimCmd.Process.Pid,
)
}
msg.Resume()
@@ -271,8 +291,8 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
ec := system.Process
if entries, _, err := handle.Entries(); err != nil {
// it is impossible to continue from this point,
// per-process state will be reverted to limit damage
// it is impossible to continue from this point, per-process
// state will be reverted to limit damage
perror(err, "read store segment entries")
} else {
// accumulate enablements of remaining instances
@@ -295,7 +315,10 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
if n == 0 {
ec |= system.User
} else {
msg.Verbosef("found %d instances, cleaning up without user-scoped operations", n)
msg.Verbosef(
"found %d instances, cleaning up without user-scoped operations",
n,
)
}
ec |= rt ^ (hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse)
if msg.IsVerbose() {
@@ -335,7 +358,9 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
// start starts the shim via cmd/hsu.
//
// If successful, a [time.Time] value for [hst.State] is stored in the value pointed to by startTime.
// If successful, a [time.Time] value for [hst.State] is stored in the value
// pointed to by startTime.
//
// The resulting [exec.Cmd] and write end of the shim setup pipe is returned.
func (k *outcome) start(ctx context.Context, msg message.Msg,
hsuPath *check.Absolute,

View File

@@ -37,9 +37,12 @@ const (
shimMsgBadPID = C.HAKUREI_SHIM_BAD_PID
)
// setupContSignal sets up the SIGCONT signal handler for the cross-uid shim exit hack.
// The signal handler is implemented in C, signals can be processed by reading from the returned reader.
// The returned function must be called after all signal processing concludes.
// setupContSignal sets up the SIGCONT signal handler for the cross-uid shim
// exit hack.
//
// The signal handler is implemented in C, signals can be processed by reading
// from the returned reader. The returned function must be called after all
// signal processing concludes.
func setupContSignal(pid int) (io.ReadCloser, func(), error) {
if r, w, err := os.Pipe(); err != nil {
return nil, nil, err
@@ -51,22 +54,30 @@ func setupContSignal(pid int) (io.ReadCloser, func(), error) {
}
}
// shimEnv is the name of the environment variable storing decimal representation of
// setup pipe fd for [container.Receive].
// shimEnv is the name of the environment variable storing decimal representation
// of setup pipe fd for [container.Receive].
const shimEnv = "HAKUREI_SHIM"
// shimParams is embedded in outcomeState and transmitted from priv side to shim.
type shimParams struct {
// Priv side pid, checked against ppid in signal handler for the syscall.SIGCONT hack.
// Priv side pid, checked against ppid in signal handler for the
// syscall.SIGCONT hack.
PrivPID int
// Duration to wait for after the initial process receives os.Interrupt before the container is killed.
// Duration to wait for after the initial process receives os.Interrupt
// before the container is killed.
//
// Limits are enforced on the priv side.
WaitDelay time.Duration
// Verbosity pass through from [message.Msg].
Verbose bool
// Copied from [hst.Config].
SchedPolicy std.SchedPolicy
// Copied from [hst.Config].
SchedPriority std.Int
// Outcome setup ops, contains setup state. Populated by outcome.finalise.
Ops []outcomeOp
}
@@ -77,7 +88,9 @@ func (p *shimParams) valid() bool { return p != nil && p.PrivPID > 0 }
// shimName is the prefix used by log.std in the shim process.
const shimName = "shim"
// Shim is called by the main function of the shim process and runs as the unconstrained target user.
// Shim is called by the main function of the shim process and runs as the
// unconstrained target user.
//
// Shim does not return.
func Shim(msg message.Msg) {
if msg == nil {
@@ -131,7 +144,8 @@ func (sp *shimPrivate) destroy() {
}
const (
// shimPipeWireTimeout is the duration pipewire-pulse is allowed to run before its socket becomes available.
// shimPipeWireTimeout is the duration pipewire-pulse is allowed to run
// before its socket becomes available.
shimPipeWireTimeout = 5 * time.Second
)
@@ -262,6 +276,9 @@ func shimEntrypoint(k syscallDispatcher) {
cancelContainer.Store(&stop)
sp := shimPrivate{k: k, id: state.id}
z := container.New(ctx, msg)
z.SetScheduler = state.Shim.SchedPolicy > 0
z.SchedPolicy = state.Shim.SchedPolicy
z.SchedPriority = state.Shim.SchedPriority
z.Params = *stateParams.params
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr

View File

@@ -27,7 +27,9 @@ const varRunNscd = fhs.Var + "run/nscd"
func init() { gob.Register(new(spParamsOp)) }
// spParamsOp initialises unordered fields of [container.Params] and the optional root filesystem.
// spParamsOp initialises unordered fields of [container.Params] and the
// optional root filesystem.
//
// This outcomeOp is hardcoded to always run first.
type spParamsOp struct {
// Value of $TERM, stored during toSystem.
@@ -67,8 +69,8 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
state.params.Args = state.Container.Args
}
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
// this behaviour is implemented in the shim
// The container is cancelled when shim is requested to exit or receives an
// interrupt or termination signal. This behaviour is implemented in the shim.
state.params.ForwardCancel = state.Shim.WaitDelay > 0
if state.Container.Flags&hst.FMultiarch != 0 {
@@ -115,7 +117,8 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
} else {
state.params.Bind(fhs.AbsDev, fhs.AbsDev, std.BindWritable|std.BindDevice)
}
// /dev is mounted readonly later on, this prevents /dev/shm from going readonly with it
// /dev is mounted readonly later on, this prevents /dev/shm from going
// readonly with it
state.params.Tmpfs(fhs.AbsDevShm, 0, 01777)
return nil
@@ -123,7 +126,9 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
func init() { gob.Register(new(spFilesystemOp)) }
// spFilesystemOp applies configured filesystems to [container.Params], excluding the optional root filesystem.
// spFilesystemOp applies configured filesystems to [container.Params],
// excluding the optional root filesystem.
//
// This outcomeOp is hardcoded to always run last.
type spFilesystemOp struct {
// Matched paths to cover. Stored during toSystem.
@@ -297,8 +302,8 @@ func (s *spFilesystemOp) toContainer(state *outcomeStateParams) error {
return nil
}
// resolveRoot handles the root filesystem special case for [hst.FilesystemConfig] and additionally resolves autoroot
// as it requires special handling during path hiding.
// resolveRoot handles the root filesystem special case for [hst.FilesystemConfig]
// and additionally resolves autoroot as it requires special handling during path hiding.
func resolveRoot(c *hst.ContainerConfig) (rootfs hst.FilesystemConfig, filesystem []hst.FilesystemConfigJSON, autoroot *hst.FSBind) {
// root filesystem special case
filesystem = c.Filesystem
@@ -316,7 +321,8 @@ func resolveRoot(c *hst.ContainerConfig) (rootfs hst.FilesystemConfig, filesyste
return
}
// evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist].
// evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors
// unwrapping to [fs.ErrNotExist].
func evalSymlinks(msg message.Msg, k syscallDispatcher, v *string) error {
if p, err := k.evalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) {

View File

@@ -12,6 +12,7 @@ import (
func init() { gob.Register(new(spDBusOp)) }
// spDBusOp maintains an xdg-dbus-proxy instance for the container.
//
// Runs after spRuntimeOp.
type spDBusOp struct {
// Whether to bind the system bus socket. Populated during toSystem.

View File

@@ -13,9 +13,12 @@ const pipewirePulseName = "pipewire-pulse"
func init() { gob.Register(new(spPipeWireOp)) }
// spPipeWireOp exports the PipeWire server to the container via SecurityContext.
//
// Runs after spRuntimeOp.
type spPipeWireOp struct {
// Path to pipewire-pulse server. Populated during toSystem if DirectPipeWire is false.
// Path to pipewire-pulse server.
//
// Populated during toSystem if DirectPipeWire is false.
CompatServerPath *check.Absolute
}

View File

@@ -20,6 +20,7 @@ const pulseCookieSizeMax = 1 << 8
func init() { gob.Register(new(spPulseOp)) }
// spPulseOp exports the PulseAudio server to the container.
//
// Runs after spRuntimeOp.
type spPulseOp struct {
// PulseAudio cookie data, populated during toSystem if a cookie is present.
@@ -37,24 +38,40 @@ func (s *spPulseOp) toSystem(state *outcomeStateSys) error {
if _, err := state.k.stat(pulseRuntimeDir.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err}
return &hst.AppError{Step: fmt.Sprintf(
"access PulseAudio directory %q",
pulseRuntimeDir,
), Err: err}
}
return newWithMessageError(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir), err)
return newWithMessageError(fmt.Sprintf(
"PulseAudio directory %q not found",
pulseRuntimeDir,
), err)
}
if fi, err := state.k.stat(pulseSocket.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err}
return &hst.AppError{Step: fmt.Sprintf(
"access PulseAudio socket %q",
pulseSocket,
), Err: err}
}
return newWithMessageError(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir), err)
return newWithMessageError(fmt.Sprintf(
"PulseAudio directory %q found but socket does not exist",
pulseRuntimeDir,
), err)
} else {
if m := fi.Mode(); m&0o006 != 0o006 {
return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m))
return newWithMessage(fmt.Sprintf(
"unexpected permissions on %q: %s",
pulseSocket, m,
))
}
}
// pulse socket is world writable and its parent directory DAC permissions prevents access;
// hard link to target-executable share directory to grant access
// PulseAudio socket is world writable and its parent directory DAC
// permissions prevents access. Hard link to target-executable share
// directory to grant access
state.sys.Link(pulseSocket, state.runtime().Append("pulse"))
// load up to pulseCookieSizeMax bytes of pulse cookie for transmission to shim
@@ -62,7 +79,13 @@ func (s *spPulseOp) toSystem(state *outcomeStateSys) error {
return err
} else if a != nil {
s.Cookie = new([pulseCookieSizeMax]byte)
if s.CookieSize, err = loadFile(state.msg, state.k, "PulseAudio cookie", a.String(), s.Cookie[:]); err != nil {
if s.CookieSize, err = loadFile(
state.msg,
state.k,
"PulseAudio cookie",
a.String(),
s.Cookie[:],
); err != nil {
return err
}
} else {
@@ -101,8 +124,9 @@ func (s *spPulseOp) commonPaths(state *outcomeState) (pulseRuntimeDir, pulseSock
return
}
// discoverPulseCookie attempts to discover the pathname of the PulseAudio cookie of the current user.
// If both returned pathname and error are nil, the cookie is likely unavailable and can be silently skipped.
// discoverPulseCookie attempts to discover the pathname of the PulseAudio
// cookie of the current user. If both returned pathname and error are nil, the
// cookie is likely unavailable and can be silently skipped.
func discoverPulseCookie(k syscallDispatcher) (*check.Absolute, error) {
const paLocateStep = "locate PulseAudio cookie"
@@ -186,7 +210,10 @@ func loadFile(
&os.PathError{Op: "stat", Path: pathname, Err: syscall.ENOMEM},
)
} else if s < int64(n) {
msg.Verbosef("%s at %q is %d bytes shorter than expected", description, pathname, int64(n)-s)
msg.Verbosef(
"%s at %q is %d bytes shorter than expected",
description, pathname, int64(n)-s,
)
} else {
msg.Verbosef("loading %d bytes from %q", n, pathname)
}

View File

@@ -67,7 +67,9 @@ const (
// spRuntimeOp sets up XDG_RUNTIME_DIR inside the container.
type spRuntimeOp struct {
// SessionType determines the value of envXDGSessionType. Populated during toSystem.
// SessionType determines the value of envXDGSessionType.
//
// Populated during toSystem.
SessionType uintptr
}

View File

@@ -12,9 +12,12 @@ import (
func init() { gob.Register(new(spWaylandOp)) }
// spWaylandOp exports the Wayland display server to the container.
//
// Runs after spRuntimeOp.
type spWaylandOp struct {
// Path to host wayland socket. Populated during toSystem if DirectWayland is true.
// Path to host wayland socket.
//
// Populated during toSystem if DirectWayland is true.
SocketPath *check.Absolute
}

View File

@@ -50,7 +50,10 @@ func (s *spX11Op) toSystem(state *outcomeStateSys) error {
if socketPath != nil {
if _, err := state.k.stat(socketPath.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err}
return &hst.AppError{Step: fmt.Sprintf(
"access X11 socket %q",
socketPath,
), Err: err}
}
} else {
state.sys.UpdatePermType(hst.EX11, socketPath, acl.Read, acl.Write, acl.Execute)

View File

@@ -39,8 +39,8 @@ type ExecPath struct {
W bool
}
// SchedPolicy is the [container] scheduling policy.
var SchedPolicy int
// SetSchedIdle is whether to set [std.SCHED_IDLE] scheduling priority.
var SetSchedIdle bool
// PromoteLayers returns artifacts with identical-by-content layers promoted to
// the highest priority instance, as if mounted via [ExecPath].
@@ -413,7 +413,8 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
z.ParentPerm = 0700
z.HostNet = hostNet
z.Hostname = "cure"
z.SchedPolicy = SchedPolicy
z.SetScheduler = SetSchedIdle
z.SchedPolicy = std.SCHED_IDLE
if z.HostNet {
z.Hostname = "cure-net"
}

View File

@@ -19,6 +19,8 @@ const (
LLVMRuntimes
LLVMClang
// EarlyInit is the Rosa OS initramfs init program.
EarlyInit
// ImageInitramfs is the Rosa OS initramfs archive.
ImageInitramfs
@@ -28,6 +30,8 @@ const (
KernelHeaders
// KernelSource is a writable kernel source tree installed to [AbsUsrSrc].
KernelSource
// Firmware is firmware blobs for use with the Linux kernel.
Firmware
ACL
ArgpStandalone
@@ -85,9 +89,11 @@ const (
NSS
NSSCACert
Ncurses
Nettle
Ninja
OpenSSL
PCRE2
Parallel
Patch
Perl
PerlLocaleGettext
@@ -103,12 +109,23 @@ const (
PkgConfig
Procps
Python
PythonCfgv
PythonDiscovery
PythonDistlib
PythonFilelock
PythonIdentify
PythonIniConfig
PythonNodeenv
PythonPackaging
PythonPlatformdirs
PythonPluggy
PythonPreCommit
PythonPyTest
PythonPyYAML
PythonPygments
PythonVirtualenv
QEMU
Rdfind
Rsync
Sed
Setuptools

View File

@@ -128,6 +128,9 @@ type CMakeHelper struct {
Cache [][2]string
// Runs after install.
Script string
// Whether to generate Makefile instead.
Make bool
}
var _ Helper = new(CMakeHelper)
@@ -141,7 +144,10 @@ func (attr *CMakeHelper) name(name, version string) string {
}
// extra returns a hardcoded slice of [CMake] and [Ninja].
func (*CMakeHelper) extra(int) []PArtifact {
func (attr *CMakeHelper) extra(int) []PArtifact {
if attr != nil && attr.Make {
return []PArtifact{CMake, Make}
}
return []PArtifact{CMake, Ninja}
}
@@ -173,11 +179,19 @@ func (attr *CMakeHelper) script(name string) string {
panic("CACHE must be non-empty")
}
generate := "Ninja"
jobs := ""
if attr.Make {
generate = "'Unix Makefiles'"
jobs += ` "--parallel=$(nproc)"`
}
return `
cmake -G Ninja \
cmake -G ` + generate + ` \
-DCMAKE_C_COMPILER_TARGET="${ROSA_TRIPLE}" \
-DCMAKE_CXX_COMPILER_TARGET="${ROSA_TRIPLE}" \
-DCMAKE_ASM_COMPILER_TARGET="${ROSA_TRIPLE}" \
-DCMAKE_INSTALL_LIBDIR=lib \
` + strings.Join(slices.Collect(func(yield func(string) bool) {
for _, v := range attr.Cache {
if !yield("-D" + v[0] + "=" + v[1]) {
@@ -185,9 +199,9 @@ cmake -G Ninja \
}
}
}), " \\\n\t") + ` \
-DCMAKE_INSTALL_PREFIX=/work/system \
-DCMAKE_INSTALL_PREFIX=/system \
'/usr/src/` + name + `/` + path.Join(attr.Append...) + `'
cmake --build .
cmake --install .
cmake --build .` + jobs + `
cmake --install . --prefix=/work/system
` + attr.Script
}

View File

@@ -678,8 +678,8 @@ func init() {
func (t Toolchain) newLibiconv() (pkg.Artifact, string) {
const (
version = "1.18"
checksum = "iV5q3VxP5VPdJ-X7O5OQI4fGm8VjeYb5viLd1L3eAHg26bbHb2_Qn63XPF3ucVZr"
version = "1.19"
checksum = "UibB6E23y4MksNqYmCCrA3zTFO6vJugD1DEDqqWYFZNuBsUWMVMcncb_5pPAr88x"
)
return t.NewPackage("libiconv", version, pkg.NewHTTPGetTar(
nil, "https://ftpmirror.gnu.org/gnu/libiconv/libiconv-"+version+".tar.gz",
@@ -741,6 +741,31 @@ func init() {
}
}
func (t Toolchain) newParallel() (pkg.Artifact, string) {
const (
version = "20260222"
checksum = "4wxjMi3G2zMxr9hvLcIn6D7_12A3e5UNObeTPhzn7mDAYwsZApmmkxfGPyllQQ7E"
)
return t.NewPackage("parallel", version, pkg.NewHTTPGetTar(
nil, "https://ftpmirror.gnu.org/gnu/parallel/parallel-"+version+".tar.bz2",
mustDecode(checksum),
pkg.TarBzip2,
), nil, (*MakeHelper)(nil),
Perl,
), version
}
func init() {
artifactsM[Parallel] = Metadata{
f: Toolchain.newParallel,
Name: "parallel",
Description: "a shell tool for executing jobs in parallel using one or more computers",
Website: "https://www.gnu.org/software/parallel/",
ID: 5448,
}
}
func (t Toolchain) newBinutils() (pkg.Artifact, string) {
const (
version = "2.46.0"

View File

@@ -2,7 +2,19 @@ package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newHakurei(suffix, script string) pkg.Artifact {
func (t Toolchain) newHakurei(
suffix, script string,
withHostname bool,
) pkg.Artifact {
hostname := `
echo '# Building test helper (hostname).'
go build -v -o /bin/hostname /usr/src/hostname/main.go
echo
`
if !withHostname {
hostname = ""
}
return t.New("hakurei"+suffix+"-"+hakureiVersion, 0, []pkg.Artifact{
t.Load(Go),
@@ -29,17 +41,12 @@ func (t Toolchain) newHakurei(suffix, script string) pkg.Artifact {
"CGO_ENABLED=1",
"GOCACHE=/tmp/gocache",
"CC=clang -O3 -Werror",
}, `
echo '# Building test helper (hostname).'
go build -v -o /bin/hostname /usr/src/hostname/main.go
echo
chmod -R +w /usr/src/hakurei
}, hostname+`
cd /usr/src/hakurei
HAKUREI_VERSION='v`+hakureiVersion+`'
`+script, pkg.Path(AbsUsrSrc.Append("hakurei"), true, t.NewPatchedSource(
"hakurei", hakureiVersion, hakureiSource, true, hakureiPatches...,
"hakurei", hakureiVersion, hakureiSource, false, hakureiPatches...,
)), pkg.Path(AbsUsrSrc.Append("hostname", "main.go"), false, pkg.NewFile(
"hostname.go",
[]byte(`
@@ -69,10 +76,11 @@ go build -trimpath -v -o /work/system/libexec/hakurei -ldflags="-s -w
-buildid=
-linkmode external
-extldflags=-static
-X hakurei.app/internal/info.buildVersion="$HAKUREI_VERSION"
-X hakurei.app/internal/info.buildVersion=${HAKUREI_VERSION}
-X hakurei.app/internal/info.hakureiPath=/system/bin/hakurei
-X hakurei.app/internal/info.hsuPath=/system/bin/hsu
-X main.hakureiPath=/system/bin/hakurei" ./...
-X main.hakureiPath=/system/bin/hakurei
" ./...
echo
echo '# Testing hakurei.'
@@ -84,7 +92,7 @@ mkdir -p /work/system/bin/
hakurei \
sharefs \
../../bin/)
`), hakureiVersion
`, true), hakureiVersion
},
Name: "hakurei",
@@ -98,7 +106,7 @@ mkdir -p /work/system/bin/
return t.newHakurei("-dist", `
export HAKUREI_VERSION
DESTDIR=/work /usr/src/hakurei/dist/release.sh
`), hakureiVersion
`, true), hakureiVersion
},
Name: "hakurei-dist",

View File

@@ -4,48 +4,15 @@ package rosa
import "hakurei.app/internal/pkg"
const hakureiVersion = "0.3.5"
const hakureiVersion = "0.3.6"
// hakureiSource is the source code of a hakurei release.
var hakureiSource = pkg.NewHTTPGetTar(
nil, "https://git.gensokyo.uk/security/hakurei/archive/"+
"v"+hakureiVersion+".tar.gz",
mustDecode("6Tn38NLezRD2d3aGdFg5qFfqn8_KvC6HwMKwJMPvaHmVw8xRgxn8B0PObswl2mOk"),
mustDecode("Yul9J2yV0x453lQP9KUnG_wEJo_DbKMNM7xHJGt4rITCSeX9VRK2J4kzAxcv_0-b"),
pkg.TarGzip,
)
// hakureiPatches are patches applied against a hakurei release.
var hakureiPatches = [][2]string{
{"createTemp-error-injection", `diff --git a/container/dispatcher_test.go b/container/dispatcher_test.go
index 5de37fc..fe0c4db 100644
--- a/container/dispatcher_test.go
+++ b/container/dispatcher_test.go
@@ -238,8 +238,11 @@ func sliceAddr[S any](s []S) *[]S { return &s }
func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile {
f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr}
- // check happens in Close, and cleanup is not guaranteed to run, so relying on it for sloppy implementations will cause sporadic test results
- f.cleanup = runtime.AddCleanup(f, func(name string) { f.t.Fatalf("checkedOsFile %s became unreachable without a call to Close", name) }, f.name)
+ // check happens in Close, and cleanup is not guaranteed to run, so relying
+ // on it for sloppy implementations will cause sporadic test results
+ f.cleanup = runtime.AddCleanup(f, func(name string) {
+ panic("checkedOsFile " + name + " became unreachable without a call to Close")
+ }, name)
return f
}
diff --git a/container/initplace_test.go b/container/initplace_test.go
index afeddbe..1c2f20b 100644
--- a/container/initplace_test.go
+++ b/container/initplace_test.go
@@ -21,7 +21,7 @@ func TestTmpfileOp(t *testing.T) {
Path: samplePath,
Data: sampleData,
}, nil, nil, []stub.Call{
- call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), stub.UniqueError(5)),
+ call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, (*checkedOsFile)(nil), stub.UniqueError(5)),
}, stub.UniqueError(5)},
{"Write", &Params{ParentPerm: 0700}, &TmpfileOp{
`},
}
var hakureiPatches [][2]string

View File

@@ -2,10 +2,32 @@ package rosa
import "hakurei.app/internal/pkg"
func init() {
artifactsM[EarlyInit] = Metadata{
Name: "earlyinit",
Description: "Rosa OS initramfs init program",
f: func(t Toolchain) (pkg.Artifact, string) {
return t.newHakurei("-early-init", `
mkdir -p /work/system/libexec/hakurei/
echo '# Building earlyinit.'
go build -trimpath -v -o /work/system/libexec/hakurei -ldflags="-s -w
-buildid=
-linkmode external
-extldflags=-static
-X hakurei.app/internal/info.buildVersion=${HAKUREI_VERSION}
" ./cmd/earlyinit
echo
`, false), Unversioned
},
}
}
func (t Toolchain) newImageInitramfs() (pkg.Artifact, string) {
return t.New("initramfs", TNoToolchain, []pkg.Artifact{
t.Load(Zstd),
t.Load(Hakurei),
t.Load(EarlyInit),
t.Load(GenInitCPIO),
}, nil, nil, `
gen_init_cpio -t 4294967295 -c /usr/src/initramfs | zstd > /work/initramfs.zst

View File

@@ -82,6 +82,11 @@ install -Dm0500 \
echo "Installing linux $1..."
cp -av "$2" "$4"
cp -av "$3" "$4"
`))),
pkg.Path(AbsUsrSrc.Append(
".depmod",
), false, pkg.NewFile("depmod", []byte(`#!/bin/sh
exec /system/sbin/depmod -m /lib/modules "$@"
`))),
},
@@ -1210,6 +1215,11 @@ cgit 1.2.3-korg
"all",
},
Install: `
# kernel is not aware of kmod moduledir
install -Dm0500 \
/usr/src/.depmod \
/sbin/depmod
make \
"-j$(nproc)" \
-f /usr/src/kernel/Makefile \
@@ -1217,9 +1227,10 @@ make \
LLVM=1 \
INSTALL_PATH=/work \
install \
INSTALL_MOD_PATH=/work \
INSTALL_MOD_PATH=/work/system \
DEPMOD=/sbin/depmod \
modules_install
rm -v /work/lib/modules/` + kernelVersion + `/build
rm -v /work/system/lib/modules/` + kernelVersion + `/build
`,
},
Flex,
@@ -1272,3 +1283,55 @@ func init() {
Description: "a program in the kernel source tree for creating initramfs archive",
}
}
func (t Toolchain) newFirmware() (pkg.Artifact, string) {
const (
version = "20260221"
checksum = "vTENPW5rZ6yLVq7YKDLHkCVgKXvwUWigEx7T4LcxoKeBVYIyf1_sEExeV4mo-e46"
)
return t.NewPackage("firmware", version, pkg.NewHTTPGetTar(
nil, "https://gitlab.com/kernel-firmware/linux-firmware/-/"+
"archive/"+version+"/linux-firmware-"+version+".tar.bz2",
mustDecode(checksum),
pkg.TarBzip2,
), &PackageAttr{
// dedup creates temporary file
Writable: true,
// does not use configure
EnterSource: true,
Env: []string{
"HOME=/proc/nonexistent",
},
}, &MakeHelper{
OmitDefaults: true,
SkipConfigure: true,
InPlace: true,
Make: []string{
"DESTDIR=/work/system",
"install-zst",
},
SkipCheck: true, // requires pre-commit
Install: `make "-j$(nproc)" DESTDIR=/work/system dedup`,
},
Perl,
Parallel,
Nettle,
Rdfind,
Zstd,
Findutils,
Coreutils,
), version
}
func init() {
artifactsM[Firmware] = Metadata{
f: Toolchain.newFirmware,
Name: "firmware",
Description: "firmware blobs for use with the Linux kernel",
Website: "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/",
ID: 141464,
}
}

View File

@@ -1,16 +1,16 @@
#
# Automatically generated file; DO NOT EDIT.
# Linux/arm64 6.12.73 Kernel Configuration
# Linux/arm64 6.12.76 Kernel Configuration
#
CONFIG_CC_VERSION_TEXT="clang version 21.1.8"
CONFIG_CC_VERSION_TEXT="clang version 22.1.0"
CONFIG_GCC_VERSION=0
CONFIG_CC_IS_CLANG=y
CONFIG_CLANG_VERSION=210108
CONFIG_CLANG_VERSION=220100
CONFIG_AS_IS_LLVM=y
CONFIG_AS_VERSION=210108
CONFIG_AS_VERSION=220100
CONFIG_LD_VERSION=0
CONFIG_LD_IS_LLD=y
CONFIG_LLD_VERSION=210108
CONFIG_LLD_VERSION=220100
CONFIG_RUSTC_VERSION=0
CONFIG_RUSTC_LLVM_VERSION=0
CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y
@@ -4984,7 +4984,7 @@ CONFIG_SERIAL_TEGRA_TCU=m
CONFIG_SERIAL_MAX3100=m
CONFIG_SERIAL_MAX310X=m
CONFIG_SERIAL_IMX=m
CONFIG_SERIAL_IMX_CONSOLE=m
# CONFIG_SERIAL_IMX_CONSOLE is not set
CONFIG_SERIAL_IMX_EARLYCON=y
CONFIG_SERIAL_UARTLITE=m
CONFIG_SERIAL_UARTLITE_NR_UARTS=1
@@ -5772,6 +5772,7 @@ CONFIG_GPIO_MADERA=m
CONFIG_GPIO_MAX77650=m
CONFIG_GPIO_PMIC_EIC_SPRD=m
CONFIG_GPIO_SL28CPLD=m
CONFIG_GPIO_TN48M_CPLD=m
CONFIG_GPIO_TPS65086=m
CONFIG_GPIO_TPS65218=m
CONFIG_GPIO_TPS65219=m
@@ -6471,6 +6472,7 @@ CONFIG_MFD_MAX5970=m
# CONFIG_MFD_CS47L85 is not set
# CONFIG_MFD_CS47L90 is not set
# CONFIG_MFD_CS47L92 is not set
CONFIG_MFD_TN48M_CPLD=m
# CONFIG_MFD_DA9052_SPI is not set
CONFIG_MFD_DA9062=m
CONFIG_MFD_DA9063=m
@@ -12532,6 +12534,7 @@ CONFIG_RESET_SUNXI=y
CONFIG_RESET_TI_SCI=m
CONFIG_RESET_TI_SYSCON=m
CONFIG_RESET_TI_TPS380X=m
CONFIG_RESET_TN48M_CPLD=m
CONFIG_RESET_UNIPHIER=m
CONFIG_RESET_UNIPHIER_GLUE=m
CONFIG_RESET_ZYNQMP=y
@@ -14022,7 +14025,6 @@ CONFIG_LOCK_DEBUGGING_SUPPORT=y
# CONFIG_DEBUG_IRQFLAGS is not set
CONFIG_STACKTRACE=y
# CONFIG_WARN_ALL_UNSEEDED_RANDOM is not set
# CONFIG_DEBUG_KOBJECT is not set
#
@@ -14057,7 +14059,7 @@ CONFIG_USER_STACKTRACE_SUPPORT=y
CONFIG_NOP_TRACER=y
CONFIG_HAVE_FUNCTION_TRACER=y
CONFIG_HAVE_FUNCTION_GRAPH_TRACER=y
CONFIG_HAVE_FUNCTION_GRAPH_RETVAL=y
CONFIG_HAVE_FUNCTION_GRAPH_FREGS=y
CONFIG_HAVE_DYNAMIC_FTRACE=y
CONFIG_HAVE_DYNAMIC_FTRACE_WITH_ARGS=y
CONFIG_HAVE_FTRACE_MCOUNT_RECORD=y

View File

@@ -14,6 +14,7 @@ func (t Toolchain) newKmod() (pkg.Artifact, string) {
pkg.TarGzip,
), nil, &MesonHelper{
Setup: [][2]string{
{"Dmoduledir", "/system/lib/modules"},
{"Dsysconfdir", "/system/etc"},
{"Dbashcompletiondir", "no"},
{"Dfishcompletiondir", "no"},

View File

@@ -23,6 +23,7 @@ func (t Toolchain) newLibxslt() (pkg.Artifact, string) {
SkipCheck: true,
},
XZ,
Zlib,
Python,
PkgConfig,

View File

@@ -125,6 +125,8 @@ func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
[2]string{"LLVM_INSTALL_BINUTILS_SYMLINKS", "ON"},
[2]string{"LLVM_INSTALL_CCTOOLS_SYMLINKS", "ON"},
[2]string{"LLVM_LIT_ARGS", "'--verbose'"},
)
}
@@ -187,6 +189,7 @@ ln -s ld.lld /work/system/bin/ld
Append: cmakeAppend,
Script: script + attr.script,
},
Zlib,
Libffi,
Python,
Perl,

View File

@@ -13,6 +13,7 @@ func (t Toolchain) newMeson() (pkg.Artifact, string) {
checksum = "w895BXF_icncnXatT_OLCFe2PYEtg4KrKooMgUYdN-nQVvbFX3PvYWHGEpogsHtd"
)
return t.New("meson-"+version, 0, []pkg.Artifact{
t.Load(Zlib),
t.Load(Python),
t.Load(Setuptools),
}, nil, nil, `
@@ -66,6 +67,7 @@ func (*MesonHelper) name(name, version string) string {
// extra returns hardcoded meson runtime dependencies.
func (*MesonHelper) extra(int) []PArtifact {
return []PArtifact{
Zlib,
Python,
Meson,
Ninja,

31
internal/rosa/nettle.go Normal file
View File

@@ -0,0 +1,31 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newNettle() (pkg.Artifact, string) {
const (
version = "4.0"
checksum = "6agC-vHzzoqAlaX3K9tX8yHgrm03HLqPZzVzq8jh_ePbuPMIvpxereu_uRJFmQK7"
)
return t.NewPackage("nettle", version, pkg.NewHTTPGetTar(
nil, "https://ftpmirror.gnu.org/gnu/nettle/nettle-"+version+".tar.gz",
mustDecode(checksum),
pkg.TarGzip,
), nil, (*MakeHelper)(nil),
M4,
Diffutils,
GMP,
), version
}
func init() {
artifactsM[Nettle] = Metadata{
f: Toolchain.newNettle,
Name: "nettle",
Description: "a low-level cryptographic library",
Website: "https://www.lysator.liu.se/~nisse/nettle/",
ID: 2073,
}
}

View File

@@ -84,7 +84,7 @@ func init() {
artifactsM[buildcatrust] = newViaPip(
"buildcatrust",
"transform certificate stores between formats",
version, "none", "any",
version, "py3", "none", "any",
"k_FGzkRCLjbTWBkuBLzQJ1S8FPAz19neJZlMHm0t10F2Y0hElmvVwdSBRc03Rjo1",
"https://github.com/nix-community/buildcatrust/"+
"releases/download/v"+version+"/",
@@ -93,6 +93,7 @@ func init() {
func (t Toolchain) newNSSCACert() (pkg.Artifact, string) {
return t.New("nss-cacert", 0, []pkg.Artifact{
t.Load(Zlib),
t.Load(Bash),
t.Load(Python),

View File

@@ -75,10 +75,10 @@ func init() {
// newViaPip is a helper for installing python dependencies via pip.
func newViaPip(
name, description, version, abi, platform, checksum, prefix string,
name, description, version, interpreter, abi, platform, checksum, prefix string,
extra ...PArtifact,
) Metadata {
wname := name + "-" + version + "-py3-" + abi + "-" + platform + ".whl"
wname := name + "-" + version + "-" + interpreter + "-" + abi + "-" + platform + ".whl"
return Metadata{
f: func(t Toolchain) (pkg.Artifact, string) {
extraRes := make([]pkg.Artifact, len(extra))
@@ -87,6 +87,7 @@ func newViaPip(
}
return t.New(name+"-"+version, 0, slices.Concat([]pkg.Artifact{
t.Load(Zlib),
t.Load(Python),
}, extraRes), nil, nil, `
pip3 install \
@@ -112,6 +113,7 @@ func (t Toolchain) newSetuptools() (pkg.Artifact, string) {
checksum = "K9f8Yi7Gg95zjmQsE1LLw9UBb8NglI6EY6pQpdD6DM0Pmc_Td5w2qs1SMngTI6Jp"
)
return t.New("setuptools-"+version, 0, []pkg.Artifact{
t.Load(Zlib),
t.Load(Python),
}, nil, nil, `
pip3 install \
@@ -142,7 +144,7 @@ func init() {
artifactsM[PythonPygments] = newViaPip(
"pygments",
" a syntax highlighting package written in Python",
"2.19.2", "none", "any",
"2.19.2", "py3", "none", "any",
"ak_lwTalmSr7W4Mjy2XBZPG9I6a0gwSy2pS87N8x4QEuZYif0ie9z0OcfRfi9msd",
"https://files.pythonhosted.org/packages/"+
"c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/",
@@ -151,7 +153,7 @@ func init() {
artifactsM[PythonPluggy] = newViaPip(
"pluggy",
"the core framework used by the pytest, tox, and devpi projects",
"1.6.0", "none", "any",
"1.6.0", "py3", "none", "any",
"2HWYBaEwM66-y1hSUcWI1MyE7dVVuNNRW24XD6iJBey4YaUdAK8WeXdtFMQGC-4J",
"https://files.pythonhosted.org/packages/"+
"54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/",
@@ -160,7 +162,7 @@ func init() {
artifactsM[PythonPackaging] = newViaPip(
"packaging",
"reusable core utilities for various Python Packaging interoperability specifications",
"26.0", "none", "any",
"26.0", "py3", "none", "any",
"iVVXcqdwHDskPKoCFUlh2x8J0Gyq-bhO4ns9DvUJ7oJjeOegRYtSIvLV33Bki-pP",
"https://files.pythonhosted.org/packages/"+
"b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/",
@@ -169,15 +171,16 @@ func init() {
artifactsM[PythonIniConfig] = newViaPip(
"iniconfig",
"a small and simple INI-file parser module",
"2.3.0", "none", "any",
"2.3.0", "py3", "none", "any",
"SDgs4S5bXi77aVOeKTPv2TUrS3M9rduiK4DpU0hCmDsSBWqnZcWInq9lsx6INxut",
"https://files.pythonhosted.org/packages/"+
"cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/",
)
artifactsM[PythonPyTest] = newViaPip(
"pytest",
"the pytest framework",
"9.0.2", "none", "any",
"9.0.2", "py3", "none", "any",
"IM2wDbLke1EtZhF92zvAjUl_Hms1uKDtM7U8Dt4acOaChMnDg1pW7ib8U0wYGDLH",
"https://files.pythonhosted.org/packages/"+
"3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/",
@@ -186,4 +189,109 @@ func init() {
PythonPluggy,
PythonPygments,
)
artifactsM[PythonCfgv] = newViaPip(
"cfgv",
"validate configuration and produce human readable error messages",
"3.5.0", "py2.py3", "none", "any",
"yFKTyVRlmnLKAxvvge15kAd_GOP1Xh3fZ0NFImO5pBdD5e0zj3GRmA6Q1HdtLTYO",
"https://files.pythonhosted.org/packages/"+
"db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/",
)
artifactsM[PythonIdentify] = newViaPip(
"identify",
"file identification library for Python",
"2.6.17", "py2.py3", "none", "any",
"9RxK3igO-Pxxof5AuCAGiF_L1SWi4SpuSF1fWNXCzE2D4oTRSob-9VpFMLlybrSv",
"https://files.pythonhosted.org/packages/"+
"40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/",
)
artifactsM[PythonNodeenv] = newViaPip(
"nodeenv",
"a tool to create isolated node.js environments",
"1.10.0", "py2.py3", "none", "any",
"ihUb4-WQXYIhYOOKSsXlKIzjzQieOYl6ojro9H-0DFzGheaRTtuyZgsCmriq58sq",
"https://files.pythonhosted.org/packages/"+
"88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/",
)
artifactsM[PythonPyYAML] = newViaPip(
"pyyaml",
"a complete YAML 1.1 parser",
"6.0.3", "cp314", "cp314", "musllinux_1_2_x86_64",
"4_jhCFpUNtyrFp2HOMqUisR005u90MHId53eS7rkUbcGXkoaJ7JRsY21dREHEfGN",
"https://files.pythonhosted.org/packages/"+
"d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/",
)
artifactsM[PythonDistlib] = newViaPip(
"distlib",
"used as the basis for third-party packaging tools",
"0.4.0", "py2.py3", "none", "any",
"lGLLfYVhUhXOTw_84zULaH2K8n6pk1OOVXmJfGavev7N42msbtHoq-XY5D_xULI_",
"https://files.pythonhosted.org/packages/"+
"33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/",
)
artifactsM[PythonFilelock] = newViaPip(
"filelock",
"a platform-independent file locking library for Python",
"3.25.0", "py3", "none", "any",
"0gSQIYNUEjOs1JBxXjGwfLnwFPFINwqyU_Zqgj7fT_EGafv_HaD5h3Xv2Rq_qQ44",
"https://files.pythonhosted.org/packages/"+
"f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/",
)
artifactsM[PythonPlatformdirs] = newViaPip(
"platformdirs",
"a Python package for determining platform-specific directories",
"4.9.4", "py3", "none", "any",
"JGNpMCX2JMn-7c9bk3QzOSNDgJRR_5lH-jIqfy0zXMZppRCdLsTNbdp4V7QFwxOI",
"https://files.pythonhosted.org/packages/"+
"63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/",
)
artifactsM[PythonDiscovery] = newViaPip(
"python_discovery",
"looks for a python installation",
"1.1.1", "py3", "none", "any",
"Jk_qGMfZYm0fdNOSvMdVQZuQbJlqu3NWRm7T2fRtiBXmHLQyOdJE3ypI_it1OJR0",
"https://files.pythonhosted.org/packages/"+
"75/0f/2bf7e3b5a4a65f623cb820feb5793e243fad58ae561015ee15a6152f67a2/",
PythonFilelock,
PythonPlatformdirs,
)
artifactsM[PythonVirtualenv] = newViaPip(
"virtualenv",
"a tool for creating isolated virtual python environments",
"21.1.0", "py3", "none", "any",
"SLvdr3gJZ7GTS-kiRyq2RvJdrQ8SZYC1pglbViWCMLCuAIcbLNjVEUJZ4hDtKUxm",
"https://files.pythonhosted.org/packages/"+
"78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/",
PythonDistlib,
PythonFilelock,
PythonPlatformdirs,
PythonDiscovery,
)
artifactsM[PythonPreCommit] = newViaPip(
"pre_commit",
"a framework for managing and maintaining multi-language pre-commit hooks",
"4.5.1", "py2.py3", "none", "any",
"9G2Hv5JpvXFZVfw4pv_KAsmHD6bvot9Z0YBDmW6JeJizqTA4xEQCKel-pCERqQFK",
"https://files.pythonhosted.org/packages/"+
"5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/",
PythonCfgv,
PythonIdentify,
PythonNodeenv,
PythonPyYAML,
PythonDistlib,
PythonFilelock,
PythonPlatformdirs,
PythonDiscovery,
PythonVirtualenv,
)
}

33
internal/rosa/rdfind.go Normal file
View File

@@ -0,0 +1,33 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newRdfind() (pkg.Artifact, string) {
const (
version = "1.8.0"
checksum = "PoaeJ2WIG6yyfe5VAYZlOdAQiR3mb3WhAUMj2ziTCx_IIEal4640HMJUb4SzU9U3"
)
return t.NewPackage("rdfind", version, pkg.NewHTTPGetTar(
nil, "https://rdfind.pauldreik.se/rdfind-"+version+".tar.gz",
mustDecode(checksum),
pkg.TarGzip,
), nil, &MakeHelper{
// test suite hard codes /bin/echo
ScriptCheckEarly: `
ln -s ../system/bin/toybox /bin/echo
`,
},
Nettle,
), version
}
func init() {
artifactsM[Rdfind] = Metadata{
f: Toolchain.newRdfind,
Name: "rdfind",
Description: "a program that finds duplicate files",
Website: "https://rdfind.pauldreik.se/",
ID: 231641,
}
}

View File

@@ -15,6 +15,7 @@ func (t Toolchain) newStage0() (pkg.Artifact, string) {
runtimes,
clang,
t.Load(Zlib),
t.Load(Bzip2),
t.Load(Patch),

View File

@@ -44,5 +44,7 @@ func init() {
Name: "tamago",
Description: "a Go toolchain extended with support for bare metal execution",
Website: "https://github.com/usbarmory/tamago-go",
ID: 388872,
}
}

View File

@@ -4,22 +4,28 @@ import "hakurei.app/internal/pkg"
func (t Toolchain) newZlib() (pkg.Artifact, string) {
const (
version = "1.3.1"
checksum = "E-eIpNzE8oJ5DsqH4UuA_0GDKuQF5csqI8ooDx2w7Vx-woJ2mb-YtSbEyIMN44mH"
version = "1.3.2"
checksum = "KHZrePe42vL2XvOUE3KlJkp1UgWhWkl0jjT_BOvFhuM4GzieEH9S7CioepOFVGYB"
)
return t.NewPackage("zlib", version, pkg.NewHTTPGetTar(
nil, "https://www.zlib.net/fossils/zlib-"+version+".tar.gz",
mustDecode(checksum),
pkg.TarGzip,
), &PackageAttr{
Env: []string{
"CC=clang -fPIC",
},
}, &MakeHelper{
OmitDefaults: true,
), nil, &CMakeHelper{
Cache: [][2]string{
{"CMAKE_BUILD_TYPE", "Release"},
Host: `""`,
Build: `""`,
{"ZLIB_BUILD_TESTING", "OFF"},
{"ZLIB_BUILD_SHARED", "ON"},
{"ZLIB_BUILD_STATIC", "ON"},
{"ZLIB_BUILD_MINIZIP", "OFF"},
{"ZLIB_INSTALL", "ON"},
{"ZLIB_PREFIX", "OFF"},
},
// ninja dependency loop
Make: true,
}), version
}
func init() {

View File

@@ -16,7 +16,6 @@ func (t Toolchain) newZstd() (pkg.Artifact, string) {
Append: []string{"build", "cmake"},
Cache: [][2]string{
{"CMAKE_BUILD_TYPE", "Release"},
{"CMAKE_INSTALL_LIBDIR", "lib"},
},
}), version
}

View File

@@ -139,6 +139,8 @@ in
inherit (app) identity groups enablements;
inherit (dbusConfig) session_bus system_bus;
direct_wayland = app.insecureWayland;
sched_policy = app.schedPolicy;
sched_priority = app.schedPriority;
container = {
inherit (app)

View File

@@ -98,6 +98,7 @@ in
ints
str
bool
enum
package
anything
submodule
@@ -237,6 +238,29 @@ in
};
hostAbstract = mkEnableOption "share abstract unix socket scope";
schedPolicy = mkOption {
type = nullOr (enum [
"fifo"
"rr"
"batch"
"idle"
"deadline"
"ext"
]);
default = null;
description = ''
Scheduling policy to set for the container.
The zero value retains the current scheduling policy.
'';
};
schedPriority = mkOption {
type = nullOr (ints.between 1 99);
default = null;
description = ''
Scheduling priority to set for the container.
'';
};
nix = mkEnableOption "nix daemon access";
mapRealUid = mkEnableOption "mapping to priv-user uid";
device = mkEnableOption "access to all devices";

View File

@@ -1,7 +1,7 @@
{
lib,
stdenv,
buildGoModule,
buildGo126Module,
makeBinaryWrapper,
xdg-dbus-proxy,
pkg-config,
@@ -17,7 +17,7 @@
fuse3,
# for passthru.buildInputs
go,
go_1_26,
clang,
# for check
@@ -28,7 +28,7 @@
withStatic ? stdenv.hostPlatform.isStatic,
}:
buildGoModule rec {
buildGo126Module rec {
pname = "hakurei";
version = "0.3.6";
@@ -51,7 +51,7 @@ buildGoModule rec {
];
nativeBuildInputs = [
go
go_1_26
pkg-config
wayland-scanner
];
@@ -125,16 +125,20 @@ buildGoModule rec {
--inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages}
'';
passthru.targetPkgs = [
go
clang
xorg.xorgproto
util-linux
passthru = {
go = go_1_26;
# for go generate
wayland-protocols
wayland-scanner
]
++ buildInputs
++ nativeBuildInputs;
targetPkgs = [
go_1_26
clang
xorg.xorgproto
util-linux
# for go generate
wayland-protocols
wayland-scanner
]
++ buildInputs
++ nativeBuildInputs;
};
}

View File

@@ -28,6 +28,15 @@
# Automatically login on tty1 as a normal user:
services.getty.autologinUser = "alice";
security.pam.loginLimits = [
{
domain = "@users";
item = "rtprio";
type = "-";
value = 1;
}
];
environment = {
systemPackages = with pkgs; [
# For D-Bus tests:

View File

@@ -34,7 +34,7 @@ testers.nixosTest {
(writeShellScriptBin "hakurei-test" ''
# Assert hst CGO_ENABLED=0: ${
with pkgs;
runCommand "hakurei-hst-cgo" { nativeBuildInputs = [ go ]; } ''
runCommand "hakurei-hst-cgo" { nativeBuildInputs = [ self.packages.${system}.hakurei.go ]; } ''
cp -r ${options.environment.hakurei.package.default.src} "$out"
chmod -R +w "$out"
cp ${writeText "hst_cgo_test.go" ''package hakurei_test;import("testing";"hakurei.app/hst");func TestTemplate(t *testing.T){hst.Template()}''} "$out/hst_cgo_test.go"

View File

@@ -23,6 +23,14 @@
security = {
sudo.wheelNeedsPassword = false;
rtkit.enable = true;
pam.loginLimits = [
{
domain = "@users";
item = "rtprio";
type = "-";
value = 1;
}
];
};
services = {

View File

@@ -206,6 +206,17 @@ machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/shim-cont-unexpected-pid")
print(machine.succeed('grep "shim: got SIGCONT from unexpected process$" /tmp/shim-cont-unexpected-pid'))
# Check setscheduler:
sched_unset = int(machine.succeed("sudo -u alice -i hakurei -v run cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
if sched_unset != 0:
raise Exception(f"unexpected unset policy: {sched_unset}")
sched_idle = int(machine.succeed("sudo -u alice -i hakurei -v run --policy=idle cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
if sched_idle != 5:
raise Exception(f"unexpected idle policy: {sched_idle}")
sched_rr = int(machine.succeed("sudo -u alice -i hakurei -v run --policy=rr cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
if sched_rr != 2:
raise Exception(f"unexpected round-robin policy: {sched_idle}")
# Start app (foot) with Wayland enablement:
swaymsg("exec ne-foot")
wait_for_window(f"u0_a{hakurei_identity(0)}@machine")