248 Commits

Author SHA1 Message Date
Kat
3546c54ced TODO: actually write tests lol 2026-04-09 11:46:25 +10:00
Kat
f64c793352 cmd/pkgserver: add noscript warning to test web UI 2026-04-09 11:46:25 +10:00
Kat
5474a75f1a cmd/pkgserver: relocate JS tests 2026-04-09 11:46:25 +10:00
Kat
fa64bd7b49 cmd/pkgserver: aria-describe test node summary with state
The summary marker does not appear in the AOM, so setting its alt text
is fruitless.
2026-04-09 11:46:25 +10:00
Kat
89859ddfa3 cmd/pkgserver: provide role descriptions for test nodes in web UI 2026-04-09 11:46:25 +10:00
Kat
7c81681eab cmd/pkgserver: fix dark mode in test web UI 2026-04-09 11:46:25 +10:00
Kat
a958149cb5 cmd/pkgserver: serialize JS enums as ints instead of strings 2026-04-09 11:46:25 +10:00
Kat
a3ff361bc8 cmd/pkgserver: set exit code when running JS tests from CLI 2026-04-09 11:46:25 +10:00
Kat
f621cebb73 cmd/pkgserver: expose verbose StreamReporter flag via CLI 2026-04-09 11:46:25 +10:00
Kat
aed0c98146 cmd/pkgserver: implement skipping JS tests from the DSL 2026-04-09 11:46:25 +10:00
Kat
0e31d8954d cmd/pkgserver: allow non-global JS test suites 2026-04-09 11:46:25 +10:00
Kat
4af44605ce cmd/pkgserver: serialize raw log list for go test consumption 2026-04-09 11:46:25 +10:00
Kat
a2473404d8 cmd/pkgserver: add JSON reporter to facilitate go test integration 2026-04-09 11:46:25 +10:00
Kat
9864f8329f cmd/pkgserver: fix multi-line JS test output display 2026-04-09 11:46:25 +10:00
Kat
b12a910d60 cmd/pkgserver: implement JS test DSL and runner 2026-04-09 11:46:25 +10:00
Kat
339aba4167 cmd/pkgserver: move StreamReporter display() to Reporter interface 2026-04-09 11:46:25 +10:00
Kat
fdbf2a43eb cmd/pkgserver: add DOM reporter for JS tests 2026-04-09 11:46:25 +10:00
Kat
9d3b98e063 cmd/pkgserver: add basic CLI reporter for testing JS 2026-04-09 11:46:25 +10:00
Kat
0183064f25 cmd/pkgserver: enable TypeScript's strict mode 2026-04-09 11:46:25 +10:00
mae
0db8da9cba cmd/pkgserver: remove get endpoint count field 2026-04-08 20:26:38 -05:00
mae
4ae5468cf6 cmd/pkgserver: search endpoint 2026-04-08 20:26:38 -05:00
mae
226b86fe38 cmd/pkgserver: pagination bugfix 2026-04-08 20:26:38 -05:00
23b5d0dd3e cmd/pkgserver: guard sass/ts behind build tag
Packaging nodejs and ruby is an immense burden for the Rosa OS base system, and these files diff poorly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
mae
ba06769c5f cmd/pkgserver: add size 2026-04-08 20:26:38 -05:00
28e84f0445 cmd/pkgserver: expose size and store pre-encoded ident
This change also handles SIGSEGV correctly in newStatusHandler, and makes serving status fully zero copy.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
51ae9496e9 cmd/pkgserver: look up status by name once
This has far less overhead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
3cd8abc451 cmd/pkgserver: refer to preset in index
This enables referencing back to internal/rosa through an entry obtained via the index.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
edf4d0d32a cmd/pkgserver: handle unversioned value
This omits the field for an unversioned artifact, and only does so once on startup.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
97657ad9fc cmd/pkgserver: determine disposition route in mux
This removes duplicate checks and uses the more sound check in mux.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
81344a1c8a cmd/pkgserver: format get error messages
This improves source code readability on smaller displays.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
50514edc00 cmd/pkgserver: constant string in pattern
This resolves patterns at compile time.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
7affd5735a cmd/pkgserver: satisfy handler signature in method
This is somewhat cleaner.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
0a2d9dbe42 cmd/pkgserver: log instead of write encoding error
This message is unlikely to be useful to the user, and output may be partially written at this point, causing the error to be even less intelligible.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
049f212a02 cmd/pkgserver: appropriately mark test helpers
This improves usefulness of test log messages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
ccecaee7ca cmd/pkgserver: do not omit report field
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
365602c9b6 cmd/pkgserver: gracefully shut down on signal
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
85e066ccc4 cmd/pkgserver: specify full addr string in flag
This allows greater flexibility.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
ffe37ae439 cmd/pkgserver: make report argument optional
This allows serving metadata only without a populated report. This also removes the out-of-bounds read on args when no arguments are passed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
222a2aa8b4 cmd/pkgserver: embed internal/rosa metadata
This change also cleans up and reduces some unnecessary copies.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:38 -05:00
29384553bf cmd/pkgserver: do not assume default mux
This helps with testing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:37 -05:00
cb0c652b18 cmd/pkgserver: create index without report
This is useful for testing, where report testdata is not available.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 20:26:37 -05:00
mae
5552598bc4 cmd/pkgserver: add sort orders, change pagination rules 2026-04-08 20:26:37 -05:00
mae
3db5603e78 cmd/pkgserver: add /status endpoint 2026-04-08 20:26:37 -05:00
mae
08d120a84d cmd/pkgserver: minimum viable frontend 2026-04-08 20:26:37 -05:00
mae
1ecdcdc243 cmd/pkgserver: api versioning 2026-04-08 20:26:37 -05:00
mae
e425c3b769 cmd/pkgserver: add get endpoint 2026-04-08 20:26:37 -05:00
mae
3d8b89e1ab cmd/pkgserver: add count endpoint and restructure 2026-04-08 20:26:37 -05:00
mae
b2777de621 cmd/pkgserver: add status endpoint 2026-04-08 20:26:37 -05:00
mae
529a641fcd cmd/pkgserver: add createPackageIndex 2026-04-08 20:26:37 -05:00
mae
0e34ec3093 cmd/pkgserver: add command handler 2026-04-08 20:26:37 -05:00
mae
31b2d5431c cmd/pkgserver: replace favicon 2026-04-08 20:26:37 -05:00
mae
d6954e6bdb cmd/pkgserver: pagination 2026-04-08 20:26:37 -05:00
mae
1cda0d83c3 cmd/pkgserver: basic web ui 2026-04-08 20:26:37 -05:00
e34e3b917e internal/kobject: process uevent message
All checks were successful
Test / Create distribution (push) Successful in 1m19s
Test / Sandbox (push) Successful in 3m21s
Test / Hakurei (push) Successful in 4m37s
Test / ShareFS (push) Successful in 4m36s
Test / Sandbox (race detector) (push) Successful in 5m54s
Test / Hakurei (race detector) (push) Successful in 7m7s
Test / Flake checks (push) Successful in 1m29s
This deals with environment variables generally present in every message.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 18:00:04 +09:00
b0ba165107 cmd/sharefs: group-accessible permission bits
All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m17s
Test / Hakurei (push) Successful in 4m21s
Test / ShareFS (push) Successful in 4m30s
Test / Sandbox (race detector) (push) Successful in 5m43s
Test / Hakurei (race detector) (push) Successful in 6m48s
Test / Flake checks (push) Successful in 1m28s
This works around the race in vfs via supplementary group.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 16:14:47 +09:00
351d6c5a35 cmd/sharefs: reproduce vfs inode file attribute race
All checks were successful
Test / ShareFS (push) Successful in 46s
Test / Sandbox (race detector) (push) Successful in 53s
Test / Sandbox (push) Successful in 55s
Test / Hakurei (push) Successful in 1m1s
Test / Hakurei (race detector) (push) Successful in 1m0s
Test / Create distribution (push) Successful in 1m11s
Test / Flake checks (push) Successful in 1m28s
This happens in the vfs permissions check only and stale data appears to never reach userspace.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-08 15:51:36 +09:00
f23f73701c cmd/mbf: optional host abstract
All checks were successful
Test / Create distribution (push) Successful in 1m30s
Test / Sandbox (push) Successful in 3m25s
Test / Hakurei (push) Successful in 4m28s
Test / ShareFS (push) Successful in 4m29s
Test / Sandbox (race detector) (push) Successful in 5m47s
Test / Hakurei (race detector) (push) Successful in 6m54s
Test / Flake checks (push) Successful in 1m23s
This works around kernels with Landlock LSM disabled. Does not affect cure outcome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-07 18:15:49 +09:00
876917229a internal/rosa/go: enable riscv64 bootstrap path
All checks were successful
Test / Create distribution (push) Successful in 44s
Test / Sandbox (push) Successful in 1m7s
Test / ShareFS (push) Successful in 3m7s
Test / Hakurei (push) Successful in 3m21s
Test / Sandbox (race detector) (push) Successful in 5m34s
Test / Hakurei (race detector) (push) Successful in 6m43s
Test / Flake checks (push) Successful in 1m24s
This is quite expensive, but no other option, unfortunately.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-07 18:11:42 +09:00
0558032c2d container: do not set static deadline
All checks were successful
Test / Create distribution (push) Successful in 1m19s
Test / Sandbox (push) Successful in 3m25s
Test / Hakurei (push) Successful in 5m0s
Test / ShareFS (push) Successful in 4m53s
Test / Sandbox (race detector) (push) Successful in 6m1s
Test / Hakurei (race detector) (push) Successful in 3m37s
Test / Flake checks (push) Successful in 1m27s
This usually ends up in the buffer, or completes well before the deadline, however this can still timeout on a very slow system.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-07 17:00:20 +09:00
c61cdc505f internal/params: relocate from package container
All checks were successful
Test / Create distribution (push) Successful in 1m19s
Test / Sandbox (push) Successful in 3m20s
Test / Hakurei (push) Successful in 4m26s
Test / ShareFS (push) Successful in 4m28s
Test / Sandbox (race detector) (push) Successful in 5m42s
Test / Hakurei (race detector) (push) Successful in 6m57s
Test / Flake checks (push) Successful in 1m27s
This does not make sense as part of the public API, so make it internal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-07 16:37:44 +09:00
062edb3487 container: remove setup pipe helper
All checks were successful
Test / Create distribution (push) Successful in 1m20s
Test / Sandbox (push) Successful in 3m10s
Test / Hakurei (push) Successful in 4m24s
Test / ShareFS (push) Successful in 4m34s
Test / Sandbox (race detector) (push) Successful in 5m46s
Test / Hakurei (race detector) (push) Successful in 6m53s
Test / Flake checks (push) Successful in 1m30s
The API forces use of finalizer to close the read end of the setup pipe, which is no longer considered acceptable. Exporting this as part of package container also imposes unnecessary maintenance burden.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-07 16:05:33 +09:00
e4355279a1 all: optionally forbid degrading in tests
All checks were successful
Test / Create distribution (push) Successful in 1m28s
Test / Sandbox (push) Successful in 3m27s
Test / Hakurei (push) Successful in 4m41s
Test / ShareFS (push) Successful in 4m47s
Test / Sandbox (race detector) (push) Successful in 5m57s
Test / Hakurei (race detector) (push) Successful in 7m3s
Test / Flake checks (push) Successful in 1m29s
This enables transparently degradable tests to be forced on in environments known to support them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-07 15:22:52 +09:00
289fdebead container: transparently degrade landlock in tests
All checks were successful
Test / Create distribution (push) Successful in 1m20s
Test / Sandbox (push) Successful in 3m15s
Test / Hakurei (push) Successful in 4m31s
Test / ShareFS (push) Successful in 4m37s
Test / Sandbox (race detector) (push) Successful in 5m53s
Test / Hakurei (race detector) (push) Successful in 6m56s
Test / Flake checks (push) Successful in 1m28s
Explicitly requiring landlock in tests will be supported in a future change.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-07 15:03:48 +09:00
9c9e190db9 ldd: remove timeout
All checks were successful
Test / Create distribution (push) Successful in 1m19s
Test / Sandbox (push) Successful in 3m16s
Test / Hakurei (push) Successful in 4m19s
Test / ShareFS (push) Successful in 4m30s
Test / Sandbox (race detector) (push) Successful in 5m49s
Test / Hakurei (race detector) (push) Successful in 6m53s
Test / Flake checks (push) Successful in 1m28s
The program generally never blocks, and it is more flexible to leave it up to the caller to set a timeout.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-07 14:49:20 +09:00
d7d42c69a1 internal/pkg: transparently degrade landlock in tests
All checks were successful
Test / Create distribution (push) Successful in 1m23s
Test / Sandbox (push) Successful in 3m26s
Test / Hakurei (push) Successful in 4m52s
Test / ShareFS (push) Successful in 4m57s
Test / Sandbox (race detector) (push) Successful in 5m56s
Test / Hakurei (race detector) (push) Successful in 7m8s
Test / Flake checks (push) Successful in 1m29s
This does not test package container, so should transparently cope with Landlock LSM being unavailable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-07 14:44:34 +09:00
c758e762bd container: skip landlock on hostnet
All checks were successful
Test / Create distribution (push) Successful in 1m34s
Test / Sandbox (push) Successful in 3m46s
Test / ShareFS (push) Successful in 5m0s
Test / Sandbox (race detector) (push) Successful in 6m10s
Test / Hakurei (race detector) (push) Successful in 7m29s
Test / Hakurei (push) Successful in 3m21s
Test / Flake checks (push) Successful in 1m43s
This overlaps with net namespace, so can be skipped without degrading security.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-07 14:36:44 +09:00
10f8b1c221 internal/pkg: optional landlock LSM
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 4m22s
Test / Sandbox (race detector) (push) Successful in 5m43s
Test / Hakurei (race detector) (push) Successful in 6m51s
Test / Flake checks (push) Successful in 1m29s
The alpine linux riscv64 kernel does not enable Landlock LSM, and kernel compilation is not yet feasible.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-07 12:44:07 +09:00
6907700d67 cmd/dist: set hsu tar header mode bits
All checks were successful
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 3m1s
Test / Hakurei (push) Successful in 4m5s
Test / ShareFS (push) Successful in 4m14s
Test / Sandbox (race detector) (push) Successful in 5m27s
Test / Hakurei (race detector) (push) Successful in 6m30s
Test / Flake checks (push) Successful in 1m20s
This has no effect, but is nice to have.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-06 23:37:38 +09:00
0243f3ffbd internal/rosa/stage0: add riscv64 tarball
All checks were successful
Test / Create distribution (push) Successful in 39s
Test / ShareFS (push) Successful in 3m44s
Test / Sandbox (race detector) (push) Successful in 49s
Test / Sandbox (push) Successful in 49s
Test / Hakurei (push) Successful in 54s
Test / Hakurei (race detector) (push) Successful in 1m48s
Test / Flake checks (push) Successful in 1m29s
This had not yet passed all test suites because emulator is prohibitively slow.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-06 13:57:43 +09:00
cd0beeaf8e internal/uevent: optionally pass UUID during coldboot
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m42s
Test / Hakurei (push) Successful in 3m49s
Test / ShareFS (push) Successful in 3m47s
Test / Sandbox (race detector) (push) Successful in 5m12s
Test / Hakurei (race detector) (push) Successful in 6m20s
Test / Flake checks (push) Successful in 1m20s
This enables rejection of non-coldboot synthetic events.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-06 12:42:47 +09:00
a69273ab2a cmd/dist: replace dist/release.sh
All checks were successful
Test / Create distribution (push) Successful in 1m19s
Test / Sandbox (push) Successful in 3m12s
Test / Hakurei (push) Successful in 4m26s
Test / ShareFS (push) Successful in 4m31s
Test / Sandbox (race detector) (push) Successful in 5m46s
Test / Hakurei (race detector) (push) Successful in 7m1s
Test / Flake checks (push) Successful in 1m29s
This is much more robust than a shell script.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-05 23:58:08 +09:00
4cd0f57e48 dist: remove redundant cleanup
All checks were successful
Test / Create distribution (push) Successful in 1m10s
Test / Sandbox (push) Successful in 2m57s
Test / Hakurei (push) Successful in 4m20s
Test / ShareFS (push) Successful in 4m17s
Test / Sandbox (race detector) (push) Successful in 5m47s
Test / Hakurei (race detector) (push) Successful in 6m47s
Test / Flake checks (push) Successful in 1m27s
This breaks on shells that do not evaluate pathnames.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-05 16:16:37 +09:00
33a0e6c01b hst: conditionally skip root remount
All checks were successful
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 56s
Test / Hakurei (push) Successful in 4m25s
Test / ShareFS (push) Successful in 2m19s
Test / Sandbox (race detector) (push) Successful in 3m22s
Test / Hakurei (race detector) (push) Successful in 6m55s
Test / Flake checks (push) Successful in 1m31s
This enables the writable root overlay use case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-05 14:04:21 +09:00
d58f5c7590 dist: destroy workdir on exist
All checks were successful
Test / Create distribution (push) Successful in 1m21s
Test / Sandbox (push) Successful in 3m22s
Test / Hakurei (push) Successful in 4m27s
Test / ShareFS (push) Successful in 4m32s
Test / Sandbox (race detector) (push) Successful in 5m47s
Test / Hakurei (race detector) (push) Successful in 6m55s
Test / Flake checks (push) Successful in 1m26s
This no longer relies on the hermetic build system to clean up.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-05 13:32:16 +09:00
1da992e342 dist: prefix from environment
All checks were successful
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m53s
Test / Hakurei (push) Successful in 3m48s
Test / ShareFS (push) Successful in 3m49s
Test / Sandbox (race detector) (push) Successful in 5m11s
Test / Hakurei (race detector) (push) Successful in 6m22s
Test / Flake checks (push) Successful in 1m27s
These are baked in, so make them configurable for the build.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-05 13:11:44 +09:00
9641805ec2 container/init: ignore finished process
All checks were successful
Test / Create distribution (push) Successful in 1m25s
Test / Sandbox (push) Successful in 4m19s
Test / Hakurei (push) Successful in 5m36s
Test / ShareFS (push) Successful in 5m38s
Test / Sandbox (race detector) (push) Successful in 6m25s
Test / Hakurei (race detector) (push) Successful in 7m38s
Test / Flake checks (push) Successful in 1m35s
This is not considered an error, if the process finishes while the signal is being delivered.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-05 00:38:33 +09:00
0738f4889a internal/rosa/gnu: fetch mpc source via git
All checks were successful
Test / Create distribution (push) Successful in 1m34s
Test / Sandbox (push) Successful in 3m38s
Test / Hakurei (push) Successful in 4m55s
Test / ShareFS (push) Successful in 4m57s
Test / Sandbox (race detector) (push) Successful in 6m13s
Test / Hakurei (race detector) (push) Successful in 7m19s
Test / Flake checks (push) Successful in 2m12s
Eliminates the xz dependency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-05 00:35:15 +09:00
7de3cfe221 internal/rosa/netfilter: fetch iptables source via git
All checks were successful
Test / Create distribution (push) Successful in 1m43s
Test / Sandbox (push) Successful in 3m54s
Test / ShareFS (push) Successful in 5m41s
Test / Hakurei (push) Successful in 5m46s
Test / Sandbox (race detector) (push) Successful in 6m46s
Test / Hakurei (race detector) (push) Successful in 8m3s
Test / Flake checks (push) Successful in 1m32s
Eliminates the xz dependency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-05 00:19:40 +09:00
8b0648dd5d internal/rosa/netfilter: fetch libnftnl source via git
All checks were successful
Test / Create distribution (push) Successful in 1m43s
Test / Sandbox (push) Successful in 3m49s
Test / Hakurei (push) Successful in 5m38s
Test / ShareFS (push) Successful in 5m36s
Test / Sandbox (race detector) (push) Successful in 6m44s
Test / Hakurei (race detector) (push) Successful in 8m0s
Test / Flake checks (push) Successful in 1m32s
Eliminates the xz dependency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-05 00:14:45 +09:00
4667fac76c internal/rosa/libbsd: fetch source via git
All checks were successful
Test / Create distribution (push) Successful in 1m21s
Test / Sandbox (push) Successful in 3m16s
Test / Hakurei (push) Successful in 4m28s
Test / ShareFS (push) Successful in 4m38s
Test / Sandbox (race detector) (push) Successful in 5m46s
Test / Hakurei (race detector) (push) Successful in 7m7s
Test / Flake checks (push) Successful in 2m20s
Eliminates the xz dependency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-05 00:05:56 +09:00
52e5443b0e internal/rosa/libbsd: fetch libmd source via git
All checks were successful
Test / Create distribution (push) Successful in 1m28s
Test / Sandbox (push) Successful in 3m26s
Test / Hakurei (push) Successful in 4m46s
Test / ShareFS (push) Successful in 4m50s
Test / Sandbox (race detector) (push) Successful in 6m4s
Test / Hakurei (race detector) (push) Successful in 7m10s
Test / Flake checks (push) Successful in 1m50s
Eliminates the xz dependency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-05 00:02:56 +09:00
130e470b60 internal/rosa/libxslt: fetch source via git
All checks were successful
Test / Create distribution (push) Successful in 49s
Test / Sandbox (push) Successful in 4m14s
Test / Hakurei (push) Successful in 7m3s
Test / ShareFS (push) Successful in 7m16s
Test / Hakurei (race detector) (push) Successful in 9m54s
Test / Sandbox (race detector) (push) Successful in 2m35s
Test / Flake checks (push) Successful in 1m30s
Eliminates the xz dependency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-04 23:05:10 +09:00
ba5ee8e3ee internal/rosa/libxml2: fetch source via git
All checks were successful
Test / Create distribution (push) Successful in 1m41s
Test / Sandbox (push) Successful in 4m29s
Test / Hakurei (push) Successful in 0s
Test / ShareFS (push) Successful in 7m20s
Test / Sandbox (race detector) (push) Successful in 8m9s
Test / Hakurei (race detector) (push) Successful in 10m5s
Test / Flake checks (push) Successful in 1m33s
Eliminates the xz dependency. This also switches to meson to avoid pulling in autotools.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-04 22:58:29 +09:00
d1cef30877 internal/rosa/gtk: fetch glib source via git
All checks were successful
Test / Create distribution (push) Successful in 1m53s
Test / Sandbox (push) Successful in 4m28s
Test / ShareFS (push) Successful in 5m30s
Test / Hakurei (push) Successful in 7m13s
Test / Sandbox (race detector) (push) Successful in 6m48s
Test / Hakurei (race detector) (push) Successful in 4m17s
Test / Flake checks (push) Successful in 1m36s
This eliminates xz dependency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-04 22:47:10 +09:00
0188a3f0c7 internal/rosa/gnu: gnutls disable arm64 hardware acceleration
All checks were successful
Test / Create distribution (push) Successful in 1m36s
Test / ShareFS (push) Successful in 7m39s
Test / Sandbox (race detector) (push) Successful in 8m11s
Test / Hakurei (race detector) (push) Successful in 9m55s
Test / Sandbox (push) Successful in 1m43s
Test / Hakurei (push) Successful in 2m57s
Test / Flake checks (push) Successful in 1m29s
Hardware on arm64 is quite messy, this miscompiles.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-04 23:37:18 +09:00
04fe3b24ce internal/rosa/gnu: gnutls configure trust store
All checks were successful
Test / Create distribution (push) Successful in 1m21s
Test / Sandbox (push) Successful in 3m18s
Test / Hakurei (push) Successful in 4m31s
Test / ShareFS (push) Successful in 4m45s
Test / Sandbox (race detector) (push) Successful in 5m49s
Test / Hakurei (race detector) (push) Successful in 6m55s
Test / Flake checks (push) Successful in 1m53s
The test suite is somehow happy on amd64 but fails on arm64.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-04 22:33:51 +09:00
93ad551054 internal/rosa/git: shallow clone
All checks were successful
Test / Create distribution (push) Successful in 1m24s
Test / Sandbox (push) Successful in 3m19s
Test / Hakurei (push) Successful in 4m32s
Test / ShareFS (push) Successful in 4m34s
Test / Sandbox (race detector) (push) Successful in 5m58s
Test / Hakurei (race detector) (push) Successful in 7m2s
Test / Flake checks (push) Successful in 1m29s
The .git directory is destroyed anyway, so no point fetching more than the bare minimum.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-04 20:14:40 +09:00
3d54d1f176 internal/rosa: drop caches
All checks were successful
Test / Create distribution (push) Successful in 1m21s
Test / Sandbox (push) Successful in 3m14s
Test / Hakurei (push) Successful in 4m27s
Test / ShareFS (push) Successful in 4m31s
Test / Sandbox (race detector) (push) Successful in 5m55s
Test / Hakurei (race detector) (push) Successful in 6m55s
Test / Flake checks (push) Successful in 1m30s
This enables accurate benchmarking of the toolchain abstraction.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 20:58:23 +09:00
9feac7738f internal/pkg: optionally suppress via assumed checksum
All checks were successful
Test / Create distribution (push) Successful in 1m20s
Test / Sandbox (push) Successful in 3m24s
Test / Hakurei (push) Successful in 4m31s
Test / ShareFS (push) Successful in 4m41s
Test / Sandbox (race detector) (push) Successful in 5m53s
Test / Hakurei (race detector) (push) Successful in 6m58s
Test / Flake checks (push) Successful in 1m28s
This is quite error-prone and causes cache inconsistency similar to the store inconsistency seen on nix when a similar condition happens. Keep this behind a flag in case it is ever beneficial.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 20:33:59 +09:00
591a60bac9 internal/pkg: per-cache SCHED_IDLE
All checks were successful
Test / Create distribution (push) Successful in 1m19s
Test / Sandbox (push) Successful in 3m11s
Test / Hakurei (push) Successful in 4m22s
Test / ShareFS (push) Successful in 4m27s
Test / Sandbox (race detector) (push) Successful in 5m49s
Test / Hakurei (race detector) (push) Successful in 6m48s
Test / Flake checks (push) Successful in 1m27s
This is cleaner than setting it globally, and is impossible to race.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 16:49:34 +09:00
5093a06026 internal/pkg: cache flags
All checks were successful
Test / Create distribution (push) Successful in 1m19s
Test / Sandbox (push) Successful in 3m11s
Test / Hakurei (push) Successful in 4m25s
Test / ShareFS (push) Successful in 4m30s
Test / Sandbox (race detector) (push) Successful in 5m48s
Test / Hakurei (race detector) (push) Successful in 6m56s
Test / Flake checks (push) Successful in 1m29s
This is cleaner for extending the API.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 16:16:55 +09:00
50c1d7f880 internal/rosa/kernel: 6.12.78 to 6.12.80
All checks were successful
Test / ShareFS (push) Successful in 47s
Test / Sandbox (push) Successful in 54s
Test / Hakurei (push) Successful in 59s
Test / Hakurei (race detector) (push) Successful in 57s
Test / Sandbox (race detector) (push) Successful in 52s
Test / Create distribution (push) Successful in 1m10s
Test / Flake checks (push) Successful in 1m27s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 16:14:16 +09:00
9e63633fbc container: remove test timeouts
All checks were successful
Test / Create distribution (push) Successful in 3m2s
Test / Sandbox (push) Successful in 5m25s
Test / Hakurei (push) Successful in 7m45s
Test / ShareFS (push) Successful in 7m54s
Test / Sandbox (race detector) (push) Successful in 8m47s
Test / Hakurei (race detector) (push) Successful in 10m38s
Test / Flake checks (push) Successful in 1m49s
These timeouts are no longer useful, and causes spurious test failures under load.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 10:51:37 +09:00
61f981a34a internal/rosa/perl: 5.42.1 to 5.42.2
All checks were successful
Test / Create distribution (push) Successful in 1m25s
Test / Sandbox (push) Successful in 2m36s
Test / Hakurei (push) Successful in 4m11s
Test / ShareFS (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 6m1s
Test / Hakurei (race detector) (push) Successful in 4m24s
Test / Flake checks (push) Successful in 4m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 10:45:25 +09:00
d717c41bbe internal/rosa/cmake: 4.3.0 to 4.3.1
All checks were successful
Test / Sandbox (push) Successful in 2m37s
Test / Create distribution (push) Successful in 1m14s
Test / Hakurei (push) Successful in 3m50s
Test / ShareFS (push) Successful in 3m19s
Test / Sandbox (race detector) (push) Successful in 5m35s
Test / Hakurei (race detector) (push) Successful in 4m14s
Test / Flake checks (push) Successful in 3m46s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 10:45:07 +09:00
b896eec9b7 internal/rosa/gnu: parallel 20260222 to 20260322
All checks were successful
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m6s
Test / ShareFS (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 6m32s
Test / Hakurei (race detector) (push) Successful in 9m45s
Test / Hakurei (push) Successful in 2m52s
Test / Flake checks (push) Successful in 2m32s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 10:44:48 +09:00
8ab99e5e40 internal/rosa/util-linux: 2.41.3 to 2.42
All checks were successful
Test / Sandbox (push) Successful in 3m25s
Test / Create distribution (push) Successful in 1m14s
Test / ShareFS (push) Successful in 4m35s
Test / Sandbox (race detector) (push) Successful in 6m50s
Test / Hakurei (race detector) (push) Successful in 4m12s
Test / Hakurei (push) Successful in 3m47s
Test / Flake checks (push) Successful in 3m13s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 10:44:25 +09:00
2b6160ef7d internal/rosa/wayland: wayland-protocols 1.47 to 1.48
All checks were successful
Test / Create distribution (push) Successful in 1m29s
Test / Sandbox (push) Successful in 5m36s
Test / Hakurei (push) Successful in 7m50s
Test / ShareFS (push) Successful in 5m41s
Test / Sandbox (race detector) (push) Successful in 8m49s
Test / Hakurei (race detector) (push) Successful in 3m41s
Test / Flake checks (push) Successful in 3m51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 10:44:04 +09:00
4dcac7f133 internal/rosa/xz: 5.8.2 to 5.8.3
All checks were successful
Test / Sandbox (push) Successful in 5m33s
Test / ShareFS (push) Successful in 7m47s
Test / Sandbox (race detector) (push) Successful in 8m53s
Test / Hakurei (race detector) (push) Successful in 10m40s
Test / Hakurei (push) Successful in 2m45s
Test / Create distribution (push) Successful in 1m51s
Test / Flake checks (push) Successful in 3m51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 10:43:37 +09:00
966fd4df9e internal/rosa: connman artifact
All checks were successful
Test / Create distribution (push) Successful in 1m20s
Test / Sandbox (push) Successful in 3m12s
Test / Hakurei (push) Successful in 4m28s
Test / ShareFS (push) Successful in 4m29s
Test / Sandbox (race detector) (push) Successful in 5m42s
Test / Hakurei (race detector) (push) Successful in 6m51s
Test / Flake checks (push) Successful in 1m26s
Will be gradually replaced with a native implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 01:17:10 +09:00
a2cf59b989 internal/rosa/ncurses: also build dynamic library
All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m3s
Test / Hakurei (push) Successful in 4m22s
Test / ShareFS (push) Successful in 4m26s
Test / Sandbox (race detector) (push) Successful in 5m40s
Test / Hakurei (race detector) (push) Successful in 6m45s
Test / Flake checks (push) Successful in 1m47s
GNU readline breaks without this.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 01:13:04 +09:00
e87f59c4e4 internal/rosa/gnu: readline artifact
All checks were successful
Test / Sandbox (push) Successful in 3m48s
Test / Hakurei (push) Successful in 5m30s
Test / ShareFS (push) Successful in 5m24s
Test / Sandbox (race detector) (push) Successful in 6m30s
Test / Hakurei (race detector) (push) Successful in 7m40s
Test / Flake checks (push) Successful in 1m30s
Test / Create distribution (push) Successful in 35s
Nice to have library for command line programs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 00:42:59 +09:00
3b221c3e77 internal/rosa/gnu: gnutls artifact
All checks were successful
Test / Create distribution (push) Successful in 1m17s
Test / Sandbox (push) Successful in 4m11s
Test / Hakurei (push) Successful in 5m37s
Test / ShareFS (push) Successful in 5m2s
Test / Sandbox (race detector) (push) Successful in 6m27s
Test / Hakurei (race detector) (push) Successful in 7m40s
Test / Flake checks (push) Successful in 1m28s
Incredibly ugly and expensive package, but unfortunately required by some packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 00:36:28 +09:00
ff3b385b12 internal/rosa: libunistring artifact
All checks were successful
Test / Create distribution (push) Successful in 1m29s
Test / Sandbox (push) Successful in 3m44s
Test / Hakurei (push) Successful in 5m22s
Test / ShareFS (push) Successful in 5m26s
Test / Sandbox (race detector) (push) Successful in 6m36s
Test / Hakurei (race detector) (push) Successful in 7m44s
Test / Flake checks (push) Successful in 1m28s
Required by GnuTLS.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 00:00:13 +09:00
c6920e6ab7 cmd/mbf: pick up $TERM
All checks were successful
Test / Create distribution (push) Successful in 45s
Test / Sandbox (push) Successful in 3m35s
Test / Hakurei (push) Successful in 5m15s
Test / ShareFS (push) Successful in 5m25s
Test / Sandbox (race detector) (push) Successful in 6m34s
Test / Hakurei (race detector) (push) Successful in 7m47s
Test / Flake checks (push) Successful in 1m27s
This improves behaviour of some programs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-02 23:59:15 +09:00
59b25d45fe internal/pkg: pick up $TERM if attaching stdin
All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m11s
Test / Hakurei (push) Successful in 4m29s
Test / ShareFS (push) Successful in 4m31s
Test / Sandbox (race detector) (push) Successful in 5m42s
Test / Hakurei (race detector) (push) Successful in 6m57s
Test / Flake checks (push) Successful in 1m26s
This improves behaviour of some programs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-02 23:50:07 +09:00
9b99650eb1 internal/rosa: libev artifact
All checks were successful
Test / Create distribution (push) Successful in 1m17s
Test / Sandbox (push) Successful in 3m14s
Test / Hakurei (push) Successful in 4m20s
Test / ShareFS (push) Successful in 4m28s
Test / Sandbox (race detector) (push) Successful in 5m43s
Test / Hakurei (race detector) (push) Successful in 3m43s
Test / Flake checks (push) Successful in 1m27s
Required by gnutls.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-02 23:35:01 +09:00
15bff9e1a6 internal/rosa/git: determine reporting name from url
All checks were successful
Test / Create distribution (push) Successful in 1m17s
Test / Sandbox (push) Successful in 3m16s
Test / Hakurei (push) Successful in 4m17s
Test / ShareFS (push) Successful in 4m25s
Test / Sandbox (race detector) (push) Successful in 5m47s
Test / Hakurei (race detector) (push) Successful in 6m48s
Test / Flake checks (push) Successful in 1m26s
This is generally correct, and is a lot cleaner to call.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-02 22:42:16 +09:00
b948525c07 internal/rosa: nettle3 artifact
All checks were successful
Test / Create distribution (push) Successful in 1m17s
Test / Sandbox (push) Successful in 3m9s
Test / Hakurei (push) Successful in 4m23s
Test / ShareFS (push) Successful in 4m31s
Test / Sandbox (race detector) (push) Successful in 5m48s
Test / Hakurei (race detector) (push) Successful in 6m45s
Test / Flake checks (push) Successful in 1m27s
Removed after all packages upgrade for nettle 4.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-02 22:12:39 +09:00
9acbd16e9a internal/rosa/p11: explicitly enable libffi
All checks were successful
Test / Create distribution (push) Successful in 1m19s
Test / Sandbox (push) Successful in 3m12s
Test / Hakurei (push) Successful in 4m19s
Test / ShareFS (push) Successful in 4m25s
Test / Sandbox (race detector) (push) Successful in 5m43s
Test / Hakurei (race detector) (push) Successful in 6m49s
Test / Flake checks (push) Successful in 1m47s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-02 22:05:07 +09:00
64e5a1068b internal/rosa: libtasn1 artifact
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m47s
Test / Hakurei (push) Successful in 3m39s
Test / ShareFS (push) Successful in 3m44s
Test / Sandbox (race detector) (push) Successful in 5m10s
Test / Hakurei (race detector) (push) Successful in 6m13s
Test / Flake checks (push) Successful in 1m23s
Optional dependency of p11-kit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-01 17:20:12 +09:00
b6cbd49d8c internal/rosa: p11-kit artifact
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m42s
Test / ShareFS (push) Successful in 3m45s
Test / Hakurei (push) Successful in 3m49s
Test / Sandbox (race detector) (push) Successful in 5m10s
Test / Hakurei (race detector) (push) Successful in 6m16s
Test / Flake checks (push) Successful in 1m22s
Another package distributed in xz only. This is fetched from the git remote directly to avoid XZ Utils.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-01 16:34:30 +09:00
6913b9224a internal/rosa/git: recursively clone submodules
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m37s
Test / Hakurei (push) Successful in 3m45s
Test / ShareFS (push) Successful in 3m50s
Test / Sandbox (race detector) (push) Successful in 5m15s
Test / Hakurei (race detector) (push) Successful in 6m22s
Test / Flake checks (push) Successful in 1m21s
There is generally no reason to disable this, so it was not made optional.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-01 16:21:46 +09:00
9584958ecc internal/rosa/pkg-config: generate build system
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / ShareFS (push) Successful in 8m9s
Test / Sandbox (race detector) (push) Successful in 8m15s
Test / Hakurei (race detector) (push) Successful in 3m42s
Test / Sandbox (push) Successful in 1m36s
Test / Hakurei (push) Successful in 2m48s
Test / Flake checks (push) Successful in 1m24s
This unfortunately pulls automake, libtool and their dependencies into stage2.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-01 15:31:24 +09:00
389844b1ea internal/rosa/gnu: mpc 1.3.1 to 1.4.0
All checks were successful
Test / Create distribution (push) Successful in 1m6s
Test / Sandbox (push) Successful in 2m43s
Test / Hakurei (push) Successful in 3m55s
Test / ShareFS (push) Successful in 3m54s
Test / Sandbox (race detector) (push) Successful in 5m13s
Test / Hakurei (race detector) (push) Successful in 6m20s
Test / Flake checks (push) Successful in 1m23s
This package now unfortunately switched to xz as well.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 23:56:20 +09:00
5b7ab35633 internal/rosa: iptables artifact
All checks were successful
Test / Create distribution (push) Successful in 1m11s
Test / Sandbox (push) Successful in 3m19s
Test / Hakurei (push) Successful in 4m27s
Test / ShareFS (push) Successful in 4m34s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m42s
Test / Flake checks (push) Successful in 1m21s
This also pulls in netlink libraries from netfilter project.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 23:47:16 +09:00
52b1a5a725 internal/rosa: use type P in helper interface
All checks were successful
Test / Create distribution (push) Successful in 42s
Test / Sandbox (push) Successful in 1m50s
Test / ShareFS (push) Successful in 3m12s
Test / Sandbox (race detector) (push) Successful in 5m32s
Test / Hakurei (race detector) (push) Successful in 6m36s
Test / Hakurei (push) Successful in 2m40s
Test / Flake checks (push) Successful in 1m26s
This is easier to type and serialises correctly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 23:45:01 +09:00
6b78df8714 internal/rosa: libmd and libbsd artifacts
All checks were successful
Test / Create distribution (push) Successful in 1m12s
Test / Sandbox (push) Successful in 3m5s
Test / Hakurei (push) Successful in 4m13s
Test / ShareFS (push) Successful in 4m18s
Test / Sandbox (race detector) (push) Successful in 5m26s
Test / Hakurei (race detector) (push) Successful in 6m33s
Test / Flake checks (push) Successful in 1m27s
These provide headers that are provided by glibc but not musl.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 22:57:08 +09:00
dadf170a46 internal/rosa: dbus artifact
All checks were successful
Test / Create distribution (push) Successful in 1m8s
Test / Sandbox (push) Successful in 2m54s
Test / ShareFS (push) Successful in 3m49s
Test / Hakurei (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 5m19s
Test / Hakurei (race detector) (push) Successful in 6m24s
Test / Flake checks (push) Successful in 2m55s
Unfortunate ugly indirect dependency we cannot yet get rid of.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 21:47:05 +09:00
9594832302 internal/rosa/meson: disallow download
All checks were successful
Test / Create distribution (push) Successful in 1m7s
Test / Sandbox (push) Successful in 2m49s
Test / ShareFS (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 5m23s
Test / Hakurei (race detector) (push) Successful in 6m33s
Test / Hakurei (push) Successful in 2m47s
Test / Flake checks (push) Successful in 1m27s
This will fail and waste time on KindExec, and cause nondeterminism in KindExecNet.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 21:38:09 +09:00
91a2d4d6e1 internal/uevent: integrate error handling in event loop
All checks were successful
Test / Create distribution (push) Successful in 1m17s
Test / Sandbox (push) Successful in 3m13s
Test / Hakurei (push) Successful in 4m18s
Test / ShareFS (push) Successful in 4m24s
Test / Sandbox (race detector) (push) Successful in 5m35s
Test / Hakurei (race detector) (push) Successful in 6m42s
Test / Flake checks (push) Successful in 1m25s
There are many subtleties when recovering from errors in the event loop, and coldboot requires internals to drain the receive buffer as synthetic uevents are being arranged.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-31 00:10:14 +09:00
a854719b9f internal/netlink: optional recvmsg without netpoll
All checks were successful
Test / Create distribution (push) Successful in 1m14s
Test / Sandbox (push) Successful in 3m7s
Test / Hakurei (push) Successful in 4m10s
Test / ShareFS (push) Successful in 4m22s
Test / Sandbox (race detector) (push) Successful in 5m34s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Flake checks (push) Successful in 1m25s
For draining the socket receive buffer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 23:18:43 +09:00
f03c0fb249 internal/uevent: synthetic events for coldboot
All checks were successful
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 3m5s
Test / Hakurei (push) Successful in 4m12s
Test / ShareFS (push) Successful in 4m16s
Test / Sandbox (race detector) (push) Successful in 5m35s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Flake checks (push) Successful in 1m24s
This causes the kernel to regenerate events that happened before earlyinit started.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 23:01:08 +09:00
a6600be34a all: use filepath
All checks were successful
Test / Create distribution (push) Successful in 1m17s
Test / Sandbox (push) Successful in 3m5s
Test / Hakurei (push) Successful in 4m12s
Test / ShareFS (push) Successful in 4m25s
Test / Sandbox (race detector) (push) Successful in 5m39s
Test / Hakurei (race detector) (push) Successful in 6m44s
Test / Flake checks (push) Successful in 1m24s
This makes package check portable, and removes nonportable behaviour from package pkg, pipewire, and system. All other packages remain nonportable due to their nature. No latency increase was observed due to this change on amd64 and arm64 linux.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 18:24:53 +09:00
b5592633f5 internal/uevent: separate recvmsg helper
All checks were successful
Test / Create distribution (push) Successful in 1m13s
Test / Sandbox (push) Successful in 3m3s
Test / Hakurei (push) Successful in 4m13s
Test / ShareFS (push) Successful in 4m16s
Test / Sandbox (race detector) (push) Successful in 5m37s
Test / Hakurei (race detector) (push) Successful in 6m46s
Test / Flake checks (push) Successful in 1m23s
This enables messages to be received separately.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 02:53:26 +09:00
584e302168 internal/netlink: set receive buffer size
All checks were successful
Test / Create distribution (push) Successful in 1m14s
Test / Sandbox (push) Successful in 3m2s
Test / Hakurei (push) Successful in 4m15s
Test / ShareFS (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 5m32s
Test / Hakurei (race detector) (push) Successful in 6m44s
Test / Flake checks (push) Successful in 1m24s
This is done by both systemd sd-device and AOSP ueventd to improve robustness. Rosa OS will still handle ENOBUFS via coldboot but a big buffer should mitigate this as well.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 02:14:47 +09:00
141958656f internal/uevent: handle state divergence
All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m9s
Test / Hakurei (push) Successful in 4m13s
Test / ShareFS (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 5m37s
Test / Hakurei (race detector) (push) Successful in 6m47s
Test / Flake checks (push) Successful in 1m25s
This requires the caller to arrange for a coldboot to happen, some time after this error is encountered, and to resume event processing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-30 01:50:09 +09:00
648079f42c internal/netlink: switch to recvmsg/sendmsg
All checks were successful
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 3m6s
Test / Hakurei (push) Successful in 4m13s
Test / ShareFS (push) Successful in 4m20s
Test / Sandbox (race detector) (push) Successful in 5m35s
Test / Hakurei (race detector) (push) Successful in 6m40s
Test / Flake checks (push) Successful in 1m25s
These are more flexible than recvfrom/sendto.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-29 23:36:00 +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
6cdb6a652b internal/rosa/gtk: glib 2.87.5 to 2.88.0
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / ShareFS (push) Successful in 41s
Test / Sandbox (push) Successful in 48s
Test / Sandbox (race detector) (push) Successful in 47s
Test / Hakurei (push) Successful in 52s
Test / Hakurei (race detector) (push) Successful in 2m0s
Test / Flake checks (push) Successful in 1m21s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 20:58:39 +09:00
7c932cbceb internal/rosa: strace artifact
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 3m1s
Test / ShareFS (push) Successful in 4m49s
Test / Hakurei (push) Successful in 5m37s
Test / Sandbox (race detector) (push) Successful in 5m35s
Test / Hakurei (race detector) (push) Successful in 8m36s
Test / Flake checks (push) Successful in 1m21s
This is not part of the system, but a useful development tool. The test suite is quite broken but that is considered acceptable for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 20:40:17 +09:00
20ebddd9bf internal/rosa: export source kind
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 3m3s
Test / ShareFS (push) Successful in 4m53s
Test / Hakurei (push) Successful in 5m30s
Test / Sandbox (race detector) (push) Successful in 5m28s
Test / Hakurei (race detector) (push) Successful in 7m57s
Test / Flake checks (push) Successful in 1m22s
This is set for an exported field, so export the constants as well.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 20:09:44 +09:00
420c721c7d all: raise timeout defaults
All checks were successful
Test / Create distribution (push) Successful in 1m47s
Test / Sandbox (push) Successful in 12m48s
Test / Sandbox (race detector) (push) Successful in 14m31s
Test / Hakurei (push) Successful in 15m26s
Test / Hakurei (race detector) (push) Successful in 16m51s
Test / ShareFS (push) Successful in 38s
Test / Flake checks (push) Successful in 1m21s
This avoids timing out on systems running very slowly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 17:04:06 +09:00
bac583f89e internal/stub: move from container
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m39s
Test / ShareFS (push) Successful in 3m39s
Test / Sandbox (race detector) (push) Successful in 5m4s
Test / Hakurei (race detector) (push) Successful in 6m8s
Test / Flake checks (push) Successful in 1m19s
This package solves a very specific stubbing use case, in a less than elegant manner.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 16:09:14 +09:00
722989c682 fhs: move from container
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m41s
Test / Hakurei (push) Successful in 3m40s
Test / ShareFS (push) Successful in 3m42s
Test / Sandbox (race detector) (push) Successful in 5m10s
Test / Hakurei (race detector) (push) Successful in 6m10s
Test / Flake checks (push) Successful in 1m23s
This package is not container-specific.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 15:56:36 +09:00
b852402f67 ext: move syscall wrappers from container
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m36s
Test / Hakurei (push) Successful in 3m42s
Test / ShareFS (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 6m11s
Test / Flake checks (push) Successful in 1m30s
These are generally useful, and none of them are container-specific. Syscalls subtle to use and requiring container-specific setup remains in container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 15:48:40 +09:00
6d015a949e check: move from container
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m45s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m11s
Test / Flake checks (push) Successful in 1m22s
This package is not container specific, and widely used across the project.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 15:39:03 +09:00
e9a72490db vfs: move from container
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m39s
Test / Hakurei (push) Successful in 3m43s
Test / ShareFS (push) Successful in 3m44s
Test / Sandbox (race detector) (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 6m7s
Test / Flake checks (push) Successful in 1m21s
This package is not container-specific.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 15:30:30 +09:00
0a12d456ce container: set CLOEXEC via close_range
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m44s
Test / Hakurei (push) Successful in 3m42s
Test / ShareFS (push) Successful in 3m46s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m7s
Test / Flake checks (push) Successful in 1m23s
This is guarded behind the close_range build tag for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 14:19:00 +09:00
d1fc1a3db7 ext: wrap close_range syscall
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m48s
Test / Hakurei (push) Successful in 3m38s
Test / ShareFS (push) Successful in 3m41s
Test / Sandbox (race detector) (push) Successful in 5m6s
Test / Hakurei (race detector) (push) Successful in 6m10s
Test / Flake checks (push) Successful in 1m17s
This is useful for container when called with CLOSE_RANGE_CLOEXEC.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 14:15:16 +09:00
1c2d5f6b57 ext: integer limit values
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m37s
Test / Hakurei (push) Successful in 3m36s
Test / ShareFS (push) Successful in 3m44s
Test / Sandbox (race detector) (push) Successful in 4m58s
Test / Hakurei (race detector) (push) Successful in 54s
Test / Flake checks (push) Successful in 1m23s
For portably using C integers without cgo.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 14:09:38 +09:00
faea1f4bd6 all: remove deprecated packages
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m15s
Test / ShareFS (push) Successful in 3m42s
Test / Sandbox (race detector) (push) Successful in 5m8s
Test / Hakurei (race detector) (push) Successful in 5m44s
Test / Flake checks (push) Successful in 1m23s
Closes #24.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 13:54:56 +09:00
0cb1007daa ldd: remove deprecated API
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m42s
Test / Hakurei (push) Successful in 3m42s
Test / ShareFS (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m12s
Test / Flake checks (push) Successful in 1m35s
Closes #25.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 13:53:14 +09:00
e292031624 ext: move lookup test
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 2m44s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m42s
Test / Sandbox (race detector) (push) Successful in 5m4s
Test / Hakurei (race detector) (push) Successful in 6m13s
Test / Flake checks (push) Successful in 1m31s
This was kept in-place to reduce patch size in the previous patch.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 13:44:59 +09:00
cd5959fe5a ext: isolate from container/std
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m39s
Test / ShareFS (push) Successful in 3m42s
Test / Hakurei (push) Successful in 3m46s
Test / Sandbox (race detector) (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 6m12s
Test / Flake checks (push) Successful in 1m30s
These are too general to belong in the container package. This targets the v0.4 release to reduce the wrapper maintenance burden.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-17 13:39:26 +09:00
08c35ca24f container: use new netlink implementation
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m43s
Test / Hakurei (push) Successful in 3m40s
Test / ShareFS (push) Successful in 3m44s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m9s
Test / Flake checks (push) Successful in 1m23s
This is adapted from the container netlink implementation and is much more reusable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-16 23:33:52 +09:00
72bd3fb05e internal/netlink: generalise implementation from container
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m41s
Test / ShareFS (push) Successful in 3m42s
Test / Hakurei (push) Successful in 3m46s
Test / Sandbox (race detector) (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 6m8s
Test / Flake checks (push) Successful in 1m23s
This is useful for uevent implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-16 23:07:51 +09:00
59c66747df internal/rosa/kernel: 6.12.76 to 6.12.77
All checks were successful
Test / Create distribution (push) Successful in 1m8s
Test / Sandbox (push) Successful in 3m18s
Test / Hakurei (push) Successful in 4m56s
Test / ShareFS (push) Successful in 5m27s
Test / Hakurei (race detector) (push) Successful in 3m32s
Test / Sandbox (race detector) (push) Successful in 2m29s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-16 15:21:33 +09:00
9e6fe8db4b internal/rosa/meson: 1.10.1 to 1.10.2
All checks were successful
Test / Create distribution (push) Successful in 42s
Test / ShareFS (push) Successful in 48s
Test / Sandbox (push) Successful in 55s
Test / Sandbox (race detector) (push) Successful in 55s
Test / Hakurei (push) Successful in 58s
Test / Hakurei (race detector) (push) Successful in 58s
Test / Flake checks (push) Successful in 1m51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-16 13:41:06 +09:00
5168ee3e13 internal/rosa/python: remove pre_commit
All checks were successful
Test / Create distribution (push) Successful in 1m48s
Test / Sandbox (push) Successful in 3m50s
Test / Hakurei (push) Successful in 5m44s
Test / ShareFS (push) Successful in 5m54s
Test / Sandbox (race detector) (push) Successful in 7m8s
Test / Hakurei (race detector) (push) Successful in 8m45s
Test / Flake checks (push) Successful in 1m58s
This is unused and introduces many dependencies.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-16 13:39:56 +09:00
c8313c2dc4 internal/rosa/tamago: disable cgo
All checks were successful
Test / Create distribution (push) Successful in 2m8s
Test / Sandbox (push) Successful in 5m15s
Test / Hakurei (push) Successful in 6m44s
Test / ShareFS (push) Successful in 6m45s
Test / Sandbox (race detector) (push) Successful in 7m28s
Test / Hakurei (race detector) (push) Successful in 8m34s
Test / Flake checks (push) Successful in 1m25s
This toolchain does not support cgo for the new target, anyway, and disabling it altogether avoids adding a dependency on arm64.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-16 13:22:10 +09:00
3fcdadb669 internal/rosa/curl: remove broken test
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 3m41s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m10s
Test / Flake checks (push) Successful in 1m16s
Upstream testdata is not broken on the arm64 builder, but breaks reproducibly on amd64.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-16 12:54:03 +09:00
3966bc5152 internal/rosa/hakurei: 0.3.6 to 0.3.7
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m44s
Test / Hakurei (push) Successful in 3m36s
Test / ShareFS (push) Successful in 3m41s
Test / Sandbox (race detector) (push) Successful in 5m0s
Test / Hakurei (race detector) (push) Successful in 6m8s
Test / Flake checks (push) Successful in 1m20s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-15 21:43:55 +09:00
b208af8b85 release: 0.3.7
All checks were successful
Release / Create release (push) Successful in 1m5s
Test / Hakurei (push) Successful in 52s
Test / Create distribution (push) Successful in 37s
Test / ShareFS (push) Successful in 40s
Test / Sandbox (push) Successful in 46s
Test / Sandbox (race detector) (push) Successful in 46s
Test / Hakurei (race detector) (push) Successful in 51s
Test / Flake checks (push) Successful in 1m20s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-15 21:04:55 +09:00
8d650c0c8f all: migrate to rosa/hakurei
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m43s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m40s
Test / Sandbox (race detector) (push) Successful in 5m0s
Test / Hakurei (race detector) (push) Successful in 6m2s
Test / Flake checks (push) Successful in 1m20s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-15 20:12:51 +09:00
a720efc32d internal/rosa/llvm: arch-specific versions
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m46s
Test / Hakurei (push) Successful in 3m39s
Test / ShareFS (push) Successful in 3m42s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m7s
Test / Flake checks (push) Successful in 1m20s
This enables temporarily avoiding a broken release on specific targets.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-15 15:06:36 +09:00
400540cd41 internal/rosa/llvm: arch-specific patches
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m37s
Test / Sandbox (race detector) (push) Successful in 4m58s
Test / Hakurei (race detector) (push) Successful in 6m5s
Test / Flake checks (push) Successful in 1m18s
Broken aarch64 tests in LLVM seem unlikely to be fixed soon.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-15 11:37:24 +09:00
1113efa5c2 internal/rosa/kernel: enable arm64 block drivers
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m41s
Test / Hakurei (push) Successful in 3m39s
Test / ShareFS (push) Successful in 3m39s
Test / Sandbox (race detector) (push) Successful in 5m2s
Test / Hakurei (race detector) (push) Successful in 6m10s
Test / Flake checks (push) Successful in 1m18s
These are added separately to the amd64 patch due to the arm64 toolchain not being available at that time.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-15 00:22:05 +09:00
8b875f865c cmd/earlyinit: remount root and set firmware path
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m38s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m44s
Test / Sandbox (race detector) (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 6m10s
Test / Flake checks (push) Successful in 1m20s
The default search paths cannot be configured, configuring them here is most sound for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-14 19:50:04 +09:00
8905d653ba cmd/earlyinit: mount pseudo-filesystems
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m42s
Test / ShareFS (push) Successful in 3m40s
Test / Hakurei (push) Successful in 3m45s
Test / Sandbox (race detector) (push) Successful in 5m2s
Test / Hakurei (race detector) (push) Successful in 6m5s
Test / Flake checks (push) Successful in 1m30s
The proposal for merging both init programs was unanimously accepted, so this is set up here alongside devtmpfs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-14 19:43:42 +09:00
9c2fb6246f internal/rosa/kernel: enable FW_LOADER
All checks were successful
Test / Create distribution (push) Successful in 2m8s
Test / Sandbox (push) Successful in 4m13s
Test / Hakurei (push) Successful in 5m18s
Test / ShareFS (push) Successful in 5m15s
Test / Sandbox (race detector) (push) Successful in 6m30s
Test / Hakurei (race detector) (push) Successful in 7m35s
Test / Flake checks (push) Successful in 1m21s
This wants to be loaded early, so having it as a dlkm is not helpful as it will always be loaded anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-14 19:32:14 +09:00
9c116acec6 internal/rosa/kernel: enable amd64 block drivers
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m42s
Test / Hakurei (push) Successful in 3m37s
Test / ShareFS (push) Successful in 3m46s
Test / Sandbox (race detector) (push) Successful in 5m9s
Test / Hakurei (race detector) (push) Successful in 6m50s
Test / Flake checks (push) Successful in 3m5s
These have to be built into initramfs, anyway, so build them into the kernel instead. The arm64 toolchain is not yet ready, so will be updated in a later patch.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-14 19:22:56 +09:00
988239a2bc internal/rosa: basic system image
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m46s
Test / ShareFS (push) Successful in 3m42s
Test / Hakurei (push) Successful in 3m48s
Test / Sandbox (race detector) (push) Successful in 5m0s
Test / Hakurei (race detector) (push) Successful in 6m7s
Test / Flake checks (push) Successful in 1m23s
This is a simple image for debugging and is not yet set up for dm-verity.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-14 15:54:13 +09:00
bc03118142 cmd/earlyinit: handle args from cmdline
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m41s
Test / Hakurei (push) Successful in 3m40s
Test / ShareFS (push) Successful in 3m41s
Test / Sandbox (race detector) (push) Successful in 5m0s
Test / Hakurei (race detector) (push) Successful in 6m11s
Test / Flake checks (push) Successful in 1m22s
These are set by the bootloader.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-14 15:13:52 +09:00
74c213264a internal/rosa/git: install libexec symlinks
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 2m41s
Test / ShareFS (push) Successful in 3m42s
Test / Hakurei (push) Successful in 3m49s
Test / Sandbox (race detector) (push) Successful in 5m11s
Test / Hakurei (race detector) (push) Successful in 3m22s
Test / Flake checks (push) Successful in 1m16s
This is less clumsy to represent.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 20:43:23 +09:00
345cffddc2 cmd/mbf: optionally export output
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m39s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m41s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m6s
Test / Flake checks (push) Successful in 1m21s
This is for debugging for now, as no program consumes this format yet.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 19:53:55 +09:00
49163758c8 internal/rosa/llvm: 22.1.0 to 22.1.1
All checks were successful
Test / Create distribution (push) Successful in 1m22s
Test / Sandbox (push) Successful in 3m14s
Test / Hakurei (push) Successful in 4m28s
Test / ShareFS (push) Successful in 4m37s
Test / Sandbox (race detector) (push) Successful in 5m41s
Test / Hakurei (race detector) (push) Successful in 6m52s
Test / Flake checks (push) Successful in 1m24s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 16:08:49 +09:00
ad22c15fb1 internal/rosa/perl: 5.42.0 to 5.42.1
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 3m19s
Test / ShareFS (push) Successful in 3m25s
Test / Sandbox (race detector) (push) Successful in 5m45s
Test / Hakurei (race detector) (push) Successful in 6m52s
Test / Hakurei (push) Successful in 4m27s
Test / Flake checks (push) Successful in 2m7s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 16:08:24 +09:00
9c774f7e0a internal/rosa/python: setuptools 82.0.0 to 82.0.1
All checks were successful
Test / Create distribution (push) Successful in 1m21s
Test / Sandbox (push) Successful in 3m6s
Test / Hakurei (push) Successful in 4m33s
Test / ShareFS (push) Successful in 4m33s
Test / Sandbox (race detector) (push) Successful in 5m55s
Test / Hakurei (race detector) (push) Successful in 7m1s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 15:32:00 +09:00
707f0a349f internal/rosa/gtk: glib 2.87.3 to 2.87.5
All checks were successful
Test / Create distribution (push) Successful in 3m43s
Test / Sandbox (push) Successful in 6m11s
Test / Hakurei (push) Successful in 7m18s
Test / ShareFS (push) Successful in 7m18s
Test / Sandbox (race detector) (push) Successful in 8m28s
Test / Hakurei (race detector) (push) Successful in 9m33s
Test / Flake checks (push) Successful in 3m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 15:26:42 +09:00
7c35be066a internal/rosa/tamago: 1.26.0 to 1.26.1
All checks were successful
Test / Create distribution (push) Successful in 1m9s
Test / Sandbox (push) Successful in 2m50s
Test / Hakurei (push) Successful in 4m34s
Test / ShareFS (push) Successful in 4m50s
Test / Sandbox (race detector) (push) Successful in 5m53s
Test / Hakurei (race detector) (push) Successful in 6m58s
Test / Flake checks (push) Successful in 4m41s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 15:23:29 +09:00
f91d55fa5e internal/rosa/curl: 8.18.0 to 8.19.0
All checks were successful
Test / Create distribution (push) Successful in 1m9s
Test / Sandbox (push) Successful in 2m54s
Test / ShareFS (push) Successful in 4m48s
Test / Sandbox (race detector) (push) Successful in 43s
Test / Hakurei (push) Successful in 6m19s
Test / Hakurei (race detector) (push) Successful in 7m8s
Test / Flake checks (push) Successful in 1m24s
The test suite now depends on python to run mock servers. SMB is disabled because it is completely unused, and pulls in a python dependency for tests. A broken test is fixed and the patch hopefully upstreamed before next release.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 15:23:07 +09:00
5862cc1966 internal/rosa/kernel: firmware 20260221 to 20260309
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m41s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 5m0s
Test / Hakurei (race detector) (push) Successful in 6m11s
Test / Flake checks (push) Successful in 1m17s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 14:06:21 +09:00
b3f0360a05 internal/rosa: populate runtime dependencies
All checks were successful
Test / Create distribution (push) Successful in 3m36s
Test / ShareFS (push) Successful in 8m53s
Test / Sandbox (race detector) (push) Successful in 9m28s
Test / Hakurei (race detector) (push) Successful in 10m25s
Test / Sandbox (push) Successful in 1m37s
Test / Hakurei (push) Successful in 2m27s
Test / Flake checks (push) Successful in 1m19s
This also removes manually resolved indirect dependencies.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 13:23:30 +09:00
8938994036 cmd/mbf: display runtime dependency info
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m41s
Test / Hakurei (push) Successful in 3m39s
Test / ShareFS (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 5m6s
Test / Hakurei (race detector) (push) Successful in 6m10s
Test / Flake checks (push) Successful in 1m14s
This only presents top-level dependencies, resolving indirect dependencies can be misleading in this context.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 10:46:37 +09:00
96d382f805 cmd/mbf: resolve runtime dependencies
All checks were successful
Test / Create distribution (push) Successful in 1m9s
Test / Sandbox (push) Successful in 2m55s
Test / Hakurei (push) Successful in 3m52s
Test / ShareFS (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 5m10s
Test / Hakurei (race detector) (push) Successful in 6m12s
Test / Flake checks (push) Successful in 1m30s
This also adds the collection meta-artifact for concurrent curing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 10:41:22 +09:00
5c785c135c internal/rosa: collection meta-artifact
All checks were successful
Test / Create distribution (push) Successful in 41s
Test / Sandbox (push) Successful in 1m51s
Test / Hakurei (push) Successful in 2m54s
Test / ShareFS (push) Successful in 3m2s
Test / Sandbox (race detector) (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m14s
Test / Flake checks (push) Successful in 1m30s
This is a stub FloodArtifact for concurrently curing multiple artifacts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 10:34:45 +09:00
0130f8ea6d internal/rosa: represent runtime dependencies
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m38s
Test / Hakurei (push) Successful in 3m37s
Test / ShareFS (push) Successful in 3m41s
Test / Sandbox (race detector) (push) Successful in 4m57s
Test / Hakurei (race detector) (push) Successful in 6m10s
Test / Flake checks (push) Successful in 1m21s
This also resolves indirect dependencies, reducing noise.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-13 10:31:14 +09:00
faac5c4a83 internal/rosa: store artifact results in struct
All checks were successful
Test / Create distribution (push) Successful in 1m0s
Test / Sandbox (push) Successful in 2m39s
Test / Hakurei (push) Successful in 3m43s
Test / ShareFS (push) Successful in 3m40s
Test / Sandbox (race detector) (push) Successful in 5m6s
Test / Hakurei (race detector) (push) Successful in 6m8s
Test / Flake checks (push) Successful in 1m21s
This is cleaner and makes adding additional values easier.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-12 18:08:41 +09:00
620062cca9 hst: expose scheduling priority
All checks were successful
Test / ShareFS (push) Successful in 43s
Test / Hakurei (push) Successful in 53s
Test / Sandbox (push) Successful in 49s
Test / Sandbox (race detector) (push) Successful in 48s
Test / Hakurei (race detector) (push) Successful in 53s
Test / Create distribution (push) Successful in 35s
Test / Flake checks (push) Successful in 1m18s
This is useful when limits are configured to allow it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-12 02:15:14 +09:00
196b200d0f container: expose priority and SCHED_OTHER policy
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / ShareFS (push) Successful in 40s
Test / Sandbox (push) Successful in 46s
Test / Sandbox (race detector) (push) Successful in 45s
Test / Hakurei (push) Successful in 52s
Test / Hakurei (race detector) (push) Successful in 50s
Test / Flake checks (push) Successful in 1m14s
The more explicit API removes the arbitrary limit preventing use of SCHED_OTHER (referred to as SCHED_NORMAL in the kernel). This change also exposes priority value to set.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-12 01:14:03 +09:00
04e6bc3c5c hst: expose scheduling policy
All checks were successful
Test / ShareFS (push) Successful in 39s
Test / Sandbox (push) Successful in 45s
Test / Hakurei (push) Successful in 50s
Test / Sandbox (race detector) (push) Successful in 45s
Test / Hakurei (race detector) (push) Successful in 49s
Test / Create distribution (push) Successful in 59s
Test / Flake checks (push) Successful in 1m19s
This is primarily useful for poorly written music players for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-12 00:52:18 +09:00
5c540f90aa internal/outcome: improve doc comments
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m44s
Test / Hakurei (push) Successful in 4m21s
Test / ShareFS (push) Successful in 4m49s
Test / Hakurei (race detector) (push) Successful in 3m42s
Test / Sandbox (race detector) (push) Successful in 2m24s
Test / Flake checks (push) Successful in 1m17s
This improves readability on smaller displays.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-11 21:04:02 +09:00
1e8ac5f68e container: use policy name in log message
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m49s
Test / Hakurei (push) Successful in 3m49s
Test / ShareFS (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 5m6s
Test / Hakurei (race detector) (push) Successful in 6m4s
Test / Flake checks (push) Successful in 1m18s
This is more helpful than having the user resolve the integer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-11 20:20:34 +09:00
fd515badff container: move scheduler policy constants to std
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m39s
Test / Sandbox (race detector) (push) Successful in 5m0s
Test / Hakurei (race detector) (push) Successful in 6m57s
Test / Flake checks (push) Successful in 2m9s
This avoids depending on cgo.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-11 20:03:08 +09:00
330a344845 hst: improve doc comments
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m40s
Test / ShareFS (push) Successful in 3m47s
Test / Sandbox (race detector) (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 5m58s
Test / Flake checks (push) Successful in 1m26s
These now read a lot better both in source and on pkgsite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-11 19:21:55 +09:00
48cdf8bf85 go: 1.26
All checks were successful
Test / Sandbox (push) Successful in 3m1s
Test / Hakurei (push) Successful in 3m58s
Test / ShareFS (push) Successful in 4m10s
Test / Sandbox (race detector) (push) Successful in 5m25s
Test / Hakurei (race detector) (push) Successful in 6m48s
Test / Create distribution (push) Successful in 1m3s
Test / Flake checks (push) Successful in 2m24s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-10 03:29:19 +09:00
7fb42ba49d internal/rosa/llvm: set LLVM_LIT_ARGS
All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m11s
Test / Hakurei (push) Successful in 4m23s
Test / ShareFS (push) Successful in 4m24s
Test / Sandbox (race detector) (push) Successful in 5m19s
Test / Hakurei (race detector) (push) Successful in 6m33s
Test / Flake checks (push) Successful in 1m30s
This replaces the progress bar, which was worse than useless.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-10 02:05:11 +09:00
19a2737148 container: sched policy string representation
All checks were successful
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m48s
Test / ShareFS (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 4m58s
Test / Hakurei (race detector) (push) Successful in 5m55s
Test / Flake checks (push) Successful in 1m31s
This also uses priority obtained via sched_get_priority_min, and improves bounds checking.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-09 18:38:31 +09:00
baf2def9cc internal/rosa/kmod: prefix moduledir
All checks were successful
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 2m38s
Test / Hakurei (push) Successful in 3m39s
Test / ShareFS (push) Successful in 3m48s
Test / Sandbox (race detector) (push) Successful in 4m58s
Test / Hakurei (race detector) (push) Successful in 5m56s
Test / Flake checks (push) Successful in 1m32s
This change also works around the kernel build system being unaware of this option.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-09 16:40:55 +09:00
242e042cb9 internal/rosa/nss: rename from ssl
All checks were successful
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m38s
Test / Hakurei (push) Successful in 3m38s
Test / ShareFS (push) Successful in 3m50s
Test / Sandbox (race detector) (push) Successful in 4m55s
Test / Hakurei (race detector) (push) Successful in 5m52s
Test / Flake checks (push) Successful in 1m32s
The SSL name came from earlier on and is counterintuitive.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-09 14:58:31 +09:00
6988c9c4db internal/rosa: firmware artifact
All checks were successful
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m32s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m52s
Test / Sandbox (race detector) (push) Successful in 4m55s
Test / Hakurei (race detector) (push) Successful in 5m57s
Test / Flake checks (push) Successful in 1m29s
Required for generic hardware.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 22:50:36 +09:00
d6e0ed8c76 internal/rosa/python: various pypi artifacts
All checks were successful
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 2m34s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 4m56s
Test / Hakurei (race detector) (push) Successful in 6m1s
Test / Flake checks (push) Successful in 1m28s
These are dependencies of pre-commit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 22:25:16 +09:00
53be3309c5 internal/rosa: rdfind artifact
All checks were successful
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m34s
Test / Hakurei (push) Successful in 3m37s
Test / ShareFS (push) Successful in 3m52s
Test / Sandbox (race detector) (push) Successful in 5m0s
Test / Hakurei (race detector) (push) Successful in 5m59s
Test / Flake checks (push) Successful in 1m25s
Required by linux firmware.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 20:26:15 +09:00
644dd18a52 internal/rosa: nettle artifact
All checks were successful
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m34s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 5m57s
Test / Flake checks (push) Successful in 1m29s
Required by rdfind, which is required by linux firmware.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 20:22:09 +09:00
27c6f976df internal/rosa/gnu: parallel artifact
All checks were successful
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m34s
Test / Hakurei (push) Successful in 3m37s
Test / ShareFS (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 4m53s
Test / Hakurei (race detector) (push) Successful in 5m52s
Test / Flake checks (push) Successful in 1m24s
Used by linux firmware.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 19:56:40 +09:00
279a973633 internal/rosa: build independent earlyinit
All checks were successful
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m39s
Test / Hakurei (push) Successful in 3m41s
Test / ShareFS (push) Successful in 3m49s
Test / Sandbox (race detector) (push) Successful in 4m57s
Test / Hakurei (race detector) (push) Successful in 5m58s
Test / Flake checks (push) Successful in 1m28s
This avoids unnecessarily rebuilding hakurei during development.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 18:29:04 +09:00
9c1b522689 internal/rosa/hakurei: optional hostname tool
All checks were successful
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 2m34s
Test / Hakurei (push) Successful in 3m42s
Test / ShareFS (push) Successful in 3m50s
Test / Sandbox (race detector) (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 5m52s
Test / Flake checks (push) Successful in 1m29s
This makes it more efficient to reuse the helper for partial builds.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 18:26:03 +09:00
5c8cd46c02 internal/rosa: update arm64 kernel config
All checks were successful
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 2m42s
Test / Hakurei (push) Successful in 3m39s
Test / ShareFS (push) Successful in 3m48s
Test / Sandbox (race detector) (push) Successful in 4m56s
Test / Hakurei (race detector) (push) Successful in 5m59s
Test / Flake checks (push) Successful in 1m26s
This was not feasible during the bump, now there is a viable toolchain.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 03:17:53 +09:00
2dba550a2b internal/rosa/zlib: 1.3.1 to 1.3.2
All checks were successful
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m42s
Test / Hakurei (push) Successful in 3m54s
Test / ShareFS (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 6m2s
Test / Flake checks (push) Successful in 1m30s
This also switches to the CMake build system because upstream broke their old build system.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 02:36:59 +09:00
8c64812b34 internal/rosa: add zlib runtime dependency
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m39s
Test / Hakurei (push) Successful in 3m47s
Test / ShareFS (push) Successful in 3m52s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 5m56s
Test / Flake checks (push) Successful in 1m30s
For transitioning to dynamically linking zlib.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 02:36:09 +09:00
d1423d980d internal/rosa/cmake: bake in CMAKE_INSTALL_LIBDIR
All checks were successful
Test / Create distribution (push) Successful in 2m17s
Test / Hakurei (push) Successful in 8m0s
Test / ShareFS (push) Successful in 8m27s
Test / Sandbox (race detector) (push) Successful in 8m43s
Test / Hakurei (race detector) (push) Successful in 9m56s
Test / Sandbox (push) Successful in 1m39s
Test / Flake checks (push) Successful in 2m14s
There is never a good reason to set this to anything else, and the default value of lib64 breaks everything. This did not manifest on LLVM (which the CMake helper was initially written for) because it did not use this value.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 01:20:41 +09:00
104da0f66a internal/rosa/cmake: pass correct prefix
All checks were successful
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m48s
Test / ShareFS (push) Successful in 4m33s
Test / Sandbox (race detector) (push) Successful in 5m19s
Test / Hakurei (race detector) (push) Successful in 6m25s
Test / Hakurei (push) Successful in 2m39s
Test / Flake checks (push) Successful in 1m29s
This can change build output similar to autotools --prefix and DESTDIR, but was not clearly indicated to do so.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 01:04:02 +09:00
d996d9fbb7 internal/rosa/cmake: pass parallel argument for make
All checks were successful
Test / Create distribution (push) Successful in 1m12s
Test / Sandbox (push) Successful in 3m24s
Test / Hakurei (push) Successful in 4m32s
Test / ShareFS (push) Successful in 4m38s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 3m51s
Test / Flake checks (push) Successful in 1m34s
This uses the default value for each build system, which is parallel for ninja but not for make.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 00:55:58 +09:00
469f97ccc1 internal/rosa/gnu: libiconv 1.18 to 1.19
All checks were successful
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 2m45s
Test / ShareFS (push) Successful in 3m55s
Test / Hakurei (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m58s
Test / Hakurei (race detector) (push) Successful in 6m0s
Test / Flake checks (push) Successful in 1m29s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-08 00:36:38 +09:00
af7a6180a1 internal/rosa/cmake: optionally use makefile
All checks were successful
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m36s
Test / Hakurei (push) Successful in 3m42s
Test / ShareFS (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m57s
Test / Hakurei (race detector) (push) Successful in 5m55s
Test / Flake checks (push) Successful in 1m30s
This breaks the dependency loop in zlib.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-07 22:47:30 +09:00
03b5c0e20a internal/rosa/tamago: populate Anitya project id
All checks were successful
Test / Create distribution (push) Successful in 1m37s
Test / Sandbox (push) Successful in 3m41s
Test / Hakurei (push) Successful in 5m22s
Test / ShareFS (push) Successful in 5m30s
Test / Sandbox (race detector) (push) Successful in 6m19s
Test / Hakurei (race detector) (push) Successful in 7m55s
Test / Flake checks (push) Successful in 2m27s
This had to wait quite a while due to Microsoft Github rate-limiting.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-07 19:37:03 +09:00
6a31fb4fa3 internal/rosa: hakurei 0.3.5 to 0.3.6
All checks were successful
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 2m38s
Test / Hakurei (push) Successful in 3m38s
Test / ShareFS (push) Successful in 3m47s
Test / Sandbox (race detector) (push) Successful in 4m58s
Test / Hakurei (race detector) (push) Successful in 5m58s
Test / Flake checks (push) Successful in 1m27s
This also removes the backport patch.

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

39
.gitignore vendored
View File

@@ -1,37 +1,22 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
*.pkg
/hakurei
# Test binary, built with `go test -c`
# produced by tools and text editors
*.qcow2
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
.idea
.vscode
# go generate
/cmd/hakurei/LICENSE
/cmd/pkgserver/.sass-cache
/cmd/pkgserver/ui/static/*.js
/cmd/pkgserver/ui/static/*.css*
/cmd/pkgserver/ui/static/*.css.map
/cmd/pkgserver/ui_test/*.js
/cmd/pkgserver/ui_test/lib/*.js
/cmd/pkgserver/ui_test/lib/*.css*
/cmd/pkgserver/ui_test/lib/*.css.map
/internal/pkg/testdata/testtool
/internal/rosa/hakurei_current.tar.gz
# release
/dist/hakurei-*
# interactive nixos vm
nixos.qcow2
# cmd/dist default destination
/dist

View File

@@ -1,5 +1,5 @@
<p align="center">
<a href="https://git.gensokyo.uk/security/hakurei">
<a href="https://git.gensokyo.uk/rosa/hakurei">
<picture>
<img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari">
</picture>
@@ -8,16 +8,16 @@
<p align="center">
<a href="https://pkg.go.dev/hakurei.app"><img src="https://pkg.go.dev/badge/hakurei.app.svg" alt="Go Reference" /></a>
<a href="https://git.gensokyo.uk/security/hakurei/actions"><img src="https://git.gensokyo.uk/security/hakurei/actions/workflows/test.yml/badge.svg?branch=staging&style=flat-square" alt="Gitea Workflow Status" /></a>
<a href="https://git.gensokyo.uk/rosa/hakurei/actions"><img src="https://git.gensokyo.uk/rosa/hakurei/actions/workflows/test.yml/badge.svg?branch=staging&style=flat-square" alt="Gitea Workflow Status" /></a>
<br/>
<a href="https://git.gensokyo.uk/security/hakurei/releases"><img src="https://img.shields.io/gitea/v/release/security/hakurei?gitea_url=https%3A%2F%2Fgit.gensokyo.uk&color=purple" alt="Release" /></a>
<a href="https://git.gensokyo.uk/rosa/hakurei/releases"><img src="https://img.shields.io/gitea/v/release/rosa/hakurei?gitea_url=https%3A%2F%2Fgit.gensokyo.uk&color=purple" alt="Release" /></a>
<a href="https://goreportcard.com/report/hakurei.app"><img src="https://goreportcard.com/badge/hakurei.app" alt="Go Report Card" /></a>
<a href="https://hakurei.app"><img src="https://img.shields.io/website?url=https%3A%2F%2Fhakurei.app" alt="Website" /></a>
</p>
Hakurei is a tool for running sandboxed desktop applications as dedicated
subordinate users on the Linux kernel. It implements the application container
of [planterette (WIP)](https://git.gensokyo.uk/security/planterette), a
of [planterette (WIP)](https://git.gensokyo.uk/rosa/planterette), a
self-contained Android-like package manager with modern security features.
Interaction with hakurei happens entirely through structures described by

6
all.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh -e
TOOLCHAIN_VERSION="$(go version)"
cd "$(dirname -- "$0")/"
echo "# Building cmd/dist using ${TOOLCHAIN_VERSION}."
go run -v --tags=dist ./cmd/dist

View File

@@ -5,7 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"path"
"path/filepath"
"slices"
"strings"
"syscall"
@@ -61,7 +61,7 @@ func (a *Absolute) Is(v *Absolute) bool {
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
func NewAbs(pathname string) (*Absolute, error) {
if !path.IsAbs(pathname) {
if !filepath.IsAbs(pathname) {
return nil, AbsoluteError(pathname)
}
return unsafeAbs(pathname), nil
@@ -76,13 +76,13 @@ func MustAbs(pathname string) *Absolute {
}
}
// Append calls [path.Join] with [Absolute] as the first element.
// Append calls [filepath.Join] with [Absolute] as the first element.
func (a *Absolute) Append(elem ...string) *Absolute {
return unsafeAbs(path.Join(append([]string{a.String()}, elem...)...))
return unsafeAbs(filepath.Join(append([]string{a.String()}, elem...)...))
}
// Dir calls [path.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return unsafeAbs(path.Dir(a.String())) }
// Dir calls [filepath.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }
// GobEncode returns the checked pathname.
func (a *Absolute) GobEncode() ([]byte, error) {
@@ -92,7 +92,7 @@ func (a *Absolute) GobEncode() ([]byte, error) {
// GobDecode stores data if it represents an absolute pathname.
func (a *Absolute) GobDecode(data []byte) error {
pathname := string(data)
if !path.IsAbs(pathname) {
if !filepath.IsAbs(pathname) {
return AbsoluteError(pathname)
}
a.pathname = unique.Make(pathname)
@@ -110,7 +110,7 @@ func (a *Absolute) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &pathname); err != nil {
return err
}
if !path.IsAbs(pathname) {
if !filepath.IsAbs(pathname) {
return AbsoluteError(pathname)
}
a.pathname = unique.Make(pathname)

View File

@@ -11,12 +11,12 @@ import (
"testing"
_ "unsafe" // for go:linkname
. "hakurei.app/container/check"
. "hakurei.app/check"
)
// unsafeAbs returns check.Absolute on any string value.
//
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
//go:linkname unsafeAbs hakurei.app/check.unsafeAbs
func unsafeAbs(pathname string) *Absolute
func TestAbsoluteError(t *testing.T) {

View File

@@ -3,7 +3,7 @@ package check_test
import (
"testing"
"hakurei.app/container/check"
"hakurei.app/check"
)
func TestEscapeOverlayDataSegment(t *testing.T) {

View File

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

237
cmd/dist/main.go vendored Normal file
View File

@@ -0,0 +1,237 @@
//go:build dist
package main
import (
"archive/tar"
"compress/gzip"
"context"
"crypto/sha512"
_ "embed"
"encoding/hex"
"fmt"
"io"
"io/fs"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
)
// getenv looks up an environment variable, and returns fallback if it is unset.
func getenv(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok {
return v
}
return fallback
}
// mustRun runs a command with the current process's environment and panics
// on error or non-zero exit code.
func mustRun(ctx context.Context, name string, arg ...string) {
cmd := exec.CommandContext(ctx, name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
//go:embed comp/_hakurei
var comp []byte
func main() {
fmt.Println()
log.SetFlags(0)
log.SetPrefix("# ")
version := getenv("HAKUREI_VERSION", "untagged")
prefix := getenv("PREFIX", "/usr")
destdir := getenv("DESTDIR", "dist")
if err := os.MkdirAll(destdir, 0755); err != nil {
log.Fatal(err)
}
s, err := os.MkdirTemp(destdir, ".dist.*")
if err != nil {
log.Fatal(err)
}
defer func() {
var code int
if err = os.RemoveAll(s); err != nil {
code = 1
log.Println(err)
}
if r := recover(); r != nil {
code = 1
log.Println(r)
}
os.Exit(code)
}()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
log.Println("Building hakurei.")
mustRun(ctx, "go", "generate", "./...")
mustRun(
ctx, "go", "build",
"-trimpath",
"-v", "-o", s,
"-ldflags=-s -w "+
"-buildid= -linkmode external -extldflags=-static "+
"-X hakurei.app/internal/info.buildVersion="+version+" "+
"-X hakurei.app/internal/info.hakureiPath="+prefix+"/bin/hakurei "+
"-X hakurei.app/internal/info.hsuPath="+prefix+"/bin/hsu "+
"-X main.hakureiPath="+prefix+"/bin/hakurei",
"./...",
)
fmt.Println()
log.Println("Testing Hakurei.")
mustRun(
ctx, "go", "test",
"-ldflags=-buildid= -linkmode external -extldflags=-static",
"./...",
)
fmt.Println()
log.Println("Creating distribution.")
const suffix = ".tar.gz"
distName := "hakurei-" + version + "-" + runtime.GOARCH
var f *os.File
if f, err = os.OpenFile(
filepath.Join(s, distName+suffix),
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
0644,
); err != nil {
panic(err)
}
defer func() {
if f == nil {
return
}
if err = f.Close(); err != nil {
log.Println(err)
}
}()
h := sha512.New()
gw := gzip.NewWriter(io.MultiWriter(f, h))
tw := tar.NewWriter(gw)
mustWriteHeader := func(name string, size int64, mode os.FileMode) {
header := tar.Header{
Name: filepath.Join(distName, name),
Size: size,
Mode: int64(mode),
Uname: "root",
Gname: "root",
}
if mode&os.ModeDir != 0 {
header.Typeflag = tar.TypeDir
fmt.Printf("%s %s\n", mode, name)
} else {
header.Typeflag = tar.TypeReg
fmt.Printf("%s %s (%d bytes)\n", mode, name, size)
}
if err = tw.WriteHeader(&header); err != nil {
panic(err)
}
}
mustWriteFile := func(name string, data []byte, mode os.FileMode) {
mustWriteHeader(name, int64(len(data)), mode)
if mode&os.ModeDir != 0 {
return
}
if _, err = tw.Write(data); err != nil {
panic(err)
}
}
mustWriteFromPath := func(dst, src string, mode os.FileMode) {
var r *os.File
if r, err = os.Open(src); err != nil {
panic(err)
}
var fi os.FileInfo
if fi, err = r.Stat(); err != nil {
_ = r.Close()
panic(err)
}
if mode == 0 {
mode = fi.Mode()
}
mustWriteHeader(dst, fi.Size(), mode)
if _, err = io.Copy(tw, r); err != nil {
_ = r.Close()
panic(err)
} else if err = r.Close(); err != nil {
panic(err)
}
}
mustWriteFile(".", nil, fs.ModeDir|0755)
mustWriteFile("comp/", nil, os.ModeDir|0755)
mustWriteFile("comp/_hakurei", comp, 0644)
mustWriteFile("install.sh", []byte(`#!/bin/sh -e
cd "$(dirname -- "$0")" || exit 1
install -vDm0755 "bin/hakurei" "${DESTDIR}`+prefix+`/bin/hakurei"
install -vDm0755 "bin/sharefs" "${DESTDIR}`+prefix+`/bin/sharefs"
install -vDm4511 "bin/hsu" "${DESTDIR}`+prefix+`/bin/hsu"
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
fi
install -vDm0644 "comp/_hakurei" "${DESTDIR}`+prefix+`/share/zsh/site-functions/_hakurei"
`), 0755)
mustWriteFromPath("README.md", "README.md", 0)
mustWriteFile("hsurc.default", []byte("1000 0"), 0400)
mustWriteFromPath("bin/hsu", filepath.Join(s, "hsu"), 04511)
for _, name := range []string{
"hakurei",
"sharefs",
} {
mustWriteFromPath(
filepath.Join("bin", name),
filepath.Join(s, name),
0,
)
}
if err = tw.Close(); err != nil {
panic(err)
} else if err = gw.Close(); err != nil {
panic(err)
} else if err = f.Close(); err != nil {
panic(err)
}
f = nil
if err = os.WriteFile(
filepath.Join(destdir, distName+suffix+".sha512"),
append(hex.AppendEncode(nil, h.Sum(nil)), " "+distName+suffix+"\n"...),
0644,
); err != nil {
panic(err)
}
if err = os.Rename(
filepath.Join(s, distName+suffix),
filepath.Join(destdir, distName+suffix),
); err != nil {
panic(err)
}
}

View File

@@ -1,9 +1,14 @@
// 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
import (
"log"
"os"
"runtime"
"strings"
. "syscall"
)
@@ -12,6 +17,22 @@ func main() {
log.SetFlags(0)
log.SetPrefix("earlyinit: ")
var (
option map[string]string
flags []string
)
if len(os.Args) > 1 {
option = make(map[string]string)
for _, s := range os.Args[1:] {
key, value, ok := strings.Cut(s, "=")
if !ok {
flags = append(flags, s)
continue
}
option[key] = value
}
}
if err := Mount(
"devtmpfs",
"/dev/",
@@ -55,4 +76,56 @@ func main() {
}
}
// staying in rootfs, these are no longer used
must(os.Remove("/root"))
must(os.Remove("/init"))
must(os.Mkdir("/proc", 0))
mustSyscall("mount proc", Mount(
"proc",
"/proc",
"proc",
MS_NOSUID|MS_NOEXEC|MS_NODEV,
"hidepid=1",
))
must(os.Mkdir("/sys", 0))
mustSyscall("mount sysfs", Mount(
"sysfs",
"/sys",
"sysfs",
0,
"",
))
// after top level has been set up
mustSyscall("remount root", Mount(
"",
"/",
"",
MS_REMOUNT|MS_BIND|
MS_RDONLY|MS_NODEV|MS_NOSUID|MS_NOEXEC,
"",
))
must(os.WriteFile(
"/sys/module/firmware_class/parameters/path",
[]byte("/system/lib/firmware"),
0,
))
}
// mustSyscall calls [log.Fatalln] if err is non-nil.
func mustSyscall(action string, err error) {
if err != nil {
log.Fatalln("cannot "+action+":", err)
}
}
// must calls [log.Fatal] with err if it is non-nil.
func must(err error) {
if err != nil {
log.Fatal(err)
}
}

View File

@@ -2,6 +2,7 @@ package main
import (
"context"
"errors"
"fmt"
"io"
"log"
@@ -11,11 +12,11 @@ import (
"strconv"
"sync"
"time"
_ "unsafe" // for go:linkname
"hakurei.app/check"
"hakurei.app/command"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/ext"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/dbus"
"hakurei.app/internal/env"
@@ -26,9 +27,14 @@ import (
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
// if it is not nil, or the original value if it is.
//
//go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap
func optionalErrorUnwrap(err error) error
func optionalErrorUnwrap(err error) error {
if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
return underlyingErr
}
return err
}
var errSuccess = errors.New("success")
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var (
@@ -59,9 +65,9 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
var (
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 {
log.Fatal("app requires at least 1 argument")
log.Fatal("run requires at least 1 argument")
}
config := tryPath(msg, args[0])
@@ -89,12 +95,15 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagHomeDir string
flagUserName string
flagSchedPolicy string
flagSchedPriority int
flagPrivateRuntime, flagPrivateTmpdir bool
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
)
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 {
log.Fatalf("identity %d out of range", flagIdentity)
}
@@ -131,7 +140,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
log.Fatal(optionalErrorUnwrap(err))
return err
} else if progPath, err = check.NewAbs(p); err != nil {
log.Fatal(err.Error())
log.Fatal(err)
return err
}
}
@@ -150,7 +159,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
et |= hst.EPipeWire
}
config := &hst.Config{
config := hst.Config{
ID: flagID,
Identity: flagIdentity,
Groups: flagGroups,
@@ -177,6 +186,13 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
},
}
if err := config.SchedPolicy.UnmarshalText(
[]byte(flagSchedPolicy),
); err != nil {
log.Fatal(err)
}
config.SchedPriority = ext.Int(flagSchedPriority)
// bind GPU stuff
if et&(hst.EX11|hst.EWayland) != 0 {
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
@@ -214,7 +230,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
homeDir = passwd.HomeDir
}
if a, err := check.NewAbs(homeDir); err != nil {
log.Fatal(err.Error())
log.Fatal(err)
return err
} else {
config.Container.Home = a
@@ -234,11 +250,11 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
} else {
if f, err := os.Open(flagDBusConfigSession); err != nil {
log.Fatal(err.Error())
log.Fatal(err)
} else {
decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus)
if err = f.Close(); err != nil {
log.Fatal(err.Error())
log.Fatal(err)
}
}
}
@@ -246,11 +262,11 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
// system bus proxy is optional
if flagDBusConfigSystem != "nil" {
if f, err := os.Open(flagDBusConfigSystem); err != nil {
log.Fatal(err.Error())
log.Fatal(err)
} else {
decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus)
if err = f.Close(); err != nil {
log.Fatal(err.Error())
log.Fatal(err)
}
}
}
@@ -266,7 +282,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
}
}
outcome.Main(ctx, msg, config, -1)
outcome.Main(ctx, msg, &config, -1)
panic("unreachable")
}).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
@@ -287,6 +303,10 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
"Container home directory").
Flag(&flagUserName, "u", command.StringFlag("chronos"),
"Passwd user name within sandbox").
Flag(&flagSchedPolicy, "policy", command.StringFlag(""),
"Scheduling policy to set for the container").
Flag(&flagSchedPriority, "priority", command.IntFlag(0),
"Scheduling priority to set for the container").
Flag(&flagPrivateRuntime, "private-runtime", command.BoolFlag(false),
"Do not share XDG_RUNTIME_DIR between containers under the same identity").
Flag(&flagPrivateTmpdir, "private-tmpdir", command.BoolFlag(false),
@@ -308,7 +328,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagShort 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) {
case 0: // system
printShowSystem(os.Stdout, flagShort, flagJSON)

View File

@@ -23,9 +23,9 @@ func TestHelp(t *testing.T) {
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Commands:
app Load and start container from configuration file
run Configure and start a permissive container
show Show live or local app configuration
run Load and start container from configuration file
exec Configure and start a permissive container
show Show live or local instance configuration
ps List active instances
version Display version information
license Show full license text
@@ -35,8 +35,8 @@ Commands:
`,
},
{
"run", []string{"run", "-h"}, `
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS]
"exec", []string{"exec", "-h"}, `
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:
-X Enable direct connection to X11
@@ -60,6 +60,10 @@ Flags:
Allow owning MPRIS D-Bus path, has no effect if custom config is available
-pipewire
Enable connection to PipeWire via SecurityContext
-policy string
Scheduling policy to set for the container
-priority int
Scheduling priority to set for the container
-private-runtime
Do not share XDG_RUNTIME_DIR between containers under the same identity
-private-tmpdir

View File

@@ -5,7 +5,7 @@ import (
"strings"
"testing"
"hakurei.app/container/stub"
"hakurei.app/internal/stub"
)
func TestDecodeJSON(t *testing.T) {

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
// this works around go:embed '..' limitation
//go:generate cp ../../LICENSE .
import (
"context"
_ "embed"
@@ -13,15 +47,13 @@ import (
"syscall"
"hakurei.app/container"
"hakurei.app/ext"
"hakurei.app/message"
)
var (
errSuccess = errors.New("success")
//go:embed LICENSE
license string
)
//go:generate cp ../../LICENSE .
//go:embed LICENSE
var license string
// earlyHardeningErrs are errors collected while setting up early hardening feature.
type earlyHardeningErrs struct{ yamaLSM, dumpable error }
@@ -30,13 +62,13 @@ func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(nil)
log.SetPrefix("hakurei: ")
log.SetFlags(0)
log.SetPrefix("hakurei: ")
msg := message.New(log.Default())
early := earlyHardeningErrs{
yamaLSM: container.SetPtracer(0),
dumpable: container.SetDumpable(container.SUID_DUMP_DISABLE),
yamaLSM: ext.SetPtracer(0),
dumpable: ext.SetDumpable(ext.SUID_DUMP_DISABLE),
}
if os.Geteuid() == 0 {

View File

@@ -17,8 +17,9 @@ import (
)
// 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) {
var r io.ReadCloser
config = new(hst.Config)
@@ -46,7 +47,8 @@ func tryPath(msg message.Msg, name string) (config *hst.Config) {
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 {
if v, err := strconv.Atoi(name); err != nil {
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)
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
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
// 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 {
return shortIdentifierString(id.String())
}
@@ -88,7 +97,8 @@ func shortIdentifierString(s string) string {
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 {
const (
likeShort = 1 << iota
@@ -96,7 +106,8 @@ func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
)
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
for _, c := range name {
if c >= '0' && c <= '9' {

View File

@@ -6,7 +6,7 @@ import (
"testing"
"time"
"hakurei.app/container/check"
"hakurei.app/check"
"hakurei.app/hst"
"hakurei.app/internal/store"
"hakurei.app/message"

View File

@@ -7,7 +7,7 @@ import (
"testing"
"time"
"hakurei.app/container/check"
"hakurei.app/check"
"hakurei.app/hst"
"hakurei.app/internal/store"
"hakurei.app/message"

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
/* copied from hst and must never be changed */
/* keep in sync with hst */
const (
userOffset = 100000

View File

@@ -1,13 +1,64 @@
// 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
// minimise imports to avoid inadvertently calling init or global variable functions
import (
"bytes"
"fmt"
"log"
"os"
"path"
"path/filepath"
"runtime"
"slices"
"strconv"
@@ -16,10 +67,13 @@ import (
)
const (
// envIdentity is the name of the environment variable holding a
// single byte representing the shim setup pipe file descriptor.
// envShim is the name of the environment variable holding a single byte
// representing the shim setup pipe file descriptor.
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.
envGroups = "HAKUREI_GROUPS"
)
@@ -35,7 +89,6 @@ func main() {
log.SetFlags(0)
log.SetPrefix("hsu: ")
log.SetOutput(os.Stderr)
if os.Geteuid() != 0 {
log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
@@ -49,13 +102,13 @@ func main() {
log.Fatal("this program must not be started by root")
}
if !path.IsAbs(hakureiPath) {
if !filepath.IsAbs(hakureiPath) {
log.Fatal("this program is compiled incorrectly")
return
}
var toolPath string
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
pexe := filepath.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") {
@@ -99,8 +152,6 @@ func main() {
// last possible uid outcome
uidEnd = 999919999
)
// cast to int for use with library functions
uid := int(toUser(userid, identity))
// 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
if err := syscall.Setresgid(uid, uid, uid); err != nil {
log.Fatalf("cannot set gid: %v", err)
}
@@ -146,10 +196,21 @@ func main() {
if err := syscall.Setresuid(uid, uid, uid); err != nil {
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())
}
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)
}

View File

@@ -18,8 +18,9 @@ const (
useridEnd = useridStart + rangeSize - 1
)
// parseUint32Fast parses a string representation of an unsigned 32-bit integer value
// using the fast path only. This limits the range of values it is defined in.
// parseUint32Fast parses a string representation of an unsigned 32-bit integer
// 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) {
sLen := len(s)
if sLen < 1 {
@@ -40,12 +41,14 @@ func parseUint32Fast(s string) (uint32, error) {
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
// of the string representation of the uid of the user wishing to start hakurei containers,
// followed by a space, followed by the string representation of its userid. Duplicate uid
// entries are ignored, with the first occurrence taking effect.
// Each line of the file specifies a hakurei userid to kernel uid mapping. A
// line consists of the string representation of the uid of the user wishing to
// start hakurei containers, followed by a space, followed by the string
// representation of its userid. Duplicate uid entries are ignored, with the
// first occurrence taking effect.
//
// All string representations are parsed by calling parseUint32Fast.
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()
}
// 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,
// 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.
@@ -112,10 +111,6 @@ func mustParseConfig(puid int) (userid uint32) {
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,
// terminating the program if the value is not set, malformed, or out of bounds.
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
import (
@@ -18,12 +30,13 @@ import (
"time"
"unique"
"hakurei.app/check"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/ext"
"hakurei.app/fhs"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
"hakurei.app/message"
@@ -56,11 +69,12 @@ func main() {
}()
var (
flagQuiet bool
flagCures int
flagBase string
flagTShift int
flagIdle bool
flagQuiet bool
flagCures int
flagBase string
flagIdle bool
flagHostAbstract bool
)
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) {
msg.SwapVerbose(!flagQuiet)
@@ -76,19 +90,15 @@ func main() {
} else if base, err = check.NewAbs(flagBase); err != nil {
return
}
if cache, err = pkg.Open(ctx, msg, flagCures, base); err == nil {
if flagTShift < 0 {
cache.SetThreshold(0)
} else if flagTShift > 31 {
cache.SetThreshold(1 << 31)
} else {
cache.SetThreshold(1 << flagTShift)
}
}
var flags int
if flagIdle {
pkg.SchedPolicy = container.SCHED_IDLE
flags |= pkg.CSchedIdle
}
if flagHostAbstract {
flags |= pkg.CHostAbstract
}
cache, err = pkg.Open(ctx, msg, flags, flagCures, base)
return
}).Flag(
@@ -103,14 +113,17 @@ func main() {
&flagBase,
"d", command.StringFlag("$MBF_CACHE_DIR"),
"Directory to store cured artifacts",
).Flag(
&flagTShift,
"tshift", command.IntFlag(-1),
"Dependency graph size exponent, to the power of 2",
).Flag(
&flagIdle,
"sched-idle", command.BoolFlag(false),
"Set SCHED_IDLE scheduling policy",
).Flag(
&flagHostAbstract,
"host-abstract", command.BoolFlag(
os.Getenv("MBF_HOST_ABSTRACT") != "",
),
"Do not restrict networked cure containers from connecting to host "+
"abstract UNIX sockets",
)
{
@@ -175,6 +188,17 @@ func main() {
fmt.Println("website : " +
strings.TrimSuffix(meta.Website, "/"))
}
if len(meta.Dependencies) > 0 {
fmt.Print("depends on :")
for _, d := range meta.Dependencies {
s := rosa.GetMetadata(d).Name
if version := rosa.Std.Version(d); version != rosa.Unversioned {
s += "-" + version
}
fmt.Print(" " + s)
}
fmt.Println()
}
const statusPrefix = "status : "
if flagStatus {
@@ -260,7 +284,7 @@ func main() {
return errors.New("report requires 1 argument")
}
if container.Isatty(int(w.Fd())) {
if ext.Isatty(int(w.Fd())) {
return errors.New("output appears to be a terminal")
}
return rosa.WriteReport(msg, w, cache)
@@ -423,7 +447,9 @@ func main() {
{
var (
flagDump string
flagDump string
flagEnter bool
flagExport string
)
c.NewCommand(
"cure",
@@ -432,15 +458,44 @@ func main() {
if len(args) != 1 {
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])
} else if flagDump == "" {
}
switch {
default:
pathname, _, err := cache.Cure(rosa.Std.Load(p))
if err == nil {
log.Println(pathname)
if err != nil {
return err
}
return err
} else {
log.Println(pathname)
if flagExport != "" {
msg.Verbosef("exporting %s to %s...", args[0], flagExport)
var f *os.File
if f, err = os.OpenFile(
flagExport,
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
0400,
); err != nil {
return err
} else if _, err = pkg.Flatten(
os.DirFS(pathname.String()),
".",
f,
); err != nil {
_ = f.Close()
return err
} else if err = f.Close(); err != nil {
return err
}
}
return nil
case flagDump != "":
f, err := os.OpenFile(
flagDump,
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
@@ -456,6 +511,15 @@ func main() {
}
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",
)
}
},
).
@@ -463,6 +527,16 @@ func main() {
&flagDump,
"dump", command.StringFlag(""),
"Write IR to specified pathname and terminate",
).
Flag(
&flagExport,
"export", command.StringFlag(""),
"Export cured artifact to specified pathname",
).
Flag(
&flagEnter,
"enter", command.BoolFlag(false),
"Enter cure container with an interactive shell",
)
}
@@ -477,17 +551,19 @@ func main() {
"shell",
"Interactive shell in the specified Rosa OS environment",
func(args []string) error {
root := make([]pkg.Artifact, 0, 6+len(args))
for _, arg := range args {
presets := make([]rosa.PArtifact, len(args))
for i, arg := range args {
p, ok := rosa.ResolveName(arg)
if !ok {
return fmt.Errorf("unknown artifact %q", arg)
}
root = append(root, rosa.Std.Load(p))
presets[i] = p
}
root := make(pkg.Collect, 0, 6+len(args))
root = rosa.Std.AppendPresets(root, presets...)
if flagWithToolchain {
musl, compilerRT, runtimes, clang := rosa.Std.NewLLVM()
musl, compilerRT, runtimes, clang := (rosa.Std - 1).NewLLVM()
root = append(root, musl, compilerRT, runtimes, clang)
} else {
root = append(root, rosa.Std.Load(rosa.Musl))
@@ -497,6 +573,12 @@ func main() {
rosa.Std.Load(rosa.Toybox),
)
if _, _, err := cache.Cure(&root); err == nil {
return errors.New("unreachable")
} else if !pkg.IsCollected(err) {
return err
}
type cureRes struct {
pathname *check.Absolute
checksum unique.Handle[pkg.Checksum]
@@ -536,6 +618,9 @@ func main() {
z.Hostname = "localhost"
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
if s, ok := os.LookupEnv("TERM"); ok {
z.Env = append(z.Env, "TERM="+s)
}
var tempdir *check.Absolute
if s, err := filepath.Abs(os.TempDir()); err != nil {
@@ -586,13 +671,13 @@ func main() {
).
Flag(
&flagSession,
"session", command.BoolFlag(false),
"session", command.BoolFlag(true),
"Retain session",
).
Flag(
&flagWithToolchain,
"with-toolchain", command.BoolFlag(false),
"Include the stage3 LLVM toolchain",
"Include the stage2 LLVM toolchain",
)
}

176
cmd/pkgserver/api.go Normal file
View File

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

183
cmd/pkgserver/api_test.go Normal file
View File

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

105
cmd/pkgserver/index.go Normal file
View File

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

115
cmd/pkgserver/main.go Normal file
View File

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

View File

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

77
cmd/pkgserver/search.go Normal file
View File

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

97
cmd/pkgserver/test_ui.go Normal file
View File

@@ -0,0 +1,97 @@
//go:build frontend && frontend_test
package main
import (
"embed"
"net/http"
"path"
"strings"
)
//go:generate tsc -p ui_test
//go:generate sass ui_test/lib/ui.scss ui_test/lib/ui.css
//go:embed ui_test/*
var test_content embed.FS
func serveTestWebUI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-XSS-Protection", "1")
w.Header().Set("X-Frame-Options", "DENY")
http.ServeFileFS(w, r, test_content, "ui_test/lib/ui.html")
}
func serveTestWebUIStaticContent(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/testui/style.css":
http.ServeFileFS(w, r, test_content, "ui_test/lib/ui.css")
case "/testui/skip-closed.svg":
http.ServeFileFS(w, r, test_content, "ui_test/lib/skip-closed.svg")
case "/testui/skip-open.svg":
http.ServeFileFS(w, r, test_content, "ui_test/lib/skip-open.svg")
case "/testui/success-closed.svg":
http.ServeFileFS(w, r, test_content, "ui_test/lib/success-closed.svg")
case "/testui/success-open.svg":
http.ServeFileFS(w, r, test_content, "ui_test/lib/success-open.svg")
case "/testui/failure-closed.svg":
http.ServeFileFS(w, r, test_content, "ui_test/lib/failure-closed.svg")
case "/testui/failure-open.svg":
http.ServeFileFS(w, r, test_content, "ui_test/lib/failure-open.svg")
default:
http.NotFound(w, r)
}
}
func serveTestLibrary(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/test/lib/test.js":
http.ServeFileFS(w, r, test_content, "ui_test/lib/test.js")
default:
http.NotFound(w, r)
}
}
func serveTests(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/test/" {
http.Redirect(w, r, "/test.html", http.StatusMovedPermanently)
return
}
testPath := strings.TrimPrefix(r.URL.Path, "/test/")
if path.Ext(testPath) != ".js" {
http.Error(w, "403 forbidden", http.StatusForbidden)
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
http.ServeFileFS(w, r, test_content, "ui_test/"+testPath)
}
func redirectUI(w http.ResponseWriter, r *http.Request) {
// The base path should not redirect to the root.
if r.URL.Path == "/ui/" {
http.NotFound(w, r)
return
}
if path.Ext(r.URL.Path) != ".js" {
http.Error(w, "403 forbidden", http.StatusForbidden)
return
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
http.Redirect(w, r, strings.TrimPrefix(r.URL.Path, "/ui"), http.StatusFound)
}
func testUiRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /test.html", serveTestWebUI)
mux.HandleFunc("GET /testui/", serveTestWebUIStaticContent)
mux.HandleFunc("GET /test/lib", serveTestLibrary)
mux.HandleFunc("GET /test/", serveTests)
mux.HandleFunc("GET /ui/", redirectUI)
}

View File

@@ -0,0 +1,7 @@
//go:build !(frontend && frontend_test)
package main
import "net/http"
func testUiRoutes(mux *http.ServeMux) {}

38
cmd/pkgserver/ui.go Normal file
View File

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

View File

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

View File

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2024"
}
}

9
cmd/pkgserver/ui_full.go Normal file
View File

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

7
cmd/pkgserver/ui_stub.go Normal file
View File

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

View File

@@ -0,0 +1,2 @@
// Import all test files to execute their suite registrations.
import "./test_tests.js";

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env node
// Many editors have terminal emulators built in, so running tests with NodeJS
// provides faster iteration, especially for those acclimated to test-driven
// development.
import "../all_tests.js";
import { StreamReporter, TESTS } from "./test.js";
// TypeScript doesn't like process and Deno as their type definitions aren't
// installed, but doesn't seem to complain if they're accessed through
// globalThis.
const process: any = (globalThis as any).process;
const Deno: any = (globalThis as any).Deno;
function getArgs(): string[] {
if (process) {
const [runtime, program, ...args] = process.argv;
return args;
}
if (Deno) return Deno.args;
return [];
}
function exit(code?: number): never {
if (Deno) Deno.exit(code);
if (process) process.exit(code);
throw `exited with code ${code ?? 0}`;
}
const args = getArgs();
let verbose = false;
if (args.length > 1) {
console.error("Too many arguments");
exit(1);
}
if (args.length === 1) {
if (args[0] === "-v" || args[0] === "--verbose" || args[0] === "-verbose") {
verbose = true;
} else if (args[0] !== "--") {
console.error(`Unknown argument '${args[0]}'`);
exit(1);
}
}
let reporter = new StreamReporter({ writeln: console.log }, verbose);
TESTS.run(reporter);
exit(reporter.succeeded() ? 0 : 1);

View File

@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
<!-- This triangle should match success-closed.svg, fill and stroke color notwithstanding. -->
<polygon points="0,0 100,50 0,100" fill="red" stroke="red" stroke-width="15" stroke-linejoin="round"/>
<!--
! y-coordinates go before x-coordinates here to highlight the difference
! (or, lack thereof) between these numbers and the ones in failure-open.svg;
! try a textual diff. Make sure to keep the numbers in sync!
-->
<line y1="30" x1="10" y2="70" x2="50" stroke="white" stroke-width="16"/>
<line y1="30" x1="50" y2="70" x2="10" stroke="white" stroke-width="16"/>
</svg>

After

Width:  |  Height:  |  Size: 788 B

View File

@@ -0,0 +1,35 @@
<?xml version="1.0"?>
<!--
! This view box is a bit weird: the strokes assume they're working in a view
! box that spans from the (0,0) to (100,100), and indeed that is convenient
! conceptualizing the strokes, but the stroke itself has a considerable width
! that gets clipped by restrictive view box dimensions. Hence, the view is
! shifted from (0,0)(100,100) to (-20,-20)(120,120), to make room for the
! clipped stroke, while leaving behind an illusion of working in a view box
! spanning from (0,0) to (100,100).
!
! However, the resulting SVG is too close to the summary text, and CSS
! properties to add padding do not seem to work with `content:` (likely because
! they're anonymous replaced elements); thus, the width of the view is
! increased considerably to provide padding in the SVG itself, while leaving
! the strokes oblivious.
!
! It gets worse: the summary text isn't vertically aligned with the icon! As
! a flexbox cannot be used in a summary to align the marker with the text, the
! simplest and most effective solution is to reduce the height of the view box
! from 140 to 130, thereby removing some of the bottom padding present.
!
! All six SVGs use the same view box (and indeed, they refer to this comment)
! so that they all appear to be the same size and position relative to each
! other on the DOM—indeed, the view box dimensions, alongside the width,
! directly control their placement on the DOM.
!
! TL;DR: CSS is janky, overflow is weird, and SVG is awesome!
-->
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
<!-- This triangle should match success-open.svg, fill and stroke color notwithstanding. -->
<polygon points="0,0 100,0 50,100" fill="red" stroke="red" stroke-width="15" stroke-linejoin="round"/>
<!-- See the comment in failure-closed.svg before modifying this. -->
<line x1="30" y1="10" x2="70" y2="50" stroke="white" stroke-width="16"/>
<line x1="30" y1="50" x2="70" y2="10" stroke="white" stroke-width="16"/>
</svg>

View File

@@ -0,0 +1,3 @@
import "../all_tests.js";
import { GoTestReporter, TESTS } from "./test.js";
TESTS.run(new GoTestReporter());

View File

@@ -0,0 +1,21 @@
<?xml version="1.0"?>
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
<!-- This triangle should match success-closed.svg, fill and stroke color notwithstanding. -->
<polygon points="0,0 100,50 0,100" fill="blue" stroke="blue" stroke-width="15" stroke-linejoin="round"/>
<!--
! This path is extremely similar to the one in skip-open.svg; before
! making minor modifications, diff the two to understand how they should
! remain in sync.
-->
<path
d="M 50,50
A 23,23 270,1,1 30,30
l -10,20
m 10,-20
l -20,-10"
fill="none"
stroke="white"
stroke-width="12"
stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 812 B

View File

@@ -0,0 +1,21 @@
<?xml version="1.0"?>
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
<!-- This triangle should match success-open.svg, fill and stroke color notwithstanding. -->
<polygon points="0,0 100,0 50,100" fill="blue" stroke="blue" stroke-width="15" stroke-linejoin="round"/>
<!--
! This path is extremely similar to the one in skip-closed.svg; before
! making minor modifications, diff the two to understand how they should
! remain in sync.
-->
<path
d="M 50,50
A 23,23 270,1,1 70,30
l 10,-20
m -10,20
l -20,-10"
fill="none"
stroke="white"
stroke-width="12"
stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 812 B

View File

@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
<style>
.adaptive-stroke {
stroke: black;
}
@media (prefers-color-scheme: dark) {
.adaptive-stroke {
stroke: ghostwhite;
}
}
</style>
<!-- When updating this triangle, also update the other five SVGs. -->
<polygon points="0,0 100,50 0,100" fill="none" class="adaptive-stroke" stroke-width="15" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 572 B

View File

@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
<style>
.adaptive-stroke {
stroke: black;
}
@media (prefers-color-scheme: dark) {
.adaptive-stroke {
stroke: ghostwhite;
}
}
</style>
<!-- When updating this triangle, also update the other five SVGs. -->
<polygon points="0,0 100,0 50,100" fill="none" class="adaptive-stroke" stroke-width="15" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 572 B

View File

@@ -0,0 +1,400 @@
// =============================================================================
// DSL
type TestTree = TestGroup | Test;
type TestGroup = { name: string, children: TestTree[] };
type Test = { name: string, test: (t: TestController) => void };
export class TestRegistrar {
#suites: TestGroup[];
constructor() {
this.#suites = [];
}
suite(name: string, children: TestTree[]) {
checkDuplicates(name, children)
this.#suites.push({ name, children });
}
run(reporter: Reporter) {
reporter.register(this.#suites);
for (const suite of this.#suites) {
for (const c of suite.children) runTests(reporter, [suite.name], c);
}
reporter.finalize();
}
}
export let TESTS = new TestRegistrar();
// Register a suite in the global registrar.
export function suite(name: string, children: TestTree[]) {
TESTS.suite(name, children);
}
export function context(name: string, children: TestTree[]): TestTree {
checkDuplicates(name, children);
return { name, children };
}
export const group = context;
export function test(name: string, test: (t: TestController) => void): TestTree {
return { name, test };
}
function checkDuplicates(parent: string, names: { name: string }[]) {
let seen = new Set<string>();
for (const { name } of names) {
if (seen.has(name)) {
throw new RangeError(`duplicate name '${name}' in '${parent}'`);
}
seen.add(name);
}
}
export type TestState = "success" | "failure" | "skip";
class AbortSentinel {}
export class TestController {
#state: TestState;
logs: string[];
constructor() {
this.#state = "success";
this.logs = [];
}
getState(): TestState {
return this.#state;
}
fail() {
this.#state = "failure";
}
failed(): boolean {
return this.#state === "failure";
}
failNow(): never {
this.fail();
throw new AbortSentinel();
}
log(message: string) {
this.logs.push(message);
}
error(message: string) {
this.log(message);
this.fail();
}
fatal(message: string): never {
this.log(message);
this.failNow();
}
skip(message?: string): never {
if (message != null) this.log(message);
if (this.#state !== "failure") this.#state = "skip";
throw new AbortSentinel();
}
skipped(): boolean {
return this.#state === "skip";
}
}
// =============================================================================
// Execution
export interface TestResult {
state: TestState;
logs: string[];
}
function runTests(reporter: Reporter, parents: string[], node: TestTree) {
const path = [...parents, node.name];
if ("children" in node) {
for (const c of node.children) runTests(reporter, path, c);
return;
}
let controller = new TestController();
try {
node.test(controller);
} catch (e) {
if (!(e instanceof AbortSentinel)) {
controller.error(extractExceptionString(e));
}
}
reporter.update(path, { state: controller.getState(), logs: controller.logs });
}
function extractExceptionString(e: any): string {
// String() instead of .toString() as null and undefined don't have
// properties.
const s = String(e);
if (!(e instanceof Error && e.stack)) return s;
// v8 (Chromium, NodeJS) includes the error message, while Firefox and
// WebKit do not.
if (e.stack.startsWith(s)) return e.stack;
return `${s}\n${e.stack}`;
}
// =============================================================================
// Reporting
export interface Reporter {
register(suites: TestGroup[]): void
update(path: string[], result: TestResult): void;
finalize(): void;
}
export class NoOpReporter implements Reporter {
suites: TestGroup[];
results: ({ path: string[] } & TestResult)[];
finalized: boolean;
constructor() {
this.suites = [];
this.results = [];
this.finalized = false;
}
register(suites: TestGroup[]) {
this.suites = suites;
}
update(path: string[], result: TestResult) {
this.results.push({ path, ...result });
}
finalize() {
this.finalized = true;
}
}
export interface Stream {
writeln(s: string): void;
}
const SEP = " ";
export class StreamReporter implements Reporter {
stream: Stream;
verbose: boolean;
#successes: ({ path: string[] } & TestResult)[];
#failures: ({ path: string[] } & TestResult)[];
#skips: ({ path: string[] } & TestResult)[];
constructor(stream: Stream, verbose: boolean = false) {
this.stream = stream;
this.verbose = verbose;
this.#successes = [];
this.#failures = [];
this.#skips = [];
}
succeeded(): boolean {
return this.#successes.length > 0 && this.#failures.length === 0;
}
register(suites: TestGroup[]) {}
update(path: string[], result: TestResult) {
if (path.length === 0) throw new RangeError("path is empty");
const pathStr = path.join(SEP);
switch (result.state) {
case "success":
this.#successes.push({ path, ...result });
if (this.verbose) this.stream.writeln(`✅️ ${pathStr}`);
break;
case "failure":
this.#failures.push({ path, ...result });
this.stream.writeln(`⚠️ ${pathStr}`);
break;
case "skip":
this.#skips.push({ path, ...result });
this.stream.writeln(`⏭️ ${pathStr}`);
break;
}
}
finalize() {
if (this.verbose) this.#displaySection("successes", this.#successes, true);
this.#displaySection("failures", this.#failures);
this.#displaySection("skips", this.#skips);
this.stream.writeln("");
this.stream.writeln(
`${this.#successes.length} succeeded, ${this.#failures.length} failed`
+ (this.#skips.length ? `, ${this.#skips.length} skipped` : "")
);
}
#displaySection(name: string, data: ({ path: string[] } & TestResult)[], ignoreEmpty: boolean = false) {
if (!data.length) return;
// Transform [{ path: ["a", "b", "c"] }, { path: ["a", "b", "d"] }]
// into { "a b": ["c", "d"] }.
let pathMap = new Map<string, ({ name: string } & TestResult)[]>();
for (const t of data) {
if (t.path.length === 0) throw new RangeError("path is empty");
const key = t.path.slice(0, -1).join(SEP);
if (!pathMap.has(key)) pathMap.set(key, []);
pathMap.get(key)!.push({ name: t.path.at(-1)!, ...t });
}
this.stream.writeln("");
this.stream.writeln(name.toUpperCase());
this.stream.writeln("=".repeat(name.length));
for (let [path, tests] of pathMap) {
if (ignoreEmpty) tests = tests.filter((t) => t.logs.length);
if (tests.length === 0) continue;
if (tests.length === 1) {
this.#writeOutput(tests[0], path ? `${path}${SEP}` : "", false);
} else {
this.stream.writeln(path);
for (const t of tests) this.#writeOutput(t, " - ", true);
}
}
}
#writeOutput(test: { name: string } & TestResult, prefix: string, nested: boolean) {
let output = "";
if (test.logs.length) {
// Individual logs might span multiple lines, so join them together
// then split it again.
const logStr = test.logs.join("\n");
const lines = logStr.split("\n");
if (lines.length <= 1) {
output = `: ${logStr}`;
} else {
const padding = nested ? " " : " ";
output = ":\n" + lines.map((line) => padding + line).join("\n");
}
}
this.stream.writeln(`${prefix}${test.name}${output}`);
}
}
function assertGetElementById(id: string): HTMLElement {
let elem = document.getElementById(id);
if (elem == null) throw new ReferenceError(`element with ID '${id}' missing from DOM`);
return elem;
}
export class DOMReporter implements Reporter {
register(suites: TestGroup[]) {}
update(path: string[], result: TestResult) {
if (path.length === 0) throw new RangeError("path is empty");
if (result.state === "skip") {
assertGetElementById("skip-counter-text").classList.remove("hidden");
}
const counter = assertGetElementById(`${result.state}-counter`);
counter.innerText = (Number(counter.innerText) + 1).toString();
let parent = assertGetElementById("root");
for (const node of path) {
let child: HTMLDetailsElement | null = null;
let summary: HTMLElement | null = null;
let d: Element;
outer: for (d of parent.children) {
if (!(d instanceof HTMLDetailsElement)) continue;
for (const s of d.children) {
if (!(s instanceof HTMLElement)) continue;
if (!(s.tagName === "SUMMARY" && s.innerText === node)) continue;
child = d;
summary = s;
break outer;
}
}
if (!child) {
child = document.createElement("details");
child.className = "test-node";
child.ariaRoleDescription = "test";
summary = document.createElement("summary");
summary.appendChild(document.createTextNode(node));
summary.ariaRoleDescription = "test name";
child.appendChild(summary);
parent.appendChild(child);
}
if (!summary) throw new Error("unreachable as assigned above");
switch (result.state) {
case "failure":
child.open = true;
child.classList.add("failure");
child.classList.remove("skip");
child.classList.remove("success");
summary.setAttribute("aria-labelledby", "failure-description");
break;
case "skip":
if (child.classList.contains("failure")) break;
child.classList.add("skip");
child.classList.remove("success");
summary.setAttribute("aria-labelledby", "skip-description");
break;
case "success":
if (child.classList.contains("failure") || child.classList.contains("skip")) break;
child.classList.add("success");
summary.setAttribute("aria-labelledby", "success-description");
break;
}
parent = child;
}
const p = document.createElement("p");
p.classList.add("test-desc");
if (result.logs.length) {
const pre = document.createElement("pre");
pre.appendChild(document.createTextNode(result.logs.join("\n")));
p.appendChild(pre);
} else {
p.classList.add("italic");
p.appendChild(document.createTextNode("No output."));
}
parent.appendChild(p);
}
finalize() {}
}
interface GoNode {
name: string;
subtests: GoNode[] | null;
}
// Used to display results via `go test`, via some glue code from the Go side.
export class GoTestReporter implements Reporter {
// Convert a test tree into the one expected by the Go code.
static serialize(node: TestTree): GoNode {
return {
name: node.name,
subtests: "children" in node ? node.children.map(GoTestReporter.serialize) : null,
};
}
register(suites: TestGroup[]) {
console.log(JSON.stringify(suites.map(GoTestReporter.serialize)));
}
update(path: string[], result: TestResult) {
let state: number;
switch (result.state) {
case "success": state = 0; break;
case "failure": state = 1; break;
case "skip": state = 2; break;
}
console.log(JSON.stringify({ path: path, state, logs: result.logs }));
}
finalize() {
console.log("null");
}
}

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/testui/style.css">
<title>PkgServer Tests</title>
</head>
<body>
<noscript>
I hate JavaScript as much as you, but this page runs tests written in
JavaScript to test the functionality of code written in JavaScript, so it
wouldn't make sense for it to work without JavaScript. <strong>Please turn
JavaScript on!</strong>
</noscript>
<h1>PkgServer Tests</h1>
<main>
<p id="counters">
<span id="success-counter">0</span> succeeded, <span id="failure-counter">0</span>
failed<span id="skip-counter-text" class="hidden">, <span id="skip-counter">0</span> skipped</span>.
</p>
<p hidden id="success-description">Successful test</p>
<p hidden id="failure-description">Failed test</p>
<p hidden id="skip-description">Partially or fully skipped test</p>
<div id="root">
</div>
<script type="module">
import "/test/all_tests.js";
import { DOMReporter, TESTS } from "/test/lib/test.js";
TESTS.run(new DOMReporter());
</script>
</main>
</body>
</html>

View File

@@ -0,0 +1,87 @@
:root {
--bg: #d3d3d3;
--fg: black;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #2c2c2c;
--fg: ghostwhite;
}
}
html {
background-color: var(--bg);
color: var(--fg);
}
h1, p, summary, noscript {
font-family: sans-serif;
}
noscript {
font-size: 16pt;
}
.root {
margin: 1rem 0;
}
details.test-node {
margin-left: 1rem;
padding: 0.2rem 0.5rem;
border-left: 2px dashed var(--fg);
> summary {
cursor: pointer;
}
&.success > summary::marker {
/*
* WebKit only supports color and font-size properties in ::marker [1],
* and its ::-webkit-details-marker only supports hiding the marker
* entirely [2], contrary to mdn's example [3]; thus, set a color as
* a fallback: while it may not be accessible for colorblind
* individuals, it's better than no indication of a test's state for
* anyone, as that there's no other way to include an indication in the
* marker on WebKit.
*
* [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::marker#browser_compatibility
* [2]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/summary#default_style
* [3]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/summary#changing_the_summarys_icon
*/
color: var(--fg);
content: url("/testui/success-closed.svg") / "success";
}
&.success[open] > summary::marker {
content: url("/testui/success-open.svg") / "success";
}
&.failure > summary::marker {
color: red;
content: url("/testui/failure-closed.svg") / "failure";
}
&.failure[open] > summary::marker {
content: url("/testui/failure-open.svg") / "failure";
}
&.skip > summary::marker {
color: blue;
content: url("/testui/skip-closed.svg") / "skip";
}
&.skip[open] > summary::marker {
content: url("/testui/skip-open.svg") / "skip";
}
}
p.test-desc {
margin: 0 0 0 1rem;
padding: 2px 0;
> pre {
margin: 0;
}
}
.hidden {
display: none;
}
.italic {
font-style: italic;
}

View File

@@ -0,0 +1,85 @@
import { NoOpReporter, TestRegistrar, context, group, suite, test } from "./lib/test.js";
suite("dog", [
group("tail", [
test("wags when happy", (t) => {
if (0 / 0 !== Infinity / Infinity) {
t.fatal("undefined must not be defined");
}
}),
test("idle when down", (t) => {
t.log("test test");
t.error("dog whining noises go here");
}),
]),
test("likes headpats", (t) => {
if (2 !== 2) {
t.error("IEEE 754 violated: 2 is NaN");
}
}),
context("near cat", [
test("is ecstatic", (t) => {
if (("b" + "a" + + "a" + "a").toLowerCase() === "banana") {
t.error("🍌🍌🍌");
t.error("🍌🍌🍌");
t.error("🍌🍌🍌");
t.failNow();
}
}),
test("playfully bites cats' tails", (t) => {
t.log("arf!");
throw new Error("nom");
}),
]),
]);
suite("cat", [
test("likes headpats", (t) => {
t.log("meow");
}),
test("owns skipping rope", (t) => {
t.skip("this cat is stuck in your machine!");
t.log("never logged");
}),
test("tester tester", (t) => {
const r = new TestRegistrar();
r.suite("explod", [
test("with yarn", (t) => {
t.log("YAY");
}),
]);
const reporter = new NoOpReporter();
r.run(reporter);
if (reporter.suites.length !== 1) {
t.fatal(`incorrect number of suites registered got=${reporter.suites.length} want=1`);
}
const suite = reporter.suites[0];
if (suite.name !== "explod") {
t.error(`suite name incorrect got='${suite.name}' want='explod'`);
}
if (suite.children.length !== 1) {
t.fatal(`incorrect number of suite children got=${suite.children.length} want=1`);
}
const test_ = suite.children[0];
if (test_.name !== "with yarn") {
t.error(`incorrect test name got='${test_.name}' want='with yarn'`);
}
if ("children" in test_) {
t.error(`expected leaf node, got group of ${test_.children.length} children`);
}
if (!reporter.finalized) t.error(`expected reporter to have been finalized`);
if (reporter.results.length !== 1) {
t.fatal(`incorrect result count got=${reporter.results.length} want=1`);
}
const result = reporter.results[0];
if (!(result.path.length === 2 &&
result.path[0] === "explod" &&
result.path[1] === "with yarn")) {
t.error(`incorrect result path got=${result.path} want=["explod", "with yarn"]`);
}
if (result.state !== "success") t.error(`expected test to succeed`);
if (!(result.logs.length === 1 && result.logs[0] === "YAY")) {
t.error(`incorrect result logs got=${result.logs} want=["YAY"]`);
}
}),
]);

View File

@@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2024"
}
}

View File

@@ -7,8 +7,8 @@
#endif
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
#define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
#define SHAREFS_PERM_REG 0600 /* permission bits for regular files presented to userspace */
#define SHAREFS_PERM_DIR 0770 /* permission bits for directories presented to userspace */
#define SHAREFS_PERM_REG 0660 /* permission bits for regular files presented to userspace */
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
/* sharefs_private is populated by sharefs_init and contains process-wide context */

View File

@@ -19,22 +19,21 @@ import (
"encoding/gob"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"path"
"path/filepath"
"runtime"
"runtime/cgo"
"strconv"
"syscall"
"unsafe"
"hakurei.app/check"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/std"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/helper/proc"
"hakurei.app/internal/info"
@@ -85,7 +84,10 @@ func destroySetup(private_data unsafe.Pointer) (ok bool) {
}
//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()
priv := (*C.struct_sharefs_private)(ctx.private_data)
setup := cgo.Handle(priv.setup).Value().(*setupState)
@@ -103,7 +105,11 @@ func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.
cfg.negative_timeout = 0
// 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)
goto fail
} else if err = syscall.Fchdir(fd); err != nil {
@@ -138,9 +144,9 @@ func sharefs_destroy(private_data unsafe.Pointer) {
func showHelp(args *fuseArgs) {
executableName := sharefsName
if args.argc > 0 {
executableName = path.Base(C.GoString(*args.argv))
executableName = filepath.Base(C.GoString(*args.argv))
} else if name, err := os.Executable(); err == nil {
executableName = path.Base(name)
executableName = filepath.Base(name)
}
fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
@@ -169,8 +175,11 @@ func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
// Decimal string representation of gid to set when running as root.
setgid *C.char
// Decimal string representation of open file descriptor to read setupState from.
// This is an internal detail for containerisation and must not be specified directly.
// Decimal string representation of open file descriptor to read
// setupState from.
//
// This is an internal detail for containerisation and must not be
// specified directly.
setup *C.char
}
@@ -253,7 +262,8 @@ func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
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 {
if len(s) == 0 {
return fuseArgs{argc: 0, argv: nil, allocated: 0}
@@ -269,6 +279,7 @@ func copyArgs(s ...string) fuseArgs {
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }
// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
//
// The last byte of arg must be 0.
func unsafeAddArgument(args *fuseArgs, arg string) {
C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
@@ -288,8 +299,8 @@ func _main(s ...string) (exitCode int) {
args := copyArgs(s...)
defer freeArgs(&args)
// this causes the kernel to enforce access control based on
// struct stat populated by sharefs_getattr
// this causes the kernel to enforce access control based on struct stat
// populated by sharefs_getattr
unsafeAddArgument(&args, "-odefault_permissions\x00")
var priv C.struct_sharefs_private
@@ -453,15 +464,19 @@ func _main(s ...string) (exitCode int) {
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
}
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
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
var setupPipe [2]*os.File
if r, w, err := os.Pipe(); err != nil {
log.Println(err)
return 5
} else {
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
setupWriter = w
z.Args = append(z.Args, "-osetup="+strconv.Itoa(3+len(z.ExtraFiles)))
z.ExtraFiles = append(z.ExtraFiles, r)
setupPipe[0], setupPipe[1] = r, w
}
if err := z.Start(); err != nil {
@@ -472,6 +487,9 @@ func _main(s ...string) (exitCode int) {
}
return 5
}
if err := setupPipe[0].Close(); err != nil {
log.Println(err)
}
if err := z.Serve(); err != nil {
if m, ok := message.GetMessage(err); ok {
log.Println(m)
@@ -481,10 +499,10 @@ func _main(s ...string) (exitCode int) {
return 5
}
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
if err := gob.NewEncoder(setupPipe[1]).Encode(&setup); err != nil {
log.Println(err)
return 5
} else if err = setupWriter.Close(); err != nil {
} else if err = setupPipe[1].Close(); err != nil {
log.Println(err)
}

View File

@@ -6,7 +6,7 @@ import (
"reflect"
"testing"
"hakurei.app/container/check"
"hakurei.app/check"
)
func TestParseOpts(t *testing.T) {

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
import (

View File

@@ -0,0 +1,122 @@
//go:build raceattr
// The raceattr program reproduces vfs inode file attribute race.
//
// Even though libfuse high-level API presents the address of a struct stat
// alongside struct fuse_context, file attributes are actually inherent to the
// inode, instead of the specific call from userspace. The kernel implementation
// in fs/fuse/xattr.c appears to make stale data in the inode (set by a previous
// call) impossible or very unlikely to reach userspace via the stat family of
// syscalls. However, when using default_permissions to have the VFS check
// permissions, this race still happens, despite the resulting struct stat being
// correct when overriding the check via capabilities otherwise.
//
// This program reproduces the failure, but because of its continuous nature, it
// is provided independent of the vm integration test suite.
package main
import (
"context"
"flag"
"log"
"os"
"os/signal"
"runtime"
"sync"
"sync/atomic"
"syscall"
)
func newStatAs(
ctx context.Context, cancel context.CancelFunc,
n *atomic.Uint64, ok *atomic.Bool,
uid uint32, pathname string,
continuous bool,
) func() {
return func() {
runtime.LockOSThread()
defer cancel()
if _, _, errno := syscall.Syscall(
syscall.SYS_SETUID, uintptr(uid),
0, 0,
); errno != 0 {
cancel()
log.Printf("cannot set uid to %d: %s", uid, errno)
}
var stat syscall.Stat_t
for {
if ctx.Err() != nil {
return
}
if err := syscall.Lstat(pathname, &stat); err != nil {
// SHAREFS_PERM_DIR not world executable, or
// SHAREFS_PERM_REG not world readable
if !continuous {
cancel()
}
ok.Store(true)
log.Printf("uid %d: %v", uid, err)
} else if stat.Uid != uid {
// appears to be unreachable
if !continuous {
cancel()
}
ok.Store(true)
log.Printf("got uid %d instead of %d", stat.Uid, uid)
}
n.Add(1)
}
}
}
func main() {
log.SetFlags(0)
log.SetPrefix("raceattr: ")
p := flag.String("target", "/sdcard/raceattr", "pathname of test file")
u0 := flag.Int("uid0", 1<<10-1, "first uid")
u1 := flag.Int("uid1", 1<<10-2, "second uid")
count := flag.Int("count", 1, "threads per uid")
continuous := flag.Bool("continuous", false, "keep running even after reproduce")
flag.Parse()
if os.Geteuid() != 0 {
log.Fatal("this program must run as root")
}
ctx, cancel := signal.NotifyContext(
context.Background(),
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGHUP,
)
if err := os.WriteFile(*p, nil, 0); err != nil {
log.Fatal(err)
}
var (
wg sync.WaitGroup
n atomic.Uint64
ok atomic.Bool
)
if *count < 1 {
*count = 1
}
for range *count {
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u0), *p, *continuous))
if *u1 >= 0 {
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u1), *p, *continuous))
}
}
wg.Wait()
if !*continuous && ok.Load() {
log.Printf("reproduced after %d calls", n.Load())
}
}

View File

@@ -4,8 +4,8 @@ import (
"encoding/gob"
"fmt"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/check"
"hakurei.app/fhs"
)
func init() { gob.Register(new(AutoEtcOp)) }

View File

@@ -5,8 +5,8 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/check"
"hakurei.app/internal/stub"
)
func TestAutoEtcOp(t *testing.T) {

View File

@@ -4,8 +4,8 @@ import (
"encoding/gob"
"fmt"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/message"
)

View File

@@ -5,9 +5,9 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/check"
"hakurei.app/container/std"
"hakurei.app/container/stub"
"hakurei.app/internal/stub"
"hakurei.app/message"
)

View File

@@ -3,6 +3,8 @@ package container
import (
"syscall"
"unsafe"
"hakurei.app/ext"
)
const (
@@ -51,15 +53,15 @@ func capset(hdrp *capHeader, datap *[2]capData) error {
// capBoundingSetDrop drops a capability from the calling thread's capability bounding set.
func capBoundingSetDrop(cap uintptr) error {
return Prctl(syscall.PR_CAPBSET_DROP, cap, 0)
return ext.Prctl(syscall.PR_CAPBSET_DROP, cap, 0)
}
// capAmbientClearAll clears the ambient capability set of the calling thread.
func capAmbientClearAll() error {
return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0)
return ext.Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0)
}
// capAmbientRaise adds to the ambient capability set of the calling thread.
func capAmbientRaise(cap uintptr) error {
return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap)
return ext.Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap)
}

View File

@@ -16,10 +16,11 @@ import (
. "syscall"
"time"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/check"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/ext"
"hakurei.app/fhs"
"hakurei.app/message"
)
@@ -27,9 +28,6 @@ const (
// CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGUSR2
// Timeout for writing initParams to Container.setup.
initSetupTimeout = 5 * time.Second
)
type (
@@ -38,9 +36,13 @@ type (
Container struct {
// Whether the container init should stay alive after its parent terminates.
AllowOrphan bool
// Scheduling policy to set via sched_setscheduler(2). The zero value
// skips this call. Supported policies are [SCHED_BATCH], [SCHED_IDLE].
SchedPolicy int
// Whether to set SchedPolicy and SchedPriority via sched_setscheduler(2).
SetScheduler bool
// Scheduling policy to set via sched_setscheduler(2).
SchedPolicy ext.SchedPolicy
// Scheduling priority to set via sched_setscheduler(2). The zero value
// implies the minimum value supported by the current SchedPolicy.
SchedPriority ext.Int
// Cgroup fd, nil to disable.
Cgroup *int
// ExtraFiles passed through to initial process in the container, with
@@ -48,7 +50,7 @@ type (
ExtraFiles []*os.File
// Write end of a pipe connected to the init to deliver [Params].
setup *os.File
setup [2]*os.File
// Cancels the context passed to the underlying cmd.
cancel context.CancelFunc
// Closed after Wait returns. Keeps the spawning thread alive.
@@ -181,31 +183,24 @@ var (
closeOnExecErr error
)
// ensureCloseOnExec ensures all currently open file descriptors have the syscall.FD_CLOEXEC flag set.
// This is only ran once as it is intended to handle files left open by the parent, and any file opened
// on this side should already have syscall.FD_CLOEXEC set.
// ensureCloseOnExec ensures all currently open file descriptors have the
// syscall.FD_CLOEXEC flag set.
//
// This is only ran once as it is intended to handle files left open by the
// parent, and any file opened on this side should already have
// syscall.FD_CLOEXEC set.
func ensureCloseOnExec() error {
closeOnExecOnce.Do(func() {
const fdPrefixPath = "/proc/self/fd/"
var entries []os.DirEntry
if entries, closeOnExecErr = os.ReadDir(fdPrefixPath); closeOnExecErr != nil {
return
}
var fd int
for _, ent := range entries {
if fd, closeOnExecErr = strconv.Atoi(ent.Name()); closeOnExecErr != nil {
break // not reached
}
CloseOnExec(fd)
}
})
closeOnExecOnce.Do(func() { closeOnExecErr = doCloseOnExec() })
if closeOnExecErr == nil {
return nil
}
return &StartError{Fatal: true, Step: "set FD_CLOEXEC on all open files", Err: closeOnExecErr, Passthrough: true}
return &StartError{
Fatal: true,
Step: "set FD_CLOEXEC on all open files",
Err: closeOnExecErr,
Passthrough: true,
}
}
// Start starts the container init. The init process blocks until Serve is called.
@@ -289,14 +284,16 @@ func (p *Container) Start() error {
}
// place setup pipe before user supplied extra files, this is later restored by init
if fd, f, err := Setup(&p.cmd.ExtraFiles); err != nil {
if r, w, err := os.Pipe(); err != nil {
return &StartError{
Fatal: true,
Step: "set up params stream",
Err: err,
}
} else {
p.setup = f
fd := 3 + len(p.cmd.ExtraFiles)
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, r)
p.setup[0], p.setup[1] = r, w
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
}
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
@@ -326,9 +323,9 @@ func (p *Container) Start() error {
}
if abi, err := LandlockGetABI(); err != nil {
if p.HostAbstract {
if p.HostAbstract || !p.HostNet {
// landlock can be skipped here as it restricts access
// to resources already covered by namespaces (pid)
// to resources already covered by namespaces (pid, net)
goto landlockOut
}
return &StartError{Step: "get landlock ABI", Err: err}
@@ -373,16 +370,38 @@ func (p *Container) Start() error {
// sched_setscheduler: thread-directed but acts on all processes
// created from the calling thread
if p.SchedPolicy > 0 {
p.msg.Verbosef("setting scheduling policy %d", p.SchedPolicy)
if p.SetScheduler {
if p.SchedPolicy < 0 || p.SchedPolicy > ext.SCHED_LAST {
return &StartError{
Fatal: false,
Step: "set scheduling policy",
Err: EINVAL,
}
}
var param schedParam
if priority, err := p.SchedPolicy.GetPriorityMin(); err != nil {
return &StartError{
Fatal: true,
Step: "get minimum priority",
Err: err,
}
} else {
param.priority = max(priority, p.SchedPriority)
}
p.msg.Verbosef(
"setting scheduling policy %s priority %d",
p.SchedPolicy, param.priority,
)
if err := schedSetscheduler(
0, // calling thread
p.SchedPolicy,
&schedParam{0},
&param,
); err != nil {
return &StartError{
Fatal: true,
Step: "enforce landlock ruleset",
Step: "set scheduling policy",
Err: err,
}
}
@@ -408,24 +427,33 @@ func (p *Container) Start() error {
// Serve serves [Container.Params] to the container init.
//
// Serve must only be called once.
func (p *Container) Serve() error {
if p.setup == nil {
func (p *Container) Serve() (err error) {
if p.setup[0] == nil || p.setup[1] == nil {
panic("invalid serve")
}
setup := p.setup
p.setup = nil
if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil {
done := make(chan struct{})
defer func() {
if closeErr := p.setup[1].Close(); err == nil {
err = closeErr
}
if err != nil {
p.cancel()
}
close(done)
p.setup[0], p.setup[1] = nil, nil
}()
if err = p.setup[0].Close(); err != nil {
return &StartError{
Fatal: true,
Step: "set init pipe deadline",
Step: "close read end of init pipe",
Err: err,
Passthrough: true,
}
}
if p.Path == nil {
p.cancel()
return &StartError{
Step: "invalid executable pathname",
Err: EINVAL,
@@ -441,18 +469,27 @@ func (p *Container) Serve() error {
p.SeccompRules = make([]std.NativeRule, 0)
}
err := gob.NewEncoder(setup).Encode(&initParams{
t := time.Now().UTC()
go func(f *os.File) {
select {
case <-p.ctx.Done():
if cancelErr := f.SetWriteDeadline(t); cancelErr != nil {
p.msg.Verbose(err)
}
case <-done:
p.msg.Verbose("setup payload took", time.Since(t))
return
}
}(p.setup[1])
return gob.NewEncoder(p.setup[1]).Encode(&initParams{
p.Params,
Getuid(),
Getgid(),
len(p.ExtraFiles),
p.msg.IsVerbose(),
})
_ = setup.Close()
if err != nil {
p.cancel()
}
return err
}
// Wait blocks until the container init process to exit and releases any

View File

@@ -16,18 +16,20 @@ import (
"strings"
"syscall"
"testing"
"time"
"hakurei.app/check"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/container/vfs"
"hakurei.app/ext"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/info"
"hakurei.app/internal/params"
"hakurei.app/ldd"
"hakurei.app/message"
"hakurei.app/vfs"
)
// Note: this package requires cgo, which is unavailable in the Go playground.
@@ -83,9 +85,9 @@ func TestStartError(t *testing.T) {
{"params env", &container.StartError{
Fatal: true,
Step: "set up params stream",
Err: container.ErrReceiveEnv,
Err: params.ErrReceiveEnv,
}, "set up params stream: environment variable not set",
container.ErrReceiveEnv, syscall.EBADF,
params.ErrReceiveEnv, syscall.EBADF,
"cannot set up params stream: environment variable not set"},
{"params", &container.StartError{
@@ -258,7 +260,7 @@ var containerTestCases = []struct {
1000, 100, nil, 0, std.PresetExt},
{"custom rules", true, true, true, false,
emptyOps, emptyMnt,
1, 31, []std.NativeRule{{Syscall: std.ScmpSyscall(syscall.SYS_SETUID), Errno: std.ScmpErrno(syscall.EPERM)}}, 0, std.PresetExt},
1, 31, []std.NativeRule{{Syscall: ext.SyscallNum(syscall.SYS_SETUID), Errno: std.ScmpErrno(syscall.EPERM)}}, 0, std.PresetExt},
{"tmpfs", true, false, false, true,
earlyOps(new(container.Ops).
@@ -435,11 +437,8 @@ func TestContainer(t *testing.T) {
wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel()
var libPaths []*check.Absolute
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c := helperNewContainerLibPaths(t.Context(), &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid
c.Gid = tc.gid
c.Hostname = hostnameFromTestCase(tc.name)
@@ -449,7 +448,6 @@ func TestContainer(t *testing.T) {
} else {
c.Stdout, c.Stderr = os.Stdout, os.Stderr
}
c.WaitDelay = helperDefaultTimeout
*c.Ops = append(*c.Ops, *wantOps...)
c.SeccompRules = tc.rules
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
@@ -457,6 +455,15 @@ func TestContainer(t *testing.T) {
c.SeccompDisable = !tc.filter
c.RetainSession = tc.session
c.HostNet = tc.net
if info.CanDegrade {
if _, err := container.LandlockGetABI(); err != nil {
if !errors.Is(err, syscall.ENOSYS) {
t.Fatalf("LandlockGetABI: error = %v", err)
}
c.HostAbstract = true
t.Log("Landlock LSM is unavailable, enabling HostAbstract")
}
}
c.
Readonly(check.MustAbs(pathReadonly), 0755).
@@ -552,11 +559,10 @@ func testContainerCancel(
) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
ctx, cancel := context.WithCancel(t.Context())
c := helperNewContainer(ctx, "block")
c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.WaitDelay = helperDefaultTimeout
if containerExtra != nil {
containerExtra(c)
}
@@ -737,8 +743,7 @@ func init() {
const (
envDoCheck = "HAKUREI_TEST_DO_CHECK"
helperDefaultTimeout = 5 * time.Second
helperInnerPath = "/usr/bin/helper"
helperInnerPath = "/usr/bin/helper"
)
var (

View File

@@ -1,8 +1,10 @@
package container
import (
"context"
"io"
"io/fs"
"net"
"os"
"os/exec"
"os/signal"
@@ -12,6 +14,9 @@ import (
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/ext"
"hakurei.app/internal/netlink"
"hakurei.app/internal/params"
"hakurei.app/message"
)
@@ -52,7 +57,7 @@ type syscallDispatcher interface {
// isatty provides [Isatty].
isatty(fd int) bool
// receive provides [Receive].
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
receive(key string, e any, fdp *int) (closeFunc func() error, err error)
// bindMount provides procPaths.bindMount.
bindMount(msg message.Msg, source, target string, flags uintptr) error
@@ -63,7 +68,7 @@ type syscallDispatcher interface {
// ensureFile provides ensureFile.
ensureFile(name string, perm, pperm os.FileMode) error
// mustLoopback provides mustLoopback.
mustLoopback(msg message.Msg)
mustLoopback(ctx context.Context, msg message.Msg)
// seccompLoad provides [seccomp.Load].
seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error
@@ -141,8 +146,8 @@ func (k direct) new(f func(k syscallDispatcher)) { go f(k) }
func (direct) lockOSThread() { runtime.LockOSThread() }
func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) }
func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
@@ -150,9 +155,9 @@ func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(h
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
func (direct) isatty(fd int) bool { return Isatty(fd) }
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return Receive(key, e, fdp)
func (direct) isatty(fd int) bool { return ext.Isatty(fd) }
func (direct) receive(key string, e any, fdp *int) (func() error, error) {
return params.Receive(key, e, fdp)
}
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
@@ -167,7 +172,50 @@ func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
return ensureFile(name, perm, pperm)
}
func (direct) mustLoopback(msg message.Msg) { mustLoopback(msg) }
func (direct) mustLoopback(ctx context.Context, msg message.Msg) {
var lo int
if ifi, err := net.InterfaceByName("lo"); err != nil {
msg.GetLogger().Fatalln(err)
} else {
lo = ifi.Index
}
c, err := netlink.DialRoute(0)
if err != nil {
msg.GetLogger().Fatalln(err)
}
must := func(err error) {
if err == nil {
return
}
if closeErr := c.Close(); closeErr != nil {
msg.Verbosef("cannot close RTNETLINK: %v", closeErr)
}
switch err.(type) {
case *os.SyscallError:
msg.GetLogger().Fatalf("cannot %v", err)
case syscall.Errno:
msg.GetLogger().Fatalf("RTNETLINK answers: %v", err)
default:
if err == context.DeadlineExceeded || err == context.Canceled {
msg.GetLogger().Fatalf("interrupted RTNETLINK operation")
}
msg.GetLogger().Fatal("RTNETLINK answers with malformed message")
}
}
must(c.SendNewaddrLo(ctx, uint32(lo)))
must(c.SendIfInfomsg(ctx, syscall.RTM_NEWLINK, 0, &syscall.IfInfomsg{
Family: syscall.AF_UNSPEC,
Index: int32(lo),
Flags: syscall.IFF_UP,
Change: syscall.IFF_UP,
}))
must(c.Close())
}
func (direct) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
return seccomp.Load(rules, flags)

View File

@@ -2,6 +2,7 @@ package container
import (
"bytes"
"context"
"fmt"
"io"
"io/fs"
@@ -18,7 +19,7 @@ import (
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/container/stub"
"hakurei.app/internal/stub"
"hakurei.app/message"
)
@@ -389,7 +390,7 @@ func (k *kstub) isatty(fd int) bool {
return expect.Ret.(bool)
}
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, err error) {
k.Helper()
expect := k.Expects("receive")
@@ -407,10 +408,17 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
}
return nil
}
// avoid changing test cases
var fdpComp *uintptr
if fdp != nil {
fdpComp = new(uintptr(*fdp))
}
err = expect.Error(
stub.CheckArg(k.Stub, "key", key, 0),
stub.CheckArgReflect(k.Stub, "e", e, 1),
stub.CheckArgReflect(k.Stub, "fdp", fdp, 2))
stub.CheckArgReflect(k.Stub, "fdp", fdpComp, 2))
// 3 is unused so stores params
if expect.Args[3] != nil {
@@ -425,7 +433,7 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
if expect.Args[4] != nil {
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
if fdp != nil {
*fdp = v
*fdp = int(v)
}
}
}
@@ -468,7 +476,7 @@ func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
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 {
k.Helper()

View File

@@ -5,9 +5,9 @@ import (
"os"
"syscall"
"hakurei.app/container/check"
"hakurei.app/container/vfs"
"hakurei.app/check"
"hakurei.app/message"
"hakurei.app/vfs"
)
// messageFromError returns a printable error message for a supported concrete type.

View File

@@ -8,9 +8,9 @@ import (
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/container/vfs"
"hakurei.app/check"
"hakurei.app/internal/stub"
"hakurei.app/vfs"
)
func TestMessageFromError(t *testing.T) {

View File

@@ -1,37 +0,0 @@
package container
import (
"fmt"
"log"
"os"
"sync"
"hakurei.app/message"
)
var (
executable string
executableOnce sync.Once
)
func copyExecutable(msg message.Msg) {
if name, err := os.Executable(); err != nil {
m := fmt.Sprintf("cannot read executable path: %v", err)
if msg != nil {
msg.BeforeExit()
msg.GetLogger().Fatal(m)
} else {
log.Fatal(m)
}
} else {
executable = name
}
}
// MustExecutable calls [os.Executable] and terminates the process on error.
//
// Deprecated: This is no longer used and will be removed in 0.4.
func MustExecutable(msg message.Msg) string {
executableOnce.Do(func() { copyExecutable(msg) })
return executable
}

View File

@@ -1,18 +0,0 @@
package container_test
import (
"os"
"testing"
"hakurei.app/container"
"hakurei.app/message"
)
func TestExecutable(t *testing.T) {
t.Parallel()
for i := 0; i < 16; i++ {
if got := container.MustExecutable(message.New(nil)); got != os.Args[0] {
t.Errorf("MustExecutable: %q, want %q", got, os.Args[0])
}
}
}

View File

@@ -7,7 +7,8 @@ import (
"log"
"os"
"os/exec"
"path"
"os/signal"
"path/filepath"
"slices"
"strconv"
"sync"
@@ -15,8 +16,10 @@ import (
. "syscall"
"time"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/ext"
"hakurei.app/fhs"
"hakurei.app/internal/params"
"hakurei.app/message"
)
@@ -145,44 +148,46 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
var (
params initParams
closeSetup func() error
setupFd uintptr
offsetSetup int
param initParams
closeSetup func() error
setupFd int
)
if f, err := k.receive(setupEnv, &params, &setupFd); err != nil {
if f, err := k.receive(setupEnv, &param, &setupFd); err != nil {
if errors.Is(err, EBADF) {
k.fatal(msg, "invalid setup descriptor")
}
if errors.Is(err, ErrReceiveEnv) {
if errors.Is(err, params.ErrReceiveEnv) {
k.fatal(msg, setupEnv+" not set")
}
k.fatalf(msg, "cannot decode init setup payload: %v", err)
} else {
if params.Ops == nil {
if param.Ops == nil {
k.fatal(msg, "invalid setup parameters")
}
if params.ParentPerm == 0 {
params.ParentPerm = 0755
if param.ParentPerm == 0 {
param.ParentPerm = 0755
}
msg.SwapVerbose(params.Verbose)
msg.SwapVerbose(param.Verbose)
msg.Verbose("received setup parameters")
closeSetup = f
offsetSetup = int(setupFd + 1)
}
if !params.HostNet {
k.mustLoopback(msg)
if !param.HostNet {
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
if err := k.setDumpable(SUID_DUMP_USER); err != nil {
if err := k.setDumpable(ext.SUID_DUMP_USER); err != nil {
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
}
if err := k.writeFile(fhs.Proc+"self/uid_map",
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
append([]byte{}, strconv.Itoa(param.Uid)+" "+strconv.Itoa(param.HostUid)+" 1\n"...),
0); err != nil {
k.fatalf(msg, "%v", err)
}
@@ -192,17 +197,17 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "%v", err)
}
if err := k.writeFile(fhs.Proc+"self/gid_map",
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
append([]byte{}, strconv.Itoa(param.Gid)+" "+strconv.Itoa(param.HostGid)+" 1\n"...),
0); err != nil {
k.fatalf(msg, "%v", err)
}
if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil {
if err := k.setDumpable(ext.SUID_DUMP_DISABLE); err != nil {
k.fatalf(msg, "cannot set SUID_DUMP_DISABLE: %v", err)
}
oldmask := k.umask(0)
if params.Hostname != "" {
if err := k.sethostname([]byte(params.Hostname)); err != nil {
if param.Hostname != "" {
if err := k.sethostname([]byte(param.Hostname)); err != nil {
k.fatalf(msg, "cannot set hostname: %v", err)
}
}
@@ -215,7 +220,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
ctx, cancel := context.WithCancel(context.Background())
state := &setupState{process: make(map[int]WaitStatus), Params: &params.Params, Msg: msg, Context: ctx}
state := &setupState{process: make(map[int]WaitStatus), Params: &param.Params, Msg: msg, Context: ctx}
defer cancel()
/* early is called right before pivot_root into intermediate root;
@@ -223,7 +228,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
difficult to obtain via library functions after pivot_root, and
implementations are expected to avoid changing the state of the mount
namespace */
for i, op := range *params.Ops {
for i, op := range *param.Ops {
if op == nil || !op.Valid() {
k.fatalf(msg, "invalid op at index %d", i)
}
@@ -266,7 +271,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
step sets up the container filesystem, and implementations are expected to
keep the host root and sysroot mount points intact but otherwise can do
whatever they need to. Calling chdir is allowed but discouraged. */
for i, op := range *params.Ops {
for i, op := range *param.Ops {
// ops already checked during early setup
if prefix, ok := op.prefix(); ok {
msg.Verbosef("%s %s", prefix, op)
@@ -290,7 +295,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
{
var fd int
if err := IgnoringEINTR(func() (err error) {
if err := ext.IgnoringEINTR(func() (err error) {
fd, err = k.open(fhs.Root, O_DIRECTORY|O_RDONLY, 0)
return
}); err != nil {
@@ -322,7 +327,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
}
for i := uintptr(0); i <= lastcap; i++ {
if params.Privileged && i == CAP_SYS_ADMIN {
if param.Privileged && i == CAP_SYS_ADMIN {
continue
}
if err := k.capBoundingSetDrop(i); err != nil {
@@ -331,7 +336,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
var keep [2]uint32
if params.Privileged {
if param.Privileged {
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
@@ -345,13 +350,13 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot capset: %v", err)
}
if !params.SeccompDisable {
rules := params.SeccompRules
if !param.SeccompDisable {
rules := param.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
msg.Verbosef("resolving presets %#x", param.SeccompPresets)
rules = seccomp.Preset(param.SeccompPresets, param.SeccompFlags)
}
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
if err := k.seccompLoad(rules, param.SeccompFlags); err != nil {
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
k.fatalf(msg, "cannot load syscall filter: %v", err)
}
@@ -360,10 +365,10 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
msg.Verbose("syscall filter not configured")
}
extraFiles := make([]*os.File, params.Count)
extraFiles := make([]*os.File, param.Count)
for i := range extraFiles {
// setup fd is placed before all extra files
extraFiles[i] = k.newFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
extraFiles[i] = k.newFile(uintptr(setupFd+1+i), "extra file "+strconv.Itoa(i))
}
k.umask(oldmask)
@@ -441,7 +446,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
// called right before startup of initial process, all state changes to the
// current process is prohibited during late
for i, op := range *params.Ops {
for i, op := range *param.Ops {
// ops already checked during early setup
if err := op.late(state, k); err != nil {
if m, ok := messageFromError(err); ok {
@@ -462,14 +467,14 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot close setup pipe: %v", err)
}
cmd := exec.Command(params.Path.String())
cmd := exec.Command(param.Path.String())
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = params.Args
cmd.Env = params.Env
cmd.Args = param.Args
cmd.Env = param.Env
cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir.String()
cmd.Dir = param.Dir.String()
msg.Verbosef("starting initial process %s", params.Path)
msg.Verbosef("starting initial process %s", param.Path)
if err := k.start(cmd); err != nil {
k.fatalf(msg, "%v", err)
}
@@ -487,9 +492,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
for {
select {
case s := <-sig:
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
if s == CancelSignal && param.ForwardCancel && cmd.Process != nil {
msg.Verbose("forwarding context cancellation")
if err := k.signal(cmd, os.Interrupt); err != nil {
if err := k.signal(cmd, os.Interrupt); err != nil && !errors.Is(err, os.ErrProcessDone) {
k.printf(msg, "cannot forward cancellation: %v", err)
}
continue
@@ -519,7 +524,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
cancel()
// start timeout early
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
go func() { time.Sleep(param.AdoptWaitDelay); close(timeout) }()
// close initial process files; this also keeps them alive
for _, f := range extraFiles {
@@ -563,7 +568,7 @@ func TryArgv0(msg message.Msg) {
msg = message.New(log.Default())
}
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == initName {
Init(msg)
msg.BeforeExit()
os.Exit(0)

View File

@@ -7,10 +7,11 @@ import (
"testing"
"time"
"hakurei.app/container/check"
"hakurei.app/check"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/container/stub"
"hakurei.app/internal/params"
"hakurei.app/internal/stub"
)
func TestInitEntrypoint(t *testing.T) {
@@ -40,7 +41,7 @@ func TestInitEntrypoint(t *testing.T) {
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
call("setPtracer", stub.ExpectArgs{uintptr(0)}, nil, nil),
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, ErrReceiveEnv),
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, params.ErrReceiveEnv),
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SETUP not set"}}, nil, nil),
},
}, nil},

View File

@@ -6,7 +6,7 @@ import (
"os"
"syscall"
"hakurei.app/container/check"
"hakurei.app/check"
"hakurei.app/container/std"
)

View File

@@ -6,9 +6,9 @@ import (
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/check"
"hakurei.app/container/std"
"hakurei.app/container/stub"
"hakurei.app/internal/stub"
)
func TestBindMountOp(t *testing.T) {

View File

@@ -12,8 +12,8 @@ import (
"syscall"
"time"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/check"
"hakurei.app/fhs"
)
func init() { gob.Register(new(DaemonOp)) }

View File

@@ -4,8 +4,8 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/check"
"hakurei.app/internal/stub"
"hakurei.app/message"
)

View File

@@ -3,11 +3,11 @@ package container
import (
"encoding/gob"
"fmt"
"path"
"path/filepath"
. "syscall"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/check"
"hakurei.app/fhs"
)
func init() { gob.Register(new(MountDevOp)) }
@@ -46,7 +46,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
}
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
targetPath := path.Join(target, name)
targetPath := filepath.Join(target, name)
if err := k.ensureFile(targetPath, 0444, state.ParentPerm); err != nil {
return err
}
@@ -62,7 +62,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
for i, name := range []string{"stdin", "stdout", "stderr"} {
if err := k.symlink(
fhs.Proc+"self/fd/"+string(rune(i+'0')),
path.Join(target, name),
filepath.Join(target, name),
); err != nil {
return err
}
@@ -72,13 +72,13 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
{fhs.Proc + "kcore", "core"},
{"pts/ptmx", "ptmx"},
} {
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
if err := k.symlink(pair[0], filepath.Join(target, pair[1])); err != nil {
return err
}
}
devShmPath := path.Join(target, "shm")
devPtsPath := path.Join(target, "pts")
devShmPath := filepath.Join(target, "shm")
devPtsPath := filepath.Join(target, "pts")
for _, name := range []string{devShmPath, devPtsPath} {
if err := k.mkdir(name, state.ParentPerm); err != nil {
return err
@@ -92,7 +92,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
if state.RetainSession {
if k.isatty(Stdout) {
consolePath := path.Join(target, "console")
consolePath := filepath.Join(target, "console")
if err := k.ensureFile(consolePath, 0444, state.ParentPerm); err != nil {
return err
}
@@ -110,7 +110,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
}
if d.Mqueue {
mqueueTarget := path.Join(target, "mqueue")
mqueueTarget := filepath.Join(target, "mqueue")
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
return err
}

View File

@@ -4,8 +4,8 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/check"
"hakurei.app/internal/stub"
)
func TestMountDevOp(t *testing.T) {

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"os"
"hakurei.app/container/check"
"hakurei.app/check"
)
func init() { gob.Register(new(MkdirOp)) }

View File

@@ -4,8 +4,8 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/check"
"hakurei.app/internal/stub"
)
func TestMkdirOp(t *testing.T) {

View File

@@ -6,8 +6,8 @@ import (
"slices"
"strings"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/check"
"hakurei.app/fhs"
)
const (

View File

@@ -5,8 +5,8 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/check"
"hakurei.app/internal/stub"
)
func TestMountOverlayOp(t *testing.T) {

View File

@@ -5,8 +5,8 @@ import (
"fmt"
"syscall"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/check"
"hakurei.app/fhs"
)
const (

View File

@@ -4,8 +4,8 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/check"
"hakurei.app/internal/stub"
)
func TestTmpfileOp(t *testing.T) {

View File

@@ -5,7 +5,7 @@ import (
"fmt"
. "syscall"
"hakurei.app/container/check"
"hakurei.app/check"
)
func init() { gob.Register(new(MountProcOp)) }

View File

@@ -4,8 +4,8 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/check"
"hakurei.app/internal/stub"
)
func TestMountProcOp(t *testing.T) {

View File

@@ -4,7 +4,7 @@ import (
"encoding/gob"
"fmt"
"hakurei.app/container/check"
"hakurei.app/check"
)
func init() { gob.Register(new(RemountOp)) }

View File

@@ -4,8 +4,8 @@ import (
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/check"
"hakurei.app/internal/stub"
)
func TestRemountOp(t *testing.T) {

View File

@@ -3,9 +3,9 @@ package container
import (
"encoding/gob"
"fmt"
"path"
"path/filepath"
"hakurei.app/container/check"
"hakurei.app/check"
)
func init() { gob.Register(new(SymlinkOp)) }
@@ -30,7 +30,7 @@ func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkN
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
if l.Dereference {
if !path.IsAbs(l.LinkName) {
if !filepath.IsAbs(l.LinkName) {
return check.AbsoluteError(l.LinkName)
}
if name, err := k.readlink(l.LinkName); err != nil {
@@ -44,7 +44,7 @@ func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
target := toSysroot(l.Target.String())
if err := k.mkdirAll(path.Dir(target), state.ParentPerm); err != nil {
if err := k.mkdirAll(filepath.Dir(target), state.ParentPerm); err != nil {
return err
}
return k.symlink(l.LinkName, target)

View File

@@ -4,8 +4,8 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/check"
"hakurei.app/internal/stub"
)
func TestSymlinkOp(t *testing.T) {

View File

@@ -8,7 +8,7 @@ import (
"strconv"
. "syscall"
"hakurei.app/container/check"
"hakurei.app/check"
)
func init() { gob.Register(new(MountTmpfsOp)) }

View File

@@ -5,8 +5,8 @@ import (
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/check"
"hakurei.app/internal/stub"
)
func TestMountTmpfsOp(t *testing.T) {

View File

@@ -5,7 +5,7 @@ import (
"syscall"
"unsafe"
"hakurei.app/container/std"
"hakurei.app/ext"
)
// include/uapi/linux/landlock.h
@@ -223,7 +223,7 @@ func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
}
rulesetFd, _, errno := syscall.Syscall(
std.SYS_LANDLOCK_CREATE_RULESET,
ext.SYS_LANDLOCK_CREATE_RULESET,
pointer, size,
flags,
)
@@ -247,7 +247,7 @@ func LandlockGetABI() (int, error) {
// LandlockRestrictSelf applies a loaded ruleset to the calling thread.
func LandlockRestrictSelf(rulesetFd int, flags uintptr) error {
r, _, errno := syscall.Syscall(
std.SYS_LANDLOCK_RESTRICT_SELF,
ext.SYS_LANDLOCK_RESTRICT_SELF,
uintptr(rulesetFd),
flags,
0,

View File

@@ -6,8 +6,9 @@ import (
"os"
. "syscall"
"hakurei.app/container/vfs"
"hakurei.app/ext"
"hakurei.app/message"
"hakurei.app/vfs"
)
/*
@@ -115,7 +116,7 @@ func (p *procPaths) remount(msg message.Msg, target string, flags uintptr) error
var targetKFinal string
{
var destFd int
if err := IgnoringEINTR(func() (err error) {
if err := ext.IgnoringEINTR(func() (err error) {
destFd, err = p.k.open(targetFinal, O_PATH|O_CLOEXEC, 0)
return
}); err != nil {

View File

@@ -5,8 +5,8 @@ import (
"syscall"
"testing"
"hakurei.app/container/stub"
"hakurei.app/container/vfs"
"hakurei.app/internal/stub"
"hakurei.app/vfs"
)
func TestBindMount(t *testing.T) {

View File

@@ -1,269 +0,0 @@
package container
import (
"encoding/binary"
"errors"
"net"
"os"
. "syscall"
"unsafe"
"hakurei.app/container/std"
"hakurei.app/message"
)
// rtnetlink represents a NETLINK_ROUTE socket.
type rtnetlink struct {
// Sent as part of rtnetlink messages.
pid uint32
// AF_NETLINK socket.
fd int
// Whether the socket is open.
ok bool
// Message sequence number.
seq uint32
}
// open creates the underlying NETLINK_ROUTE socket.
func (s *rtnetlink) open() (err error) {
if s.ok || s.fd < 0 {
return os.ErrInvalid
}
s.pid = uint32(Getpid())
if s.fd, err = Socket(
AF_NETLINK,
SOCK_RAW|SOCK_CLOEXEC,
NETLINK_ROUTE,
); err != nil {
return os.NewSyscallError("socket", err)
} else if err = Bind(s.fd, &SockaddrNetlink{
Family: AF_NETLINK,
Pid: s.pid,
}); err != nil {
_ = s.close()
return os.NewSyscallError("bind", err)
} else {
s.ok = true
return nil
}
}
// close closes the underlying NETLINK_ROUTE socket.
func (s *rtnetlink) close() error {
if !s.ok {
return os.ErrInvalid
}
s.ok = false
err := Close(s.fd)
s.fd = -1
return err
}
// roundtrip sends a netlink message and handles the reply.
func (s *rtnetlink) roundtrip(data []byte) error {
if !s.ok {
return os.ErrInvalid
}
defer func() { s.seq++ }()
if err := Sendto(s.fd, data, 0, &SockaddrNetlink{
Family: AF_NETLINK,
}); err != nil {
return os.NewSyscallError("sendto", err)
}
buf := make([]byte, Getpagesize())
done:
for {
p := buf
if n, _, err := Recvfrom(s.fd, p, 0); err != nil {
return os.NewSyscallError("recvfrom", err)
} else if n < NLMSG_HDRLEN {
return errors.ErrUnsupported
} else {
p = p[:n]
}
if msgs, err := ParseNetlinkMessage(p); err != nil {
return err
} else {
for _, m := range msgs {
if m.Header.Seq != s.seq || m.Header.Pid != s.pid {
return errors.ErrUnsupported
}
if m.Header.Type == NLMSG_DONE {
break done
}
if m.Header.Type == NLMSG_ERROR {
if len(m.Data) >= 4 {
errno := Errno(-std.Int(binary.NativeEndian.Uint32(m.Data)))
if errno == 0 {
return nil
}
return errno
}
return errors.ErrUnsupported
}
}
}
}
return nil
}
// mustRoundtrip calls roundtrip and terminates via msg for a non-nil error.
func (s *rtnetlink) mustRoundtrip(msg message.Msg, data []byte) {
err := s.roundtrip(data)
if err == nil {
return
}
if closeErr := Close(s.fd); closeErr != nil {
msg.Verbosef("cannot close: %v", err)
}
switch err.(type) {
case *os.SyscallError:
msg.GetLogger().Fatalf("cannot %v", err)
case Errno:
msg.GetLogger().Fatalf("RTNETLINK answers: %v", err)
default:
msg.GetLogger().Fatalln("RTNETLINK answers with unexpected message")
}
}
// newaddrLo represents a RTM_NEWADDR message with two addresses.
type newaddrLo struct {
header NlMsghdr
data IfAddrmsg
r0 RtAttr
a0 [4]byte // in_addr
r1 RtAttr
a1 [4]byte // in_addr
}
// sizeofNewaddrLo is the expected size of newaddrLo.
const sizeofNewaddrLo = NLMSG_HDRLEN + SizeofIfAddrmsg + (SizeofRtAttr+4)*2
// newaddrLo returns the address of a populated newaddrLo.
func (s *rtnetlink) newaddrLo(lo int) *newaddrLo {
return &newaddrLo{NlMsghdr{
Len: sizeofNewaddrLo,
Type: RTM_NEWADDR,
Flags: NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL,
Seq: s.seq,
Pid: s.pid,
}, IfAddrmsg{
Family: AF_INET,
Prefixlen: 8,
Flags: IFA_F_PERMANENT,
Scope: RT_SCOPE_HOST,
Index: uint32(lo),
}, RtAttr{
Len: uint16(SizeofRtAttr + len(newaddrLo{}.a0)),
Type: IFA_LOCAL,
}, [4]byte{127, 0, 0, 1}, RtAttr{
Len: uint16(SizeofRtAttr + len(newaddrLo{}.a1)),
Type: IFA_ADDRESS,
}, [4]byte{127, 0, 0, 1}}
}
func (msg *newaddrLo) toWireFormat() []byte {
var buf [sizeofNewaddrLo]byte
*(*uint32)(unsafe.Pointer(&buf[0:4][0])) = msg.header.Len
*(*uint16)(unsafe.Pointer(&buf[4:6][0])) = msg.header.Type
*(*uint16)(unsafe.Pointer(&buf[6:8][0])) = msg.header.Flags
*(*uint32)(unsafe.Pointer(&buf[8:12][0])) = msg.header.Seq
*(*uint32)(unsafe.Pointer(&buf[12:16][0])) = msg.header.Pid
buf[16] = msg.data.Family
buf[17] = msg.data.Prefixlen
buf[18] = msg.data.Flags
buf[19] = msg.data.Scope
*(*uint32)(unsafe.Pointer(&buf[20:24][0])) = msg.data.Index
*(*uint16)(unsafe.Pointer(&buf[24:26][0])) = msg.r0.Len
*(*uint16)(unsafe.Pointer(&buf[26:28][0])) = msg.r0.Type
copy(buf[28:32], msg.a0[:])
*(*uint16)(unsafe.Pointer(&buf[32:34][0])) = msg.r1.Len
*(*uint16)(unsafe.Pointer(&buf[34:36][0])) = msg.r1.Type
copy(buf[36:40], msg.a1[:])
return buf[:]
}
// newlinkLo represents a RTM_NEWLINK message.
type newlinkLo struct {
header NlMsghdr
data IfInfomsg
}
// sizeofNewlinkLo is the expected size of newlinkLo.
const sizeofNewlinkLo = NLMSG_HDRLEN + SizeofIfInfomsg
// newlinkLo returns the address of a populated newlinkLo.
func (s *rtnetlink) newlinkLo(lo int) *newlinkLo {
return &newlinkLo{NlMsghdr{
Len: sizeofNewlinkLo,
Type: RTM_NEWLINK,
Flags: NLM_F_REQUEST | NLM_F_ACK,
Seq: s.seq,
Pid: s.pid,
}, IfInfomsg{
Family: AF_UNSPEC,
Index: int32(lo),
Flags: IFF_UP,
Change: IFF_UP,
}}
}
func (msg *newlinkLo) toWireFormat() []byte {
var buf [sizeofNewlinkLo]byte
*(*uint32)(unsafe.Pointer(&buf[0:4][0])) = msg.header.Len
*(*uint16)(unsafe.Pointer(&buf[4:6][0])) = msg.header.Type
*(*uint16)(unsafe.Pointer(&buf[6:8][0])) = msg.header.Flags
*(*uint32)(unsafe.Pointer(&buf[8:12][0])) = msg.header.Seq
*(*uint32)(unsafe.Pointer(&buf[12:16][0])) = msg.header.Pid
buf[16] = msg.data.Family
*(*uint16)(unsafe.Pointer(&buf[18:20][0])) = msg.data.Type
*(*int32)(unsafe.Pointer(&buf[20:24][0])) = msg.data.Index
*(*uint32)(unsafe.Pointer(&buf[24:28][0])) = msg.data.Flags
*(*uint32)(unsafe.Pointer(&buf[28:32][0])) = msg.data.Change
return buf[:]
}
// mustLoopback creates the loopback address and brings the lo interface up.
// mustLoopback calls a fatal method of the underlying [log.Logger] of m with a
// user-facing error message if RTNETLINK behaves unexpectedly.
func mustLoopback(msg message.Msg) {
log := msg.GetLogger()
var lo int
if ifi, err := net.InterfaceByName("lo"); err != nil {
log.Fatalln(err)
} else {
lo = ifi.Index
}
var s rtnetlink
if err := s.open(); err != nil {
log.Fatalln(err)
}
defer func() {
if err := s.close(); err != nil {
msg.Verbosef("cannot close netlink: %v", err)
}
}()
s.mustRoundtrip(msg, s.newaddrLo(lo).toWireFormat())
s.mustRoundtrip(msg, s.newlinkLo(lo).toWireFormat())
}

View File

@@ -1,72 +0,0 @@
package container
import (
"testing"
"unsafe"
)
func TestSizeof(t *testing.T) {
if got := unsafe.Sizeof(newaddrLo{}); got != sizeofNewaddrLo {
t.Fatalf("newaddrLo: sizeof = %#x, want %#x", got, sizeofNewaddrLo)
}
if got := unsafe.Sizeof(newlinkLo{}); got != sizeofNewlinkLo {
t.Fatalf("newlinkLo: sizeof = %#x, want %#x", got, sizeofNewlinkLo)
}
}
func TestRtnetlinkMessage(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
msg interface{ toWireFormat() []byte }
want []byte
}{
{"newaddrLo", (&rtnetlink{pid: 1, seq: 0}).newaddrLo(1), []byte{
/* Len */ 0x28, 0, 0, 0,
/* Type */ 0x14, 0,
/* Flags */ 5, 6,
/* Seq */ 0, 0, 0, 0,
/* Pid */ 1, 0, 0, 0,
/* Family */ 2,
/* Prefixlen */ 8,
/* Flags */ 0x80,
/* Scope */ 0xfe,
/* Index */ 1, 0, 0, 0,
/* Len */ 8, 0,
/* Type */ 2, 0,
/* in_addr */ 127, 0, 0, 1,
/* Len */ 8, 0,
/* Type */ 1, 0,
/* in_addr */ 127, 0, 0, 1,
}},
{"newlinkLo", (&rtnetlink{pid: 1, seq: 1}).newlinkLo(1), []byte{
/* Len */ 0x20, 0, 0, 0,
/* Type */ 0x10, 0,
/* Flags */ 5, 0,
/* Seq */ 1, 0, 0, 0,
/* Pid */ 1, 0, 0, 0,
/* Family */ 0,
/* pad */ 0,
/* Type */ 0, 0,
/* Index */ 1, 0, 0, 0,
/* Flags */ 1, 0, 0, 0,
/* Change */ 1, 0, 0, 0,
}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.msg.toWireFormat(); string(got) != string(tc.want) {
t.Fatalf("toWireFormat: %#v, want %#v", got, tc.want)
}
})
}
}

Some files were not shown because too many files have changed in this diff Show More