42 Commits

Author SHA1 Message Date
121fcfa406 internal/uevent: enumerate objects via sysfs
All checks were successful
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 3m8s
Test / Hakurei (push) Successful in 4m17s
Test / ShareFS (push) Successful in 4m20s
Test / Sandbox (race detector) (push) Successful in 5m34s
Test / Hakurei (race detector) (push) Successful in 6m40s
Test / Flake checks (push) Successful in 1m26s
This is not a great way to implement cold boot, but I already have the implementation lying around.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-29 19:50:20 +09:00
19c76e0831 cmd: document Rosa OS programs
All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m6s
Test / Hakurei (push) Successful in 4m12s
Test / ShareFS (push) Successful in 4m17s
Test / Sandbox (race detector) (push) Successful in 5m43s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Flake checks (push) Successful in 1m24s
The earlyinit and mbf program are not covered by the compatibility promise, so specify that here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 22:43:25 +09:00
71fcc972ba cmd/hsu: alternative hsurc path for Rosa OS
All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m15s
Test / Hakurei (push) Successful in 4m25s
Test / ShareFS (push) Successful in 4m31s
Test / Sandbox (race detector) (push) Successful in 5m40s
Test / Hakurei (race detector) (push) Successful in 6m52s
Test / Flake checks (push) Successful in 1m26s
Rosa OS does not have /etc.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 18:22:55 +09:00
62002efd08 cmd/hsu: document hsurc format and internals
All checks were successful
Test / Create distribution (push) Successful in 1m11s
Test / Sandbox (push) Successful in 2m3s
Test / Sandbox (race detector) (push) Successful in 3m7s
Test / ShareFS (push) Successful in 3m18s
Test / Hakurei (race detector) (push) Successful in 4m14s
Test / Hakurei (push) Successful in 3m9s
Test / Flake checks (push) Successful in 1m36s
This was previously only documented via an unexported function.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 18:17:31 +09:00
e33294db9c cmd/hakurei: document stable behaviour
All checks were successful
Test / Create distribution (push) Successful in 1m14s
Test / Sandbox (push) Successful in 2m59s
Test / Hakurei (push) Successful in 4m10s
Test / ShareFS (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m40s
Test / Flake checks (push) Successful in 1m25s
These are undocumented anywhere else and is required by tools invoking hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 17:31:46 +09:00
b1ea3b4acf cmd/hakurei: rename app to run
All checks were successful
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 3m7s
Test / Hakurei (push) Successful in 4m21s
Test / ShareFS (push) Successful in 4m20s
Test / Sandbox (race detector) (push) Successful in 5m39s
Test / Hakurei (race detector) (push) Successful in 6m36s
Test / Flake checks (push) Successful in 1m24s
The run command was a legacy holdover from very early days and is only useful for testing and demonstration these days. This change also renames it to exec.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 16:48:26 +09:00
2c254c70b8 cmd/hakurei: remove linkname directive
All checks were successful
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 3m4s
Test / Hakurei (push) Successful in 4m25s
Test / ShareFS (push) Successful in 4m21s
Test / Sandbox (race detector) (push) Successful in 5m43s
Test / Hakurei (race detector) (push) Successful in 6m41s
Test / Flake checks (push) Successful in 1m25s
This used to be a function that did much more, and was later relocated to another package and exported.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 16:20:02 +09:00
ea014d6af2 internal/uevent: consume kernel-originated events
All checks were successful
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 3m5s
Test / Hakurei (push) Successful in 4m18s
Test / ShareFS (push) Successful in 4m20s
Test / Sandbox (race detector) (push) Successful in 5m42s
Test / Hakurei (race detector) (push) Successful in 6m46s
Test / Flake checks (push) Successful in 1m25s
These are not possible to cover outside integration vm. Extreme care is required when dealing with this method, so keep it simple.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 15:39:16 +09:00
1b48484c16 internal/uevent: exclusive socket access
All checks were successful
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 3m2s
Test / Hakurei (push) Successful in 4m15s
Test / ShareFS (push) Successful in 4m16s
Test / Sandbox (race detector) (push) Successful in 5m34s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Flake checks (push) Successful in 1m45s
This is a much simplified mutex, since blocking is not required.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 01:01:06 +09:00
713bff3eb0 internal/uevent: decode uevent messages
All checks were successful
Test / Create distribution (push) Successful in 1m14s
Test / Sandbox (push) Successful in 3m3s
Test / Hakurei (push) Successful in 4m13s
Test / ShareFS (push) Successful in 4m17s
Test / Sandbox (race detector) (push) Successful in 5m35s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m24s
The wire format and behaviour is entirely undocumented. This is implemented by reading lib/kobject_uevent.c, with testdata collected from the internal/rosa kernel.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 00:49:34 +09:00
30f459e690 internal/uevent: nontrivial errors
All checks were successful
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 3m11s
Test / Hakurei (push) Successful in 4m20s
Test / ShareFS (push) Successful in 4m18s
Test / Sandbox (race detector) (push) Successful in 5m32s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Flake checks (push) Successful in 1m25s
These errors are best represented as JSON.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-28 00:07:56 +09:00
8766fddcb3 internal/uevent: recoverable errors
All checks were successful
Test / Create distribution (push) Successful in 1m13s
Test / Sandbox (push) Successful in 3m1s
Test / Hakurei (push) Successful in 4m9s
Test / ShareFS (push) Successful in 4m17s
Test / Sandbox (race detector) (push) Successful in 5m36s
Test / Hakurei (race detector) (push) Successful in 6m42s
Test / Flake checks (push) Successful in 1m26s
This runs in the Rosa OS init, so recover as much as possible, as otherwise it is likely to require a full system reboot to resume event processing. The caller is responsible for reporting the error.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-27 22:58:16 +09:00
2745602be3 internal/uevent: wrap netlink socket
All checks were successful
Test / Create distribution (push) Successful in 1m18s
Test / Sandbox (push) Successful in 3m13s
Test / Hakurei (push) Successful in 4m14s
Test / ShareFS (push) Successful in 4m21s
Test / Sandbox (race detector) (push) Successful in 5m37s
Test / Hakurei (race detector) (push) Successful in 6m41s
Test / Flake checks (push) Successful in 1m26s
Unfortunately these messages do not have the same format as rtnetlink.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-27 22:46:18 +09:00
ee22847dde internal/uevent: kobject_action lookup
All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m12s
Test / Hakurei (push) Successful in 4m13s
Test / ShareFS (push) Successful in 4m22s
Test / Sandbox (race detector) (push) Successful in 5m35s
Test / Hakurei (race detector) (push) Successful in 6m46s
Test / Flake checks (push) Successful in 1m47s
This is encoded as part of kobject uevent message headers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-27 22:39:43 +09:00
c61188649b internal/netlink: export generic connection
All checks were successful
Test / Create distribution (push) Successful in 1m14s
Test / Sandbox (push) Successful in 3m2s
Test / Hakurei (push) Successful in 4m18s
Test / ShareFS (push) Successful in 4m17s
Test / Sandbox (race detector) (push) Successful in 5m50s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m25s
This enables abstractions around some families to be implemented in a separate package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-27 19:08:48 +09:00
6a87a96838 internal/rosa/kernel: 6.12.77 to 6.12.78
All checks were successful
Test / Create distribution (push) Successful in 3m10s
Test / Sandbox (push) Successful in 6m21s
Test / ShareFS (push) Successful in 8m35s
Test / Sandbox (race detector) (push) Successful in 9m16s
Test / Hakurei (push) Successful in 3m23s
Test / Hakurei (race detector) (push) Successful in 4m16s
Test / Flake checks (push) Successful in 1m30s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-27 10:40:27 +09:00
2548a681e9 internal/rosa: key-value type
All checks were successful
Test / Create distribution (push) Successful in 1m34s
Test / Sandbox (push) Successful in 4m52s
Test / Hakurei (push) Successful in 5m53s
Test / ShareFS (push) Successful in 5m56s
Test / Sandbox (race detector) (push) Successful in 6m52s
Test / Hakurei (race detector) (push) Successful in 8m8s
Test / Flake checks (push) Successful in 1m29s
This type is used very frequently. The new type is much easier to type and can receive helper methods eventually if needed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 16:23:15 +09:00
d514d0679f internal/rosa: set PYTHONUNBUFFERED=1
All checks were successful
Test / Create distribution (push) Successful in 2m53s
Test / Sandbox (push) Successful in 5m48s
Test / Hakurei (push) Successful in 7m43s
Test / ShareFS (push) Successful in 7m41s
Test / Sandbox (race detector) (push) Successful in 8m14s
Test / Hakurei (race detector) (push) Successful in 9m16s
Test / Flake checks (push) Successful in 1m30s
Some python tools try to be clever and buffers output. This makes the build process appear to hang and is quite frustrating. Instead of trying to address this on a case-by-case basis, this is turned off globally for the interpreter.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 15:29:29 +09:00
4407892632 cmd/mbf: optionally enter cure container
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m42s
Test / Hakurei (push) Successful in 3m40s
Test / ShareFS (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 5m10s
Test / Hakurei (race detector) (push) Successful in 6m16s
Test / Flake checks (push) Successful in 1m22s
This is very useful for troubleshooting failing tests and such. The ephemeral state is cleaned up by internal/pkg.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 15:10:11 +09:00
e661260607 internal/pkg: enter exec container
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m38s
Test / ShareFS (push) Successful in 3m42s
Test / Sandbox (race detector) (push) Successful in 5m21s
Test / Hakurei (race detector) (push) Successful in 6m20s
Test / Flake checks (push) Successful in 1m22s
This enables much easier troubleshooting of failing cures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 15:05:04 +09:00
044490e0a5 cmd/mbf: retain session by default
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m38s
Test / Hakurei (push) Successful in 3m43s
Test / ShareFS (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 6m14s
Test / Flake checks (push) Successful in 1m30s
This almost never make sense to be turned off.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 14:59:17 +09:00
af038c89ff internal/pkg: collection helper-artifact
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m46s
Test / Hakurei (push) Successful in 3m40s
Test / ShareFS (push) Successful in 3m45s
Test / Sandbox (race detector) (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 3m27s
Test / Flake checks (push) Successful in 1m20s
This was moved from internal/rosa because it is considered generally useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 14:11:10 +09:00
d2f30173cd internal/pkg: isolate container params
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m43s
Test / Hakurei (push) Successful in 3m44s
Test / ShareFS (push) Successful in 3m47s
Test / Sandbox (race detector) (push) Successful in 5m12s
Test / Hakurei (race detector) (push) Successful in 6m11s
Test / Flake checks (push) Successful in 1m20s
This enables exporting container params for interactive troubleshooting within the cure container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 14:02:58 +09:00
5319ea994c internal/rosa/libseccomp: fix upstream out-of-bounds read
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m39s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m40s
Test / Sandbox (race detector) (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m9s
Test / Flake checks (push) Successful in 1m15s
This was revealed by optimisation changes in the latest toolchain.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 10:43:11 +09:00
bbe178be3e internal/rosa/llvm: 22.1.1 to 22.1.2
All checks were successful
Test / Create distribution (push) Successful in 1m14s
Test / Sandbox (push) Successful in 3m7s
Test / Hakurei (push) Successful in 4m30s
Test / ShareFS (push) Successful in 4m34s
Test / Sandbox (race detector) (push) Successful in 5m47s
Test / Hakurei (race detector) (push) Successful in 6m58s
Test / Flake checks (push) Successful in 1m24s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 09:56:34 +09:00
ca28e9936b internal/rosa/musl: 1.2.5 to 1.2.6
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 2m30s
Test / Hakurei (push) Successful in 4m25s
Test / ShareFS (push) Successful in 2m53s
Test / Sandbox (race detector) (push) Successful in 5m22s
Test / Hakurei (race detector) (push) Successful in 6m41s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 09:56:06 +09:00
f61c6ade56 internal/rosa/nss: 3.121 to 3.122
All checks were successful
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 3m5s
Test / ShareFS (push) Successful in 3m41s
Test / Sandbox (race detector) (push) Successful in 5m49s
Test / Hakurei (race detector) (push) Successful in 6m55s
Test / Hakurei (push) Successful in 2m27s
Test / Flake checks (push) Successful in 1m18s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 09:55:27 +09:00
fce3d63823 internal/rosa/gnu: autoconf 2.72 to 2.73
All checks were successful
Test / Create distribution (push) Successful in 1m10s
Test / Sandbox (push) Successful in 3m8s
Test / Hakurei (push) Successful in 4m25s
Test / ShareFS (push) Successful in 4m31s
Test / Sandbox (race detector) (push) Successful in 5m53s
Test / Hakurei (race detector) (push) Successful in 6m48s
Test / Flake checks (push) Successful in 1m24s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 09:54:44 +09:00
722c3cc54f internal/netlink: optional check header as reply
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m48s
Test / ShareFS (push) Successful in 3m41s
Test / Sandbox (race detector) (push) Successful in 5m10s
Test / Hakurei (race detector) (push) Successful in 6m18s
Test / Flake checks (push) Successful in 1m19s
Not every received message is a reply.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-25 19:33:01 +09:00
372d509e5c internal/netlink: expose multicast groups
All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m14s
Test / Hakurei (push) Successful in 4m19s
Test / ShareFS (push) Successful in 4m19s
Test / Sandbox (race detector) (push) Successful in 5m36s
Test / Hakurei (race detector) (push) Successful in 6m43s
Test / Flake checks (push) Successful in 1m25s
This also gets rid of the cached pid value for port since that prevents multiple sockets from being open at once.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-25 17:55:35 +09:00
d62516ed1e internal/netlink: enlarge recvfrom buffer
All checks were successful
Test / Create distribution (push) Successful in 1m13s
Test / Sandbox (push) Successful in 3m3s
Test / Hakurei (push) Successful in 4m19s
Test / ShareFS (push) Successful in 4m16s
Test / Sandbox (race detector) (push) Successful in 5m32s
Test / Hakurei (race detector) (push) Successful in 6m44s
Test / Flake checks (push) Successful in 1m22s
This also uses an array type for the buffer since its size now uses the hardcoded value found in the kernel.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-25 17:18:56 +09:00
d2b635eb55 cmd/mbf: correctly describe --with-toolchain
All checks were successful
Test / Create distribution (push) Successful in 1m12s
Test / Sandbox (push) Successful in 2m58s
Test / ShareFS (push) Successful in 4m11s
Test / Hakurei (push) Successful in 4m23s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m34s
Test / Flake checks (push) Successful in 1m23s
The behaviour of this was changed to include the stage2 toolchain instead, but the help text was never updated.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-25 15:41:28 +09:00
50403e9d60 internal/netlink: wrap netpoll via context
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m46s
Test / ShareFS (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m9s
Test / Flake checks (push) Successful in 1m18s
This removes netpoll boilerplate for the most common use case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-25 15:39:29 +09:00
b98c5f2e21 internal/netlink: nonblocking socket I/O
All checks were successful
Test / Create distribution (push) Successful in 1m13s
Test / Sandbox (push) Successful in 2m59s
Test / Hakurei (push) Successful in 4m0s
Test / ShareFS (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 5m21s
Test / Hakurei (race detector) (push) Successful in 3m21s
Test / Flake checks (push) Successful in 1m18s
This enables use with blocking calls like when used with NETLINK_KOBJECT_UEVENT.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-25 14:06:59 +09:00
d972cffe5a internal/netlink: make full response available
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m53s
Test / ShareFS (push) Successful in 4m43s
Test / Sandbox (race detector) (push) Successful in 5m31s
Test / Hakurei (push) Successful in 5m39s
Test / Hakurei (race detector) (push) Successful in 7m40s
Test / Flake checks (push) Successful in 1m20s
The previous API makes it impossible to retrieve remaining messages in the current iteration.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-23 16:39:25 +09:00
d8648304bb internal/netlink: isolate receive method
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m59s
Test / ShareFS (push) Successful in 4m49s
Test / Hakurei (push) Successful in 5m36s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 8m6s
Test / Flake checks (push) Successful in 1m23s
This enables use with epoll for receiving events only.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-23 15:03:15 +09:00
f7bfa9a6c2 internal/rosa/go: disable go1.25.7 smtp test
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 1m55s
Test / Sandbox (race detector) (push) Successful in 2m41s
Test / ShareFS (push) Successful in 3m35s
Test / Hakurei (push) Successful in 4m38s
Test / Hakurei (race detector) (push) Successful in 5m5s
Test / Flake checks (push) Successful in 1m21s
This uses certs that had just expired.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 17:52:54 +09:00
7035b4b598 internal/rosa/cmake: 4.2.3 to 4.3.0
All checks were successful
Test / Create distribution (push) Successful in 1m26s
Test / Sandbox (push) Successful in 3m42s
Test / ShareFS (push) Successful in 5m56s
Test / Sandbox (race detector) (push) Successful in 6m23s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Hakurei (push) Successful in 5m25s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 16:39:57 +09:00
094b8400dd internal/rosa/qemu: 10.2.1 to 10.2.2
All checks were successful
Test / Create distribution (push) Successful in 1m14s
Test / Sandbox (push) Successful in 3m34s
Test / Sandbox (race detector) (push) Successful in 6m56s
Test / ShareFS (push) Successful in 7m1s
Test / Hakurei (race detector) (push) Successful in 10m13s
Test / Hakurei (push) Successful in 4m28s
Test / Flake checks (push) Successful in 2m56s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 16:13:51 +09:00
4652d921d8 internal/rosa/wayland: 1.24.91 to 1.25.0
All checks were successful
Test / Create distribution (push) Successful in 1m17s
Test / Sandbox (push) Successful in 3m37s
Test / Sandbox (race detector) (push) Successful in 7m2s
Test / ShareFS (push) Successful in 7m13s
Test / Hakurei (push) Successful in 8m11s
Test / Hakurei (race detector) (push) Successful in 10m22s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 16:13:28 +09:00
066213c245 internal/rosa/libexpat: 2.7.4 to 2.7.5
All checks were successful
Test / Create distribution (push) Successful in 1m0s
Test / Sandbox (push) Successful in 3m49s
Test / Sandbox (race detector) (push) Successful in 6m57s
Test / ShareFS (push) Successful in 6m5s
Test / Hakurei (race detector) (push) Successful in 10m9s
Test / Hakurei (push) Successful in 5m29s
Test / Flake checks (push) Successful in 1m58s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 16:00:50 +09:00
98832c21ee internal/rosa/fuse: 3.18.1 to 3.18.2
All checks were successful
Test / Create distribution (push) Successful in 1m11s
Test / Sandbox (push) Successful in 3m12s
Test / ShareFS (push) Successful in 5m5s
Test / Sandbox (race detector) (push) Successful in 5m38s
Test / Hakurei (push) Successful in 5m46s
Test / Hakurei (race detector) (push) Successful in 8m18s
Test / Flake checks (push) Successful in 1m43s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 15:57:49 +09:00
78 changed files with 1784 additions and 432 deletions

View File

@@ -1,3 +1,7 @@
// The earlyinit is part of the Rosa OS initramfs and serves as the system init.
//
// This program is an internal detail of Rosa OS and is not usable on its own.
// It is not covered by the compatibility promise.
package main package main
import ( import (

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -11,7 +12,6 @@ import (
"strconv" "strconv"
"sync" "sync"
"time" "time"
_ "unsafe" // for go:linkname
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/command" "hakurei.app/command"
@@ -27,9 +27,14 @@ import (
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value // optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
// if it is not nil, or the original value if it is. // if it is not nil, or the original value if it is.
// func optionalErrorUnwrap(err error) error {
//go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
func optionalErrorUnwrap(err error) error return underlyingErr
}
return err
}
var errSuccess = errors.New("success")
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command { func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var ( var (
@@ -60,9 +65,9 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
var ( var (
flagIdentifierFile int flagIdentifierFile int
) )
c.NewCommand("app", "Load and start container from configuration file", func(args []string) error { c.NewCommand("run", "Load and start container from configuration file", func(args []string) error {
if len(args) < 1 { if len(args) < 1 {
log.Fatal("app requires at least 1 argument") log.Fatal("run requires at least 1 argument")
} }
config := tryPath(msg, args[0]) config := tryPath(msg, args[0])
@@ -98,7 +103,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
) )
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error { c.NewCommand("exec", "Configure and start a permissive container", func(args []string) error {
if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd { if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd {
log.Fatalf("identity %d out of range", flagIdentity) log.Fatalf("identity %d out of range", flagIdentity)
} }
@@ -323,7 +328,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagShort bool flagShort bool
flagNoStore bool flagNoStore bool
) )
c.NewCommand("show", "Show live or local app configuration", func(args []string) error { c.NewCommand("show", "Show live or local instance configuration", func(args []string) error {
switch len(args) { switch len(args) {
case 0: // system case 0: // system
printShowSystem(os.Stdout, flagShort, flagJSON) printShowSystem(os.Stdout, flagShort, flagJSON)

View File

@@ -23,9 +23,9 @@ func TestHelp(t *testing.T) {
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS] Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Commands: Commands:
app Load and start container from configuration file run Load and start container from configuration file
run Configure and start a permissive container exec Configure and start a permissive container
show Show live or local app configuration show Show live or local instance configuration
ps List active instances ps List active instances
version Display version information version Display version information
license Show full license text license Show full license text
@@ -35,8 +35,8 @@ Commands:
`, `,
}, },
{ {
"run", []string{"run", "-h"}, ` "exec", []string{"exec", "-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>] [--policy <value>] [--priority <int>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS] Usage: hakurei exec [-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: Flags:
-X Enable direct connection to X11 -X Enable direct connection to X11

View File

@@ -1,8 +1,42 @@
// Hakurei runs user-specified containers as subordinate users.
//
// This program is generally invoked by another, higher level program, which
// creates container configuration via package [hst] or an implementation of it.
//
// The parent may leave files open and specify their file descriptor for various
// uses. In these cases, standard streams and netpoll files are treated as
// invalid file descriptors and rejected. All string representations must be in
// decimal.
//
// When specifying a [hst.Config] JSON stream or file to the run subcommand, the
// argument "-" is equivalent to stdin. Otherwise, file descriptor rules
// described above applies. Invalid file descriptors are treated as file names
// in their string representation, with the exception that if a netpoll file
// descriptor is attempted, the program fails.
//
// The flag --identifier-fd can be optionally specified to the run subcommand to
// receive the identifier of the newly started instance. File descriptor rules
// described above applies, and the file must be writable. This is sent after
// its state is made available, so the client must not attempt to poll for it.
// This uses the internal binary format of [hst.ID].
//
// For the show and ps subcommands, the flag --json can be applied to the main
// hakurei command to serialise output in JSON when applicable. Additionally,
// the flag --short targeting each subcommand is used to omit some information
// in both JSON and user-facing output. Only JSON-encoded output is covered
// under the compatibility promise.
//
// A template for [hst.Config] demonstrating all available configuration fields
// is returned by [hst.Template]. The JSON-encoded equivalent of this can be
// obtained via the template subcommand. Fields left unpopulated in the template
// (the direct_* family of fields, which are insecure under any configuration if
// enabled) are unsupported.
//
// For simple (but insecure) testing scenarios, the exec subcommand can be used
// to generate a simple, permissive configuration in-memory. See its help
// message for all available options.
package main package main
// this works around go:embed '..' limitation
//go:generate cp ../../LICENSE .
import ( import (
"context" "context"
_ "embed" _ "embed"
@@ -17,12 +51,9 @@ import (
"hakurei.app/message" "hakurei.app/message"
) )
var ( //go:generate cp ../../LICENSE .
errSuccess = errors.New("success") //go:embed LICENSE
var license string
//go:embed LICENSE
license string
)
// earlyHardeningErrs are errors collected while setting up early hardening feature. // earlyHardeningErrs are errors collected while setting up early hardening feature.
type earlyHardeningErrs struct{ yamaLSM, dumpable error } type earlyHardeningErrs struct{ yamaLSM, dumpable error }
@@ -31,8 +62,8 @@ func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE // early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(nil) container.TryArgv0(nil)
log.SetPrefix("hakurei: ")
log.SetFlags(0) log.SetFlags(0)
log.SetPrefix("hakurei: ")
msg := message.New(log.Default()) msg := message.New(log.Default())
early := earlyHardeningErrs{ early := earlyHardeningErrs{

View File

@@ -17,8 +17,9 @@ import (
) )
// tryPath attempts to read [hst.Config] from multiple sources. // tryPath attempts to read [hst.Config] from multiple sources.
// tryPath reads from [os.Stdin] if name has value "-". //
// Otherwise, name is passed to tryFd, and if that returns nil, name is passed to [os.Open]. // tryPath reads from [os.Stdin] if name has value "-". Otherwise, name is
// passed to tryFd, and if that returns nil, name is passed to [os.Open].
func tryPath(msg message.Msg, name string) (config *hst.Config) { func tryPath(msg message.Msg, name string) (config *hst.Config) {
var r io.ReadCloser var r io.ReadCloser
config = new(hst.Config) config = new(hst.Config)
@@ -46,7 +47,8 @@ func tryPath(msg message.Msg, name string) (config *hst.Config) {
return return
} }
// tryFd returns a [io.ReadCloser] if name represents an integer corresponding to a valid file descriptor. // tryFd returns a [io.ReadCloser] if name represents an integer corresponding
// to a valid file descriptor.
func tryFd(msg message.Msg, name string) io.ReadCloser { func tryFd(msg message.Msg, name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil { if v, err := strconv.Atoi(name); err != nil {
if !errors.Is(err, strconv.ErrSyntax) { if !errors.Is(err, strconv.ErrSyntax) {
@@ -60,7 +62,12 @@ func tryFd(msg message.Msg, name string) io.ReadCloser {
msg.Verbosef("trying config stream from %d", v) msg.Verbosef("trying config stream from %d", v)
fd := uintptr(v) fd := uintptr(v)
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 { if _, _, errno := syscall.Syscall(
syscall.SYS_FCNTL,
fd,
syscall.F_GETFD,
0,
); errno != 0 {
if errors.Is(errno, syscall.EBADF) { // reject bad fd if errors.Is(errno, syscall.EBADF) { // reject bad fd
return nil return nil
} }
@@ -75,10 +82,12 @@ func tryFd(msg message.Msg, name string) io.ReadCloser {
} }
} }
// shortLengthMin is the minimum length a short form identifier can have and still be interpreted as an identifier. // shortLengthMin is the minimum length a short form identifier can have and
// still be interpreted as an identifier.
const shortLengthMin = 1 << 3 const shortLengthMin = 1 << 3
// shortIdentifier returns an eight character short representation of [hst.ID] from its random bytes. // shortIdentifier returns an eight character short representation of [hst.ID]
// from its random bytes.
func shortIdentifier(id *hst.ID) string { func shortIdentifier(id *hst.ID) string {
return shortIdentifierString(id.String()) return shortIdentifierString(id.String())
} }
@@ -88,7 +97,8 @@ func shortIdentifierString(s string) string {
return s[len(hst.ID{}) : len(hst.ID{})+shortLengthMin] return s[len(hst.ID{}) : len(hst.ID{})+shortLengthMin]
} }
// tryIdentifier attempts to match [hst.State] from a [hex] representation of [hst.ID] or a prefix of its lower half. // tryIdentifier attempts to match [hst.State] from a [hex] representation of
// [hst.ID] or a prefix of its lower half.
func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State { func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
const ( const (
likeShort = 1 << iota likeShort = 1 << iota
@@ -96,7 +106,8 @@ func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
) )
var likely uintptr var likely uintptr
if len(name) >= shortLengthMin && len(name) <= len(hst.ID{}) { // half the hex representation // half the hex representation
if len(name) >= shortLengthMin && len(name) <= len(hst.ID{}) {
// cannot safely decode here due to unknown alignment // cannot safely decode here due to unknown alignment
for _, c := range name { for _, c := range name {
if c >= '0' && c <= '9' { if c >= '0' && c <= '9' {

7
cmd/hsu/conf.go Normal file
View File

@@ -0,0 +1,7 @@
//go:build !rosa
package main
// hsuConfPath is an absolute pathname to the hsu configuration file. Its
// contents are interpreted by parseConfig.
const hsuConfPath = "/etc/hsurc"

7
cmd/hsu/config_rosa.go Normal file
View File

@@ -0,0 +1,7 @@
//go:build rosa
package main
// hsuConfPath is the pathname to the hsu configuration file, specific to
// Rosa OS. Its contents are interpreted by parseConfig.
const hsuConfPath = "/system/etc/hsurc"

View File

@@ -1,6 +1,6 @@
package main package main
/* copied from hst and must never be changed */ /* keep in sync with hst */
const ( const (
userOffset = 100000 userOffset = 100000

View File

@@ -1,7 +1,58 @@
// hsu starts the hakurei shim as the target subordinate user.
//
// The hsu program must be installed with the setuid and setgid bit set, and
// owned by root. A configuration file must be installed at /etc/hsurc with
// permission bits 0400, and owned by root. Each line of the file specifies a
// hakurei userid to kernel uid mapping. A line consists of the decimal string
// representation of the uid of the user wishing to start hakurei containers,
// followed by a space, followed by the decimal string representation of its
// userid. Duplicate uid entries are ignored, with the first occurrence taking
// effect.
//
// For example, to map the kernel uid 1000 to the hakurei user id 0:
//
// 1000 0
//
// # Internals
//
// Hakurei and hsu holds pathnames pointing to each other set at link time. For
// this reason, a distribution of hakurei has fixed installation prefix. Since
// this program is never invoked by the user, behaviour described in the
// following paragraphs are considered an internal detail and not covered by the
// compatibility promise.
//
// After checking credentials, hsu checks via /proc/ the absolute pathname of
// its parent process, and fails if it does not match the hakurei pathname set
// at link time. This is not a security feature: the priv-side is considered
// trusted, and this feature makes no attempt to address the racy nature of
// querying /proc/, or debuggers attached to the parent process. Instead, this
// aims to discourage misuse and reduce confusion if the user accidentally
// stumbles upon this program. It also prevents accidental use of the incorrect
// installation of hsu in some environments.
//
// Since target container environment variables are set up in shim via the
// [container] infrastructure, the environment is used for parameters from the
// parent process.
//
// HAKUREI_SHIM specifies a single byte between '3' and '9' representing the
// setup pipe file descriptor. It is passed as is to the shim process and is the
// only value in the environment of the shim process. Since hsurc is not
// accessible to the parent process, leaving this unset causes hsu to print the
// corresponding hakurei user id of the parent and terminate.
//
// HAKUREI_IDENTITY specifies the identity of the instance being started and is
// used to produce the kernel uid alongside hakurei user id looked up from hsurc.
//
// HAKUREI_GROUPS specifies supplementary groups to inherit from the credentials
// of the parent process in a ' ' separated list of decimal string
// representations of gid. This has the unfortunate consequence of allowing
// users mapped via hsurc to effectively drop group membership, so special care
// must be taken to ensure this does not lead to an increase in access. This is
// not applicable to Rosa OS since unsigned code execution is not permitted
// outside hakurei containers, and is generally nonapplicable to the security
// model of hakurei, where all untrusted code runs within containers.
package main package main
// minimise imports to avoid inadvertently calling init or global variable functions
import ( import (
"bytes" "bytes"
"fmt" "fmt"
@@ -16,10 +67,13 @@ import (
) )
const ( const (
// envIdentity is the name of the environment variable holding a // envShim is the name of the environment variable holding a single byte
// single byte representing the shim setup pipe file descriptor. // representing the shim setup pipe file descriptor.
envShim = "HAKUREI_SHIM" envShim = "HAKUREI_SHIM"
// envGroups holds a ' ' separated list of string representations of // envIdentity is the name of the environment variable holding a decimal
// string representation of the current application identity.
envIdentity = "HAKUREI_IDENTITY"
// envGroups holds a ' ' separated list of decimal string representations of
// supplementary group gid. Membership requirements are enforced. // supplementary group gid. Membership requirements are enforced.
envGroups = "HAKUREI_GROUPS" envGroups = "HAKUREI_GROUPS"
) )
@@ -35,7 +89,6 @@ func main() {
log.SetFlags(0) log.SetFlags(0)
log.SetPrefix("hsu: ") log.SetPrefix("hsu: ")
log.SetOutput(os.Stderr)
if os.Geteuid() != 0 { if os.Geteuid() != 0 {
log.Fatal("this program must be owned by uid 0 and have the setuid bit set") log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
@@ -99,8 +152,6 @@ func main() {
// last possible uid outcome // last possible uid outcome
uidEnd = 999919999 uidEnd = 999919999
) )
// cast to int for use with library functions
uid := int(toUser(userid, identity)) uid := int(toUser(userid, identity))
// final bounds check to catch any bugs // final bounds check to catch any bugs
@@ -136,7 +187,6 @@ func main() {
} }
// careful! users in the allowlist is effectively allowed to drop groups via hsu // careful! users in the allowlist is effectively allowed to drop groups via hsu
if err := syscall.Setresgid(uid, uid, uid); err != nil { if err := syscall.Setresgid(uid, uid, uid); err != nil {
log.Fatalf("cannot set gid: %v", err) log.Fatalf("cannot set gid: %v", err)
} }
@@ -146,10 +196,21 @@ func main() {
if err := syscall.Setresuid(uid, uid, uid); err != nil { if err := syscall.Setresuid(uid, uid, uid); err != nil {
log.Fatalf("cannot set uid: %v", err) log.Fatalf("cannot set uid: %v", err)
} }
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
if _, _, errno := syscall.AllThreadsSyscall(
syscall.SYS_PRCTL,
PR_SET_NO_NEW_PRIVS, 1,
0,
); errno != 0 {
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error()) log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
} }
if err := syscall.Exec(toolPath, []string{"hakurei", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
if err := syscall.Exec(toolPath, []string{
"hakurei",
"shim",
}, []string{
envShim + "=" + shimSetupFd,
}); err != nil {
log.Fatalf("cannot start shim: %v", err) log.Fatalf("cannot start shim: %v", err)
} }

View File

@@ -18,8 +18,9 @@ const (
useridEnd = useridStart + rangeSize - 1 useridEnd = useridStart + rangeSize - 1
) )
// parseUint32Fast parses a string representation of an unsigned 32-bit integer value // parseUint32Fast parses a string representation of an unsigned 32-bit integer
// using the fast path only. This limits the range of values it is defined in. // value using the fast path only. This limits the range of values it is defined
// in but is perfectly adequate for this use case.
func parseUint32Fast(s string) (uint32, error) { func parseUint32Fast(s string) (uint32, error) {
sLen := len(s) sLen := len(s)
if sLen < 1 { if sLen < 1 {
@@ -40,12 +41,14 @@ func parseUint32Fast(s string) (uint32, error) {
return n, nil return n, nil
} }
// parseConfig reads a list of allowed users from r until it encounters puid or [io.EOF]. // parseConfig reads a list of allowed users from r until it encounters puid or
// [io.EOF].
// //
// Each line of the file specifies a hakurei userid to kernel uid mapping. A line consists // Each line of the file specifies a hakurei userid to kernel uid mapping. A
// of the string representation of the uid of the user wishing to start hakurei containers, // line consists of the string representation of the uid of the user wishing to
// followed by a space, followed by the string representation of its userid. Duplicate uid // start hakurei containers, followed by a space, followed by the string
// entries are ignored, with the first occurrence taking effect. // representation of its userid. Duplicate uid entries are ignored, with the
// first occurrence taking effect.
// //
// All string representations are parsed by calling parseUint32Fast. // All string representations are parsed by calling parseUint32Fast.
func parseConfig(r io.Reader, puid uint32) (userid uint32, ok bool, err error) { func parseConfig(r io.Reader, puid uint32) (userid uint32, ok bool, err error) {
@@ -81,10 +84,6 @@ func parseConfig(r io.Reader, puid uint32) (userid uint32, ok bool, err error) {
return useridEnd + 1, false, s.Err() return useridEnd + 1, false, s.Err()
} }
// hsuConfPath is an absolute pathname to the hsu configuration file.
// Its contents are interpreted by parseConfig.
const hsuConfPath = "/etc/hsurc"
// mustParseConfig calls parseConfig to interpret the contents of hsuConfPath, // mustParseConfig calls parseConfig to interpret the contents of hsuConfPath,
// terminating the program if an error is encountered, the syntax is incorrect, // terminating the program if an error is encountered, the syntax is incorrect,
// or the current user is not authorised to use hsu because its uid is missing. // or the current user is not authorised to use hsu because its uid is missing.
@@ -112,10 +111,6 @@ func mustParseConfig(puid int) (userid uint32) {
return return
} }
// envIdentity is the name of the environment variable holding a
// string representation of the current application identity.
var envIdentity = "HAKUREI_IDENTITY"
// mustReadIdentity calls parseUint32Fast to interpret the value stored in envIdentity, // mustReadIdentity calls parseUint32Fast to interpret the value stored in envIdentity,
// terminating the program if the value is not set, malformed, or out of bounds. // terminating the program if the value is not set, malformed, or out of bounds.
func mustReadIdentity() uint32 { func mustReadIdentity() uint32 {

View File

@@ -1,3 +1,15 @@
// The mbf program is a frontend for [hakurei.app/internal/rosa].
//
// This program is not covered by the compatibility promise. The command line
// interface, available packages and their behaviour, and even the on-disk
// format, may change at any time.
//
// # Name
//
// The name mbf stands for maiden's best friend, as a tribute to the DOOM source
// port of [the same name]. This name is a placeholder and is subject to change.
//
// [the same name]: https://www.doomwiki.org/wiki/MBF
package main package main
import ( import (
@@ -436,6 +448,7 @@ func main() {
{ {
var ( var (
flagDump string flagDump string
flagEnter bool
flagExport string flagExport string
) )
c.NewCommand( c.NewCommand(
@@ -445,9 +458,13 @@ func main() {
if len(args) != 1 { if len(args) != 1 {
return errors.New("cure requires 1 argument") return errors.New("cure requires 1 argument")
} }
if p, ok := rosa.ResolveName(args[0]); !ok { p, ok := rosa.ResolveName(args[0])
if !ok {
return fmt.Errorf("unknown artifact %q", args[0]) return fmt.Errorf("unknown artifact %q", args[0])
} else if flagDump == "" { }
switch {
default:
pathname, _, err := cache.Cure(rosa.Std.Load(p)) pathname, _, err := cache.Cure(rosa.Std.Load(p))
if err != nil { if err != nil {
return err return err
@@ -477,7 +494,8 @@ func main() {
} }
return nil return nil
} else {
case flagDump != "":
f, err := os.OpenFile( f, err := os.OpenFile(
flagDump, flagDump,
os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.O_WRONLY|os.O_CREATE|os.O_EXCL,
@@ -493,6 +511,15 @@ func main() {
} }
return f.Close() return f.Close()
case flagEnter:
return cache.EnterExec(
ctx,
rosa.Std.Load(p),
true, os.Stdin, os.Stdout, os.Stderr,
rosa.AbsSystem.Append("bin", "mksh"),
"sh",
)
} }
}, },
). ).
@@ -505,6 +532,11 @@ func main() {
&flagExport, &flagExport,
"export", command.StringFlag(""), "export", command.StringFlag(""),
"Export cured artifact to specified pathname", "Export cured artifact to specified pathname",
).
Flag(
&flagEnter,
"enter", command.BoolFlag(false),
"Enter cure container with an interactive shell",
) )
} }
@@ -527,7 +559,7 @@ func main() {
} }
presets[i] = p presets[i] = p
} }
root := make(rosa.Collect, 0, 6+len(args)) root := make(pkg.Collect, 0, 6+len(args))
root = rosa.Std.AppendPresets(root, presets...) root = rosa.Std.AppendPresets(root, presets...)
if flagWithToolchain { if flagWithToolchain {
@@ -543,7 +575,7 @@ func main() {
if _, _, err := cache.Cure(&root); err == nil { if _, _, err := cache.Cure(&root); err == nil {
return errors.New("unreachable") return errors.New("unreachable")
} else if !errors.Is(err, rosa.Collected{}) { } else if !pkg.IsCollected(err) {
return err return err
} }
@@ -636,13 +668,13 @@ func main() {
). ).
Flag( Flag(
&flagSession, &flagSession,
"session", command.BoolFlag(false), "session", command.BoolFlag(true),
"Retain session", "Retain session",
). ).
Flag( Flag(
&flagWithToolchain, &flagWithToolchain,
"with-toolchain", command.BoolFlag(false), "with-toolchain", command.BoolFlag(false),
"Include the stage3 LLVM toolchain", "Include the stage2 LLVM toolchain",
) )
} }

View File

@@ -85,7 +85,10 @@ func destroySetup(private_data unsafe.Pointer) (ok bool) {
} }
//export sharefs_init //export sharefs_init
func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.Pointer { func sharefs_init(
_ *C.struct_fuse_conn_info,
cfg *C.struct_fuse_config,
) unsafe.Pointer {
ctx := C.fuse_get_context() ctx := C.fuse_get_context()
priv := (*C.struct_sharefs_private)(ctx.private_data) priv := (*C.struct_sharefs_private)(ctx.private_data)
setup := cgo.Handle(priv.setup).Value().(*setupState) setup := cgo.Handle(priv.setup).Value().(*setupState)
@@ -103,7 +106,11 @@ func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.
cfg.negative_timeout = 0 cfg.negative_timeout = 0
// all future filesystem operations happen through this dirfd // all future filesystem operations happen through this dirfd
if fd, err := syscall.Open(setup.Source.String(), syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC, 0); err != nil { if fd, err := syscall.Open(
setup.Source.String(),
syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC,
0,
); err != nil {
log.Printf("cannot open %q: %v", setup.Source, err) log.Printf("cannot open %q: %v", setup.Source, err)
goto fail goto fail
} else if err = syscall.Fchdir(fd); err != nil { } else if err = syscall.Fchdir(fd); err != nil {
@@ -169,8 +176,11 @@ func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
// Decimal string representation of gid to set when running as root. // Decimal string representation of gid to set when running as root.
setgid *C.char setgid *C.char
// Decimal string representation of open file descriptor to read setupState from. // Decimal string representation of open file descriptor to read
// This is an internal detail for containerisation and must not be specified directly. // setupState from.
//
// This is an internal detail for containerisation and must not be
// specified directly.
setup *C.char setup *C.char
} }
@@ -253,7 +263,8 @@ func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
return true return true
} }
// copyArgs returns a heap allocated copy of an argument slice in fuse_args representation. // copyArgs returns a heap allocated copy of an argument slice in fuse_args
// representation.
func copyArgs(s ...string) fuseArgs { func copyArgs(s ...string) fuseArgs {
if len(s) == 0 { if len(s) == 0 {
return fuseArgs{argc: 0, argv: nil, allocated: 0} return fuseArgs{argc: 0, argv: nil, allocated: 0}
@@ -269,6 +280,7 @@ func copyArgs(s ...string) fuseArgs {
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) } func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }
// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg. // unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
//
// The last byte of arg must be 0. // The last byte of arg must be 0.
func unsafeAddArgument(args *fuseArgs, arg string) { func unsafeAddArgument(args *fuseArgs, arg string) {
C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg)))) C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
@@ -288,8 +300,8 @@ func _main(s ...string) (exitCode int) {
args := copyArgs(s...) args := copyArgs(s...)
defer freeArgs(&args) defer freeArgs(&args)
// this causes the kernel to enforce access control based on // this causes the kernel to enforce access control based on struct stat
// struct stat populated by sharefs_getattr // populated by sharefs_getattr
unsafeAddArgument(&args, "-odefault_permissions\x00") unsafeAddArgument(&args, "-odefault_permissions\x00")
var priv C.struct_sharefs_private var priv C.struct_sharefs_private
@@ -453,7 +465,10 @@ func _main(s ...string) (exitCode int) {
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
} }
z.Bind(z.Path, z.Path, 0) z.Bind(z.Path, z.Path, 0)
setup.Fuse = int(proc.ExtraFileSlice(&z.ExtraFiles, os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"))) setup.Fuse = int(proc.ExtraFileSlice(
&z.ExtraFiles,
os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"),
))
var setupWriter io.WriteCloser var setupWriter io.WriteCloser
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil { if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {

View File

@@ -1,3 +1,10 @@
// The sharefs FUSE filesystem is a permissionless shared filesystem.
//
// This filesystem is the primary means of file sharing between hakurei
// application containers. It serves the same purpose in Rosa OS as /sdcard
// does in AOSP.
//
// See help message for all available options.
package main package main
import ( import (

View File

@@ -1,6 +1,7 @@
package container package container
import ( import (
"context"
"io" "io"
"io/fs" "io/fs"
"net" "net"
@@ -66,7 +67,7 @@ type syscallDispatcher interface {
// ensureFile provides ensureFile. // ensureFile provides ensureFile.
ensureFile(name string, perm, pperm os.FileMode) error ensureFile(name string, perm, pperm os.FileMode) error
// mustLoopback provides mustLoopback. // mustLoopback provides mustLoopback.
mustLoopback(msg message.Msg) mustLoopback(ctx context.Context, msg message.Msg)
// seccompLoad provides [seccomp.Load]. // seccompLoad provides [seccomp.Load].
seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error
@@ -170,7 +171,7 @@ func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm
func (direct) ensureFile(name string, perm, pperm os.FileMode) error { func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
return ensureFile(name, perm, pperm) return ensureFile(name, perm, pperm)
} }
func (direct) mustLoopback(msg message.Msg) { func (direct) mustLoopback(ctx context.Context, msg message.Msg) {
var lo int var lo int
if ifi, err := net.InterfaceByName("lo"); err != nil { if ifi, err := net.InterfaceByName("lo"); err != nil {
msg.GetLogger().Fatalln(err) msg.GetLogger().Fatalln(err)
@@ -199,11 +200,14 @@ func (direct) mustLoopback(msg message.Msg) {
msg.GetLogger().Fatalf("RTNETLINK answers: %v", err) msg.GetLogger().Fatalf("RTNETLINK answers: %v", err)
default: default:
msg.GetLogger().Fatalf("RTNETLINK answers with malformed message") if err == context.DeadlineExceeded || err == context.Canceled {
msg.GetLogger().Fatalf("interrupted RTNETLINK operation")
}
msg.GetLogger().Fatal("RTNETLINK answers with malformed message")
} }
} }
must(c.SendNewaddrLo(uint32(lo))) must(c.SendNewaddrLo(ctx, uint32(lo)))
must(c.SendIfInfomsg(syscall.RTM_NEWLINK, 0, &syscall.IfInfomsg{ must(c.SendIfInfomsg(ctx, syscall.RTM_NEWLINK, 0, &syscall.IfInfomsg{
Family: syscall.AF_UNSPEC, Family: syscall.AF_UNSPEC,
Index: int32(lo), Index: int32(lo),
Flags: syscall.IFF_UP, Flags: syscall.IFF_UP,

View File

@@ -2,6 +2,7 @@ package container
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@@ -468,7 +469,7 @@ func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
stub.CheckArg(k.Stub, "pperm", pperm, 2)) stub.CheckArg(k.Stub, "pperm", pperm, 2))
} }
func (*kstub) mustLoopback(message.Msg) { /* noop */ } func (*kstub) mustLoopback(context.Context, message.Msg) { /* noop */ }
func (k *kstub) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error { func (k *kstub) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
k.Helper() k.Helper()

View File

@@ -7,6 +7,7 @@ import (
"log" "log"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"path" "path"
"slices" "slices"
"strconv" "strconv"
@@ -175,7 +176,11 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
} }
if !params.HostNet { if !params.HostNet {
k.mustLoopback(msg) ctx, cancel := signal.NotifyContext(context.Background(), CancelSignal,
os.Interrupt, SIGTERM, SIGQUIT)
defer cancel() // for panics
k.mustLoopback(ctx, msg)
cancel()
} }
// write uid/gid map here so parent does not need to set dumpable // write uid/gid map here so parent does not need to set dumpable

10
dist/comp/_hakurei vendored
View File

@@ -1,11 +1,11 @@
#compdef hakurei #compdef hakurei
_hakurei_app() { _hakurei_run() {
__hakurei_files __hakurei_files
return $? return $?
} }
_hakurei_run() { _hakurei_exec() {
_arguments \ _arguments \
'--id[Reverse-DNS style Application identifier, leave empty to inherit instance identifier]:id' \ '--id[Reverse-DNS style Application identifier, leave empty to inherit instance identifier]:id' \
'-a[Application identity]: :_numbers' \ '-a[Application identity]: :_numbers' \
@@ -57,9 +57,9 @@ __hakurei_instances() {
{ {
local -a _hakurei_cmds local -a _hakurei_cmds
_hakurei_cmds=( _hakurei_cmds=(
"app:Load and start container from configuration file" "run:Load and start container from configuration file"
"run:Configure and start a permissive container" "exec:Configure and start a permissive container"
"show:Show live or local app configuration" "show:Show live or local instance configuration"
"ps:List active instances" "ps:List active instances"
"version:Display version information" "version:Display version information"
"license:Show full license text" "license:Show full license text"

View File

@@ -2,29 +2,32 @@
package netlink package netlink
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"sync"
"syscall" "syscall"
"time"
"unsafe" "unsafe"
) )
// AF_NETLINK socket is never shared // net/netlink/af_netlink.c
var ( const maxRecvmsgLen = 32768
nlPid uint32
nlPidOnce sync.Once const (
// stateOpen denotes an open conn.
stateOpen uint32 = 1 << iota
) )
// getpid returns a cached pid value. // A Conn represents resources associated to a netlink socket.
func getpid() uint32 { type Conn struct {
nlPidOnce.Do(func() { nlPid = uint32(os.Getpid()) })
return nlPid
}
// A conn represents resources associated to a netlink socket.
type conn struct {
// AF_NETLINK socket. // AF_NETLINK socket.
fd int f *os.File
// For using runtime polling via f.
raw syscall.RawConn
// Port ID assigned by the kernel.
port uint32
// Internal connection status.
state uint32
// Kernel module or netlink group to communicate with. // Kernel module or netlink group to communicate with.
family int family int
// Message sequence number. // Message sequence number.
@@ -33,40 +36,155 @@ type conn struct {
typ, flags uint16 typ, flags uint16
// Outgoing position in buf. // Outgoing position in buf.
pos int pos int
// A page holding incoming and outgoing messages. // Pages holding incoming and outgoing messages.
buf []byte buf [maxRecvmsgLen]byte
// An instant some time after conn was established, but before the first
// I/O operation on f through raw. This serves as a cached deadline to
// cancel blocking I/O.
t time.Time
} }
// dial returns the address of a newly connected conn of specified family. // Dial returns the address of a newly connected generic netlink connection of
func dial(family int) (*conn, error) { // specified family and groups.
var c conn func Dial(family int, groups uint32) (*Conn, error) {
var c Conn
if fd, err := syscall.Socket( if fd, err := syscall.Socket(
syscall.AF_NETLINK, syscall.AF_NETLINK,
syscall.SOCK_RAW|syscall.SOCK_CLOEXEC, syscall.SOCK_RAW|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC,
family, family,
); err != nil { ); err != nil {
return nil, os.NewSyscallError("socket", err) return nil, os.NewSyscallError("socket", err)
} else if err = syscall.Bind(fd, &syscall.SockaddrNetlink{ } else if err = syscall.Bind(fd, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK, Family: syscall.AF_NETLINK,
Pid: getpid(), Groups: groups,
}); err != nil { }); err != nil {
_ = syscall.Close(fd) _ = syscall.Close(fd)
return nil, os.NewSyscallError("bind", err) return nil, os.NewSyscallError("bind", err)
} else { } else {
c.fd, c.family = fd, family var addr syscall.Sockaddr
if addr, err = syscall.Getsockname(fd); err != nil {
_ = syscall.Close(fd)
return nil, os.NewSyscallError("getsockname", err)
}
switch a := addr.(type) {
case *syscall.SockaddrNetlink:
c.port = a.Pid
default: // unreachable
_ = syscall.Close(fd)
return nil, syscall.ENOTRECOVERABLE
}
c.family = family
c.f = os.NewFile(uintptr(fd), "netlink")
if c.raw, err = c.f.SyscallConn(); err != nil {
_ = c.f.Close()
return nil, err
}
c.state |= stateOpen
} }
c.pos = syscall.NLMSG_HDRLEN c.pos = syscall.NLMSG_HDRLEN
c.buf = make([]byte, os.Getpagesize()) c.t = time.Now().UTC()
return &c, nil return &c, nil
} }
// ok returns whether conn is still open.
func (c *Conn) ok() bool { return c.state&stateOpen != 0 }
// Close closes the underlying socket. // Close closes the underlying socket.
func (c *conn) Close() error { func (c *Conn) Close() error {
if c.buf == nil { if !c.ok() {
return syscall.EINVAL return syscall.EINVAL
} }
c.buf = nil c.state &= ^stateOpen
return syscall.Close(c.fd) return c.f.Close()
}
// Recvfrom wraps recv(2) with nonblocking behaviour via the runtime network poller.
//
// The returned slice is valid until the next call to Recvfrom.
func (c *Conn) Recvfrom(
ctx context.Context,
flags int,
) (data []byte, from syscall.Sockaddr, err error) {
if err = c.f.SetReadDeadline(time.Time{}); err != nil {
return
}
var n int
data = c.buf[:]
done := make(chan error, 1)
go func() {
rcErr := c.raw.Read(func(fd uintptr) (done bool) {
n, from, err = syscall.Recvfrom(int(fd), data, flags)
return err != syscall.EWOULDBLOCK
})
if n >= 0 {
data = data[:n]
}
done <- rcErr
}()
select {
case rcErr := <-done:
if err != nil {
err = os.NewSyscallError("recvfrom", err)
} else {
err = rcErr
}
return
case <-ctx.Done():
cancelErr := c.f.SetReadDeadline(c.t)
<-done
if cancelErr != nil {
err = cancelErr
} else {
err = ctx.Err()
}
return
}
}
// Sendto wraps send(2) with nonblocking behaviour via the runtime network poller.
func (c *Conn) Sendto(
ctx context.Context,
p []byte,
flags int,
to syscall.Sockaddr,
) (err error) {
if err = c.f.SetWriteDeadline(time.Time{}); err != nil {
return
}
done := make(chan error, 1)
go func() {
done <- c.raw.Write(func(fd uintptr) (done bool) {
err = syscall.Sendto(int(fd), p, flags, to)
return err != syscall.EWOULDBLOCK
})
}()
select {
case rcErr := <-done:
if err != nil {
err = os.NewSyscallError("sendto", err)
} else {
err = rcErr
}
return
case <-ctx.Done():
cancelErr := c.f.SetWriteDeadline(c.t)
<-done
if cancelErr != nil {
err = cancelErr
} else {
err = ctx.Err()
}
return
}
} }
// Msg is type constraint for types sent over the wire via netlink. // Msg is type constraint for types sent over the wire via netlink.
@@ -88,7 +206,7 @@ func As[M Msg](data []byte) *M {
} }
// add queues a value to be sent by conn. // add queues a value to be sent by conn.
func add[M Msg](c *conn, p *M) bool { func add[M Msg](c *Conn, p *M) bool {
pos := c.pos pos := c.pos
c.pos += int(unsafe.Sizeof(*p)) c.pos += int(unsafe.Sizeof(*p))
if c.pos > len(c.buf) { if c.pos > len(c.buf) {
@@ -122,8 +240,16 @@ func (e *InconsistentError) Error() string {
return s return s
} }
// checkReply checks the message header of a reply from the kernel.
func (c *Conn) checkReply(header *syscall.NlMsghdr) error {
if header.Seq != c.seq || header.Pid != c.port {
return &InconsistentError{*header, c.seq, c.port}
}
return nil
}
// pending returns the valid slice of buf and initialises pos. // pending returns the valid slice of buf and initialises pos.
func (c *conn) pending() []byte { func (c *Conn) pending() []byte {
buf := c.buf[:c.pos] buf := c.buf[:c.pos]
c.pos = syscall.NLMSG_HDRLEN c.pos = syscall.NLMSG_HDRLEN
@@ -132,7 +258,7 @@ func (c *conn) pending() []byte {
Type: c.typ, Type: c.typ,
Flags: c.flags, Flags: c.flags,
Seq: c.seq, Seq: c.seq,
Pid: getpid(), Pid: c.port,
} }
return buf return buf
} }
@@ -143,44 +269,44 @@ type Complete struct{}
// Error returns a hardcoded string that should never be displayed to the user. // Error returns a hardcoded string that should never be displayed to the user.
func (Complete) Error() string { return "returning from roundtrip" } func (Complete) Error() string { return "returning from roundtrip" }
// HandlerFunc handles [syscall.NetlinkMessage] and returns a non-nil error to
// discontinue the receiving of more messages.
type HandlerFunc func(resp []syscall.NetlinkMessage) error
// receive receives from a socket with specified flags until a non-nil error is
// returned by f. An error of type [Complete] is returned as nil.
func (c *Conn) receive(ctx context.Context, f HandlerFunc, flags int) error {
for {
var resp []syscall.NetlinkMessage
if data, _, err := c.Recvfrom(ctx, flags); err != nil {
return err
} else if len(data) < syscall.NLMSG_HDRLEN {
return syscall.EBADE
} else if resp, err = syscall.ParseNetlinkMessage(data); err != nil {
return err
}
if err := f(resp); err != nil {
if err == (Complete{}) {
return nil
}
return err
}
}
}
// Roundtrip sends the pending message and handles the reply. // Roundtrip sends the pending message and handles the reply.
func (c *conn) Roundtrip(f func(msg *syscall.NetlinkMessage) error) error { func (c *Conn) Roundtrip(ctx context.Context, f HandlerFunc) error {
if c.buf == nil { if !c.ok() {
return syscall.EINVAL return syscall.EINVAL
} }
defer func() { c.seq++ }() defer func() { c.seq++ }()
if err := syscall.Sendto(c.fd, c.pending(), 0, &syscall.SockaddrNetlink{ if err := c.Sendto(ctx, c.pending(), 0, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK, Family: syscall.AF_NETLINK,
}); err != nil { }); err != nil {
return os.NewSyscallError("sendto", err) return err
} }
for { return c.receive(ctx, f, 0)
buf := c.buf
if n, _, err := syscall.Recvfrom(c.fd, buf, 0); err != nil {
return os.NewSyscallError("recvfrom", err)
} else if n < syscall.NLMSG_HDRLEN {
return syscall.EBADE
} else {
buf = buf[:n]
}
msgs, err := syscall.ParseNetlinkMessage(buf)
if err != nil {
return err
}
for _, msg := range msgs {
if msg.Header.Seq != c.seq || msg.Header.Pid != getpid() {
return &InconsistentError{msg.Header, c.seq, getpid()}
}
if err = f(&msg); err != nil {
if err == (Complete{}) {
return nil
}
return err
}
}
}
} }

View File

@@ -1,16 +1,13 @@
package netlink package netlink
import ( import (
"os"
"syscall" "syscall"
"testing" "testing"
) )
func init() { nlPidOnce.Do(func() {}); nlPid = 1 }
type payloadTestCase struct { type payloadTestCase struct {
name string name string
f func(c *conn) f func(c *Conn)
want []byte want []byte
} }
@@ -22,11 +19,9 @@ func checkPayload(t *testing.T, testCases []payloadTestCase) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
t.Helper()
c := conn{ c := Conn{port: 1, pos: syscall.NLMSG_HDRLEN}
pos: syscall.NLMSG_HDRLEN,
buf: make([]byte, os.Getpagesize()),
}
tc.f(&c) tc.f(&c)
if got := c.pending(); string(got) != string(tc.want) { if got := c.pending(); string(got) != string(tc.want) {
t.Errorf("pending: %#v, want %#v", got, tc.want) t.Errorf("pending: %#v, want %#v", got, tc.want)

View File

@@ -1,16 +1,20 @@
package netlink package netlink
import ( import (
"context"
"syscall" "syscall"
"unsafe" "unsafe"
) )
// RouteConn represents a NETLINK_ROUTE socket. // RouteConn represents a NETLINK_ROUTE socket.
type RouteConn struct{ *conn } type RouteConn struct{ conn *Conn }
// Close closes the underlying socket.
func (c *RouteConn) Close() error { return c.conn.Close() }
// DialRoute returns the address of a newly connected [RouteConn]. // DialRoute returns the address of a newly connected [RouteConn].
func DialRoute() (*RouteConn, error) { func DialRoute() (*RouteConn, error) {
c, err := dial(syscall.NETLINK_ROUTE) c, err := Dial(syscall.NETLINK_ROUTE, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -18,23 +22,27 @@ func DialRoute() (*RouteConn, error) {
} }
// rtnlConsume consumes a message from rtnetlink. // rtnlConsume consumes a message from rtnetlink.
func rtnlConsume(msg *syscall.NetlinkMessage) error { func (c *RouteConn) rtnlConsume(resp []syscall.NetlinkMessage) error {
switch msg.Header.Type { for i := range resp {
case syscall.NLMSG_DONE: if err := c.conn.checkReply(&resp[i].Header); err != nil {
return Complete{} return err
case syscall.NLMSG_ERROR:
if e := As[syscall.NlMsgerr](msg.Data); e != nil {
if e.Error == 0 {
return Complete{}
}
return syscall.Errno(-e.Error)
} }
return syscall.EBADE
default: switch resp[i].Header.Type {
return nil case syscall.NLMSG_DONE:
return Complete{}
case syscall.NLMSG_ERROR:
if e := As[syscall.NlMsgerr](resp[i].Data); e != nil {
if e.Error == 0 {
return Complete{}
}
return syscall.Errno(-e.Error)
}
return syscall.EBADE
}
} }
return nil
} }
// InAddr is equivalent to struct in_addr. // InAddr is equivalent to struct in_addr.
@@ -57,7 +65,7 @@ func (c *RouteConn) writeIfAddrmsg(
msg *syscall.IfAddrmsg, msg *syscall.IfAddrmsg,
attrs ...RtAttrMsg[InAddr], attrs ...RtAttrMsg[InAddr],
) bool { ) bool {
c.typ, c.flags = typ, syscall.NLM_F_REQUEST|syscall.NLM_F_ACK|flags c.conn.typ, c.conn.flags = typ, syscall.NLM_F_REQUEST|syscall.NLM_F_ACK|flags
if !add(c.conn, msg) { if !add(c.conn, msg) {
return false return false
} }
@@ -72,6 +80,7 @@ func (c *RouteConn) writeIfAddrmsg(
// SendIfAddrmsg sends an ifaddrmsg structure to rtnetlink. // SendIfAddrmsg sends an ifaddrmsg structure to rtnetlink.
func (c *RouteConn) SendIfAddrmsg( func (c *RouteConn) SendIfAddrmsg(
ctx context.Context,
typ, flags uint16, typ, flags uint16,
msg *syscall.IfAddrmsg, msg *syscall.IfAddrmsg,
attrs ...RtAttrMsg[InAddr], attrs ...RtAttrMsg[InAddr],
@@ -79,7 +88,7 @@ func (c *RouteConn) SendIfAddrmsg(
if !c.writeIfAddrmsg(typ, flags, msg, attrs...) { if !c.writeIfAddrmsg(typ, flags, msg, attrs...) {
return syscall.ENOMEM return syscall.ENOMEM
} }
return c.Roundtrip(rtnlConsume) return c.conn.Roundtrip(ctx, c.rtnlConsume)
} }
// writeNewaddrLo writes a RTM_NEWADDR message for the loopback address. // writeNewaddrLo writes a RTM_NEWADDR message for the loopback address.
@@ -104,11 +113,11 @@ func (c *RouteConn) writeNewaddrLo(lo uint32) bool {
} }
// SendNewaddrLo sends a RTM_NEWADDR message for the loopback address to the kernel. // SendNewaddrLo sends a RTM_NEWADDR message for the loopback address to the kernel.
func (c *RouteConn) SendNewaddrLo(lo uint32) error { func (c *RouteConn) SendNewaddrLo(ctx context.Context, lo uint32) error {
if !c.writeNewaddrLo(lo) { if !c.writeNewaddrLo(lo) {
return syscall.ENOMEM return syscall.ENOMEM
} }
return c.Roundtrip(rtnlConsume) return c.conn.Roundtrip(ctx, c.rtnlConsume)
} }
// writeIfInfomsg writes an ifinfomsg structure to conn. // writeIfInfomsg writes an ifinfomsg structure to conn.
@@ -116,17 +125,18 @@ func (c *RouteConn) writeIfInfomsg(
typ, flags uint16, typ, flags uint16,
msg *syscall.IfInfomsg, msg *syscall.IfInfomsg,
) bool { ) bool {
c.typ, c.flags = typ, syscall.NLM_F_REQUEST|syscall.NLM_F_ACK|flags c.conn.typ, c.conn.flags = typ, syscall.NLM_F_REQUEST|syscall.NLM_F_ACK|flags
return add(c.conn, msg) return add(c.conn, msg)
} }
// SendIfInfomsg sends an ifinfomsg structure to rtnetlink. // SendIfInfomsg sends an ifinfomsg structure to rtnetlink.
func (c *RouteConn) SendIfInfomsg( func (c *RouteConn) SendIfInfomsg(
ctx context.Context,
typ, flags uint16, typ, flags uint16,
msg *syscall.IfInfomsg, msg *syscall.IfInfomsg,
) error { ) error {
if !c.writeIfInfomsg(typ, flags, msg) { if !c.writeIfInfomsg(typ, flags, msg) {
return syscall.ENOMEM return syscall.ENOMEM
} }
return c.Roundtrip(rtnlConsume) return c.conn.Roundtrip(ctx, c.rtnlConsume)
} }

View File

@@ -9,7 +9,7 @@ func TestPayloadRTNETLINK(t *testing.T) {
t.Parallel() t.Parallel()
checkPayload(t, []payloadTestCase{ checkPayload(t, []payloadTestCase{
{"RTM_NEWADDR lo", func(c *conn) { {"RTM_NEWADDR lo", func(c *Conn) {
(&RouteConn{c}).writeNewaddrLo(1) (&RouteConn{c}).writeNewaddrLo(1)
}, []byte{ }, []byte{
/* Len */ 0x28, 0, 0, 0, /* Len */ 0x28, 0, 0, 0,
@@ -33,7 +33,7 @@ func TestPayloadRTNETLINK(t *testing.T) {
/* in_addr */ 127, 0, 0, 1, /* in_addr */ 127, 0, 0, 1,
}}, }},
{"RTM_NEWLINK", func(c *conn) { {"RTM_NEWLINK", func(c *Conn) {
c.seq++ c.seq++
(&RouteConn{c}).writeIfInfomsg( (&RouteConn{c}).writeIfInfomsg(
syscall.RTM_NEWLINK, 0, syscall.RTM_NEWLINK, 0,

View File

@@ -40,14 +40,17 @@ type ExecPath struct {
W bool W bool
} }
// SetSchedIdle is whether to set [std.SCHED_IDLE] scheduling priority. // SetSchedIdle is whether to set [ext.SCHED_IDLE] scheduling priority.
var SetSchedIdle bool var SetSchedIdle bool
// GetArtifactFunc is the function signature of [FContext.GetArtifact].
type GetArtifactFunc func(Artifact) (*check.Absolute, unique.Handle[Checksum])
// PromoteLayers returns artifacts with identical-by-content layers promoted to // PromoteLayers returns artifacts with identical-by-content layers promoted to
// the highest priority instance, as if mounted via [ExecPath]. // the highest priority instance, as if mounted via [ExecPath].
func PromoteLayers( func PromoteLayers(
artifacts []Artifact, artifacts []Artifact,
getArtifact func(Artifact) (*check.Absolute, unique.Handle[Checksum]), getArtifact GetArtifactFunc,
report func(i int, d Artifact), report func(i int, d Artifact),
) []*check.Absolute { ) []*check.Absolute {
layers := make([]*check.Absolute, 0, len(artifacts)) layers := make([]*check.Absolute, 0, len(artifacts))
@@ -67,14 +70,14 @@ func PromoteLayers(
} }
// layers returns pathnames collected from A deduplicated via [PromoteLayers]. // layers returns pathnames collected from A deduplicated via [PromoteLayers].
func (p *ExecPath) layers(f *FContext) []*check.Absolute { func (p *ExecPath) layers(
msg := f.GetMessage() msg message.Msg,
return PromoteLayers(p.A, f.GetArtifact, func(i int, d Artifact) { getArtifact GetArtifactFunc,
ident func(a Artifact) unique.Handle[ID],
) []*check.Absolute {
return PromoteLayers(p.A, getArtifact, func(i int, d Artifact) {
if msg.IsVerbose() { if msg.IsVerbose() {
msg.Verbosef( msg.Verbosef("promoted layer %d as %s", i, reportName(d, ident(d)))
"promoted layer %d as %s",
i, reportName(d, f.cache.Ident(d)),
)
} }
}) })
} }
@@ -382,17 +385,30 @@ func scanVerbose(
} }
} }
var (
// ErrInvalidPaths is returned for an [Artifact] of [KindExec] or
// [KindExecNet] specified with invalid paths.
ErrInvalidPaths = errors.New("invalid mount point")
)
// SeccompPresets is the [seccomp] presets used by exec artifacts. // SeccompPresets is the [seccomp] presets used by exec artifacts.
const SeccompPresets = std.PresetStrict & const SeccompPresets = std.PresetStrict &
^(std.PresetDenyNS | std.PresetDenyDevel) ^(std.PresetDenyNS | std.PresetDenyDevel)
// cure is like Cure but allows optional host net namespace. This is used for // makeContainer sets up the specified temp and work directories and returns the
// the [KnownChecksum] variant where networking is allowed. // corresponding [container.Container] that would have run for cure.
func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) { func (a *execArtifact) makeContainer(
ctx context.Context,
msg message.Msg,
hostNet bool,
temp, work *check.Absolute,
getArtifact GetArtifactFunc,
ident func(a Artifact) unique.Handle[ID],
) (z *container.Container, err error) {
overlayWorkIndex := -1 overlayWorkIndex := -1
for i, p := range a.paths { for i, p := range a.paths {
if p.P == nil || len(p.A) == 0 { if p.P == nil || len(p.A) == 0 {
return os.ErrInvalid return nil, ErrInvalidPaths
} }
if p.P.Is(AbsWork) { if p.P.Is(AbsWork) {
overlayWorkIndex = i overlayWorkIndex = i
@@ -404,10 +420,7 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
artifactCount += len(p.A) artifactCount += len(p.A)
} }
ctx, cancel := context.WithTimeout(f.Unwrap(), a.timeout) z = container.New(ctx, msg)
defer cancel()
z := container.New(ctx, f.GetMessage())
z.WaitDelay = execWaitDelay z.WaitDelay = execWaitDelay
z.SeccompPresets = SeccompPresets z.SeccompPresets = SeccompPresets
z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompFlags |= seccomp.AllowMultiarch
@@ -421,12 +434,183 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
} }
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1 z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
z.Dir, z.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args
z.Grow(len(a.paths) + 4)
for i, b := range a.paths {
if i == overlayWorkIndex {
if err = os.MkdirAll(work.String(), 0700); err != nil {
return
}
tempWork := temp.Append(".work")
if err = os.MkdirAll(tempWork.String(), 0700); err != nil {
return
}
z.Overlay(
AbsWork,
work,
tempWork,
b.layers(msg, getArtifact, ident)...,
)
continue
}
if a.paths[i].W {
tempUpper, tempWork := temp.Append(
".upper", strconv.Itoa(i),
), temp.Append(
".work", strconv.Itoa(i),
)
if err = os.MkdirAll(tempUpper.String(), 0700); err != nil {
return
}
if err = os.MkdirAll(tempWork.String(), 0700); err != nil {
return
}
z.Overlay(b.P, tempUpper, tempWork, b.layers(msg, getArtifact, ident)...)
} else if len(b.A) == 1 {
pathname, _ := getArtifact(b.A[0])
z.Bind(pathname, b.P, 0)
} else {
z.OverlayReadonly(b.P, b.layers(msg, getArtifact, ident)...)
}
}
if overlayWorkIndex < 0 {
z.Bind(
work,
AbsWork,
std.BindWritable|std.BindEnsure,
)
}
z.Bind(
temp,
fhs.AbsTmp,
std.BindWritable|std.BindEnsure,
)
z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
return
}
var (
// ErrExecBusy is returned entering [Cache.EnterExec] while another
// goroutine has not yet returned from it.
ErrExecBusy = errors.New("scratch directories in use")
// ErrNotExec is returned for unsupported implementations of [Artifact]
// passed to [Cache.EnterExec].
ErrNotExec = errors.New("attempting to run a non-exec artifact")
)
// EnterExec runs the container of an [Artifact] of [KindExec] or [KindExecNet]
// with its entry point, argument, and standard streams replaced with values
// supplied by the caller.
func (c *Cache) EnterExec(
ctx context.Context,
a Artifact,
retainSession bool,
stdin io.Reader,
stdout, stderr io.Writer,
path *check.Absolute,
args ...string,
) (err error) {
if !c.inExec.CompareAndSwap(false, true) {
return ErrExecBusy
}
defer c.inExec.Store(false)
var hostNet bool
var e *execArtifact
switch f := a.(type) {
case *execArtifact:
e = f
case *execNetArtifact:
e = &f.execArtifact
hostNet = true
default:
return ErrNotExec
}
deps := Collect(a.Dependencies())
if _, _, err = c.Cure(&deps); err == nil {
return errors.New("unreachable")
} else if !IsCollected(err) {
return
}
dm := make(map[Artifact]cureRes)
for i, p := range deps {
var res cureRes
res.pathname, res.checksum, err = c.Cure(p)
if err != nil {
return
}
dm[deps[i]] = res
}
scratch := c.base.Append(dirExecScratch)
temp, work := scratch.Append("temp"), scratch.Append("work")
// work created during makeContainer
if err = os.MkdirAll(temp.String(), 0700); err != nil {
return
}
defer func() {
if chmodErr, removeErr := removeAll(scratch); chmodErr != nil || removeErr != nil {
err = errors.Join(err, chmodErr, removeErr)
}
}()
var z *container.Container
z, err = e.makeContainer(
ctx, c.msg,
hostNet,
temp, work,
func(a Artifact) (*check.Absolute, unique.Handle[Checksum]) {
if res, ok := dm[a]; ok {
return res.pathname, res.checksum
}
panic(InvalidLookupError(c.Ident(a).Value()))
},
c.Ident,
)
if err != nil {
return
}
z.Stdin, z.Stdout, z.Stderr = stdin, stdout, stderr
z.Path, z.Args = path, args
z.RetainSession = retainSession
if err = z.Start(); err != nil {
return
}
if err = z.Serve(); err != nil {
return
}
return z.Wait()
}
// cure is like Cure but allows optional host net namespace.
func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
ctx, cancel := context.WithTimeout(f.Unwrap(), a.timeout)
defer cancel()
msg := f.GetMessage()
var z *container.Container
if z, err = a.makeContainer(
ctx, msg, hostNet,
f.GetTempDir(), f.GetWorkDir(),
f.GetArtifact,
f.cache.Ident,
); err != nil {
return
}
var status io.Writer var status io.Writer
if status, err = f.GetStatusWriter(); err != nil { if status, err = f.GetStatusWriter(); err != nil {
return return
} }
if msg := f.GetMessage(); msg.IsVerbose() { if msg.IsVerbose() {
var stdout, stderr io.ReadCloser var stdout, stderr io.ReadCloser
if stdout, err = z.StdoutPipe(); err != nil { if stdout, err = z.StdoutPipe(); err != nil {
return return
@@ -464,62 +648,6 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
z.Stdout, z.Stderr = status, status z.Stdout, z.Stderr = status, status
} }
z.Dir, z.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args
z.Grow(len(a.paths) + 4)
temp, work := f.GetTempDir(), f.GetWorkDir()
for i, b := range a.paths {
if i == overlayWorkIndex {
if err = os.MkdirAll(work.String(), 0700); err != nil {
return
}
tempWork := temp.Append(".work")
if err = os.MkdirAll(tempWork.String(), 0700); err != nil {
return
}
z.Overlay(
AbsWork,
work,
tempWork,
b.layers(f)...,
)
continue
}
if a.paths[i].W {
tempUpper, tempWork := temp.Append(
".upper", strconv.Itoa(i),
), temp.Append(
".work", strconv.Itoa(i),
)
if err = os.MkdirAll(tempUpper.String(), 0700); err != nil {
return
}
if err = os.MkdirAll(tempWork.String(), 0700); err != nil {
return
}
z.Overlay(b.P, tempUpper, tempWork, b.layers(f)...)
} else if len(b.A) == 1 {
pathname, _ := f.GetArtifact(b.A[0])
z.Bind(pathname, b.P, 0)
} else {
z.OverlayReadonly(b.P, b.layers(f)...)
}
}
if overlayWorkIndex < 0 {
z.Bind(
work,
AbsWork,
std.BindWritable|std.BindEnsure,
)
}
z.Bind(
f.GetTempDir(),
fhs.AbsTmp,
std.BindWritable|std.BindEnsure,
)
z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
if err = z.Start(); err != nil { if err = z.Start(); err != nil {
return return
} }
@@ -532,7 +660,7 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
// do not allow empty directories to succeed // do not allow empty directories to succeed
for { for {
err = syscall.Rmdir(work.String()) err = syscall.Rmdir(f.GetWorkDir().String())
if err != syscall.EINTR { if err != syscall.EINTR {
break break
} }

View File

@@ -92,7 +92,7 @@ func TestExec(t *testing.T) {
[]string{"testtool"}, []string{"testtool"},
pkg.ExecPath{}, pkg.ExecPath{},
), nil, pkg.Checksum{}, os.ErrInvalid}, ), nil, pkg.Checksum{}, pkg.ErrInvalidPaths},
}) })
// check init failure passthrough // check init failure passthrough

View File

@@ -22,6 +22,7 @@ import (
"slices" "slices"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"testing" "testing"
"unique" "unique"
@@ -366,7 +367,7 @@ type TrivialArtifact interface {
} }
// KnownIdent is optionally implemented by [Artifact] and is used instead of // KnownIdent is optionally implemented by [Artifact] and is used instead of
// [Kind.Ident] when it is available. // [Cache.Ident] when it is available.
// //
// This is very subtle to use correctly. The implementation must ensure that // This is very subtle to use correctly. The implementation must ensure that
// this value is globally unique, otherwise [Cache] can enter an inconsistent // this value is globally unique, otherwise [Cache] can enter an inconsistent
@@ -439,6 +440,11 @@ const (
KindCustomOffset = 1 << 31 KindCustomOffset = 1 << 31
) )
const (
// kindCollection is the kind of [Collect]. It never cures successfully.
kindCollection Kind = KindCustomOffset - 1 - iota
)
const ( const (
// fileLock is the file name appended to Cache.base for guaranteeing // fileLock is the file name appended to Cache.base for guaranteeing
// exclusive access to the cache directory. // exclusive access to the cache directory.
@@ -461,6 +467,11 @@ const (
// pathnames allocated during [Cache.Cure]. // pathnames allocated during [Cache.Cure].
dirTemp = "temp" dirTemp = "temp"
// dirExecScratch is the directory name appended to Cache.base for scratch
// space setting up the container started by [Cache.EnterExec]. Exclusivity
// via Cache.inExec.
dirExecScratch = "scratch"
// checksumLinknamePrefix is prepended to the encoded [Checksum] value // checksumLinknamePrefix is prepended to the encoded [Checksum] value
// of an [Artifact] when creating a symbolic link to dirChecksum. // of an [Artifact] when creating a symbolic link to dirChecksum.
checksumLinknamePrefix = "../" + dirChecksum + "/" checksumLinknamePrefix = "../" + dirChecksum + "/"
@@ -476,7 +487,7 @@ type cureRes struct {
// subject to the cures limit. Values pointed to by result addresses are safe // subject to the cures limit. Values pointed to by result addresses are safe
// to access after the [sync.WaitGroup] associated with this pendingArtifactDep // to access after the [sync.WaitGroup] associated with this pendingArtifactDep
// is done. pendingArtifactDep must not be reused or modified after it is sent // is done. pendingArtifactDep must not be reused or modified after it is sent
// to Cache.cureDep. // to cure.
type pendingArtifactDep struct { type pendingArtifactDep struct {
// Dependency artifact populated during [Cache.Cure]. // Dependency artifact populated during [Cache.Cure].
a Artifact a Artifact
@@ -548,6 +559,9 @@ type Cache struct {
unlock func() unlock func()
// Synchronises calls to Close. // Synchronises calls to Close.
closeOnce sync.Once closeOnce sync.Once
// Whether EnterExec has not yet returned.
inExec atomic.Bool
} }
// IsStrict returns whether the [Cache] strictly verifies checksums. // IsStrict returns whether the [Cache] strictly verifies checksums.
@@ -1890,3 +1904,33 @@ func open(
return &c, nil return &c, nil
} }
// Collected is returned by [Collect.Cure] to indicate a successful collection.
type Collected struct{}
// Error returns a constant string to satisfy error, but should never be seen
// by the user.
func (Collected) Error() string { return "artifacts successfully collected" }
// IsCollected returns whether the underlying error contains that of the result
// of curing a [Collect] helper.
func IsCollected(err error) bool { return errors.As(err, new(Collected)) }
// Collect implements [pkg.FloodArtifact] to concurrently cure multiple
// [pkg.Artifact]. It returns [Collected].
type Collect []Artifact
// Cure returns [Collected].
func (*Collect) Cure(*FContext) error { return Collected{} }
// Kind returns the hardcoded [pkg.Kind] value.
func (*Collect) Kind() Kind { return kindCollection }
// Params is a noop: dependencies are already represented in the header.
func (*Collect) Params(*IContext) {}
// Dependencies returns [Collect] as is.
func (c *Collect) Dependencies() []Artifact { return *c }
// IsExclusive returns false: Cure is a noop.
func (*Collect) IsExclusive() bool { return false }

View File

@@ -13,7 +13,7 @@ func (t Toolchain) newAttr() (pkg.Artifact, string) {
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, pkg.TarGzip,
), &PackageAttr{ ), &PackageAttr{
Patches: [][2]string{ Patches: []KV{
{"libgen-basename", `From 8a80d895dfd779373363c3a4b62ecce5a549efb2 Mon Sep 17 00:00:00 2001 {"libgen-basename", `From 8a80d895dfd779373363c3a4b62ecce5a549efb2 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Sat, 30 Mar 2024 10:17:10 +0100 Date: Sat, 30 Mar 2024 10:17:10 +0100

View File

@@ -10,8 +10,8 @@ import (
func (t Toolchain) newCMake() (pkg.Artifact, string) { func (t Toolchain) newCMake() (pkg.Artifact, string) {
const ( const (
version = "4.2.3" version = "4.3.0"
checksum = "Y4uYGnLrDQX78UdzH7fMzfok46Nt_1taDIHSmqgboU1yFi6f0iAXBDegMCu4eS-J" checksum = "amBtnY2eGsEdlrB-cTRuOESBTsIqtyaxWlEKNlnp2EWLwAKWINjssilo4KXE6El9"
) )
return t.NewPackage("cmake", version, pkg.NewHTTPGetTar( return t.NewPackage("cmake", version, pkg.NewHTTPGetTar(
nil, "https://github.com/Kitware/CMake/releases/download/"+ nil, "https://github.com/Kitware/CMake/releases/download/"+
@@ -25,7 +25,7 @@ func (t Toolchain) newCMake() (pkg.Artifact, string) {
// expected to be writable in the copy made during bootstrap // expected to be writable in the copy made during bootstrap
Chmod: true, Chmod: true,
Patches: [][2]string{ Patches: []KV{
{"bootstrap-test-no-openssl", `diff --git a/Tests/BootstrapTest.cmake b/Tests/BootstrapTest.cmake {"bootstrap-test-no-openssl", `diff --git a/Tests/BootstrapTest.cmake b/Tests/BootstrapTest.cmake
index 137de78bc1..b4da52e664 100644 index 137de78bc1..b4da52e664 100644
--- a/Tests/BootstrapTest.cmake --- a/Tests/BootstrapTest.cmake
@@ -88,7 +88,7 @@ index 2ead810437..f85cbb8b1c 100644
OmitDefaults: true, OmitDefaults: true,
ConfigureName: "/usr/src/cmake/bootstrap", ConfigureName: "/usr/src/cmake/bootstrap",
Configure: [][2]string{ Configure: []KV{
{"prefix", "/system"}, {"prefix", "/system"},
{"parallel", `"$(nproc)"`}, {"parallel", `"$(nproc)"`},
{"--"}, {"--"},
@@ -125,7 +125,7 @@ type CMakeHelper struct {
Append []string Append []string
// CMake CACHE entries. // CMake CACHE entries.
Cache [][2]string Cache []KV
// Runs after install. // Runs after install.
Script string Script string
@@ -170,7 +170,7 @@ func (*CMakeHelper) wantsDir() string { return "/cure/" }
func (attr *CMakeHelper) script(name string) string { func (attr *CMakeHelper) script(name string) string {
if attr == nil { if attr == nil {
attr = &CMakeHelper{ attr = &CMakeHelper{
Cache: [][2]string{ Cache: []KV{
{"CMAKE_BUILD_TYPE", "Release"}, {"CMAKE_BUILD_TYPE", "Release"},
}, },
} }

View File

@@ -18,7 +18,7 @@ func (t Toolchain) newCurl() (pkg.Artifact, string) {
chmod +w tests/data && rm tests/data/test459 chmod +w tests/data && rm tests/data/test459
`, `,
}, &MakeHelper{ }, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
{"with-openssl"}, {"with-openssl"},
{"with-ca-bundle", "/system/etc/ssl/certs/ca-bundle.crt"}, {"with-ca-bundle", "/system/etc/ssl/certs/ca-bundle.crt"},

View File

@@ -18,7 +18,7 @@ func (t Toolchain) newDTC() (pkg.Artifact, string) {
Writable: true, Writable: true,
Chmod: true, Chmod: true,
}, &MesonHelper{ }, &MesonHelper{
Setup: [][2]string{ Setup: []KV{
{"Dyaml", "disabled"}, {"Dyaml", "disabled"},
{"Dstatic-build", "true"}, {"Dstatic-build", "true"},
}, },

View File

@@ -22,7 +22,7 @@ func (t Toolchain) newElfutils() (pkg.Artifact, string) {
// nonstandard glibc extension // nonstandard glibc extension
SkipCheck: true, SkipCheck: true,
Configure: [][2]string{ Configure: []KV{
{"enable-deterministic-archives"}, {"enable-deterministic-archives"},
}, },
}, },

View File

@@ -25,7 +25,7 @@ func (a cureEtc) Cure(t *pkg.FContext) (err error) {
if err = os.MkdirAll(etc.String(), 0700); err != nil { if err = os.MkdirAll(etc.String(), 0700); err != nil {
return return
} }
for _, f := range [][2]string{ for _, f := range []KV{
{"hosts", "127.0.0.1 localhost cure cure-net\n"}, {"hosts", "127.0.0.1 localhost cure cure-net\n"},
{"passwd", `root:x:0:0:System administrator:/proc/nonexistent:/bin/sh {"passwd", `root:x:0:0:System administrator:/proc/nonexistent:/bin/sh
cure:x:1023:1023:Cure:/usr/src:/bin/sh cure:x:1023:1023:Cure:/usr/src:/bin/sh

View File

@@ -13,7 +13,7 @@ func (t Toolchain) newFakeroot() (pkg.Artifact, string) {
mustDecode(checksum), mustDecode(checksum),
pkg.TarBzip2, pkg.TarBzip2,
), &PackageAttr{ ), &PackageAttr{
Patches: [][2]string{ Patches: []KV{
{"remove-broken-docs", `diff --git a/doc/Makefile.am b/doc/Makefile.am {"remove-broken-docs", `diff --git a/doc/Makefile.am b/doc/Makefile.am
index f135ad9..85c784c 100644 index f135ad9..85c784c 100644
--- a/doc/Makefile.am --- a/doc/Makefile.am

View File

@@ -4,8 +4,8 @@ import "hakurei.app/internal/pkg"
func (t Toolchain) newFuse() (pkg.Artifact, string) { func (t Toolchain) newFuse() (pkg.Artifact, string) {
const ( const (
version = "3.18.1" version = "3.18.2"
checksum = "COb-BgJRWXLbt9XUkNeuiroQizpMifXqxgieE1SlkMXhs_WGSyJStrmyewAw2hd6" checksum = "iL-7b7eUtmlVSf5cSq0dzow3UiqSjBmzV3cI_ENPs1tXcHdktkG45j1V12h-4jZe"
) )
return t.NewPackage("fuse", version, pkg.NewHTTPGetTar( return t.NewPackage("fuse", version, pkg.NewHTTPGetTar(
nil, "https://github.com/libfuse/libfuse/releases/download/"+ nil, "https://github.com/libfuse/libfuse/releases/download/"+
@@ -13,7 +13,7 @@ func (t Toolchain) newFuse() (pkg.Artifact, string) {
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, pkg.TarGzip,
), nil, &MesonHelper{ ), nil, &MesonHelper{
Setup: [][2]string{ Setup: []KV{
{"Ddefault_library", "both"}, {"Ddefault_library", "both"},
{"Dtests", "true"}, {"Dtests", "true"},
{"Duseroot", "false"}, {"Duseroot", "false"},

View File

@@ -88,8 +88,8 @@ func init() {
func (t Toolchain) newAutoconf() (pkg.Artifact, string) { func (t Toolchain) newAutoconf() (pkg.Artifact, string) {
const ( const (
version = "2.72" version = "2.73"
checksum = "-c5blYkC-xLDer3TWEqJTyh1RLbOd1c5dnRLKsDnIrg_wWNOLBpaqMY8FvmUFJ33" checksum = "yGabDTeOfaCUB0JX-h3REYLYzMzvpDwFmFFzHNR7QilChCUNE4hR6q7nma4viDYg"
) )
return t.NewPackage("autoconf", version, pkg.NewHTTPGetTar( return t.NewPackage("autoconf", version, pkg.NewHTTPGetTar(
nil, "https://ftpmirror.gnu.org/gnu/autoconf/autoconf-"+version+".tar.gz", nil, "https://ftpmirror.gnu.org/gnu/autoconf/autoconf-"+version+".tar.gz",
@@ -351,7 +351,7 @@ func (t Toolchain) newBash() (pkg.Artifact, string) {
Flag: TEarly, Flag: TEarly,
}, &MakeHelper{ }, &MakeHelper{
Script: "ln -s bash /work/system/bin/sh\n", Script: "ln -s bash /work/system/bin/sh\n",
Configure: [][2]string{ Configure: []KV{
{"without-bash-malloc"}, {"without-bash-malloc"},
}, },
}), version }), version
@@ -390,7 +390,7 @@ test_disable 'int main(){return 0;}' gnulib-tests/test-fchownat.c
test_disable 'int main(){return 0;}' gnulib-tests/test-lchown.c test_disable 'int main(){return 0;}' gnulib-tests/test-lchown.c
`, `,
Patches: [][2]string{ Patches: []KV{
{"tests-fix-job-control", `From 21d287324aa43aa3a31f39619ade0deac7fd6013 Mon Sep 17 00:00:00 2001 {"tests-fix-job-control", `From 21d287324aa43aa3a31f39619ade0deac7fd6013 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <P@draigBrady.com> From: =?UTF-8?q?P=C3=A1draig=20Brady?= <P@draigBrady.com>
Date: Tue, 24 Feb 2026 15:44:41 +0000 Date: Tue, 24 Feb 2026 15:44:41 +0000
@@ -485,7 +485,7 @@ index 9a395416b..fbb043312 100755
Flag: TEarly, Flag: TEarly,
}, &MakeHelper{ }, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
{"enable-single-binary", "symlinks"}, {"enable-single-binary", "symlinks"},
}, },
}, },
@@ -720,7 +720,7 @@ func (t Toolchain) newTar() (pkg.Artifact, string) {
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, pkg.TarGzip,
), nil, &MakeHelper{ ), nil, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
{"disable-acl"}, {"disable-acl"},
{"without-posix-acls"}, {"without-posix-acls"},
{"without-xattrs"}, {"without-xattrs"},
@@ -903,7 +903,7 @@ func (t Toolchain) newGCC() (pkg.Artifact, string) {
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, pkg.TarGzip,
), &PackageAttr{ ), &PackageAttr{
Patches: [][2]string{ Patches: []KV{
{"musl-off64_t-loff_t", `diff --git a/libgo/sysinfo.c b/libgo/sysinfo.c {"musl-off64_t-loff_t", `diff --git a/libgo/sysinfo.c b/libgo/sysinfo.c
index 180f5c31d74..44d7ea73f7d 100644 index 180f5c31d74..44d7ea73f7d 100644
--- a/libgo/sysinfo.c --- a/libgo/sysinfo.c
@@ -1062,7 +1062,7 @@ ln -s system/lib /work/
// it also saturates the CPU for a consequential amount of time. // it also saturates the CPU for a consequential amount of time.
Flag: TExclusive, Flag: TExclusive,
}, &MakeHelper{ }, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
{"disable-multilib"}, {"disable-multilib"},
{"with-multilib-list", `""`}, {"with-multilib-list", `""`},
{"enable-default-pie"}, {"enable-default-pie"},

View File

@@ -135,7 +135,8 @@ sed -i \
cmd/link/internal/`+runtime.GOARCH+`/obj.go cmd/link/internal/`+runtime.GOARCH+`/obj.go
rm \ rm \
os/root_unix_test.go os/root_unix_test.go \
net/smtp/smtp_test.go
`, go123, `, go123,
) )

View File

@@ -35,7 +35,7 @@ func (t Toolchain) newGLib() (pkg.Artifact, string) {
)), )),
}, },
}, &MesonHelper{ }, &MesonHelper{
Setup: [][2]string{ Setup: []KV{
{"Ddefault_library", "both"}, {"Ddefault_library", "both"},
}, },
}, },

View File

@@ -66,7 +66,7 @@ mkdir -p /work/system/libexec/hakurei/
echo '# Building hakurei.' echo '# Building hakurei.'
go generate -v ./... go generate -v ./...
go build -trimpath -v -o /work/system/libexec/hakurei -ldflags="-s -w go build -trimpath -v -tags=rosa -o /work/system/libexec/hakurei -ldflags="-s -w
-buildid= -buildid=
-linkmode external -linkmode external
-extldflags=-static -extldflags=-static

View File

@@ -23,4 +23,4 @@ var hakureiSource = pkg.NewTar(pkg.NewFile(
), pkg.TarGzip) ), pkg.TarGzip)
// hakureiPatches are patches applied against the compile-time source tree. // hakureiPatches are patches applied against the compile-time source tree.
var hakureiPatches [][2]string var hakureiPatches []KV

View File

@@ -15,4 +15,4 @@ var hakureiSource = pkg.NewHTTPGetTar(
) )
// hakureiPatches are patches applied against a hakurei release. // hakureiPatches are patches applied against a hakurei release.
var hakureiPatches [][2]string var hakureiPatches []KV

View File

@@ -2,12 +2,12 @@ package rosa
import "hakurei.app/internal/pkg" import "hakurei.app/internal/pkg"
const kernelVersion = "6.12.77" const kernelVersion = "6.12.78"
var kernelSource = pkg.NewHTTPGetTar( var kernelSource = pkg.NewHTTPGetTar(
nil, "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/"+ nil, "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/"+
"snapshot/linux-"+kernelVersion+".tar.gz", "snapshot/linux-"+kernelVersion+".tar.gz",
mustDecode("_MyFL0MqqNwAJx4fP8L9FkUayXIqEJto5trAPr_9UJvaT5TK1tvlU8leS82Hw2uw"), mustDecode("iUlZA-nv04TUOL0TmgDGBjaOe0sIaXTqLvuR4owYgHMZM8vecusnMMqbeuuZP4_G"),
pkg.TarGzip, pkg.TarGzip,
) )
@@ -90,7 +90,7 @@ exec /system/sbin/depmod -m /lib/modules "$@"
`))), `))),
}, },
Patches: [][2]string{ Patches: []KV{
{"f54a91f5337cd918eb86cf600320d25b6cfd8209", `From f54a91f5337cd918eb86cf600320d25b6cfd8209 Mon Sep 17 00:00:00 2001 {"f54a91f5337cd918eb86cf600320d25b6cfd8209", `From f54a91f5337cd918eb86cf600320d25b6cfd8209 Mon Sep 17 00:00:00 2001
From: Nathan Chancellor <nathan@kernel.org> From: Nathan Chancellor <nathan@kernel.org>
Date: Sat, 13 Dec 2025 19:58:10 +0900 Date: Sat, 13 Dec 2025 19:58:10 +0900

View File

@@ -1,16 +1,16 @@
# #
# Automatically generated file; DO NOT EDIT. # Automatically generated file; DO NOT EDIT.
# Linux/x86 6.12.76 Kernel Configuration # Linux/x86 6.12.78 Kernel Configuration
# #
CONFIG_CC_VERSION_TEXT="clang version 22.1.1" CONFIG_CC_VERSION_TEXT="clang version 22.1.2"
CONFIG_GCC_VERSION=0 CONFIG_GCC_VERSION=0
CONFIG_CC_IS_CLANG=y CONFIG_CC_IS_CLANG=y
CONFIG_CLANG_VERSION=220101 CONFIG_CLANG_VERSION=220102
CONFIG_AS_IS_LLVM=y CONFIG_AS_IS_LLVM=y
CONFIG_AS_VERSION=220101 CONFIG_AS_VERSION=220102
CONFIG_LD_VERSION=0 CONFIG_LD_VERSION=0
CONFIG_LD_IS_LLD=y CONFIG_LD_IS_LLD=y
CONFIG_LLD_VERSION=220101 CONFIG_LLD_VERSION=220102
CONFIG_RUSTC_VERSION=0 CONFIG_RUSTC_VERSION=0
CONFIG_RUSTC_LLVM_VERSION=0 CONFIG_RUSTC_LLVM_VERSION=0
CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y

View File

@@ -1,16 +1,16 @@
# #
# Automatically generated file; DO NOT EDIT. # Automatically generated file; DO NOT EDIT.
# Linux/arm64 6.12.76 Kernel Configuration # Linux/arm64 6.12.78 Kernel Configuration
# #
CONFIG_CC_VERSION_TEXT="clang version 22.1.1" CONFIG_CC_VERSION_TEXT="clang version 21.1.8"
CONFIG_GCC_VERSION=0 CONFIG_GCC_VERSION=0
CONFIG_CC_IS_CLANG=y CONFIG_CC_IS_CLANG=y
CONFIG_CLANG_VERSION=220101 CONFIG_CLANG_VERSION=210108
CONFIG_AS_IS_LLVM=y CONFIG_AS_IS_LLVM=y
CONFIG_AS_VERSION=220101 CONFIG_AS_VERSION=210108
CONFIG_LD_VERSION=0 CONFIG_LD_VERSION=0
CONFIG_LD_IS_LLD=y CONFIG_LD_IS_LLD=y
CONFIG_LLD_VERSION=220101 CONFIG_LLD_VERSION=210108
CONFIG_RUSTC_VERSION=0 CONFIG_RUSTC_VERSION=0
CONFIG_RUSTC_LLVM_VERSION=0 CONFIG_RUSTC_LLVM_VERSION=0
CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y
@@ -73,6 +73,7 @@ CONFIG_IRQ_MSI_IOMMU=y
CONFIG_IRQ_FORCED_THREADING=y CONFIG_IRQ_FORCED_THREADING=y
CONFIG_SPARSE_IRQ=y CONFIG_SPARSE_IRQ=y
# CONFIG_GENERIC_IRQ_DEBUGFS is not set # CONFIG_GENERIC_IRQ_DEBUGFS is not set
CONFIG_GENERIC_IRQ_KEXEC_CLEAR_VM_FORWARD=y
# end of IRQ subsystem # end of IRQ subsystem
CONFIG_GENERIC_TIME_VSYSCALL=y CONFIG_GENERIC_TIME_VSYSCALL=y

View File

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

View File

@@ -8,8 +8,8 @@ import (
func (t Toolchain) newLibexpat() (pkg.Artifact, string) { func (t Toolchain) newLibexpat() (pkg.Artifact, string) {
const ( const (
version = "2.7.4" version = "2.7.5"
checksum = "W6NI2FESBjrTqRPcvs15fK5c3nwF6f9RT8U-XHKQKblXVzJB3nt_ez5B5jO0ZVDG" checksum = "vTRUjjg-qbHSXUBYKXgzVHkUO7UNyuhrkSYrE7ikApQm0g-OvQ8tspw4w55M-1Tp"
) )
return t.NewPackage("libexpat", version, pkg.NewHTTPGetTar( return t.NewPackage("libexpat", version, pkg.NewHTTPGetTar(
nil, "https://github.com/libexpat/libexpat/releases/download/"+ nil, "https://github.com/libexpat/libexpat/releases/download/"+

View File

@@ -16,6 +16,23 @@ func (t Toolchain) newLibseccomp() (pkg.Artifact, string) {
ScriptEarly: ` ScriptEarly: `
ln -s ../system/bin/bash /bin/ ln -s ../system/bin/bash /bin/
`, `,
Patches: []KV{
{"fix-export-oob-read", `diff --git a/src/api.c b/src/api.c
index adccef3..65a277a 100644
--- a/src/api.c
+++ b/src/api.c
@@ -786,7 +786,7 @@ API int seccomp_export_bpf_mem(const scmp_filter_ctx ctx, void *buf,
if (BPF_PGM_SIZE(program) > *len)
rc = _rc_filter(-ERANGE);
else
- memcpy(buf, program->blks, *len);
+ memcpy(buf, program->blks, BPF_PGM_SIZE(program));
}
*len = BPF_PGM_SIZE(program);
`},
},
}, (*MakeHelper)(nil), }, (*MakeHelper)(nil),
Bash, Bash,
Diffutils, Diffutils,

View File

@@ -19,7 +19,7 @@ type llvmAttr struct {
// Concatenated with default environment for PackageAttr.Env. // Concatenated with default environment for PackageAttr.Env.
env []string env []string
// Concatenated with generated entries for CMakeHelper.Cache. // Concatenated with generated entries for CMakeHelper.Cache.
cmake [][2]string cmake []KV
// Override CMakeHelper.Append. // Override CMakeHelper.Append.
append []string append []string
// Passed through to PackageAttr.NonStage0. // Passed through to PackageAttr.NonStage0.
@@ -30,7 +30,7 @@ type llvmAttr struct {
script string script string
// Patch name and body pairs. // Patch name and body pairs.
patches [][2]string patches []KV
} }
const ( const (
@@ -94,43 +94,45 @@ func (t Toolchain) newLLVMVariant(variant string, attr *llvmAttr) pkg.Artifact {
var script string var script string
cache := [][2]string{ cache := []KV{
{"CMAKE_BUILD_TYPE", "Release"}, {"CMAKE_BUILD_TYPE", "Release"},
{"LLVM_HOST_TRIPLE", `"${ROSA_TRIPLE}"`}, {"LLVM_HOST_TRIPLE", `"${ROSA_TRIPLE}"`},
{"LLVM_DEFAULT_TARGET_TRIPLE", `"${ROSA_TRIPLE}"`}, {"LLVM_DEFAULT_TARGET_TRIPLE", `"${ROSA_TRIPLE}"`},
} }
if len(projects) > 0 { if len(projects) > 0 {
cache = append(cache, cache = append(cache, []KV{
[2]string{"LLVM_ENABLE_PROJECTS", `"${ROSA_LLVM_PROJECTS}"`}) {"LLVM_ENABLE_PROJECTS", `"${ROSA_LLVM_PROJECTS}"`},
}...)
} }
if len(runtimes) > 0 { if len(runtimes) > 0 {
cache = append(cache, cache = append(cache, []KV{
[2]string{"LLVM_ENABLE_RUNTIMES", `"${ROSA_LLVM_RUNTIMES}"`}) {"LLVM_ENABLE_RUNTIMES", `"${ROSA_LLVM_RUNTIMES}"`},
}...)
} }
cmakeAppend := []string{"llvm"} cmakeAppend := []string{"llvm"}
if attr.append != nil { if attr.append != nil {
cmakeAppend = attr.append cmakeAppend = attr.append
} else { } else {
cache = append(cache, cache = append(cache, []KV{
[2]string{"LLVM_ENABLE_LIBCXX", "ON"}, {"LLVM_ENABLE_LIBCXX", "ON"},
[2]string{"LLVM_USE_LINKER", "lld"}, {"LLVM_USE_LINKER", "lld"},
[2]string{"LLVM_INSTALL_BINUTILS_SYMLINKS", "ON"}, {"LLVM_INSTALL_BINUTILS_SYMLINKS", "ON"},
[2]string{"LLVM_INSTALL_CCTOOLS_SYMLINKS", "ON"}, {"LLVM_INSTALL_CCTOOLS_SYMLINKS", "ON"},
[2]string{"LLVM_LIT_ARGS", "'--verbose'"}, {"LLVM_LIT_ARGS", "'--verbose'"},
) }...)
} }
if attr.flags&llvmProjectClang != 0 { if attr.flags&llvmProjectClang != 0 {
cache = append(cache, cache = append(cache, []KV{
[2]string{"CLANG_DEFAULT_LINKER", "lld"}, {"CLANG_DEFAULT_LINKER", "lld"},
[2]string{"CLANG_DEFAULT_CXX_STDLIB", "libc++"}, {"CLANG_DEFAULT_CXX_STDLIB", "libc++"},
[2]string{"CLANG_DEFAULT_RTLIB", "compiler-rt"}, {"CLANG_DEFAULT_RTLIB", "compiler-rt"},
[2]string{"CLANG_DEFAULT_UNWINDLIB", "libunwind"}, {"CLANG_DEFAULT_UNWINDLIB", "libunwind"},
) }...)
} }
if attr.flags&llvmProjectLld != 0 { if attr.flags&llvmProjectLld != 0 {
script += ` script += `
@@ -139,25 +141,27 @@ ln -s ld.lld /work/system/bin/ld
} }
if attr.flags&llvmRuntimeCompilerRT != 0 { if attr.flags&llvmRuntimeCompilerRT != 0 {
if attr.append == nil { if attr.append == nil {
cache = append(cache, cache = append(cache, []KV{
[2]string{"COMPILER_RT_USE_LLVM_UNWINDER", "ON"}) {"COMPILER_RT_USE_LLVM_UNWINDER", "ON"},
}...)
} }
} }
if attr.flags&llvmRuntimeLibunwind != 0 { if attr.flags&llvmRuntimeLibunwind != 0 {
cache = append(cache, cache = append(cache, []KV{
[2]string{"LIBUNWIND_USE_COMPILER_RT", "ON"}) {"LIBUNWIND_USE_COMPILER_RT", "ON"},
}...)
} }
if attr.flags&llvmRuntimeLibcxx != 0 { if attr.flags&llvmRuntimeLibcxx != 0 {
cache = append(cache, cache = append(cache, []KV{
[2]string{"LIBCXX_HAS_MUSL_LIBC", "ON"}, {"LIBCXX_HAS_MUSL_LIBC", "ON"},
[2]string{"LIBCXX_USE_COMPILER_RT", "ON"}, {"LIBCXX_USE_COMPILER_RT", "ON"},
) }...)
} }
if attr.flags&llvmRuntimeLibcxxABI != 0 { if attr.flags&llvmRuntimeLibcxxABI != 0 {
cache = append(cache, cache = append(cache, []KV{
[2]string{"LIBCXXABI_USE_COMPILER_RT", "ON"}, {"LIBCXXABI_USE_COMPILER_RT", "ON"},
[2]string{"LIBCXXABI_USE_LLVM_UNWINDER", "ON"}, {"LIBCXXABI_USE_LLVM_UNWINDER", "ON"},
) }...)
} }
return t.NewPackage("llvm", llvmVersion, pkg.NewHTTPGetTar( return t.NewPackage("llvm", llvmVersion, pkg.NewHTTPGetTar(
@@ -208,7 +212,7 @@ func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) {
panic("unsupported target " + runtime.GOARCH) panic("unsupported target " + runtime.GOARCH)
} }
minimalDeps := [][2]string{ minimalDeps := []KV{
{"LLVM_ENABLE_ZLIB", "OFF"}, {"LLVM_ENABLE_ZLIB", "OFF"},
{"LLVM_ENABLE_ZSTD", "OFF"}, {"LLVM_ENABLE_ZSTD", "OFF"},
{"LLVM_ENABLE_LIBXML2", "OFF"}, {"LLVM_ENABLE_LIBXML2", "OFF"},
@@ -222,7 +226,7 @@ func (t Toolchain) newLLVM() (musl, compilerRT, runtimes, clang pkg.Artifact) {
env: stage0ExclConcat(t, []string{}, env: stage0ExclConcat(t, []string{},
"LDFLAGS="+earlyLDFLAGS(false), "LDFLAGS="+earlyLDFLAGS(false),
), ),
cmake: [][2]string{ cmake: []KV{
// libc++ not yet available // libc++ not yet available
{"CMAKE_CXX_COMPILER_TARGET", ""}, {"CMAKE_CXX_COMPILER_TARGET", ""},
@@ -272,7 +276,7 @@ ln -s \
"LDFLAGS="+earlyLDFLAGS(false), "LDFLAGS="+earlyLDFLAGS(false),
), ),
flags: llvmRuntimeLibunwind | llvmRuntimeLibcxx | llvmRuntimeLibcxxABI, flags: llvmRuntimeLibunwind | llvmRuntimeLibcxx | llvmRuntimeLibcxxABI,
cmake: slices.Concat([][2]string{ cmake: slices.Concat([]KV{
// libc++ not yet available // libc++ not yet available
{"CMAKE_CXX_COMPILER_WORKS", "ON"}, {"CMAKE_CXX_COMPILER_WORKS", "ON"},
@@ -293,7 +297,7 @@ ln -s \
"CXXFLAGS="+earlyCXXFLAGS(), "CXXFLAGS="+earlyCXXFLAGS(),
"LDFLAGS="+earlyLDFLAGS(false), "LDFLAGS="+earlyLDFLAGS(false),
), ),
cmake: slices.Concat([][2]string{ cmake: slices.Concat([]KV{
{"LLVM_TARGETS_TO_BUILD", target}, {"LLVM_TARGETS_TO_BUILD", target},
{"CMAKE_CROSSCOMPILING", "OFF"}, {"CMAKE_CROSSCOMPILING", "OFF"},
{"CXX_SUPPORTS_CUSTOM_LINKER", "ON"}, {"CXX_SUPPORTS_CUSTOM_LINKER", "ON"},
@@ -310,7 +314,7 @@ ln -s clang++ /work/system/bin/c++
ninja check-all ninja check-all
`, `,
patches: slices.Concat([][2]string{ patches: slices.Concat([]KV{
{"add-rosa-vendor", `diff --git a/llvm/include/llvm/TargetParser/Triple.h b/llvm/include/llvm/TargetParser/Triple.h {"add-rosa-vendor", `diff --git a/llvm/include/llvm/TargetParser/Triple.h b/llvm/include/llvm/TargetParser/Triple.h
index 9c83abeeb3b1..5acfe5836a23 100644 index 9c83abeeb3b1..5acfe5836a23 100644
--- a/llvm/include/llvm/TargetParser/Triple.h --- a/llvm/include/llvm/TargetParser/Triple.h

View File

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

View File

@@ -1,7 +1,7 @@
package rosa package rosa
// clangPatches are patches applied to the LLVM source tree for building clang. // clangPatches are patches applied to the LLVM source tree for building clang.
var clangPatches [][2]string var clangPatches []KV
// one version behind, latest fails 5 tests with 2 flaky on arm64 // one version behind, latest fails 5 tests with 2 flaky on arm64
const ( const (

View File

@@ -5,7 +5,7 @@ package rosa
// latest version of LLVM, conditional to temporarily avoid broken new releases // latest version of LLVM, conditional to temporarily avoid broken new releases
const ( const (
llvmVersionMajor = "22" llvmVersionMajor = "22"
llvmVersion = llvmVersionMajor + ".1.1" llvmVersion = llvmVersionMajor + ".1.2"
llvmChecksum = "bQvV6D8AZvQykg7-uQb_saTbVavnSo1ykNJ3g57F5iE-evU3HuOYtcRnVIXTK76e" llvmChecksum = "FwsmurWDVyYYQlOowowFjekwIGSB5__aKTpW_VGP3eWoZGXvBny-bOn1DuQ1U5xE"
) )

View File

@@ -60,7 +60,7 @@ type MakeHelper struct {
// Alternative name for the configure script. // Alternative name for the configure script.
ConfigureName string ConfigureName string
// Flags passed to the configure script. // Flags passed to the configure script.
Configure [][2]string Configure []KV
// Host target triple, zero value is equivalent to the Rosa OS triple. // Host target triple, zero value is equivalent to the Rosa OS triple.
Host string Host string
// Target triple, zero value is equivalent to the Rosa OS triple. // Target triple, zero value is equivalent to the Rosa OS triple.

View File

@@ -59,7 +59,7 @@ type MesonHelper struct {
Script string Script string
// Flags passed to the setup command. // Flags passed to the setup command.
Setup [][2]string Setup []KV
// Whether to skip meson test. // Whether to skip meson test.
SkipTest bool SkipTest bool
} }
@@ -113,7 +113,7 @@ meson test \
cd "$(mktemp -d)" cd "$(mktemp -d)"
meson setup \ meson setup \
` + strings.Join(slices.Collect(func(yield func(string) bool) { ` + strings.Join(slices.Collect(func(yield func(string) bool) {
for _, v := range append([][2]string{ for _, v := range append([]KV{
{"prefix", "/system"}, {"prefix", "/system"},
{"buildtype", "release"}, {"buildtype", "release"},
}, attr.Setup...) { }, attr.Setup...) {

View File

@@ -8,8 +8,8 @@ func (t Toolchain) newMusl(
extra ...pkg.Artifact, extra ...pkg.Artifact,
) (pkg.Artifact, string) { ) (pkg.Artifact, string) {
const ( const (
version = "1.2.5" version = "1.2.6"
checksum = "y6USdIeSdHER_Fw2eT2CNjqShEye85oEg2jnOur96D073ukmIpIqDOLmECQroyDb" checksum = "WtWb_OV_XxLDAB5NerOL9loLlHVadV00MmGk65PPBU1evaolagoMHfvpZp_vxEzS"
) )
name := "musl" name := "musl"

View File

@@ -15,7 +15,7 @@ func (t Toolchain) newNcurses() (pkg.Artifact, string) {
// "tests" are actual demo programs, not a test suite. // "tests" are actual demo programs, not a test suite.
SkipCheck: true, SkipCheck: true,
Configure: [][2]string{ Configure: []KV{
{"with-pkg-config"}, {"with-pkg-config"},
{"enable-pc-files"}, {"enable-pc-files"},
}, },

View File

@@ -8,8 +8,8 @@ import (
func (t Toolchain) newNSS() (pkg.Artifact, string) { func (t Toolchain) newNSS() (pkg.Artifact, string) {
const ( const (
version = "3.121" version = "3.122"
checksum = "MTS4Eg-1vBN3T7gdUAdNO0y_e9x9BE3f_k_DHdM_BIovc7y57vhsZTfB5f6BeQfi" checksum = "QvC6TBO4BAUEh6wmgUrb1hwH5podQAN-QdcAaWL32cWEppmZs6oKkZpD9GvZf59S"
version0 = "4_38_2" version0 = "4_38_2"
checksum0 = "25x2uJeQnOHIiq_zj17b4sYqKgeoU8-IsySUptoPcdHZ52PohFZfGuIisBreWzx0" checksum0 = "25x2uJeQnOHIiq_zj17b4sYqKgeoU8-IsySUptoPcdHZ52PohFZfGuIisBreWzx0"

View File

@@ -20,7 +20,7 @@ func (t Toolchain) newOpenSSL() (pkg.Artifact, string) {
OmitDefaults: true, OmitDefaults: true,
ConfigureName: "/usr/src/openssl/Configure", ConfigureName: "/usr/src/openssl/Configure",
Configure: [][2]string{ Configure: []KV{
{"prefix", "/system"}, {"prefix", "/system"},
{"libdir", "lib"}, {"libdir", "lib"},
{"openssldir", "etc/ssl"}, {"openssldir", "etc/ssl"},

View File

@@ -20,7 +20,7 @@ func (t Toolchain) newPCRE2() (pkg.Artifact, string) {
ln -s ../system/bin/toybox /bin/echo ln -s ../system/bin/toybox /bin/echo
`, `,
}, &MakeHelper{ }, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
{"enable-jit"}, {"enable-jit"},
{"enable-pcre2-8"}, {"enable-pcre2-8"},
{"enable-pcre2-16"}, {"enable-pcre2-16"},

View File

@@ -31,7 +31,7 @@ rm -f /system/bin/ps # perl does not like toybox ps
InPlace: true, InPlace: true,
ConfigureName: "./Configure", ConfigureName: "./Configure",
Configure: [][2]string{ Configure: []KV{
{"-des"}, {"-des"},
{"Dprefix", "/system"}, {"Dprefix", "/system"},
{"Dcc", "clang"}, {"Dcc", "clang"},
@@ -67,7 +67,7 @@ func init() {
func (t Toolchain) newViaPerlModuleBuild( func (t Toolchain) newViaPerlModuleBuild(
name, version string, name, version string,
source pkg.Artifact, source pkg.Artifact,
patches [][2]string, patches []KV,
extra ...PArtifact, extra ...PArtifact,
) pkg.Artifact { ) pkg.Artifact {
if name == "" || version == "" { if name == "" || version == "" {
@@ -116,7 +116,7 @@ func init() {
func (t Toolchain) newViaPerlMakeMaker( func (t Toolchain) newViaPerlMakeMaker(
name, version string, name, version string,
source pkg.Artifact, source pkg.Artifact,
patches [][2]string, patches []KV,
extra ...PArtifact, extra ...PArtifact,
) pkg.Artifact { ) pkg.Artifact {
return t.NewPackage("perl-"+name, version, source, &PackageAttr{ return t.NewPackage("perl-"+name, version, source, &PackageAttr{
@@ -131,7 +131,7 @@ func (t Toolchain) newViaPerlMakeMaker(
InPlace: true, InPlace: true,
ConfigureName: "perl Makefile.PL", ConfigureName: "perl Makefile.PL",
Configure: [][2]string{ Configure: []KV{
{"PREFIX", "/system"}, {"PREFIX", "/system"},
}, },
Check: []string{"test"}, Check: []string{"test"},

View File

@@ -13,7 +13,7 @@ func (t Toolchain) newPkgConfig() (pkg.Artifact, string) {
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, pkg.TarGzip,
), nil, &MakeHelper{ ), nil, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
{"CFLAGS", "'-Wno-int-conversion'"}, {"CFLAGS", "'-Wno-int-conversion'"},
{"with-internal-glib"}, {"with-internal-glib"},
}, },

View File

@@ -14,7 +14,7 @@ func (t Toolchain) newProcps() (pkg.Artifact, string) {
pkg.TarBzip2, pkg.TarBzip2,
), nil, &MakeHelper{ ), nil, &MakeHelper{
Generate: "./autogen.sh", Generate: "./autogen.sh",
Configure: [][2]string{ Configure: []KV{
{"without-ncurses"}, {"without-ncurses"},
}, },
}, },

View File

@@ -4,15 +4,15 @@ import "hakurei.app/internal/pkg"
func (t Toolchain) newQEMU() (pkg.Artifact, string) { func (t Toolchain) newQEMU() (pkg.Artifact, string) {
const ( const (
version = "10.2.1" version = "10.2.2"
checksum = "rjLTSgHJd3X3Vgpxrsus_ZZiaYLiNix1YhcHaGbLd_odYixwZjCcAIt8CVQPJGdZ" checksum = "uNzRxlrVoLWe-EmZmBp75SezymgE512iE5XN90Bl7wi6CjE_oQGQB-9ocs7E16QG"
) )
return t.NewPackage("qemu", version, pkg.NewHTTPGetTar( return t.NewPackage("qemu", version, pkg.NewHTTPGetTar(
nil, "https://download.qemu.org/qemu-"+version+".tar.bz2", nil, "https://download.qemu.org/qemu-"+version+".tar.bz2",
mustDecode(checksum), mustDecode(checksum),
pkg.TarBzip2, pkg.TarBzip2,
), &PackageAttr{ ), &PackageAttr{
Patches: [][2]string{ Patches: []KV{
{"disable-mcast-test", `diff --git a/tests/qtest/netdev-socket.c b/tests/qtest/netdev-socket.c {"disable-mcast-test", `diff --git a/tests/qtest/netdev-socket.c b/tests/qtest/netdev-socket.c
index b731af0ad9..b5cbed4801 100644 index b731af0ad9..b5cbed4801 100644
--- a/tests/qtest/netdev-socket.c --- a/tests/qtest/netdev-socket.c
@@ -58,7 +58,7 @@ _notrun 'appears to spuriously fail on zfs'
EOF EOF
`, `,
}, &MakeHelper{ }, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
{"disable-download"}, {"disable-download"},
{"disable-docs"}, {"disable-docs"},

View File

@@ -20,9 +20,6 @@ const (
// kindBusyboxBin is the kind of [pkg.Artifact] of busyboxBin. // kindBusyboxBin is the kind of [pkg.Artifact] of busyboxBin.
kindBusyboxBin kindBusyboxBin
// kindCollection is the kind of [Collect]. It never cures successfully.
kindCollection
) )
// mustDecode is like [pkg.MustDecode], but replaces the zero value and prints // mustDecode is like [pkg.MustDecode], but replaces the zero value and prints
@@ -40,6 +37,9 @@ func mustDecode(s string) pkg.Checksum {
return pkg.MustDecode(s) return pkg.MustDecode(s)
} }
// KV is a key-value pair of strings.
type KV [2]string
var ( var (
// AbsUsrSrc is the conventional directory to place source code under. // AbsUsrSrc is the conventional directory to place source code under.
AbsUsrSrc = fhs.AbsUsr.Append("src") AbsUsrSrc = fhs.AbsUsr.Append("src")
@@ -202,6 +202,10 @@ func lastIndexFunc[S ~[]E, E any](s S, f func(E) bool) (i int) {
// fixupEnviron fixes up PATH, prepends extras and returns the resulting slice. // fixupEnviron fixes up PATH, prepends extras and returns the resulting slice.
func fixupEnviron(env, extras []string, paths ...string) []string { func fixupEnviron(env, extras []string, paths ...string) []string {
// some python tools try to be clever and buffers their output, making the
// build process appear to hang
env = append(env, "PYTHONUNBUFFERED=1")
const pathPrefix = "PATH=" const pathPrefix = "PATH="
pathVal := strings.Join(paths, ":") pathVal := strings.Join(paths, ":")
@@ -363,7 +367,7 @@ func (t Toolchain) NewPatchedSource(
name, version string, name, version string,
source pkg.Artifact, source pkg.Artifact,
passthrough bool, passthrough bool,
patches ...[2]string, patches ...KV,
) pkg.Artifact { ) pkg.Artifact {
if passthrough && len(patches) == 0 { if passthrough && len(patches) == 0 {
return source return source
@@ -445,7 +449,7 @@ type PackageAttr struct {
ScriptEarly string ScriptEarly string
// Passed to [Toolchain.NewPatchedSource]. // Passed to [Toolchain.NewPatchedSource].
Patches [][2]string Patches []KV
// Kind of source artifact. // Kind of source artifact.
SourceKind int SourceKind int
@@ -591,29 +595,3 @@ cd '/usr/src/` + name + `/'
})..., })...,
) )
} }
// Collected is returned by [Collect.Cure] to indicate a successful collection.
type Collected struct{}
// Error returns a constant string to satisfy error, but should never be seen
// by the user.
func (Collected) Error() string { return "artifacts successfully collected" }
// Collect implements [pkg.FloodArtifact] to concurrently cure multiple
// [pkg.Artifact]. It returns [Collected].
type Collect []pkg.Artifact
// Cure returns [Collected].
func (*Collect) Cure(*pkg.FContext) error { return Collected{} }
// Kind returns the hardcoded [pkg.Kind] value.
func (*Collect) Kind() pkg.Kind { return kindCollection }
// Params does not write anything, dependencies are already represented in the header.
func (*Collect) Params(*pkg.IContext) {}
// Dependencies returns [Collect] as is.
func (c *Collect) Dependencies() []pkg.Artifact { return *c }
// IsExclusive returns false: Cure is a noop.
func (*Collect) IsExclusive() bool { return false }

View File

@@ -15,7 +15,7 @@ func (t Toolchain) newRsync() (pkg.Artifact, string) {
), &PackageAttr{ ), &PackageAttr{
Flag: TEarly, Flag: TEarly,
}, &MakeHelper{ }, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
{"disable-openssl"}, {"disable-openssl"},
{"disable-xxhash"}, {"disable-xxhash"},
{"disable-zstd"}, {"disable-zstd"},

View File

@@ -23,7 +23,7 @@ sed -i 's/unsigned int msg_len;$/uint32_t msg_len;/g' \
tests/nlattr.c tests/nlattr.c
`, `,
}, &MakeHelper{ }, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
// tests broken on clang // tests broken on clang
{"disable-gcc-Werror"}, {"disable-gcc-Werror"},

View File

@@ -22,7 +22,7 @@ func (t Toolchain) newUtilLinux() (pkg.Artifact, string) {
ln -s ../system/bin/bash /bin/ ln -s ../system/bin/bash /bin/
`, `,
}, &MakeHelper{ }, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
{"disable-use-tty-group"}, {"disable-use-tty-group"},
{"disable-makeinstall-setuid"}, {"disable-makeinstall-setuid"},
{"disable-makeinstall-chown"}, {"disable-makeinstall-chown"},

View File

@@ -4,8 +4,8 @@ import "hakurei.app/internal/pkg"
func (t Toolchain) newWayland() (pkg.Artifact, string) { func (t Toolchain) newWayland() (pkg.Artifact, string) {
const ( const (
version = "1.24.91" version = "1.25.0"
checksum = "SQkjYShk2TutoBOfmeJcdLU9iDExVKOg0DZhLeL8U_qjc9olLTC7h3vuUBvVtx9w" checksum = "q-4dYXme46JPgLGtXAxyZGTy7udll9RfT0VXtcW2YRR1WWViUhvdZXZneXzLqpCg"
) )
return t.NewPackage("wayland", version, pkg.NewHTTPGetTar( return t.NewPackage("wayland", version, pkg.NewHTTPGetTar(
nil, "https://gitlab.freedesktop.org/wayland/wayland/"+ nil, "https://gitlab.freedesktop.org/wayland/wayland/"+
@@ -20,7 +20,7 @@ chmod +w tests tests/sanity-test.c
echo 'int main(){}' > tests/sanity-test.c echo 'int main(){}' > tests/sanity-test.c
`, `,
}, &MesonHelper{ }, &MesonHelper{
Setup: [][2]string{ Setup: []KV{
{"Ddefault_library", "both"}, {"Ddefault_library", "both"},
{"Ddocumentation", "false"}, {"Ddocumentation", "false"},
{"Dtests", "true"}, {"Dtests", "true"},
@@ -63,7 +63,7 @@ func (t Toolchain) newWaylandProtocols() (pkg.Artifact, string) {
mustDecode(checksum), mustDecode(checksum),
pkg.TarBzip2, pkg.TarBzip2,
), &PackageAttr{ ), &PackageAttr{
Patches: [][2]string{ Patches: []KV{
{"build-only", `From 8b4c76275fa1b6e0a99a53494151d9a2c907144d Mon Sep 17 00:00:00 2001 {"build-only", `From 8b4c76275fa1b6e0a99a53494151d9a2c907144d Mon Sep 17 00:00:00 2001
From: "A. Wilcox" <AWilcox@Wilcox-Tech.com> From: "A. Wilcox" <AWilcox@Wilcox-Tech.com>
Date: Fri, 8 Nov 2024 11:27:25 -0600 Date: Fri, 8 Nov 2024 11:27:25 -0600

View File

@@ -12,7 +12,7 @@ func (t Toolchain) newZlib() (pkg.Artifact, string) {
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, pkg.TarGzip,
), nil, &CMakeHelper{ ), nil, &CMakeHelper{
Cache: [][2]string{ Cache: []KV{
{"CMAKE_BUILD_TYPE", "Release"}, {"CMAKE_BUILD_TYPE", "Release"},
{"ZLIB_BUILD_TESTING", "OFF"}, {"ZLIB_BUILD_TESTING", "OFF"},

View File

@@ -14,7 +14,7 @@ func (t Toolchain) newZstd() (pkg.Artifact, string) {
pkg.TarGzip, pkg.TarGzip,
), nil, &CMakeHelper{ ), nil, &CMakeHelper{
Append: []string{"build", "cmake"}, Append: []string{"build", "cmake"},
Cache: [][2]string{ Cache: []KV{
{"CMAKE_BUILD_TYPE", "Release"}, {"CMAKE_BUILD_TYPE", "Release"},
}, },
}), version }), version

85
internal/uevent/action.go Normal file
View File

@@ -0,0 +1,85 @@
package uevent
import (
"strconv"
"syscall"
)
// KobjectAction represents enum kobject_action found in include/linux/kobject.h
// and their corresponding string representations in lib/kobject_uevent.c.
type KobjectAction uint32
// include/linux/kobject.h
const (
KOBJ_ADD KobjectAction = iota
KOBJ_REMOVE
KOBJ_CHANGE
KOBJ_MOVE
KOBJ_ONLINE
KOBJ_OFFLINE
KOBJ_BIND
KOBJ_UNBIND
// Synthetic denotes a [Message] that originates from outside the kernel. It
// is not valid in the wire format and is only meaningful within this package.
Synthetic KobjectAction = 0xfeed
)
// lib/kobject_uevent.c
var kobject_actions = [...]string{
KOBJ_ADD: "add",
KOBJ_REMOVE: "remove",
KOBJ_CHANGE: "change",
KOBJ_MOVE: "move",
KOBJ_ONLINE: "online",
KOBJ_OFFLINE: "offline",
KOBJ_BIND: "bind",
KOBJ_UNBIND: "unbind",
}
// Valid returns whether the value of act is defined.
func (act KobjectAction) Valid() bool { return int(act) < len(kobject_actions) }
// String returns the corresponding string sent over netlink.
func (act KobjectAction) String() string {
if act == Synthetic {
return "synthetic"
}
if !act.Valid() {
return "unsupported kobject_action " + strconv.Itoa(int(act))
}
return kobject_actions[act]
}
func (act KobjectAction) AppendText(b []byte) ([]byte, error) {
if !act.Valid() && act != Synthetic {
return b, syscall.EINVAL
}
return append(b, act.String()...), nil
}
func (act KobjectAction) MarshalText() ([]byte, error) {
return act.AppendText(nil)
}
// An UnsupportedActionError describes a string representation of [KobjectAction]
// not yet supported by this package.
type UnsupportedActionError string
var _ Recoverable = UnsupportedActionError("")
func (UnsupportedActionError) recoverable() {}
func (e UnsupportedActionError) Error() string {
return "unsupported kobject_action " + strconv.Quote(string(e))
}
func (act *KobjectAction) UnmarshalText(data []byte) error {
for v, s := range kobject_actions {
if string(data) == s {
*act = KobjectAction(v)
return nil
}
}
return UnsupportedActionError(data)
}

View File

@@ -0,0 +1,43 @@
package uevent_test
import (
"syscall"
"testing"
"hakurei.app/internal/uevent"
)
func TestKobjectAction(t *testing.T) {
t.Parallel()
adeT(t, "add", uevent.KOBJ_ADD, "add", nil, nil)
adeT(t, "remove", uevent.KOBJ_REMOVE, "remove", nil, nil)
adeT(t, "change", uevent.KOBJ_CHANGE, "change", nil, nil)
adeT(t, "move", uevent.KOBJ_MOVE, "move", nil, nil)
adeT(t, "online", uevent.KOBJ_ONLINE, "online", nil, nil)
adeT(t, "offline", uevent.KOBJ_OFFLINE, "offline", nil, nil)
adeT(t, "bind", uevent.KOBJ_BIND, "bind", nil, nil)
adeT(t, "unbind", uevent.KOBJ_UNBIND, "unbind", nil, nil)
adeT(t, "unsupported", uevent.KobjectAction(0xbad), "explode",
uevent.UnsupportedActionError("explode"), syscall.EINVAL)
t.Run("oob string", func(t *testing.T) {
t.Parallel()
const want = "unsupported kobject_action 2989"
if got := uevent.KobjectAction(0xbad).String(); got != want {
t.Errorf("String: %q, want %q", got, want)
}
})
adeT(t, "synthetic", uevent.Synthetic, "synthetic",
uevent.UnsupportedActionError("synthetic"), nil)
t.Run("validate synthetic", func(t *testing.T) {
t.Parallel()
if uevent.Synthetic.Valid() {
t.Errorf("Valid unexpectedly succeeded")
}
})
}

137
internal/uevent/message.go Normal file
View File

@@ -0,0 +1,137 @@
package uevent
import (
"bytes"
"strconv"
"strings"
)
// A Message represents a kernel message to userspace.
type Message struct {
// alloc_uevent_skb: action_string
Action KobjectAction `json:"action"`
// alloc_uevent_skb: devpath
DevPath string `json:"devpath"`
// add_uevent_var: key value strings
Env []string `json:"env"`
}
// String returns a multiline user-facing string representation of [Message].
func (msg *Message) String() string {
var buf strings.Builder
buf.WriteString(msg.Action.String() + " event")
if msg.DevPath != "" {
buf.WriteString(" on " + msg.DevPath)
}
buf.WriteString(":")
for _, s := range msg.Env {
buf.WriteString("\n" + s)
}
return buf.String()
}
var (
// zero is a single pre-allocated NUL character.
zero = []byte{0}
// sepHeader is the separator in a [Message] header.
sepHeader = []byte{'@'}
)
func (msg *Message) AppendBinary(b []byte) (_ []byte, err error) {
if b, err = msg.Action.AppendText(b); err != nil {
return
}
b = append(b, sepHeader...)
b = append(b, msg.DevPath...)
b = append(b, zero...)
for _, s := range msg.Env {
b = append(b, s...)
b = append(b, zero...)
}
return b, nil
}
func (msg *Message) MarshalBinary() ([]byte, error) {
return msg.AppendBinary(nil)
}
// MissingHeaderError is an invalid representation of [Message] which is missing
// its header added by alloc_uevent_skb.
type MissingHeaderError string
var _ Recoverable = MissingHeaderError("")
func (MissingHeaderError) recoverable() {}
func (e MissingHeaderError) Error() string {
return "message " + strconv.Quote(string(e)) + " has no header"
}
// MessageError describes a malformed representation of [Message].
type MessageError struct {
// Full offending data.
Data string `json:"data"`
// Offending section.
Section string `json:"section"`
// Part of header in Section.
Kind int `json:"kind"`
}
var _ Recoverable = new(MessageError)
var _ Nontrivial = new(MessageError)
const (
// MErrorKindHeaderSep denotes a message header missing its separator.
MErrorKindHeaderSep = iota
// MErrorKindFinalNUL denotes a message body missing its final NUL terminator.
MErrorKindFinalNUL
)
func (*MessageError) recoverable() {}
func (*MessageError) nontrivial() {}
func (e *MessageError) Error() string {
switch e.Kind {
case MErrorKindHeaderSep:
return "header " + strconv.Quote(e.Section) + " missing separator"
case MErrorKindFinalNUL:
return "entry " + strconv.Quote(e.Section) + " missing NUL"
default:
return "section " + strconv.Quote(e.Section) + " is invalid"
}
}
func (msg *Message) UnmarshalBinary(data []byte) error {
header, body, ok := bytes.Cut(data, zero)
if !ok {
return MissingHeaderError(data)
}
action_string, devpath, ok := bytes.Cut(header, sepHeader)
if !ok {
return &MessageError{string(data), string(header), MErrorKindHeaderSep}
}
if err := msg.Action.UnmarshalText(action_string); err != nil {
return err
}
msg.DevPath = string(devpath)
if len(body) == 0 {
msg.Env = nil
return nil
}
msg.Env = make([]string, 0, bytes.Count(body, zero))
var s []byte
for len(body) != 0 {
var r []byte
s, r, ok = bytes.Cut(body, zero)
if !ok {
return &MessageError{string(data), string(body), MErrorKindFinalNUL}
}
body = r
msg.Env = append(msg.Env, string(s))
}
return nil
}

View File

@@ -0,0 +1,132 @@
package uevent_test
import (
"syscall"
"testing"
"hakurei.app/internal/uevent"
)
func TestMessage(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
v uevent.Message
want string
wantErr error
wantErrE error
s string
}{
{"sample virtio-sound-pci add", uevent.Message{
Action: uevent.KOBJ_ADD,
DevPath: "/devices/pci0000:00/0000:00:04.0/virtio1",
Env: []string{
"ACTION=add",
"DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1",
"SUBSYSTEM=virtio",
"MODALIAS=virtio:d00000019v00001AF4",
"SEQNUM=779",
},
}, "add@/devices/pci0000:00/0000:00:04.0/virtio1\x00" +
"ACTION=add\x00" +
"DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1\x00" +
"SUBSYSTEM=virtio\x00" +
"MODALIAS=virtio:d00000019v00001AF4\x00" +
"SEQNUM=779\x00",
nil, nil, `add event on /devices/pci0000:00/0000:00:04.0/virtio1:
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1
SUBSYSTEM=virtio
MODALIAS=virtio:d00000019v00001AF4
SEQNUM=779`},
{"sample virtio-sound-pci bind", uevent.Message{
Action: uevent.KOBJ_BIND,
DevPath: "/devices/pci0000:00/0000:00:04.0",
Env: []string{
"ACTION=bind",
"DEVPATH=/devices/pci0000:00/0000:00:04.0",
"SUBSYSTEM=pci",
"DRIVER=virtio-pci",
"PCI_CLASS=40100",
"PCI_ID=1AF4:1059",
"PCI_SUBSYS_ID=1AF4:1100",
"PCI_SLOT_NAME=0000:00:04.0",
"MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00",
"SEQNUM=780",
},
}, "bind@/devices/pci0000:00/0000:00:04.0\x00" +
"ACTION=bind\x00" +
"DEVPATH=/devices/pci0000:00/0000:00:04.0\x00" +
"SUBSYSTEM=pci\x00" +
"DRIVER=virtio-pci\x00" +
"PCI_CLASS=40100\x00" +
"PCI_ID=1AF4:1059\x00" +
"PCI_SUBSYS_ID=1AF4:1100\x00" +
"PCI_SLOT_NAME=0000:00:04.0\x00" +
"MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00\x00" +
"SEQNUM=780\x00", nil, nil, `bind event on /devices/pci0000:00/0000:00:04.0:
ACTION=bind
DEVPATH=/devices/pci0000:00/0000:00:04.0
SUBSYSTEM=pci
DRIVER=virtio-pci
PCI_CLASS=40100
PCI_ID=1AF4:1059
PCI_SUBSYS_ID=1AF4:1100
PCI_SLOT_NAME=0000:00:04.0
MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00
SEQNUM=780`},
{"zero devpath env", uevent.Message{
Action: uevent.KOBJ_MOVE,
}, "move@\x00", nil, nil, "move event:"},
{"d final NUL e bad action", uevent.Message{
Action: 0xbad,
}, "move@\x00truncated", &uevent.MessageError{
Data: "move@\x00truncated",
Section: "truncated",
Kind: uevent.MErrorKindFinalNUL,
}, syscall.EINVAL, "unsupported kobject_action 2989 event:"},
{"bad action", uevent.Message{
Action: 0xbad,
}, "nonexistent@\x00", uevent.UnsupportedActionError(
"nonexistent",
), syscall.EINVAL, "unsupported kobject_action 2989 event:"},
{"d header sep e bad action", uevent.Message{
Action: 0xbad,
}, "move\x00", &uevent.MessageError{
Data: "move\x00",
Section: "move",
Kind: uevent.MErrorKindHeaderSep,
}, syscall.EINVAL, "unsupported kobject_action 2989 event:"},
{"d missing header e bad action", uevent.Message{
Action: 0xbad,
}, "move", uevent.MissingHeaderError(
"move",
), syscall.EINVAL, "unsupported kobject_action 2989 event:"},
{"synthetic", uevent.Message{
Action: uevent.Synthetic,
}, "synthetic@\x00", uevent.UnsupportedActionError(
"synthetic",
), nil, "synthetic event:"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
adeB(t, "", tc.v, tc.want, tc.wantErr, tc.wantErrE)
t.Run("string", func(t *testing.T) {
if got := tc.v.String(); got != tc.s {
t.Errorf("String: %q, want %q", got, tc.s)
}
})
})
}
}

87
internal/uevent/sysfs.go Normal file
View File

@@ -0,0 +1,87 @@
package uevent
import (
"bytes"
"errors"
"io/fs"
"log"
"path/filepath"
"unsafe"
)
// Enumerate scans sysfs and emits [Synthetic] events. It returns the first
// error it encounters.
//
// The specified filesystem must present the sysfs root.
func Enumerate(
sysfs fs.FS,
handleWalkErr func(error) error,
events chan<- *Message,
) error {
if handleWalkErr == nil {
handleWalkErr = func(err error) error {
if errors.Is(err, fs.ErrNotExist) {
log.Println("enumerate", err)
return nil
}
return err
}
}
return fs.WalkDir(sysfs, "devices", func(
path string,
d fs.DirEntry,
err error,
) error {
if err != nil {
return handleWalkErr(err)
}
if d.IsDir() || d.Name() != "uevent" {
return nil
}
msg := Message{
Action: Synthetic,
// cleans path, appears to be compatible with kernel behaviour
DevPath: filepath.Dir(path),
}
var target string
if target, err = fs.ReadLink(
sysfs,
filepath.Join(msg.DevPath, "subsystem"),
); err != nil {
if err = handleWalkErr(err); err != nil {
return err
}
} else {
msg.Env = append(msg.Env, "SUBSYSTEM="+filepath.Base(target))
}
// read entire file: slicing does not copy
var env []byte
if env, err = fs.ReadFile(sysfs, path); err != nil {
return handleWalkErr(err)
}
for _, s := range bytes.Split(env, []byte{'\n'}) {
if len(s) == 0 {
continue
}
msg.Env = append(msg.Env, unsafe.String(unsafe.SliceData(s), len(s)))
}
if len(msg.Env) == 0 {
// this implies absent subsystem, its error is already handled
return nil
}
if msg.DevPath != "" && msg.DevPath[0] != '/' {
msg.DevPath = "/" + msg.DevPath
}
events <- &msg
return nil
})
}

View File

@@ -0,0 +1,28 @@
package uevent_test
import (
"os"
"sync"
"testing"
"hakurei.app/internal/uevent"
)
func TestEnumerate(t *testing.T) {
t.Parallel()
var wg sync.WaitGroup
defer wg.Wait()
events := make(chan *uevent.Message, 1<<10)
wg.Go(func() {
for msg := range events {
t.Log(msg)
}
})
if err := uevent.Enumerate(os.DirFS("/sys"), nil, events); err != nil {
t.Fatalf("Enumerate: error = %v", err)
}
close(events)
}

105
internal/uevent/uevent.go Normal file
View File

@@ -0,0 +1,105 @@
// Package uevent provides userspace client for consuming events from a
// NETLINK_KOBJECT_UEVENT socket, as well as helpers for supplementing
// events received from the kernel.
package uevent
import (
"context"
"errors"
"strconv"
"sync/atomic"
"syscall"
"hakurei.app/internal/netlink"
)
type (
// Recoverable is satisfied by errors that are safe to recover from.
Recoverable interface{ recoverable() }
// Nontrivial is satisfied by errors preferring a JSON encoding.
Nontrivial interface{ nontrivial() }
)
// Conn represents a NETLINK_KOBJECT_UEVENT socket.
type Conn struct {
conn *netlink.Conn
// Whether currently between a call to enterExcl and exitExcl.
excl atomic.Bool
}
// enterExcl must be called entering a critical section that interacts with conn.
func (c *Conn) enterExcl() error {
if !c.excl.CompareAndSwap(false, true) {
return syscall.EAGAIN
}
return nil
}
// exitExcl must be called exiting a critical section that interacts with conn.
func (c *Conn) exitExcl() { c.excl.Store(false) }
// Close closes the underlying socket.
func (c *Conn) Close() error { return c.conn.Close() }
// Dial returns the address of a newly connected [Conn].
func Dial() (*Conn, error) {
// kernel group is hard coded in lib/kobject_uevent.c, undocumented
c, err := netlink.Dial(syscall.NETLINK_KOBJECT_UEVENT, 1)
if err != nil {
return nil, err
}
return &Conn{conn: c}, err
}
var (
// ErrBadSocket is returned by [Conn.Consume] for a reply from a
// syscall.Sockaddr with unexpected concrete type.
ErrBadSocket = errors.New("unexpected socket address")
)
// BadPortError is returned by [Conn.Consume] upon receiving a message that did
// not come from the kernel.
type BadPortError syscall.SockaddrNetlink
var _ Recoverable = new(BadPortError)
func (*BadPortError) recoverable() {}
func (e *BadPortError) Error() string {
return "unexpected message from port id " + strconv.Itoa(int(e.Pid)) +
" on NETLINK_KOBJECT_UEVENT"
}
// Consume continuously receives and parses events from the kernel. It returns
// the first error it encounters.
//
// Callers must not restart event processing after a non-nil error that does not
// satisfy [Recoverable] is returned.
func (c *Conn) Consume(ctx context.Context, events chan<- *Message) error {
if err := c.enterExcl(); err != nil {
return err
}
defer c.exitExcl()
for {
data, from, err := c.conn.Recvfrom(ctx, 0)
if err != nil {
return err
}
// lib/kobject_uevent.c:
// set portid 0 to inform userspace message comes from kernel
if v, ok := from.(*syscall.SockaddrNetlink); !ok {
return ErrBadSocket
} else if v.Pid != 0 {
return (*BadPortError)(v)
}
var msg Message
if err = msg.UnmarshalBinary(data); err != nil {
return err
}
events <- &msg
}
}

View File

@@ -0,0 +1,246 @@
package uevent_test
import (
"context"
"encoding"
"os"
"reflect"
"sync"
"syscall"
"testing"
"time"
"hakurei.app/internal/uevent"
)
// adeT sets up a parallel subtest for a textual appender/decoder/encoder.
func adeT[V any, S interface {
encoding.TextAppender
encoding.TextMarshaler
encoding.TextUnmarshaler
*V
}](t *testing.T, name string, v V, want string, wantErr, wantErrE error) {
t.Helper()
f := func(t *testing.T) {
if name != "" {
t.Parallel()
}
t.Helper()
t.Run("decode", func(t *testing.T) {
t.Parallel()
t.Helper()
var got V
if err := S(&got).UnmarshalText([]byte(want)); !reflect.DeepEqual(err, wantErr) {
t.Fatalf("UnmarshalText: error = %v, want %v", err, wantErr)
}
if wantErr != nil {
return
}
if !reflect.DeepEqual(&got, &v) {
t.Errorf("UnmarshalText: %#v, want %#v", got, v)
}
})
t.Run("encode", func(t *testing.T) {
t.Parallel()
t.Helper()
if got, err := S(&v).MarshalText(); !reflect.DeepEqual(err, wantErrE) {
t.Fatalf("MarshalText: error = %v, want %v", err, wantErrE)
} else if err == nil && string(got) != want {
t.Errorf("MarshalText: %q, want %q", string(got), want)
}
})
}
if name != "" {
t.Run(name, f)
} else {
f(t)
}
}
// adeT sets up a binary subtest for a textual appender/decoder/encoder.
func adeB[V any, S interface {
encoding.BinaryAppender
encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
*V
}](t *testing.T, name string, v V, want string, wantErr, wantErrE error) {
t.Helper()
f := func(t *testing.T) {
if name != "" {
t.Parallel()
}
t.Helper()
t.Run("decode", func(t *testing.T) {
t.Parallel()
t.Helper()
var got V
if err := S(&got).UnmarshalBinary([]byte(want)); !reflect.DeepEqual(err, wantErr) {
t.Fatalf("UnmarshalBinary: error = %v, want %v", err, wantErr)
}
if wantErr != nil {
return
}
if !reflect.DeepEqual(&got, &v) {
t.Errorf("UnmarshalBinary: %#v, want %#v", got, v)
}
})
t.Run("encode", func(t *testing.T) {
t.Parallel()
t.Helper()
if got, err := S(&v).MarshalBinary(); !reflect.DeepEqual(err, wantErrE) {
t.Fatalf("MarshalBinary: error = %v, want %v", err, wantErrE)
} else if err == nil && string(got) != want {
t.Errorf("MarshalBinary: %q, want %q", string(got), want)
}
})
}
if name != "" {
t.Run(name, f)
} else {
f(t)
}
}
func TestDialConsume(t *testing.T) {
t.Parallel()
c, err := uevent.Dial()
if err != nil {
t.Fatalf("Dial: error = %v", err)
}
t.Cleanup(func() {
if closeErr := c.Close(); closeErr != nil {
t.Fatal(err)
}
})
// check kernel-assigned port id
c0, err0 := uevent.Dial()
if err0 != nil {
t.Fatalf("Dial: error = %v", err)
}
t.Cleanup(func() {
if closeErr := c0.Close(); closeErr != nil {
t.Fatal(closeErr)
}
})
var wg sync.WaitGroup
done := make(chan struct{})
events := make(chan *uevent.Message, 1<<10)
go func() {
defer close(done)
for msg := range events {
t.Log(msg)
}
}()
t.Cleanup(func() {
wg.Wait()
close(events)
<-done
})
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
wg.Go(func() {
if err = c.Consume(ctx, events); err != context.Canceled {
panic(err)
}
})
wg.Go(func() {
if err0 = c0.Consume(ctx, events); err0 != context.Canceled {
panic(err0)
}
})
if testing.Verbose() {
if d, perr := time.ParseDuration(os.Getenv(
"ROSA_UEVENT_TEST_DURATION",
)); perr != nil {
t.Logf("skipping long test: error = %v", perr)
} else {
time.Sleep(d)
}
}
cancel()
wg.Wait()
ctx, cancel = context.WithCancel(t.Context())
defer cancel()
var errs [2]error
exclExit := make(chan struct{})
wg.Go(func() {
defer func() { exclExit <- struct{}{} }()
errs[0] = c.Consume(ctx, events)
})
wg.Go(func() {
defer func() { exclExit <- struct{}{} }()
errs[1] = c.Consume(ctx, events)
})
<-exclExit
cancel()
<-exclExit
if errs[0] != syscall.EAGAIN && errs[1] != syscall.EAGAIN {
t.Fatalf("enterExcl: err0 = %v, err1 = %v", errs[0], errs[1])
}
}
func TestErrors(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
want string
}{
{"UnsupportedActionError", uevent.UnsupportedActionError("explode"),
`unsupported kobject_action "explode"`},
{"MissingHeaderError", uevent.MissingHeaderError("move"),
`message "move" has no header`},
{"MessageError MErrorKindHeaderSep", &uevent.MessageError{
Data: "move\x00",
Section: "move",
Kind: uevent.MErrorKindHeaderSep,
}, `header "move" missing separator`},
{"MessageError MErrorKindFinalNUL", &uevent.MessageError{
Data: "move\x00truncated",
Section: "truncated",
Kind: uevent.MErrorKindFinalNUL,
}, `entry "truncated" missing NUL`},
{"MessageError bad", &uevent.MessageError{
Data: "\x00",
Kind: 0xbad,
}, `section "" is invalid`},
{"BadPortError", &uevent.BadPortError{
Pid: 1,
}, "unexpected message from port id 1 on NETLINK_KOBJECT_UEVENT"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want)
}
})
}
}

View File

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

View File

@@ -30,7 +30,7 @@ in
# For checking pd outcome: # For checking pd outcome:
(pkgs.writeShellScriptBin "check-sandbox-pd" '' (pkgs.writeShellScriptBin "check-sandbox-pd" ''
hakurei -v run hakurei-test \ hakurei -v exec hakurei-test \
-p "/var/tmp/.hakurei-check-ok.0" \ -p "/var/tmp/.hakurei-check-ok.0" \
-t ${toString (builtins.toFile "hakurei-pd-want.json" (builtins.toJSON testCases.pd.want))} \ -t ${toString (builtins.toFile "hakurei-pd-want.json" (builtins.toJSON testCases.pd.want))} \
-s ${testCases.pd.expectedFilter.${pkgs.stdenv.hostPlatform.system}} "$@" -s ${testCases.pd.expectedFilter.${pkgs.stdenv.hostPlatform.system}} "$@"

View File

@@ -42,23 +42,23 @@ machine.wait_for_file("/run/user/1000/wayland-1")
machine.wait_for_file("/tmp/sway-ipc.sock") machine.wait_for_file("/tmp/sway-ipc.sock")
# Check pd seccomp outcome: # Check pd seccomp outcome:
swaymsg("exec hakurei run cat") swaymsg("exec hakurei exec cat")
check_filter(0, "pdlike", "cat") check_filter(0, "pdlike", "cat")
# Check fd leak: # Check fd leak:
swaymsg("exec exec 127</proc/cmdline && hakurei -v run sleep infinity") swaymsg("exec exec 127</proc/cmdline && hakurei -v exec sleep infinity")
pd_identity0_sleep_pid = int(machine.wait_until_succeeds("pgrep -U 10000 -x sleep", timeout=60)) pd_identity0_sleep_pid = int(machine.wait_until_succeeds("pgrep -U 10000 -x sleep", timeout=60))
print(machine.succeed(f"hakurei-test fd {pd_identity0_sleep_pid}")) print(machine.succeed(f"hakurei-test fd {pd_identity0_sleep_pid}"))
machine.succeed(f"kill -INT {pd_identity0_sleep_pid}") machine.succeed(f"kill -INT {pd_identity0_sleep_pid}")
# Verify capabilities/securebits in user namespace: # Verify capabilities/securebits in user namespace:
print(machine.succeed("sudo -u alice -i hakurei run capsh --print")) print(machine.succeed("sudo -u alice -i hakurei exec capsh --print"))
print(machine.succeed("sudo -u alice -i hakurei run capsh --has-no-new-privs")) print(machine.succeed("sudo -u alice -i hakurei exec capsh --has-no-new-privs"))
print(machine.fail("sudo -u alice -i hakurei run capsh --has-a=CAP_SYS_ADMIN")) print(machine.fail("sudo -u alice -i hakurei exec capsh --has-a=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i hakurei run capsh --has-b=CAP_SYS_ADMIN")) print(machine.fail("sudo -u alice -i hakurei exec capsh --has-b=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i hakurei run capsh --has-i=CAP_SYS_ADMIN")) print(machine.fail("sudo -u alice -i hakurei exec capsh --has-i=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i hakurei run capsh --has-p=CAP_SYS_ADMIN")) print(machine.fail("sudo -u alice -i hakurei exec capsh --has-p=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i hakurei run umount -R /dev")) print(machine.fail("sudo -u alice -i hakurei exec umount -R /dev"))
# Check sandbox outcome: # Check sandbox outcome:
machine.succeed("install -dm0777 /tmp/.hakurei-store-rw/{upper,work}") machine.succeed("install -dm0777 /tmp/.hakurei-store-rw/{upper,work}")

View File

@@ -87,9 +87,9 @@ machine.wait_for_file("/tmp/sway-ipc.sock")
swaymsg("exec hakurei-test") swaymsg("exec hakurei-test")
# Deny unmapped uid: # Deny unmapped uid:
denyOutput = machine.fail("sudo -u untrusted -i hakurei run &>/dev/stdout") denyOutput = machine.fail("sudo -u untrusted -i hakurei exec &>/dev/stdout")
print(denyOutput) print(denyOutput)
denyOutputVerbose = machine.fail("sudo -u untrusted -i hakurei -v run &>/dev/stdout") denyOutputVerbose = machine.fail("sudo -u untrusted -i hakurei -v exec &>/dev/stdout")
print(denyOutputVerbose) print(denyOutputVerbose)
# Fail direct hsu call: # Fail direct hsu call:
@@ -118,11 +118,11 @@ def hakurei_identity(offset):
# Start hakurei permissive defaults outside Wayland session: # Start hakurei permissive defaults outside Wayland session:
print(machine.succeed("sudo -u alice -i hakurei -v run -a 0 touch /tmp/pd-bare-ok")) print(machine.succeed("sudo -u alice -i hakurei -v exec -a 0 touch /tmp/pd-bare-ok"))
machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-bare-ok", timeout=5) machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-bare-ok", timeout=5)
# Verify silent output permissive defaults: # Verify silent output permissive defaults:
output = machine.succeed("sudo -u alice -i hakurei run -a 0 true &>/dev/stdout") output = machine.succeed("sudo -u alice -i hakurei exec -a 0 true &>/dev/stdout")
if output != "": if output != "":
raise Exception(f"unexpected output\n{output}") raise Exception(f"unexpected output\n{output}")
@@ -131,12 +131,12 @@ def silent_output_interrupt(flags):
swaymsg("exec foot") swaymsg("exec foot")
wait_for_window("alice@machine") wait_for_window("alice@machine")
# identity 0 does not have home-manager # identity 0 does not have home-manager
machine.send_chars(f"exec hakurei run {flags}-a 0 sh -c 'export PATH=/run/current-system/sw/bin:$PATH && touch /tmp/pd-silent-ready && sleep infinity' &>/tmp/pd-silent\n") machine.send_chars(f"exec hakurei exec {flags}-a 0 sh -c 'export PATH=/run/current-system/sw/bin:$PATH && touch /tmp/pd-silent-ready && sleep infinity' &>/tmp/pd-silent\n")
machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-silent-ready", timeout=15) machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-silent-ready", timeout=15)
machine.succeed("rm /tmp/hakurei.0/tmpdir/0/pd-silent-ready") machine.succeed("rm /tmp/hakurei.0/tmpdir/0/pd-silent-ready")
machine.send_key("ctrl-c") machine.send_key("ctrl-c")
machine.wait_until_fails("pgrep foot", timeout=5) machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_until_fails(f"pgrep -u alice -f 'hakurei run {flags}-a 0 '", timeout=5) machine.wait_until_fails(f"pgrep -u alice -f 'hakurei exec {flags}-a 0 '", timeout=5)
output = machine.succeed("cat /tmp/pd-silent && rm /tmp/pd-silent") output = machine.succeed("cat /tmp/pd-silent && rm /tmp/pd-silent")
if output != "": if output != "":
raise Exception(f"unexpected output\n{output}") raise Exception(f"unexpected output\n{output}")
@@ -147,10 +147,10 @@ silent_output_interrupt("--dbus ") # this one is especially painful as it mainta
silent_output_interrupt("--wayland -X --dbus --pulse ") silent_output_interrupt("--wayland -X --dbus --pulse ")
# Verify graceful failure on bad Wayland display name: # Verify graceful failure on bad Wayland display name:
print(machine.fail("sudo -u alice -i hakurei -v run --wayland true")) print(machine.fail("sudo -u alice -i hakurei -v exec --wayland true"))
# Start hakurei permissive defaults within Wayland session: # Start hakurei permissive defaults within Wayland session:
hakurei('-v run --wayland --dbus --dbus-log notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-ok') hakurei('-v exec --wayland --dbus --dbus-log notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-ok')
machine.wait_for_file("/tmp/dbus-ok", timeout=15) machine.wait_for_file("/tmp/dbus-ok", timeout=15)
collect_state_ui("dbus_notify_exited") collect_state_ui("dbus_notify_exited")
# not in pid namespace, verify termination # not in pid namespace, verify termination
@@ -158,10 +158,10 @@ machine.wait_until_fails("pgrep xdg-dbus-proxy")
machine.succeed("pkill -9 mako") machine.succeed("pkill -9 mako")
# Check revert type selection: # Check revert type selection:
hakurei("-v run --wayland -X --dbus --pulse -u p0 foot && touch /tmp/p0-exit-ok") hakurei("-v exec --wayland -X --dbus --pulse -u p0 foot && touch /tmp/p0-exit-ok")
wait_for_window("p0@machine") wait_for_window("p0@machine")
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")) print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
hakurei("-v run --wayland -X --dbus --pulse -u p1 foot && touch /tmp/p1-exit-ok") hakurei("-v exec --wayland -X --dbus --pulse -u p1 foot && touch /tmp/p1-exit-ok")
wait_for_window("p1@machine") wait_for_window("p1@machine")
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")) print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
machine.send_chars("exit\n") machine.send_chars("exit\n")
@@ -173,14 +173,14 @@ machine.wait_for_file("/tmp/p0-exit-ok", timeout=15)
machine.fail("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000") machine.fail("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")
# Check invalid identifier fd behaviour: # Check invalid identifier fd behaviour:
machine.fail('echo \'{"container":{"shell":"/proc/nonexistent","home":"/proc/nonexistent","path":"/proc/nonexistent"}}\' | sudo -u alice -i hakurei -v app --identifier-fd 32767 - 2>&1 | tee > /tmp/invalid-identifier-fd') machine.fail('echo \'{"container":{"shell":"/proc/nonexistent","home":"/proc/nonexistent","path":"/proc/nonexistent"}}\' | sudo -u alice -i hakurei -v run --identifier-fd 32767 - 2>&1 | tee > /tmp/invalid-identifier-fd')
machine.wait_for_file("/tmp/invalid-identifier-fd") machine.wait_for_file("/tmp/invalid-identifier-fd")
print(machine.succeed('grep "^hakurei: cannot write identifier: bad file descriptor$" /tmp/invalid-identifier-fd')) print(machine.succeed('grep "^hakurei: cannot write identifier: bad file descriptor$" /tmp/invalid-identifier-fd'))
# Check interrupt shim behaviour: # Check interrupt shim behaviour:
swaymsg("exec sh -c 'ne-foot; echo -n $? > /tmp/monitor-exit-code'") swaymsg("exec sh -c 'ne-foot; echo -n $? > /tmp/monitor-exit-code'")
wait_for_window(f"u0_a{hakurei_identity(0)}@machine") wait_for_window(f"u0_a{hakurei_identity(0)}@machine")
machine.succeed("pkill -INT -f 'hakurei -v app '") machine.succeed("pkill -INT -f 'hakurei -v run '")
machine.wait_until_fails("pgrep foot", timeout=5) machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/monitor-exit-code") machine.wait_for_file("/tmp/monitor-exit-code")
interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code")) interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code"))
@@ -190,7 +190,7 @@ if interrupt_exit_code != 230:
# Check interrupt shim behaviour immediate termination: # Check interrupt shim behaviour immediate termination:
swaymsg("exec sh -c 'ne-foot-immediate; echo -n $? > /tmp/monitor-exit-code'") swaymsg("exec sh -c 'ne-foot-immediate; echo -n $? > /tmp/monitor-exit-code'")
wait_for_window(f"u0_a{hakurei_identity(0)}@machine") wait_for_window(f"u0_a{hakurei_identity(0)}@machine")
machine.succeed("pkill -INT -f 'hakurei -v app '") machine.succeed("pkill -INT -f 'hakurei -v run '")
machine.wait_until_fails("pgrep foot", timeout=5) machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/monitor-exit-code") machine.wait_for_file("/tmp/monitor-exit-code")
interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code")) interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code"))
@@ -201,19 +201,19 @@ if interrupt_exit_code != 254:
swaymsg("exec sh -c 'ne-foot &> /tmp/shim-cont-unexpected-pid'") swaymsg("exec sh -c 'ne-foot &> /tmp/shim-cont-unexpected-pid'")
wait_for_window(f"u0_a{hakurei_identity(0)}@machine") wait_for_window(f"u0_a{hakurei_identity(0)}@machine")
machine.succeed("pkill -CONT -f 'hakurei shim'") machine.succeed("pkill -CONT -f 'hakurei shim'")
machine.succeed("pkill -INT -f 'hakurei -v app '") machine.succeed("pkill -INT -f 'hakurei -v run '")
machine.wait_until_fails("pgrep foot", timeout=5) machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/shim-cont-unexpected-pid") machine.wait_for_file("/tmp/shim-cont-unexpected-pid")
print(machine.succeed('grep "shim: got SIGCONT from unexpected process$" /tmp/shim-cont-unexpected-pid')) print(machine.succeed('grep "shim: got SIGCONT from unexpected process$" /tmp/shim-cont-unexpected-pid'))
# Check setscheduler: # 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")) sched_unset = int(machine.succeed("sudo -u alice -i hakurei -v exec cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
if sched_unset != 0: if sched_unset != 0:
raise Exception(f"unexpected unset policy: {sched_unset}") 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")) sched_idle = int(machine.succeed("sudo -u alice -i hakurei -v exec --policy=idle cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
if sched_idle != 5: if sched_idle != 5:
raise Exception(f"unexpected idle policy: {sched_idle}") 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")) sched_rr = int(machine.succeed("sudo -u alice -i hakurei -v exec --policy=rr cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2"))
if sched_rr != 2: if sched_rr != 2:
raise Exception(f"unexpected round-robin policy: {sched_idle}") raise Exception(f"unexpected round-robin policy: {sched_idle}")
@@ -243,11 +243,11 @@ machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_until_fails("pgrep -x hakurei", timeout=5) machine.wait_until_fails("pgrep -x hakurei", timeout=5)
machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +") machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")
# Test PipeWire SecurityContext: # Test PipeWire SecurityContext:
machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pulse pactl info") machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pulse pactl info")
machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pulse pactl set-sink-mute @DEFAULT_SINK@ toggle") machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pulse pactl set-sink-mute @DEFAULT_SINK@ toggle")
# Test PipeWire direct access: # Test PipeWire direct access:
machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 pw-dump") machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 pw-dump")
machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pipewire pw-dump") machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pipewire pw-dump")
# Test XWayland (foot does not support X): # Test XWayland (foot does not support X):
swaymsg("exec x11-alacritty") swaymsg("exec x11-alacritty")