1
0
forked from rosa/hakurei

128 Commits

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

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

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

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

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

Closes #30.

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

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

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

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

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-10 00:56:51 +09:00
bd88f10524 internal/rosa/llvm: 22.1.2 to 22.1.3
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-09 17:36:23 +09:00
e34e3b917e internal/kobject: process uevent message
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 16:14:16 +09:00
9e63633fbc container: remove test timeouts
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
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
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 10:45:07 +09:00
b896eec9b7 internal/rosa/gnu: parallel 20260222 to 20260322
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
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
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
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 10:43:37 +09:00
966fd4df9e internal/rosa: connman artifact
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
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
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
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
Required by GnuTLS.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-03 00:00:13 +09:00
c6920e6ab7 cmd/mbf: pick up $TERM
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
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
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
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
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
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-04-02 22:05:07 +09:00
64e5a1068b internal/rosa: libtasn1 artifact
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-27 10:40:27 +09:00
2548a681e9 internal/rosa: key-value type
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
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
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
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
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
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
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
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
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
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 09:56:06 +09:00
f61c6ade56 internal/rosa/nss: 3.121 to 3.122
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
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-26 09:54:44 +09:00
722c3cc54f internal/netlink: optional check header as reply
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
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
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
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
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
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
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
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
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
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-20 16:39:57 +09:00
189 changed files with 16315 additions and 1376 deletions

31
.gitignore vendored
View File

@@ -1,27 +1,7 @@
# Binaries for programs and plugins # produced by tools and text editors
*.exe *.qcow2
*.exe~
*.dll
*.so
*.dylib
*.pkg
/hakurei
# Test binary, built with `go test -c`
*.test *.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
.idea .idea
.vscode .vscode
@@ -30,8 +10,5 @@ go.work.sum
/internal/pkg/testdata/testtool /internal/pkg/testdata/testtool
/internal/rosa/hakurei_current.tar.gz /internal/rosa/hakurei_current.tar.gz
# release # cmd/dist default destination
/dist/hakurei-* /dist
# interactive nixos vm
nixos.qcow2

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

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

View File

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

View File

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

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

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -11,7 +12,6 @@ import (
"strconv" "strconv"
"sync" "sync"
"time" "time"
_ "unsafe" // for go:linkname
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/command" "hakurei.app/command"
@@ -27,13 +27,19 @@ import (
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value // optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
// if it is not nil, or the original value if it is. // if it is not nil, or the original value if it is.
// func optionalErrorUnwrap(err error) error {
//go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
func optionalErrorUnwrap(err error) error return underlyingErr
}
return err
}
var errSuccess = errors.New("success")
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command { func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var ( var (
flagVerbose bool flagVerbose bool
flagInsecure bool
flagJSON bool flagJSON bool
) )
c := command.New(out, log.Printf, "hakurei", func([]string) error { c := command.New(out, log.Printf, "hakurei", func([]string) error {
@@ -52,6 +58,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
return nil return nil
}). }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity"). Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagInsecure, "insecure", command.BoolFlag(false), "Allow use of insecure compatibility options").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable") Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess }) c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
@@ -60,9 +67,9 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
var ( var (
flagIdentifierFile int flagIdentifierFile int
) )
c.NewCommand("app", "Load and start container from configuration file", func(args []string) error { c.NewCommand("run", "Load and start container from configuration file", func(args []string) error {
if len(args) < 1 { if len(args) < 1 {
log.Fatal("app requires at least 1 argument") log.Fatal("run requires at least 1 argument")
} }
config := tryPath(msg, args[0]) config := tryPath(msg, args[0])
@@ -70,7 +77,12 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
config.Container.Args = append(config.Container.Args, args[1:]...) config.Container.Args = append(config.Container.Args, args[1:]...)
} }
outcome.Main(ctx, msg, config, flagIdentifierFile) var flags int
if flagInsecure {
flags |= hst.VAllowInsecure
}
outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
panic("unreachable") panic("unreachable")
}). }).
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1), Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
@@ -98,7 +110,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
) )
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error { c.NewCommand("exec", "Configure and start a permissive container", func(args []string) error {
if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd { if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd {
log.Fatalf("identity %d out of range", flagIdentity) log.Fatalf("identity %d out of range", flagIdentity)
} }
@@ -140,7 +152,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
} }
} }
var et hst.Enablement var et hst.Enablements
if flagWayland { if flagWayland {
et |= hst.EWayland et |= hst.EWayland
} }
@@ -158,7 +170,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
ID: flagID, ID: flagID,
Identity: flagIdentity, Identity: flagIdentity,
Groups: flagGroups, Groups: flagGroups,
Enablements: hst.NewEnablements(et), Enablements: &et,
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
@@ -277,7 +289,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
} }
} }
outcome.Main(ctx, msg, &config, -1) outcome.Main(ctx, msg, &config, 0, -1)
panic("unreachable") panic("unreachable")
}). }).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"), Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
@@ -323,7 +335,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagShort bool flagShort bool
flagNoStore bool flagNoStore bool
) )
c.NewCommand("show", "Show live or local app configuration", func(args []string) error { c.NewCommand("show", "Show live or local instance configuration", func(args []string) error {
switch len(args) { switch len(args) {
case 0: // system case 0: // system
printShowSystem(os.Stdout, flagShort, flagJSON) printShowSystem(os.Stdout, flagShort, flagJSON)

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

@@ -1,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 package main
// minimise imports to avoid inadvertently calling init or global variable functions
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"log" "log"
"os" "os"
"path" "path/filepath"
"runtime" "runtime"
"slices" "slices"
"strconv" "strconv"
@@ -16,10 +67,13 @@ import (
) )
const ( const (
// envIdentity is the name of the environment variable holding a // envShim is the name of the environment variable holding a single byte
// single byte representing the shim setup pipe file descriptor. // representing the shim setup pipe file descriptor.
envShim = "HAKUREI_SHIM" envShim = "HAKUREI_SHIM"
// envGroups holds a ' ' separated list of string representations of // envIdentity is the name of the environment variable holding a decimal
// string representation of the current application identity.
envIdentity = "HAKUREI_IDENTITY"
// envGroups holds a ' ' separated list of decimal string representations of
// supplementary group gid. Membership requirements are enforced. // supplementary group gid. Membership requirements are enforced.
envGroups = "HAKUREI_GROUPS" envGroups = "HAKUREI_GROUPS"
) )
@@ -35,7 +89,6 @@ func main() {
log.SetFlags(0) log.SetFlags(0)
log.SetPrefix("hsu: ") log.SetPrefix("hsu: ")
log.SetOutput(os.Stderr)
if os.Geteuid() != 0 { if os.Geteuid() != 0 {
log.Fatal("this program must be owned by uid 0 and have the setuid bit set") log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
@@ -49,13 +102,13 @@ func main() {
log.Fatal("this program must not be started by root") 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") log.Fatal("this program is compiled incorrectly")
return return
} }
var toolPath string 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 { if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err) log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") { } else if strings.HasSuffix(p, " (deleted)") {
@@ -99,8 +152,6 @@ func main() {
// last possible uid outcome // last possible uid outcome
uidEnd = 999919999 uidEnd = 999919999
) )
// cast to int for use with library functions
uid := int(toUser(userid, identity)) uid := int(toUser(userid, identity))
// final bounds check to catch any bugs // final bounds check to catch any bugs
@@ -136,7 +187,6 @@ func main() {
} }
// careful! users in the allowlist is effectively allowed to drop groups via hsu // careful! users in the allowlist is effectively allowed to drop groups via hsu
if err := syscall.Setresgid(uid, uid, uid); err != nil { if err := syscall.Setresgid(uid, uid, uid); err != nil {
log.Fatalf("cannot set gid: %v", err) log.Fatalf("cannot set gid: %v", err)
} }
@@ -146,10 +196,21 @@ func main() {
if err := syscall.Setresuid(uid, uid, uid); err != nil { if err := syscall.Setresuid(uid, uid, uid); err != nil {
log.Fatalf("cannot set uid: %v", err) log.Fatalf("cannot set uid: %v", err)
} }
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
if _, _, errno := syscall.AllThreadsSyscall(
syscall.SYS_PRCTL,
PR_SET_NO_NEW_PRIVS, 1,
0,
); errno != 0 {
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error()) log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
} }
if err := syscall.Exec(toolPath, []string{"hakurei", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
if err := syscall.Exec(toolPath, []string{
"hakurei",
"shim",
}, []string{
envShim + "=" + shimSetupFd,
}); err != nil {
log.Fatalf("cannot start shim: %v", err) log.Fatalf("cannot start shim: %v", err)
} }

View File

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

View File

@@ -1,3 +1,15 @@
// The mbf program is a frontend for [hakurei.app/internal/rosa].
//
// This program is not covered by the compatibility promise. The command line
// interface, available packages and their behaviour, and even the on-disk
// format, may change at any time.
//
// # Name
//
// The name mbf stands for maiden's best friend, as a tribute to the DOOM source
// port of [the same name]. This name is a placeholder and is subject to change.
//
// [the same name]: https://www.doomwiki.org/wiki/MBF
package main package main
import ( import (
@@ -60,8 +72,9 @@ func main() {
flagQuiet bool flagQuiet bool
flagCures int flagCures int
flagBase string flagBase string
flagTShift int
flagIdle bool flagIdle bool
flagHostAbstract bool
) )
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) { c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) {
msg.SwapVerbose(!flagQuiet) msg.SwapVerbose(!flagQuiet)
@@ -77,19 +90,15 @@ func main() {
} else if base, err = check.NewAbs(flagBase); err != nil { } else if base, err = check.NewAbs(flagBase); err != nil {
return 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 { if flagIdle {
pkg.SetSchedIdle = true flags |= pkg.CSchedIdle
} }
if flagHostAbstract {
flags |= pkg.CHostAbstract
}
cache, err = pkg.Open(ctx, msg, flags, flagCures, base)
return return
}).Flag( }).Flag(
@@ -104,14 +113,17 @@ func main() {
&flagBase, &flagBase,
"d", command.StringFlag("$MBF_CACHE_DIR"), "d", command.StringFlag("$MBF_CACHE_DIR"),
"Directory to store cured artifacts", "Directory to store cured artifacts",
).Flag(
&flagTShift,
"tshift", command.IntFlag(-1),
"Dependency graph size exponent, to the power of 2",
).Flag( ).Flag(
&flagIdle, &flagIdle,
"sched-idle", command.BoolFlag(false), "sched-idle", command.BoolFlag(false),
"Set SCHED_IDLE scheduling policy", "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",
) )
{ {
@@ -436,6 +448,7 @@ func main() {
{ {
var ( var (
flagDump string flagDump string
flagEnter bool
flagExport string flagExport string
) )
c.NewCommand( c.NewCommand(
@@ -445,9 +458,13 @@ func main() {
if len(args) != 1 { if len(args) != 1 {
return errors.New("cure requires 1 argument") return errors.New("cure requires 1 argument")
} }
if p, ok := rosa.ResolveName(args[0]); !ok { p, ok := rosa.ResolveName(args[0])
if !ok {
return fmt.Errorf("unknown artifact %q", args[0]) return fmt.Errorf("unknown artifact %q", args[0])
} else if flagDump == "" { }
switch {
default:
pathname, _, err := cache.Cure(rosa.Std.Load(p)) pathname, _, err := cache.Cure(rosa.Std.Load(p))
if err != nil { if err != nil {
return err return err
@@ -477,7 +494,8 @@ func main() {
} }
return nil return nil
} else {
case flagDump != "":
f, err := os.OpenFile( f, err := os.OpenFile(
flagDump, flagDump,
os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.O_WRONLY|os.O_CREATE|os.O_EXCL,
@@ -493,6 +511,15 @@ func main() {
} }
return f.Close() return f.Close()
case flagEnter:
return cache.EnterExec(
ctx,
rosa.Std.Load(p),
true, os.Stdin, os.Stdout, os.Stderr,
rosa.AbsSystem.Append("bin", "mksh"),
"sh",
)
} }
}, },
). ).
@@ -505,6 +532,11 @@ func main() {
&flagExport, &flagExport,
"export", command.StringFlag(""), "export", command.StringFlag(""),
"Export cured artifact to specified pathname", "Export cured artifact to specified pathname",
).
Flag(
&flagEnter,
"enter", command.BoolFlag(false),
"Enter cure container with an interactive shell",
) )
} }
@@ -527,7 +559,7 @@ func main() {
} }
presets[i] = p presets[i] = p
} }
root := make(rosa.Collect, 0, 6+len(args)) root := make(pkg.Collect, 0, 6+len(args))
root = rosa.Std.AppendPresets(root, presets...) root = rosa.Std.AppendPresets(root, presets...)
if flagWithToolchain { if flagWithToolchain {
@@ -543,7 +575,7 @@ func main() {
if _, _, err := cache.Cure(&root); err == nil { if _, _, err := cache.Cure(&root); err == nil {
return errors.New("unreachable") return errors.New("unreachable")
} else if !errors.Is(err, rosa.Collected{}) { } else if !pkg.IsCollected(err) {
return err return err
} }
@@ -586,6 +618,9 @@ func main() {
z.Hostname = "localhost" z.Hostname = "localhost"
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1 z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr 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 var tempdir *check.Absolute
if s, err := filepath.Abs(os.TempDir()); err != nil { if s, err := filepath.Abs(os.TempDir()); err != nil {
@@ -636,13 +671,13 @@ func main() {
). ).
Flag( Flag(
&flagSession, &flagSession,
"session", command.BoolFlag(false), "session", command.BoolFlag(true),
"Retain session", "Retain session",
). ).
Flag( Flag(
&flagWithToolchain, &flagWithToolchain,
"with-toolchain", command.BoolFlag(false), "with-toolchain", command.BoolFlag(false),
"Include the stage3 LLVM toolchain", "Include the stage2 LLVM toolchain",
) )
} }

View File

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

View File

@@ -19,12 +19,11 @@ import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path" "path/filepath"
"runtime" "runtime"
"runtime/cgo" "runtime/cgo"
"strconv" "strconv"
@@ -85,7 +84,10 @@ func destroySetup(private_data unsafe.Pointer) (ok bool) {
} }
//export sharefs_init //export sharefs_init
func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.Pointer { func sharefs_init(
_ *C.struct_fuse_conn_info,
cfg *C.struct_fuse_config,
) unsafe.Pointer {
ctx := C.fuse_get_context() ctx := C.fuse_get_context()
priv := (*C.struct_sharefs_private)(ctx.private_data) priv := (*C.struct_sharefs_private)(ctx.private_data)
setup := cgo.Handle(priv.setup).Value().(*setupState) setup := cgo.Handle(priv.setup).Value().(*setupState)
@@ -103,7 +105,11 @@ func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.
cfg.negative_timeout = 0 cfg.negative_timeout = 0
// all future filesystem operations happen through this dirfd // all future filesystem operations happen through this dirfd
if fd, err := syscall.Open(setup.Source.String(), syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC, 0); err != nil { if fd, err := syscall.Open(
setup.Source.String(),
syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC,
0,
); err != nil {
log.Printf("cannot open %q: %v", setup.Source, err) log.Printf("cannot open %q: %v", setup.Source, err)
goto fail goto fail
} else if err = syscall.Fchdir(fd); err != nil { } else if err = syscall.Fchdir(fd); err != nil {
@@ -138,9 +144,9 @@ func sharefs_destroy(private_data unsafe.Pointer) {
func showHelp(args *fuseArgs) { func showHelp(args *fuseArgs) {
executableName := sharefsName executableName := sharefsName
if args.argc > 0 { 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 { } 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) 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. // Decimal string representation of gid to set when running as root.
setgid *C.char setgid *C.char
// Decimal string representation of open file descriptor to read setupState from. // Decimal string representation of open file descriptor to read
// This is an internal detail for containerisation and must not be specified directly. // setupState from.
//
// This is an internal detail for containerisation and must not be
// specified directly.
setup *C.char setup *C.char
} }
@@ -253,7 +262,8 @@ func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
return true return true
} }
// copyArgs returns a heap allocated copy of an argument slice in fuse_args representation. // copyArgs returns a heap allocated copy of an argument slice in fuse_args
// representation.
func copyArgs(s ...string) fuseArgs { func copyArgs(s ...string) fuseArgs {
if len(s) == 0 { if len(s) == 0 {
return fuseArgs{argc: 0, argv: nil, allocated: 0} return fuseArgs{argc: 0, argv: nil, allocated: 0}
@@ -269,6 +279,7 @@ func copyArgs(s ...string) fuseArgs {
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) } func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }
// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg. // unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
//
// The last byte of arg must be 0. // The last byte of arg must be 0.
func unsafeAddArgument(args *fuseArgs, arg string) { func unsafeAddArgument(args *fuseArgs, arg string) {
C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg)))) C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
@@ -288,8 +299,8 @@ func _main(s ...string) (exitCode int) {
args := copyArgs(s...) args := copyArgs(s...)
defer freeArgs(&args) defer freeArgs(&args)
// this causes the kernel to enforce access control based on // this causes the kernel to enforce access control based on struct stat
// struct stat populated by sharefs_getattr // populated by sharefs_getattr
unsafeAddArgument(&args, "-odefault_permissions\x00") unsafeAddArgument(&args, "-odefault_permissions\x00")
var priv C.struct_sharefs_private var priv C.struct_sharefs_private
@@ -453,15 +464,19 @@ func _main(s ...string) (exitCode int) {
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
} }
z.Bind(z.Path, z.Path, 0) z.Bind(z.Path, z.Path, 0)
setup.Fuse = int(proc.ExtraFileSlice(&z.ExtraFiles, os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"))) setup.Fuse = int(proc.ExtraFileSlice(
&z.ExtraFiles,
os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"),
))
var setupWriter io.WriteCloser var setupPipe [2]*os.File
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil { if r, w, err := os.Pipe(); err != nil {
log.Println(err) log.Println(err)
return 5 return 5
} else { } else {
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd)) z.Args = append(z.Args, "-osetup="+strconv.Itoa(3+len(z.ExtraFiles)))
setupWriter = w z.ExtraFiles = append(z.ExtraFiles, r)
setupPipe[0], setupPipe[1] = r, w
} }
if err := z.Start(); err != nil { if err := z.Start(); err != nil {
@@ -472,6 +487,9 @@ func _main(s ...string) (exitCode int) {
} }
return 5 return 5
} }
if err := setupPipe[0].Close(); err != nil {
log.Println(err)
}
if err := z.Serve(); err != nil { if err := z.Serve(); err != nil {
if m, ok := message.GetMessage(err); ok { if m, ok := message.GetMessage(err); ok {
log.Println(m) log.Println(m)
@@ -481,10 +499,10 @@ func _main(s ...string) (exitCode int) {
return 5 return 5
} }
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil { if err := gob.NewEncoder(setupPipe[1]).Encode(&setup); err != nil {
log.Println(err) log.Println(err)
return 5 return 5
} else if err = setupWriter.Close(); err != nil { } else if err = setupPipe[1].Close(); err != nil {
log.Println(err) log.Println(err)
} }

View File

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

View File

@@ -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

@@ -21,6 +21,7 @@ import (
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/internal/landlock"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -28,9 +29,6 @@ const (
// CancelSignal is the signal expected by container init on context cancel. // CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal. // A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGUSR2 CancelSignal = SIGUSR2
// Timeout for writing initParams to Container.setup.
initSetupTimeout = 5 * time.Second
) )
type ( type (
@@ -53,7 +51,7 @@ type (
ExtraFiles []*os.File ExtraFiles []*os.File
// Write end of a pipe connected to the init to deliver [Params]. // 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. // Cancels the context passed to the underlying cmd.
cancel context.CancelFunc cancel context.CancelFunc
// Closed after Wait returns. Keeps the spawning thread alive. // Closed after Wait returns. Keeps the spawning thread alive.
@@ -287,14 +285,16 @@ func (p *Container) Start() error {
} }
// place setup pipe before user supplied extra files, this is later restored by init // 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{ return &StartError{
Fatal: true, Fatal: true,
Step: "set up params stream", Step: "set up params stream",
Err: err, Err: err,
} }
} else { } 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.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
} }
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...) p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
@@ -308,7 +308,7 @@ func (p *Container) Start() error {
done <- func() error { done <- func() error {
// PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes // PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes
// created from the calling thread // created from the calling thread
if err := SetNoNewPrivs(); err != nil { if err := setNoNewPrivs(); err != nil {
return &StartError{ return &StartError{
Fatal: true, Fatal: true,
Step: "prctl(PR_SET_NO_NEW_PRIVS)", Step: "prctl(PR_SET_NO_NEW_PRIVS)",
@@ -318,15 +318,17 @@ func (p *Container) Start() error {
// landlock: depends on per-thread state but acts on a process group // landlock: depends on per-thread state but acts on a process group
{ {
rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL} rulesetAttr := &landlock.RulesetAttr{
Scoped: landlock.LANDLOCK_SCOPE_SIGNAL,
}
if !p.HostAbstract { if !p.HostAbstract {
rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET rulesetAttr.Scoped |= landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
} }
if abi, err := LandlockGetABI(); err != nil { if abi, err := landlock.GetABI(); err != nil {
if p.HostAbstract { if p.HostAbstract || !p.HostNet {
// landlock can be skipped here as it restricts access // landlock can be skipped here as it restricts access
// to resources already covered by namespaces (pid) // to resources already covered by namespaces (pid, net)
goto landlockOut goto landlockOut
} }
return &StartError{Step: "get landlock ABI", Err: err} return &StartError{Step: "get landlock ABI", Err: err}
@@ -352,7 +354,7 @@ func (p *Container) Start() error {
} }
} else { } else {
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr) p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil { if err = landlock.RestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd) _ = Close(rulesetFd)
return &StartError{ return &StartError{
Fatal: true, Fatal: true,
@@ -428,24 +430,33 @@ func (p *Container) Start() error {
// Serve serves [Container.Params] to the container init. // Serve serves [Container.Params] to the container init.
// //
// Serve must only be called once. // Serve must only be called once.
func (p *Container) Serve() error { func (p *Container) Serve() (err error) {
if p.setup == nil { if p.setup[0] == nil || p.setup[1] == nil {
panic("invalid serve") panic("invalid serve")
} }
setup := p.setup done := make(chan struct{})
p.setup = nil defer func() {
if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil { 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{ return &StartError{
Fatal: true, Fatal: true,
Step: "set init pipe deadline", Step: "close read end of init pipe",
Err: err, Err: err,
Passthrough: true, Passthrough: true,
} }
} }
if p.Path == nil { if p.Path == nil {
p.cancel()
return &StartError{ return &StartError{
Step: "invalid executable pathname", Step: "invalid executable pathname",
Err: EINVAL, Err: EINVAL,
@@ -461,18 +472,27 @@ func (p *Container) Serve() error {
p.SeccompRules = make([]std.NativeRule, 0) 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, p.Params,
Getuid(), Getuid(),
Getgid(), Getgid(),
len(p.ExtraFiles), len(p.ExtraFiles),
p.msg.IsVerbose(), p.msg.IsVerbose(),
}) })
_ = setup.Close()
if err != nil {
p.cancel()
}
return err
} }
// Wait blocks until the container init process to exit and releases any // Wait blocks until the container init process to exit and releases any

View File

@@ -16,7 +16,6 @@ import (
"strings" "strings"
"syscall" "syscall"
"testing" "testing"
"time"
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/command" "hakurei.app/command"
@@ -26,6 +25,9 @@ import (
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/info"
"hakurei.app/internal/landlock"
"hakurei.app/internal/params"
"hakurei.app/ldd" "hakurei.app/ldd"
"hakurei.app/message" "hakurei.app/message"
"hakurei.app/vfs" "hakurei.app/vfs"
@@ -84,9 +86,9 @@ func TestStartError(t *testing.T) {
{"params env", &container.StartError{ {"params env", &container.StartError{
Fatal: true, Fatal: true,
Step: "set up params stream", Step: "set up params stream",
Err: container.ErrReceiveEnv, Err: params.ErrReceiveEnv,
}, "set up params stream: environment variable not set", }, "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"}, "cannot set up params stream: environment variable not set"},
{"params", &container.StartError{ {"params", &container.StartError{
@@ -436,11 +438,8 @@ func TestContainer(t *testing.T) {
wantOps, wantOpsCtx := tc.ops(t) wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx) wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel()
var libPaths []*check.Absolute 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.Uid = tc.uid
c.Gid = tc.gid c.Gid = tc.gid
c.Hostname = hostnameFromTestCase(tc.name) c.Hostname = hostnameFromTestCase(tc.name)
@@ -450,7 +449,6 @@ func TestContainer(t *testing.T) {
} else { } else {
c.Stdout, c.Stderr = os.Stdout, os.Stderr c.Stdout, c.Stderr = os.Stdout, os.Stderr
} }
c.WaitDelay = helperDefaultTimeout
*c.Ops = append(*c.Ops, *wantOps...) *c.Ops = append(*c.Ops, *wantOps...)
c.SeccompRules = tc.rules c.SeccompRules = tc.rules
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
@@ -458,6 +456,15 @@ func TestContainer(t *testing.T) {
c.SeccompDisable = !tc.filter c.SeccompDisable = !tc.filter
c.RetainSession = tc.session c.RetainSession = tc.session
c.HostNet = tc.net c.HostNet = tc.net
if info.CanDegrade {
if _, err := landlock.GetABI(); 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. c.
Readonly(check.MustAbs(pathReadonly), 0755). Readonly(check.MustAbs(pathReadonly), 0755).
@@ -553,11 +560,10 @@ func testContainerCancel(
) func(t *testing.T) { ) func(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
t.Parallel() t.Parallel()
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) ctx, cancel := context.WithCancel(t.Context())
c := helperNewContainer(ctx, "block") c := helperNewContainer(ctx, "block")
c.Stdout, c.Stderr = os.Stdout, os.Stderr c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.WaitDelay = helperDefaultTimeout
if containerExtra != nil { if containerExtra != nil {
containerExtra(c) containerExtra(c)
} }
@@ -738,7 +744,6 @@ func init() {
const ( const (
envDoCheck = "HAKUREI_TEST_DO_CHECK" envDoCheck = "HAKUREI_TEST_DO_CHECK"
helperDefaultTimeout = 5 * time.Second
helperInnerPath = "/usr/bin/helper" helperInnerPath = "/usr/bin/helper"
) )

View File

@@ -1,6 +1,7 @@
package container package container
import ( import (
"context"
"io" "io"
"io/fs" "io/fs"
"net" "net"
@@ -15,6 +16,7 @@ import (
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/internal/netlink" "hakurei.app/internal/netlink"
"hakurei.app/internal/params"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -55,7 +57,7 @@ type syscallDispatcher interface {
// isatty provides [Isatty]. // isatty provides [Isatty].
isatty(fd int) bool isatty(fd int) bool
// receive provides [Receive]. // 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 provides procPaths.bindMount.
bindMount(msg message.Msg, source, target string, flags uintptr) error bindMount(msg message.Msg, source, target string, flags uintptr) error
@@ -66,7 +68,7 @@ type syscallDispatcher interface {
// ensureFile provides ensureFile. // ensureFile provides ensureFile.
ensureFile(name string, perm, pperm os.FileMode) error ensureFile(name string, perm, pperm os.FileMode) error
// mustLoopback provides mustLoopback. // mustLoopback provides mustLoopback.
mustLoopback(msg message.Msg) mustLoopback(ctx context.Context, msg message.Msg)
// seccompLoad provides [seccomp.Load]. // seccompLoad provides [seccomp.Load].
seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error
@@ -146,7 +148,7 @@ func (direct) lockOSThread() { runtime.LockOSThread() }
func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) } func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) } func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() } func (direct) setNoNewPrivs() error { return setNoNewPrivs() }
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) } func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) } func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
@@ -154,8 +156,8 @@ func (direct) capBoundingSetDrop(cap uintptr) error { return capBound
func (direct) capAmbientClearAll() error { return capAmbientClearAll() } func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) } func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
func (direct) isatty(fd int) bool { return ext.Isatty(fd) } func (direct) isatty(fd int) bool { return ext.Isatty(fd) }
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) { func (direct) receive(key string, e any, fdp *int) (func() error, error) {
return Receive(key, e, fdp) return params.Receive(key, e, fdp)
} }
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error { func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
@@ -170,7 +172,7 @@ func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm
func (direct) ensureFile(name string, perm, pperm os.FileMode) error { func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
return ensureFile(name, perm, pperm) return ensureFile(name, perm, pperm)
} }
func (direct) mustLoopback(msg message.Msg) { func (direct) mustLoopback(ctx context.Context, msg message.Msg) {
var lo int var lo int
if ifi, err := net.InterfaceByName("lo"); err != nil { if ifi, err := net.InterfaceByName("lo"); err != nil {
msg.GetLogger().Fatalln(err) msg.GetLogger().Fatalln(err)
@@ -178,7 +180,7 @@ func (direct) mustLoopback(msg message.Msg) {
lo = ifi.Index lo = ifi.Index
} }
c, err := netlink.DialRoute() c, err := netlink.DialRoute(0)
if err != nil { if err != nil {
msg.GetLogger().Fatalln(err) msg.GetLogger().Fatalln(err)
} }
@@ -199,11 +201,14 @@ func (direct) mustLoopback(msg message.Msg) {
msg.GetLogger().Fatalf("RTNETLINK answers: %v", err) msg.GetLogger().Fatalf("RTNETLINK answers: %v", err)
default: default:
msg.GetLogger().Fatalf("RTNETLINK answers with malformed message") if err == context.DeadlineExceeded || err == context.Canceled {
msg.GetLogger().Fatalf("interrupted RTNETLINK operation")
}
msg.GetLogger().Fatal("RTNETLINK answers with malformed message")
} }
} }
must(c.SendNewaddrLo(uint32(lo))) must(c.SendNewaddrLo(ctx, uint32(lo)))
must(c.SendIfInfomsg(syscall.RTM_NEWLINK, 0, &syscall.IfInfomsg{ must(c.SendIfInfomsg(ctx, syscall.RTM_NEWLINK, 0, &syscall.IfInfomsg{
Family: syscall.AF_UNSPEC, Family: syscall.AF_UNSPEC,
Index: int32(lo), Index: int32(lo),
Flags: syscall.IFF_UP, Flags: syscall.IFF_UP,

View File

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

View File

@@ -7,7 +7,8 @@ import (
"log" "log"
"os" "os"
"os/exec" "os/exec"
"path" "os/signal"
"path/filepath"
"slices" "slices"
"strconv" "strconv"
"sync" "sync"
@@ -18,6 +19,7 @@ import (
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/internal/params"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -146,36 +148,38 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
} }
var ( var (
params initParams param initParams
closeSetup func() error closeSetup func() error
setupFd uintptr setupFd int
offsetSetup 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) { if errors.Is(err, EBADF) {
k.fatal(msg, "invalid setup descriptor") k.fatal(msg, "invalid setup descriptor")
} }
if errors.Is(err, ErrReceiveEnv) { if errors.Is(err, params.ErrReceiveEnv) {
k.fatal(msg, setupEnv+" not set") k.fatal(msg, setupEnv+" not set")
} }
k.fatalf(msg, "cannot decode init setup payload: %v", err) k.fatalf(msg, "cannot decode init setup payload: %v", err)
} else { } else {
if params.Ops == nil { if param.Ops == nil {
k.fatal(msg, "invalid setup parameters") k.fatal(msg, "invalid setup parameters")
} }
if params.ParentPerm == 0 { if param.ParentPerm == 0 {
params.ParentPerm = 0755 param.ParentPerm = 0755
} }
msg.SwapVerbose(params.Verbose) msg.SwapVerbose(param.Verbose)
msg.Verbose("received setup parameters") msg.Verbose("received setup parameters")
closeSetup = f closeSetup = f
offsetSetup = int(setupFd + 1)
} }
if !params.HostNet { if !param.HostNet {
k.mustLoopback(msg) ctx, cancel := signal.NotifyContext(context.Background(), CancelSignal,
os.Interrupt, SIGTERM, SIGQUIT)
defer cancel() // for panics
k.mustLoopback(ctx, msg)
cancel()
} }
// write uid/gid map here so parent does not need to set dumpable // write uid/gid map here so parent does not need to set dumpable
@@ -183,7 +187,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err) k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
} }
if err := k.writeFile(fhs.Proc+"self/uid_map", 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 { 0); err != nil {
k.fatalf(msg, "%v", err) k.fatalf(msg, "%v", err)
} }
@@ -193,7 +197,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "%v", err) k.fatalf(msg, "%v", err)
} }
if err := k.writeFile(fhs.Proc+"self/gid_map", 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 { 0); err != nil {
k.fatalf(msg, "%v", err) k.fatalf(msg, "%v", err)
} }
@@ -202,8 +206,8 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
} }
oldmask := k.umask(0) oldmask := k.umask(0)
if params.Hostname != "" { if param.Hostname != "" {
if err := k.sethostname([]byte(params.Hostname)); err != nil { if err := k.sethostname([]byte(param.Hostname)); err != nil {
k.fatalf(msg, "cannot set hostname: %v", err) k.fatalf(msg, "cannot set hostname: %v", err)
} }
} }
@@ -216,7 +220,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
} }
ctx, cancel := context.WithCancel(context.Background()) 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() defer cancel()
/* early is called right before pivot_root into intermediate root; /* early is called right before pivot_root into intermediate root;
@@ -224,7 +228,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
difficult to obtain via library functions after pivot_root, and difficult to obtain via library functions after pivot_root, and
implementations are expected to avoid changing the state of the mount implementations are expected to avoid changing the state of the mount
namespace */ namespace */
for i, op := range *params.Ops { for i, op := range *param.Ops {
if op == nil || !op.Valid() { if op == nil || !op.Valid() {
k.fatalf(msg, "invalid op at index %d", i) k.fatalf(msg, "invalid op at index %d", i)
} }
@@ -267,7 +271,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
step sets up the container filesystem, and implementations are expected to step sets up the container filesystem, and implementations are expected to
keep the host root and sysroot mount points intact but otherwise can do keep the host root and sysroot mount points intact but otherwise can do
whatever they need to. Calling chdir is allowed but discouraged. */ 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 // ops already checked during early setup
if prefix, ok := op.prefix(); ok { if prefix, ok := op.prefix(); ok {
msg.Verbosef("%s %s", prefix, op) msg.Verbosef("%s %s", prefix, op)
@@ -323,7 +327,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot clear the ambient capability set: %v", err) k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
} }
for i := uintptr(0); i <= lastcap; i++ { for i := uintptr(0); i <= lastcap; i++ {
if params.Privileged && i == CAP_SYS_ADMIN { if param.Privileged && i == CAP_SYS_ADMIN {
continue continue
} }
if err := k.capBoundingSetDrop(i); err != nil { if err := k.capBoundingSetDrop(i); err != nil {
@@ -332,7 +336,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
} }
var keep [2]uint32 var keep [2]uint32
if params.Privileged { if param.Privileged {
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN) keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil { if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
@@ -346,13 +350,13 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot capset: %v", err) k.fatalf(msg, "cannot capset: %v", err)
} }
if !params.SeccompDisable { if !param.SeccompDisable {
rules := params.SeccompRules rules := param.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets if len(rules) == 0 { // non-empty rules slice always overrides presets
msg.Verbosef("resolving presets %#x", params.SeccompPresets) msg.Verbosef("resolving presets %#x", param.SeccompPresets)
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags) 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 // this also indirectly asserts PR_SET_NO_NEW_PRIVS
k.fatalf(msg, "cannot load syscall filter: %v", err) k.fatalf(msg, "cannot load syscall filter: %v", err)
} }
@@ -361,10 +365,10 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
msg.Verbose("syscall filter not configured") msg.Verbose("syscall filter not configured")
} }
extraFiles := make([]*os.File, params.Count) extraFiles := make([]*os.File, param.Count)
for i := range extraFiles { for i := range extraFiles {
// setup fd is placed before all extra files // 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) k.umask(oldmask)
@@ -442,7 +446,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
// called right before startup of initial process, all state changes to the // called right before startup of initial process, all state changes to the
// current process is prohibited during late // current process is prohibited during late
for i, op := range *params.Ops { for i, op := range *param.Ops {
// ops already checked during early setup // ops already checked during early setup
if err := op.late(state, k); err != nil { if err := op.late(state, k); err != nil {
if m, ok := messageFromError(err); ok { if m, ok := messageFromError(err); ok {
@@ -463,14 +467,14 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot close setup pipe: %v", err) 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.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = params.Args cmd.Args = param.Args
cmd.Env = params.Env cmd.Env = param.Env
cmd.ExtraFiles = extraFiles 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 { if err := k.start(cmd); err != nil {
k.fatalf(msg, "%v", err) k.fatalf(msg, "%v", err)
} }
@@ -488,9 +492,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
for { for {
select { select {
case s := <-sig: case s := <-sig:
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil { if s == CancelSignal && param.ForwardCancel && cmd.Process != nil {
msg.Verbose("forwarding context cancellation") 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) k.printf(msg, "cannot forward cancellation: %v", err)
} }
continue continue
@@ -520,7 +524,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
cancel() cancel()
// start timeout early // 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 // close initial process files; this also keeps them alive
for _, f := range extraFiles { for _, f := range extraFiles {
@@ -564,7 +568,7 @@ func TryArgv0(msg message.Msg) {
msg = message.New(log.Default()) 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) Init(msg)
msg.BeforeExit() msg.BeforeExit()
os.Exit(0) os.Exit(0)

View File

@@ -10,6 +10,7 @@ import (
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/internal/params"
"hakurei.app/internal/stub" "hakurei.app/internal/stub"
) )
@@ -40,7 +41,7 @@ func TestInitEntrypoint(t *testing.T) {
call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil), call("getpid", stub.ExpectArgs{}, 1, nil),
call("setPtracer", stub.ExpectArgs{uintptr(0)}, nil, 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), call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SETUP not set"}}, nil, nil),
}, },
}, nil}, }, nil},

View File

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

View File

@@ -3,7 +3,7 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"path" "path/filepath"
"hakurei.app/check" "hakurei.app/check"
) )
@@ -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 { func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
if l.Dereference { if l.Dereference {
if !path.IsAbs(l.LinkName) { if !filepath.IsAbs(l.LinkName) {
return check.AbsoluteError(l.LinkName) return check.AbsoluteError(l.LinkName)
} }
if name, err := k.readlink(l.LinkName); err != nil { 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 { func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
target := toSysroot(l.Target.String()) 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 err
} }
return k.symlink(l.LinkName, target) return k.symlink(l.LinkName, target)

View File

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

View File

@@ -1,47 +0,0 @@
package container
import (
"encoding/gob"
"errors"
"os"
"strconv"
"syscall"
)
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
func Setup(extraFiles *[]*os.File) (int, *os.File, error) {
if r, w, err := os.Pipe(); err != nil {
return -1, nil, err
} else {
fd := 3 + len(*extraFiles)
*extraFiles = append(*extraFiles, r)
return fd, w, nil
}
}
var (
ErrReceiveEnv = errors.New("environment variable not set")
)
// Receive retrieves setup fd from the environment and receives params.
func Receive(key string, e any, fdp *uintptr) (func() error, error) {
var setup *os.File
if s, ok := os.LookupEnv(key); !ok {
return nil, ErrReceiveEnv
} else {
if fd, err := strconv.Atoi(s); err != nil {
return nil, optionalErrorUnwrap(err)
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
return nil, syscall.EDOM
}
if fdp != nil {
*fdp = setup.Fd()
}
}
}
return setup.Close, gob.NewDecoder(setup).Decode(e)
}

View File

@@ -4,7 +4,7 @@ import (
"errors" "errors"
"io/fs" "io/fs"
"os" "os"
"path" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@@ -29,16 +29,16 @@ const (
func toSysroot(name string) string { func toSysroot(name string) string {
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' }) name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
return path.Join(sysrootPath, name) return filepath.Join(sysrootPath, name)
} }
func toHost(name string) string { func toHost(name string) string {
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' }) name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
return path.Join(hostPath, name) return filepath.Join(hostPath, name)
} }
func createFile(name string, perm, pperm os.FileMode, content []byte) error { func createFile(name string, perm, pperm os.FileMode, content []byte) error {
if err := os.MkdirAll(path.Dir(name), pperm); err != nil { if err := os.MkdirAll(filepath.Dir(name), pperm); err != nil {
return err return err
} }
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm) f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)

View File

@@ -4,7 +4,7 @@ import (
"io" "io"
"math" "math"
"os" "os"
"path" "path/filepath"
"reflect" "reflect"
"syscall" "syscall"
"testing" "testing"
@@ -61,7 +61,7 @@ func TestCreateFile(t *testing.T) {
Path: "/proc/nonexistent", Path: "/proc/nonexistent",
Err: syscall.ENOENT, Err: syscall.ENOENT,
} }
if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) { if err := createFile(filepath.Join(Nonexistent, ":3"), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
t.Errorf("createFile: error = %#v, want %#v", err, wantErr) t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
} }
}) })
@@ -72,7 +72,7 @@ func TestCreateFile(t *testing.T) {
Path: "/proc/nonexistent", Path: "/proc/nonexistent",
Err: syscall.ENOENT, Err: syscall.ENOENT,
} }
if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) { if err := createFile(filepath.Join(Nonexistent), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
t.Errorf("createFile: error = %#v, want %#v", err, wantErr) t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
} }
}) })
@@ -80,7 +80,7 @@ func TestCreateFile(t *testing.T) {
t.Run("touch", func(t *testing.T) { t.Run("touch", func(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
pathname := path.Join(tempDir, "empty") pathname := filepath.Join(tempDir, "empty")
if err := createFile(pathname, 0644, 0755, nil); err != nil { if err := createFile(pathname, 0644, 0755, nil); err != nil {
t.Fatalf("createFile: error = %v", err) t.Fatalf("createFile: error = %v", err)
} }
@@ -93,7 +93,7 @@ func TestCreateFile(t *testing.T) {
t.Run("write", func(t *testing.T) { t.Run("write", func(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
pathname := path.Join(tempDir, "zero") pathname := filepath.Join(tempDir, "zero")
if err := createFile(pathname, 0644, 0755, []byte{0}); err != nil { if err := createFile(pathname, 0644, 0755, []byte{0}); err != nil {
t.Fatalf("createFile: error = %v", err) t.Fatalf("createFile: error = %v", err)
} }
@@ -107,7 +107,7 @@ func TestCreateFile(t *testing.T) {
func TestEnsureFile(t *testing.T) { func TestEnsureFile(t *testing.T) {
t.Run("create", func(t *testing.T) { t.Run("create", func(t *testing.T) {
if err := ensureFile(path.Join(t.TempDir(), "ensure"), 0644, 0755); err != nil { if err := ensureFile(filepath.Join(t.TempDir(), "ensure"), 0644, 0755); err != nil {
t.Errorf("ensureFile: error = %v", err) t.Errorf("ensureFile: error = %v", err)
} }
}) })
@@ -115,7 +115,7 @@ func TestEnsureFile(t *testing.T) {
t.Run("stat", func(t *testing.T) { t.Run("stat", func(t *testing.T) {
t.Run("inaccessible", func(t *testing.T) { t.Run("inaccessible", func(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
pathname := path.Join(tempDir, "inaccessible") pathname := filepath.Join(tempDir, "inaccessible")
if f, err := os.Create(pathname); err != nil { if f, err := os.Create(pathname); err != nil {
t.Fatalf("Create: error = %v", err) t.Fatalf("Create: error = %v", err)
} else { } else {
@@ -150,7 +150,7 @@ func TestEnsureFile(t *testing.T) {
t.Run("ensure", func(t *testing.T) { t.Run("ensure", func(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
pathname := path.Join(tempDir, "ensure") pathname := filepath.Join(tempDir, "ensure")
if f, err := os.Create(pathname); err != nil { if f, err := os.Create(pathname); err != nil {
t.Fatalf("Create: error = %v", err) t.Fatalf("Create: error = %v", err)
} else { } else {
@@ -195,12 +195,12 @@ func TestProcPaths(t *testing.T) {
t.Run("sample", func(t *testing.T) { t.Run("sample", func(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
if err := os.MkdirAll(path.Join(tempDir, "proc/self"), 0755); err != nil { if err := os.MkdirAll(filepath.Join(tempDir, "proc/self"), 0755); err != nil {
t.Fatalf("MkdirAll: error = %v", err) t.Fatalf("MkdirAll: error = %v", err)
} }
t.Run("clean", func(t *testing.T) { t.Run("clean", func(t *testing.T) {
if err := os.WriteFile(path.Join(tempDir, "proc/self/mountinfo"), []byte(`15 20 0:3 / /proc rw,relatime - proc /proc rw if err := os.WriteFile(filepath.Join(tempDir, "proc/self/mountinfo"), []byte(`15 20 0:3 / /proc rw,relatime - proc /proc rw
16 20 0:15 / /sys rw,relatime - sysfs /sys rw 16 20 0:15 / /sys rw,relatime - sysfs /sys rw
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755`), 0644); err != nil { 17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755`), 0644); err != nil {
t.Fatalf("WriteFile: error = %v", err) t.Fatalf("WriteFile: error = %v", err)
@@ -243,8 +243,8 @@ func TestProcPaths(t *testing.T) {
}) })
t.Run("malformed", func(t *testing.T) { t.Run("malformed", func(t *testing.T) {
path.Join(tempDir, "proc/self/mountinfo") filepath.Join(tempDir, "proc/self/mountinfo")
if err := os.WriteFile(path.Join(tempDir, "proc/self/mountinfo"), []byte{0}, 0644); err != nil { if err := os.WriteFile(filepath.Join(tempDir, "proc/self/mountinfo"), []byte{0}, 0644); err != nil {
t.Fatalf("WriteFile: error = %v", err) t.Fatalf("WriteFile: error = %v", err)
} }

View File

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

1
dist/hsurc.default vendored
View File

@@ -1 +0,0 @@
1000 0

12
dist/install.sh vendored
View File

@@ -1,12 +0,0 @@
#!/bin/sh
cd "$(dirname -- "$0")" || exit 1
install -vDm0755 "bin/hakurei" "${DESTDIR}/usr/bin/hakurei"
install -vDm0755 "bin/sharefs" "${DESTDIR}/usr/bin/sharefs"
install -vDm4511 "bin/hsu" "${DESTDIR}/usr/bin/hsu"
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
fi
install -vDm0644 "comp/_hakurei" "${DESTDIR}/usr/share/zsh/site-functions/_hakurei"

31
dist/release.sh vendored
View File

@@ -1,31 +0,0 @@
#!/bin/sh -e
cd "$(dirname -- "$0")/.."
VERSION="${HAKUREI_VERSION:-untagged}"
pname="hakurei-${VERSION}-$(go env GOARCH)"
out="${DESTDIR:-dist}/${pname}"
echo '# Preparing distribution files.'
mkdir -p "${out}"
cp -v "README.md" "dist/hsurc.default" "dist/install.sh" "${out}"
cp -rv "dist/comp" "${out}"
echo
echo '# Building hakurei.'
go generate ./...
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w
-buildid= -linkmode external -extldflags=-static
-X hakurei.app/internal/info.buildVersion=${VERSION}
-X hakurei.app/internal/info.hakureiPath=/usr/bin/hakurei
-X hakurei.app/internal/info.hsuPath=/usr/bin/hsu
-X main.hakureiPath=/usr/bin/hakurei" ./...
echo
echo '# Testing hakurei.'
go test -ldflags='-buildid= -linkmode external -extldflags=-static' ./...
echo
echo '# Creating distribution.'
rm -f "${out}.tar.gz" && tar -C "${out}/.." -vczf "${out}.tar.gz" "${pname}"
rm -rf "${out}"
(cd "${out}/.." && sha512sum "${pname}.tar.gz" > "${pname}.tar.gz.sha512")
echo

View File

@@ -137,11 +137,10 @@
CC="musl-clang -O3 -Werror -Qunused-arguments" \ CC="musl-clang -O3 -Werror -Qunused-arguments" \
GOCACHE="$(mktemp -d)" \ GOCACHE="$(mktemp -d)" \
HAKUREI_TEST_SKIP_ACL=1 \
PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \ PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \
DESTDIR="$out" \ DESTDIR="$out" \
HAKUREI_VERSION="v${hakurei.version}" \ HAKUREI_VERSION="v${hakurei.version}" \
./dist/release.sh ./all.sh
''; '';
} }
); );
@@ -196,6 +195,7 @@
./test/interactive/vm.nix ./test/interactive/vm.nix
./test/interactive/hakurei.nix ./test/interactive/hakurei.nix
./test/interactive/trace.nix ./test/interactive/trace.nix
./test/interactive/raceattr.nix
self.nixosModules.hakurei self.nixosModules.hakurei
home-manager.nixosModules.home-manager home-manager.nixosModules.home-manager

View File

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

View File

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

View File

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

View File

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

View File

@@ -56,8 +56,10 @@ type Ops interface {
// ApplyState holds the address of [Ops] and any relevant application state. // ApplyState holds the address of [Ops] and any relevant application state.
type ApplyState struct { type ApplyState struct {
// AutoEtcPrefix is the prefix for [FSBind] in autoetc [FSBind.Special] condition. // Prefix for [FSBind] in autoetc [FSBind.Special] condition.
AutoEtcPrefix string AutoEtcPrefix string
// Whether to skip remounting root.
NoRemountRoot bool
Ops Ops
} }

View File

@@ -2,7 +2,7 @@ package hst
import ( import (
"encoding/gob" "encoding/gob"
"path" "path/filepath"
"hakurei.app/check" "hakurei.app/check"
) )
@@ -28,7 +28,7 @@ func (l *FSLink) Valid() bool {
if l == nil || l.Target == nil || l.Linkname == "" { if l == nil || l.Target == nil || l.Linkname == "" {
return false return false
} }
return !l.Dereference || path.IsAbs(l.Linkname) return !l.Dereference || filepath.IsAbs(l.Linkname)
} }
func (l *FSLink) Path() *check.Absolute { func (l *FSLink) Path() *check.Absolute {

View File

@@ -5,6 +5,7 @@ import (
"strings" "strings"
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/fhs"
) )
func init() { gob.Register(new(FSOverlay)) } func init() { gob.Register(new(FSOverlay)) }
@@ -69,9 +70,12 @@ func (o *FSOverlay) Apply(z *ApplyState) {
return return
} }
if o.Upper != nil && o.Work != nil { // rw if o.Upper != nil && o.Work != nil {
z.Overlay(o.Target, o.Upper, o.Work, o.Lower...) z.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
} else { // ro if o.Target.Is(fhs.AbsRoot) {
z.NoRemountRoot = true
}
} else {
z.OverlayReadonly(o.Target, o.Lower...) z.OverlayReadonly(o.Target, o.Lower...)
} }
} }

View File

@@ -49,5 +49,18 @@ func TestFSOverlay(t *testing.T) {
Lower: ms("/tmp/.src0", "/tmp/.src1"), Lower: ms("/tmp/.src0", "/tmp/.src1"),
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"), }}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
"*/mnt/src:/tmp/.src0:/tmp/.src1"}, "*/mnt/src:/tmp/.src0:/tmp/.src1"},
{"no remount root", &hst.FSOverlay{
Target: m("/"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
Upper: m("/tmp/upper"),
Work: m("/tmp/work"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
Upper: m("/tmp/upper"),
Work: m("/tmp/work"),
}}, m("/"), ms("/tmp/upper", "/tmp/work", "/tmp/.src0", "/tmp/.src1"),
"w*/:/tmp/upper:/tmp/work:/tmp/.src0:/tmp/.src1"},
}) })
} }

View File

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

View File

@@ -8,12 +8,14 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path" "path/filepath"
"reflect" "reflect"
"strconv" "strconv"
"syscall"
"testing" "testing"
"hakurei.app/internal/acl" "hakurei.app/internal/acl"
"hakurei.app/internal/info"
) )
const testFileName = "acl.test" const testFileName = "acl.test"
@@ -24,11 +26,17 @@ var (
) )
func TestUpdate(t *testing.T) { func TestUpdate(t *testing.T) {
if os.Getenv("HAKUREI_TEST_SKIP_ACL") == "1" { if info.CanDegrade {
t.Skip("acl test skipped") name := filepath.Join(t.TempDir(), "check-degrade")
if err := os.WriteFile(name, nil, 0); err != nil {
t.Fatal(err)
}
if err := acl.Update(name, os.Geteuid()); errors.Is(err, syscall.ENOTSUP) {
t.Skip(err)
}
} }
testFilePath := path.Join(t.TempDir(), testFileName) testFilePath := filepath.Join(t.TempDir(), testFileName)
if f, err := os.Create(testFilePath); err != nil { if f, err := os.Create(testFilePath); err != nil {
t.Fatalf("Create: error = %v", err) t.Fatalf("Create: error = %v", err)

View File

@@ -0,0 +1,7 @@
//go:build !noskip
package info
// CanDegrade is whether tests are allowed to transparently degrade or skip due
// to required system features being denied or unavailable.
const CanDegrade = true

View File

@@ -0,0 +1,5 @@
//go:build noskip
package info
const CanDegrade = false

90
internal/kobject/event.go Normal file
View File

@@ -0,0 +1,90 @@
package kobject
import (
"errors"
"strconv"
"strings"
"unsafe"
"hakurei.app/internal/uevent"
)
// Event is a [uevent.Message] with known environment variables processed.
type Event struct {
// alloc_uevent_skb: action_string
Action uevent.KobjectAction `json:"action"`
// alloc_uevent_skb: devpath
DevPath string `json:"devpath"`
// Uninterpreted environment variable pairs. An entry missing a separator
// gains the value "\x00".
Env map[string]string `json:"env"`
// SEQNUM value set by the kernel.
Sequence uint64 `json:"seqnum"`
// SYNTH_UUID value set on trigger, nil denotes a non-synthetic event.
Synth *uevent.UUID `json:"synth_uuid,omitempty"`
// SUBSYSTEM value set by the kernel.
Subsystem string `json:"subsystem"`
}
// Populate populates e with the contents of a [uevent.Message].
//
// The ACTION and DEVPATH environment variables are ignored and assumed to be
// consistent with the header.
func (e *Event) Populate(reportErr func(error), m *uevent.Message) {
if reportErr == nil {
reportErr = func(error) {}
}
*e = Event{
Action: m.Action,
DevPath: m.DevPath,
Env: make(map[string]string),
}
for _, s := range m.Env {
k, v, ok := strings.Cut(s, "=")
if !ok {
if _, ok = e.Env[s]; !ok {
e.Env[s] = "\x00"
}
continue
}
switch k {
case "ACTION", "DEVPATH":
continue
case "SEQNUM":
seq, err := strconv.ParseUint(v, 10, 64)
if err != nil {
if _e := errors.Unwrap(err); _e != nil {
err = _e
}
reportErr(err)
e.Env[k] = v
continue
}
e.Sequence = seq
case "SYNTH_UUID":
var uuid uevent.UUID
err := uuid.UnmarshalText(unsafe.Slice(unsafe.StringData(v), len(v)))
if err != nil {
reportErr(err)
e.Env[k] = v
continue
}
e.Synth = &uuid
case "SUBSYSTEM":
e.Subsystem = v
default:
e.Env[k] = v
}
}
}

View File

@@ -0,0 +1,92 @@
package kobject_test
import (
"reflect"
"strconv"
"testing"
"hakurei.app/internal/kobject"
"hakurei.app/internal/uevent"
)
func TestEvent(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
msg uevent.Message
want kobject.Event
errs []error
}{
{"sample coldboot qemu", uevent.Message{
Action: uevent.KOBJ_ADD,
DevPath: "/devices/LNXSYSTM:00/LNXPWRBN:00",
Env: []string{
"ACTION=add",
"DEVPATH=/devices/LNXSYSTM:00/LNXPWRBN:00",
"SUBSYSTEM=acpi",
"SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed",
"MODALIAS=acpi:LNXPWRBN:",
"SEQNUM=777",
}}, kobject.Event{
Action: uevent.KOBJ_ADD,
DevPath: "/devices/LNXSYSTM:00/LNXPWRBN:00",
Env: map[string]string{
"MODALIAS": "acpi:LNXPWRBN:",
},
Sequence: 777,
Synth: &uevent.UUID{
0xfe, 0x4d, 0x7c, 0x9d,
0xb8, 0xc6,
0x4a, 0x70,
0x9e, 0xf1,
0x3d, 0x8a, 0x58, 0xd1, 0x8e, 0xed,
},
Subsystem: "acpi",
}, []error{}},
{"nil reportErr", uevent.Message{Env: []string{
"SEQNUM=\x00",
}}, kobject.Event{Env: map[string]string{
"SEQNUM": "\x00",
}}, nil},
{"bad SEQNUM SYNTH_UUID", uevent.Message{Env: []string{
"SEQNUM=\x00",
"SYNTH_UUID=\x00",
"SUBSYSTEM=\x00",
}}, kobject.Event{Subsystem: "\x00", Env: map[string]string{
"SEQNUM": "\x00",
"SYNTH_UUID": "\x00",
}}, []error{strconv.ErrSyntax, uevent.UUIDSizeError(1)}},
{"bad sep", uevent.Message{Env: []string{
"SYNTH_UUID",
}}, kobject.Event{Env: map[string]string{
"SYNTH_UUID": "\x00",
}}, []error{}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var f func(error)
gotErrs := make([]error, 0)
if tc.errs != nil {
f = func(err error) {
gotErrs = append(gotErrs, err)
}
}
var got kobject.Event
got.Populate(f, &tc.msg)
if !reflect.DeepEqual(&got, &tc.want) {
t.Errorf("Populate: %#v, want %#v", got, tc.want)
}
if tc.errs != nil && !reflect.DeepEqual(gotErrs, tc.errs) {
t.Errorf("Populate: errs = %v, want %v", gotErrs, tc.errs)
}
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ import (
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/internal/dbus" "hakurei.app/internal/dbus"
"hakurei.app/internal/info" "hakurei.app/internal/info"
"hakurei.app/internal/params"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -84,7 +85,7 @@ type syscallDispatcher interface {
// setDumpable provides [container.SetDumpable]. // setDumpable provides [container.SetDumpable].
setDumpable(dumpable uintptr) error setDumpable(dumpable uintptr) error
// receive provides [container.Receive]. // receive provides [container.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)
// containerStart provides the Start method of [container.Container]. // containerStart provides the Start method of [container.Container].
containerStart(z *container.Container) error containerStart(z *container.Container) error
@@ -154,8 +155,8 @@ func (direct) prctl(op, arg2, arg3 uintptr) error { return ext.Prctl(op, arg2, a
func (direct) overflowUid(msg message.Msg) int { return container.OverflowUid(msg) } func (direct) overflowUid(msg message.Msg) int { return container.OverflowUid(msg) }
func (direct) overflowGid(msg message.Msg) int { return container.OverflowGid(msg) } func (direct) overflowGid(msg message.Msg) int { return container.OverflowGid(msg) }
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) } func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) { func (direct) receive(key string, e any, fdp *int) (func() error, error) {
return container.Receive(key, e, fdp) return params.Receive(key, e, fdp)
} }
func (direct) containerStart(z *container.Container) error { return z.Start() } func (direct) containerStart(z *container.Container) error { return z.Start() }

View File

@@ -401,12 +401,12 @@ func (k *kstub) setDumpable(dumpable uintptr) error {
stub.CheckArg(k.Stub, "dumpable", dumpable, 0)) stub.CheckArg(k.Stub, "dumpable", dumpable, 0))
} }
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() k.Helper()
expect := k.Expects("receive") expect := k.Expects("receive")
reflect.ValueOf(e).Elem().Set(reflect.ValueOf(expect.Args[1])) reflect.ValueOf(e).Elem().Set(reflect.ValueOf(expect.Args[1]))
if expect.Args[2] != nil { if expect.Args[2] != nil {
*fdp = expect.Args[2].(uintptr) *fdp = int(expect.Args[2].(uintptr))
} }
return func() error { return k.Expects("closeReceive").Err }, expect.Error( return func() error { return k.Expects("closeReceive").Err }, expect.Error(
stub.CheckArg(k.Stub, "key", key, 0)) stub.CheckArg(k.Stub, "key", key, 0))
@@ -712,7 +712,7 @@ func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { pa
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") } func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") } func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
func (panicDispatcher) setDumpable(uintptr) error { panic("unreachable") } func (panicDispatcher) setDumpable(uintptr) error { panic("unreachable") }
func (panicDispatcher) receive(string, any, *uintptr) (func() error, error) { panic("unreachable") } func (panicDispatcher) receive(string, any, *int) (func() error, error) { panic("unreachable") }
func (panicDispatcher) containerStart(*container.Container) error { panic("unreachable") } func (panicDispatcher) containerStart(*container.Container) error { panic("unreachable") }
func (panicDispatcher) containerServe(*container.Container) error { panic("unreachable") } func (panicDispatcher) containerServe(*container.Container) error { panic("unreachable") }
func (panicDispatcher) containerWait(*container.Container) error { panic("unreachable") } func (panicDispatcher) containerWait(*container.Container) error { panic("unreachable") }

View File

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

View File

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

View File

@@ -13,7 +13,6 @@ import (
"time" "time"
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/container"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/info" "hakurei.app/internal/info"
@@ -298,12 +297,12 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
// accumulate enablements of remaining instances // accumulate enablements of remaining instances
var ( var (
// alive enablement bits // alive enablement bits
rt hst.Enablement rt hst.Enablements
// alive instance count // alive instance count
n int n int
) )
for eh := range entries { for eh := range entries {
var et hst.Enablement var et hst.Enablements
if et, err = eh.Load(nil); err != nil { if et, err = eh.Load(nil); err != nil {
perror(err, "read state header of instance "+eh.ID.String()) perror(err, "read state header of instance "+eh.ID.String())
} else { } else {
@@ -372,17 +371,18 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
// shim runs in the same session as monitor; see shim.go for behaviour // shim runs in the same session as monitor; see shim.go for behaviour
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) } cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
var shimPipe *os.File var shimPipe [2]*os.File
if fd, w, err := container.Setup(&cmd.ExtraFiles); err != nil { if r, w, err := os.Pipe(); err != nil {
return cmd, nil, &hst.AppError{Step: "create shim setup pipe", Err: err} return cmd, nil, &hst.AppError{Step: "create shim setup pipe", Err: err}
} else { } else {
shimPipe = w
cmd.Env = []string{ cmd.Env = []string{
// passed through to shim by hsu // passed through to shim by hsu
shimEnv + "=" + strconv.Itoa(fd), shimEnv + "=" + strconv.Itoa(3+len(cmd.ExtraFiles)),
// interpreted by hsu // interpreted by hsu
"HAKUREI_IDENTITY=" + k.state.identity.String(), "HAKUREI_IDENTITY=" + k.state.identity.String(),
} }
cmd.ExtraFiles = append(cmd.ExtraFiles, r)
shimPipe[0], shimPipe[1] = r, w
} }
if len(k.supp) > 0 { if len(k.supp) > 0 {
@@ -393,12 +393,16 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
msg.Verbosef("setuid helper at %s", hsuPath) msg.Verbosef("setuid helper at %s", hsuPath)
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
_, _ = shimPipe[0].Close(), shimPipe[1].Close()
msg.Resume() msg.Resume()
return cmd, shimPipe, &hst.AppError{Step: "start setuid wrapper", Err: err} return cmd, nil, &hst.AppError{Step: "start setuid wrapper", Err: err}
}
if err := shimPipe[0].Close(); err != nil {
msg.Verbose(err)
} }
*startTime = time.Now().UTC() *startTime = time.Now().UTC()
return cmd, shimPipe, nil return cmd, shimPipe[1], nil
} }
// serveShim serves outcomeState through the shim setup pipe. // serveShim serves outcomeState through the shim setup pipe.
@@ -411,11 +415,11 @@ func serveShim(msg message.Msg, shimPipe *os.File, state *outcomeState) error {
msg.Verbose(err.Error()) msg.Verbose(err.Error())
} }
if err := gob.NewEncoder(shimPipe).Encode(state); err != nil { if err := gob.NewEncoder(shimPipe).Encode(state); err != nil {
_ = shimPipe.Close()
msg.Resume() msg.Resume()
return &hst.AppError{Step: "transmit shim config", Err: err} return &hst.AppError{Step: "transmit shim config", Err: err}
} }
_ = shimPipe.Close() return shimPipe.Close()
return nil
} }
// printMessageError prints the error message according to [message.GetMessage], // printMessageError prints the error message according to [message.GetMessage],

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ import (
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/params"
"hakurei.app/internal/pipewire" "hakurei.app/internal/pipewire"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -197,7 +198,7 @@ func shimEntrypoint(k syscallDispatcher) {
if errors.Is(err, syscall.EBADF) { if errors.Is(err, syscall.EBADF) {
k.fatal("invalid config descriptor") k.fatal("invalid config descriptor")
} }
if errors.Is(err, container.ErrReceiveEnv) { if errors.Is(err, params.ErrReceiveEnv) {
k.fatal(shimEnv + " not set") k.fatal(shimEnv + " not set")
} }

View File

@@ -16,6 +16,7 @@ import (
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/env" "hakurei.app/internal/env"
"hakurei.app/internal/params"
"hakurei.app/internal/stub" "hakurei.app/internal/stub"
) )
@@ -172,7 +173,7 @@ func TestShimEntrypoint(t *testing.T) {
call("setDumpable", stub.ExpectArgs{uintptr(ext.SUID_DUMP_DISABLE)}, nil, nil), call("setDumpable", stub.ExpectArgs{uintptr(ext.SUID_DUMP_DISABLE)}, nil, nil),
call("getppid", stub.ExpectArgs{}, 0xbad, nil), call("getppid", stub.ExpectArgs{}, 0xbad, nil),
call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil),
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, container.ErrReceiveEnv), call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, params.ErrReceiveEnv),
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SHIM not set"}}, nil, nil), call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SHIM not set"}}, nil, nil),
// deferred // deferred

View File

@@ -5,7 +5,7 @@ import (
"errors" "errors"
"io/fs" "io/fs"
"os" "os"
"path" "path/filepath"
"slices" "slices"
"strconv" "strconv"
"syscall" "syscall"
@@ -165,9 +165,9 @@ func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error {
} }
for _, pair := range entry.Values { for _, pair := range entry.Values {
if pair[0] == "path" { if pair[0] == "path" {
if path.IsAbs(pair[1]) { if filepath.IsAbs(pair[1]) {
// get parent dir of socket // get parent dir of socket
dir := path.Dir(pair[1]) dir := filepath.Dir(pair[1])
if dir == "." || dir == fhs.Root { if dir == "." || dir == fhs.Root {
state.msg.Verbosef("dbus socket %q is in an unusual location", pair[1]) state.msg.Verbosef("dbus socket %q is in an unusual location", pair[1])
} }
@@ -290,7 +290,9 @@ func (s *spFilesystemOp) toContainer(state *outcomeStateParams) error {
if state.Container.Flags&hst.FDevice == 0 { if state.Container.Flags&hst.FDevice == 0 {
state.params.Remount(fhs.AbsDev, syscall.MS_RDONLY) state.params.Remount(fhs.AbsDev, syscall.MS_RDONLY)
} }
if !state.as.NoRemountRoot {
state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY) state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY)
}
state.params.Env = make([]string, 0, len(state.env)) state.params.Env = make([]string, 0, len(state.env))
for key, value := range state.env { for key, value := range state.env {

View File

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

42
internal/params/params.go Normal file
View File

@@ -0,0 +1,42 @@
// Package params provides helpers for receiving setup payload from parent.
package params
import (
"encoding/gob"
"errors"
"os"
"strconv"
"syscall"
)
// ErrReceiveEnv is returned by [Receive] if setup fd is not present in environment.
var ErrReceiveEnv = errors.New("environment variable not set")
// Receive retrieves setup fd from the environment and receives params.
//
// The file descriptor written to the value pointed to by fdp must not be passed
// to any system calls. It is made available for ordering file descriptor only.
func Receive(key string, v any, fdp *int) (func() error, error) {
var setup *os.File
if s, ok := os.LookupEnv(key); !ok {
return nil, ErrReceiveEnv
} else {
if fd, err := strconv.Atoi(s); err != nil {
if _err := errors.Unwrap(err); _err != nil {
err = _err
}
return nil, err
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
return nil, syscall.EDOM
}
if fdp != nil {
*fdp = fd
}
}
}
return setup.Close, gob.NewDecoder(setup).Decode(v)
}

View File

@@ -1,4 +1,4 @@
package container_test package params_test
import ( import (
"encoding/gob" "encoding/gob"
@@ -9,7 +9,7 @@ import (
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container" "hakurei.app/internal/params"
) )
func TestSetupReceive(t *testing.T) { func TestSetupReceive(t *testing.T) {
@@ -30,8 +30,8 @@ func TestSetupReceive(t *testing.T) {
}) })
} }
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrReceiveEnv) { if _, err := params.Receive(key, nil, nil); !errors.Is(err, params.ErrReceiveEnv) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrReceiveEnv) t.Errorf("Receive: error = %v, want %v", err, params.ErrReceiveEnv)
} }
}) })
@@ -39,7 +39,7 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_FORMAT" const key = "TEST_ENV_FORMAT"
t.Setenv(key, "") t.Setenv(key, "")
if _, err := container.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) { if _, err := params.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax) t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax)
} }
}) })
@@ -48,7 +48,7 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_RANGE" const key = "TEST_ENV_RANGE"
t.Setenv(key, "-1") t.Setenv(key, "-1")
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) { if _, err := params.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM) t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM)
} }
}) })
@@ -60,16 +60,22 @@ func TestSetupReceive(t *testing.T) {
encoderDone := make(chan error, 1) encoderDone := make(chan error, 1)
extraFiles := make([]*os.File, 0, 1) extraFiles := make([]*os.File, 0, 1)
deadline, _ := t.Deadline() if r, w, err := os.Pipe(); err != nil {
if fd, f, err := container.Setup(&extraFiles); err != nil {
t.Fatalf("Setup: error = %v", err) t.Fatalf("Setup: error = %v", err)
} else if fd != 3 {
t.Fatalf("Setup: fd = %d, want 3", fd)
} else { } else {
if err = f.SetDeadline(deadline); err != nil { t.Cleanup(func() {
t.Fatal(err.Error()) if err = errors.Join(r.Close(), w.Close()); err != nil {
t.Fatal(err)
} }
go func() { encoderDone <- gob.NewEncoder(f).Encode(payload) }() })
extraFiles = append(extraFiles, r)
if deadline, ok := t.Deadline(); ok {
if err = w.SetDeadline(deadline); err != nil {
t.Fatal(err)
}
}
go func() { encoderDone <- gob.NewEncoder(w).Encode(payload) }()
} }
if len(extraFiles) != 1 { if len(extraFiles) != 1 {
@@ -87,13 +93,13 @@ func TestSetupReceive(t *testing.T) {
var ( var (
gotPayload []uint64 gotPayload []uint64
fdp *uintptr fdp *int
) )
if !useNilFdp { if !useNilFdp {
fdp = new(uintptr) fdp = new(int)
} }
var closeFile func() error var closeFile func() error
if f, err := container.Receive(key, &gotPayload, fdp); err != nil { if f, err := params.Receive(key, &gotPayload, fdp); err != nil {
t.Fatalf("Receive: error = %v", err) t.Fatalf("Receive: error = %v", err)
} else { } else {
closeFile = f closeFile = f
@@ -103,7 +109,7 @@ func TestSetupReceive(t *testing.T) {
} }
} }
if !useNilFdp { if !useNilFdp {
if int(*fdp) != dupFd { if *fdp != dupFd {
t.Errorf("Fd: %d, want %d", *fdp, dupFd) t.Errorf("Fd: %d, want %d", *fdp, dupFd)
} }
} }

View File

@@ -20,7 +20,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path" "path/filepath"
"runtime" "runtime"
"slices" "slices"
"strconv" "strconv"
@@ -973,23 +973,23 @@ func connectName(name string, manager bool) (conn Conn, err error) {
return connectName(name+"-manager", false) return connectName(name+"-manager", false)
} }
if path.IsAbs(name) || (len(name) > 0 && name[0] == '@') { if filepath.IsAbs(name) || (len(name) > 0 && name[0] == '@') {
return Dial(name) return Dial(name)
} else { } else {
runtimeDir, ok := os.LookupEnv("PIPEWIRE_RUNTIME_DIR") runtimeDir, ok := os.LookupEnv("PIPEWIRE_RUNTIME_DIR")
if !ok || !path.IsAbs(runtimeDir) { if !ok || !filepath.IsAbs(runtimeDir) {
runtimeDir, ok = os.LookupEnv("XDG_RUNTIME_DIR") runtimeDir, ok = os.LookupEnv("XDG_RUNTIME_DIR")
} }
if !ok || !path.IsAbs(runtimeDir) { if !ok || !filepath.IsAbs(runtimeDir) {
// this is cargo culted from windows stuff and has no effect normally; // this is cargo culted from windows stuff and has no effect normally;
// keeping it to maintain compatibility in case someone sets this // keeping it to maintain compatibility in case someone sets this
runtimeDir, ok = os.LookupEnv("USERPROFILE") runtimeDir, ok = os.LookupEnv("USERPROFILE")
} }
if !ok || !path.IsAbs(runtimeDir) { if !ok || !filepath.IsAbs(runtimeDir) {
runtimeDir = DEFAULT_SYSTEM_RUNTIME_DIR runtimeDir = DEFAULT_SYSTEM_RUNTIME_DIR
} }
return Dial(path.Join(runtimeDir, name)) return Dial(filepath.Join(runtimeDir, name))
} }
} }

View File

@@ -27,6 +27,31 @@ func TestFlatten(t *testing.T) {
fs.ModeCharDevice | 0400, fs.ModeCharDevice | 0400,
)}, )},
{"coldboot", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"devices": {Mode: fs.ModeDir | 0700},
"devices/uevent": {Mode: 0600, Data: []byte("add")},
"devices/empty": {Mode: fs.ModeDir | 0700},
"devices/sub": {Mode: fs.ModeDir | 0700},
"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"block": {Mode: fs.ModeDir | 0700},
"block/uevent": {Mode: 0600, Data: []byte{}},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "block"},
{Mode: 0600, Path: "block/uevent", Data: []byte{}},
{Mode: fs.ModeDir | 0700, Path: "devices"},
{Mode: fs.ModeDir | 0700, Path: "devices/empty"},
{Mode: fs.ModeDir | 0700, Path: "devices/sub"},
{Mode: 0600, Path: "devices/sub/uevent", Data: []byte("add")},
{Mode: 0600, Path: "devices/uevent", Data: []byte("add")},
}, pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8"), nil},
{"empty", fstest.MapFS{ {"empty", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700}, ".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700}, "checksum": {Mode: fs.ModeDir | 0700},
@@ -159,6 +184,32 @@ func TestFlatten(t *testing.T) {
{Mode: fs.ModeDir | 0700, Path: "work"}, {Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("WVpvsVqVKg9Nsh744x57h51AuWUoUR2nnh8Md-EYBQpk6ziyTuUn6PLtF2e0Eu_d"), nil}, }, pkg.MustDecode("WVpvsVqVKg9Nsh744x57h51AuWUoUR2nnh8Md-EYBQpk6ziyTuUn6PLtF2e0Eu_d"), nil},
{"sample no assume checksum", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M": {Mode: fs.ModeDir | 0500},
"checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M/check": {Mode: 0400, Data: []byte{}},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/_wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
"identifier/_wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0500, Path: "checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M"},
{Mode: 0400, Path: "checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M/check", Data: []byte{}},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/_wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
{Mode: fs.ModeSymlink | 0777, Path: "identifier/_wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("OC290t23aimNo2Rp2pPwan5GI2KRLRdOwYxXQMD9jw0QROgHnNXWodoWdV0hwu2w"), nil},
{"sample tar step unpack", fstest.MapFS{ {"sample tar step unpack", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500}, ".": {Mode: fs.ModeDir | 0500},

View File

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

View File

@@ -33,8 +33,7 @@ func TestExec(t *testing.T) {
) )
checkWithCache(t, []cacheTestCase{ checkWithCache(t, []cacheTestCase{
{"offline", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"offline", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
testtool, testtoolDestroy := newTesttool() testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{ cureMany(t, c, []cureStep{
@@ -92,7 +91,7 @@ func TestExec(t *testing.T) {
[]string{"testtool"}, []string{"testtool"},
pkg.ExecPath{}, pkg.ExecPath{},
), nil, pkg.Checksum{}, os.ErrInvalid}, ), nil, pkg.Checksum{}, pkg.ErrInvalidPaths},
}) })
// check init failure passthrough // check init failure passthrough
@@ -111,8 +110,7 @@ func TestExec(t *testing.T) {
testtoolDestroy(t, base, c) testtoolDestroy(t, base, c)
}, pkg.MustDecode("Q5DluWQCAeohLoiGRImurwFp3vdz9IfQCoj7Fuhh73s4KQPRHpEQEnHTdNHmB8Fx")}, }, pkg.MustDecode("Q5DluWQCAeohLoiGRImurwFp3vdz9IfQCoj7Fuhh73s4KQPRHpEQEnHTdNHmB8Fx")},
{"net", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"net", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
testtool, testtoolDestroy := newTesttool() testtool, testtoolDestroy := newTesttool()
wantChecksum := pkg.MustDecode( wantChecksum := pkg.MustDecode(
@@ -146,8 +144,7 @@ func TestExec(t *testing.T) {
testtoolDestroy(t, base, c) testtoolDestroy(t, base, c)
}, pkg.MustDecode("bPYvvqxpfV7xcC1EptqyKNK1klLJgYHMDkzBcoOyK6j_Aj5hb0mXNPwTwPSK5F6Z")}, }, pkg.MustDecode("bPYvvqxpfV7xcC1EptqyKNK1klLJgYHMDkzBcoOyK6j_Aj5hb0mXNPwTwPSK5F6Z")},
{"overlay root", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"overlay root", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
testtool, testtoolDestroy := newTesttool() testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{ cureMany(t, c, []cureStep{
@@ -172,8 +169,7 @@ func TestExec(t *testing.T) {
testtoolDestroy(t, base, c) testtoolDestroy(t, base, c)
}, pkg.MustDecode("PO2DSSCa4yoSgEYRcCSZfQfwow1yRigL3Ry-hI0RDI4aGuFBha-EfXeSJnG_5_Rl")}, }, pkg.MustDecode("PO2DSSCa4yoSgEYRcCSZfQfwow1yRigL3Ry-hI0RDI4aGuFBha-EfXeSJnG_5_Rl")},
{"overlay work", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"overlay work", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
testtool, testtoolDestroy := newTesttool() testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{ cureMany(t, c, []cureStep{
@@ -203,8 +199,7 @@ func TestExec(t *testing.T) {
testtoolDestroy(t, base, c) testtoolDestroy(t, base, c)
}, pkg.MustDecode("iaRt6l_Wm2n-h5UsDewZxQkCmjZjyL8r7wv32QT2kyV55-Lx09Dq4gfg9BiwPnKs")}, }, pkg.MustDecode("iaRt6l_Wm2n-h5UsDewZxQkCmjZjyL8r7wv32QT2kyV55-Lx09Dq4gfg9BiwPnKs")},
{"multiple layers", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"multiple layers", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
testtool, testtoolDestroy := newTesttool() testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{ cureMany(t, c, []cureStep{
@@ -256,8 +251,7 @@ func TestExec(t *testing.T) {
testtoolDestroy(t, base, c) testtoolDestroy(t, base, c)
}, pkg.MustDecode("O2YzyR7IUGU5J2CADy0hUZ3A5NkP_Vwzs4UadEdn2oMZZVWRtH0xZGJ3HXiimTnZ")}, }, pkg.MustDecode("O2YzyR7IUGU5J2CADy0hUZ3A5NkP_Vwzs4UadEdn2oMZZVWRtH0xZGJ3HXiimTnZ")},
{"overlay layer promotion", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"overlay layer promotion", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
testtool, testtoolDestroy := newTesttool() testtool, testtoolDestroy := newTesttool()
cureMany(t, c, []cureStep{ cureMany(t, c, []cureStep{

View File

@@ -11,9 +11,7 @@ func TestFile(t *testing.T) {
t.Parallel() t.Parallel()
checkWithCache(t, []cacheTestCase{ checkWithCache(t, []cacheTestCase{
{"file", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"file", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
cureMany(t, c, []cureStep{ cureMany(t, c, []cureStep{
{"short", pkg.NewFile("null", []byte{0}), base.Append( {"short", pkg.NewFile("null", []byte{0}), base.Append(
"identifier", "identifier",

View File

@@ -85,7 +85,7 @@ func TestIRRoundtrip(t *testing.T) {
testCasesCache := make([]cacheTestCase, len(testCases)) testCasesCache := make([]cacheTestCase, len(testCases))
for i, tc := range testCases { for i, tc := range testCases {
want := tc.a want := tc.a
testCasesCache[i] = cacheTestCase{tc.name, nil, testCasesCache[i] = cacheTestCase{tc.name, 0, nil,
func(t *testing.T, base *check.Absolute, c *pkg.Cache) { func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
r, w := io.Pipe() r, w := io.Pipe()

View File

@@ -32,7 +32,7 @@ func TestHTTPGet(t *testing.T) {
})) }))
checkWithCache(t, []cacheTestCase{ checkWithCache(t, []cacheTestCase{
{"direct", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"direct", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
var r pkg.RContext var r pkg.RContext
rCacheVal := reflect.ValueOf(&r).Elem().FieldByName("cache") rCacheVal := reflect.ValueOf(&r).Elem().FieldByName("cache")
reflect.NewAt( reflect.NewAt(
@@ -94,7 +94,7 @@ func TestHTTPGet(t *testing.T) {
} }
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C")}, }, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C")},
{"cure", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"cure", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
var r pkg.RContext var r pkg.RContext
rCacheVal := reflect.ValueOf(&r).Elem().FieldByName("cache") rCacheVal := reflect.ValueOf(&r).Elem().FieldByName("cache")
reflect.NewAt( reflect.NewAt(

View File

@@ -13,15 +13,14 @@ import (
"hash" "hash"
"io" "io"
"io/fs" "io/fs"
"iter"
"maps" "maps"
"os" "os"
"path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"slices" "slices"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"testing" "testing"
"unique" "unique"
@@ -331,23 +330,6 @@ type FloodArtifact interface {
Artifact Artifact
} }
// Flood returns an iterator over the dependency tree of an [Artifact].
func Flood(a Artifact) iter.Seq[Artifact] {
return func(yield func(Artifact) bool) {
for _, d := range a.Dependencies() {
if !yield(d) {
return
}
for d0 := range Flood(d) {
if !yield(d0) {
return
}
}
}
}
}
// TrivialArtifact refers to an [Artifact] that cures without requiring that // TrivialArtifact refers to an [Artifact] that cures without requiring that
// any other [Artifact] is cured before it. Its dependency tree is ignored after // any other [Artifact] is cured before it. Its dependency tree is ignored after
// computing its identifier. // computing its identifier.
@@ -366,7 +348,7 @@ type TrivialArtifact interface {
} }
// KnownIdent is optionally implemented by [Artifact] and is used instead of // KnownIdent is optionally implemented by [Artifact] and is used instead of
// [Kind.Ident] when it is available. // [Cache.Ident] when it is available.
// //
// This is very subtle to use correctly. The implementation must ensure that // This is very subtle to use correctly. The implementation must ensure that
// this value is globally unique, otherwise [Cache] can enter an inconsistent // this value is globally unique, otherwise [Cache] can enter an inconsistent
@@ -439,6 +421,11 @@ const (
KindCustomOffset = 1 << 31 KindCustomOffset = 1 << 31
) )
const (
// kindCollection is the kind of [Collect]. It never cures successfully.
kindCollection Kind = KindCustomOffset - 1 - iota
)
const ( const (
// fileLock is the file name appended to Cache.base for guaranteeing // fileLock is the file name appended to Cache.base for guaranteeing
// exclusive access to the cache directory. // exclusive access to the cache directory.
@@ -461,6 +448,11 @@ const (
// pathnames allocated during [Cache.Cure]. // pathnames allocated during [Cache.Cure].
dirTemp = "temp" dirTemp = "temp"
// dirExecScratch is the directory name appended to Cache.base for scratch
// space setting up the container started by [Cache.EnterExec]. Exclusivity
// via Cache.inExec.
dirExecScratch = "scratch"
// checksumLinknamePrefix is prepended to the encoded [Checksum] value // checksumLinknamePrefix is prepended to the encoded [Checksum] value
// of an [Artifact] when creating a symbolic link to dirChecksum. // of an [Artifact] when creating a symbolic link to dirChecksum.
checksumLinknamePrefix = "../" + dirChecksum + "/" checksumLinknamePrefix = "../" + dirChecksum + "/"
@@ -476,7 +468,7 @@ type cureRes struct {
// subject to the cures limit. Values pointed to by result addresses are safe // subject to the cures limit. Values pointed to by result addresses are safe
// to access after the [sync.WaitGroup] associated with this pendingArtifactDep // to access after the [sync.WaitGroup] associated with this pendingArtifactDep
// is done. pendingArtifactDep must not be reused or modified after it is sent // is done. pendingArtifactDep must not be reused or modified after it is sent
// to Cache.cureDep. // to cure.
type pendingArtifactDep struct { type pendingArtifactDep struct {
// Dependency artifact populated during [Cache.Cure]. // Dependency artifact populated during [Cache.Cure].
a Artifact a Artifact
@@ -496,6 +488,49 @@ type pendingArtifactDep struct {
*sync.WaitGroup *sync.WaitGroup
} }
const (
// CValidateKnown arranges for [KnownChecksum] outcomes to be validated to
// match its intended checksum.
//
// A correct implementation of [KnownChecksum] does not successfully cure
// with output not matching its intended checksum. When an implementation
// fails to perform this validation correctly, the on-disk format enters
// an inconsistent state (correctable by [Cache.Scrub]).
//
// This flag causes [Cache.Cure] to always compute the checksum, and reject
// a cure if it does not match the intended checksum.
//
// This behaviour significantly reduces performance and is not recommended
// outside of testing a custom [Artifact] implementation.
CValidateKnown = 1 << iota
// CSchedIdle arranges for the [ext.SCHED_IDLE] scheduling priority to be
// set for [KindExec] and [KindExecNet] containers.
CSchedIdle
// CAssumeChecksum enables the use of [KnownChecksum] for duplicate function
// call suppression via the on-disk cache.
//
// This may cause incorrect cure outcome if an impossible checksum is
// specified that matches an output already present in the on-disk cache.
// This may be avoided by purposefully specifying a statistically
// unattainable checksum, like the zero value.
//
// While this optimisation might seem appealing, it is almost never
// applicable in real world use. Almost every time this path was taken, it
// was caused by an incorrect checksum accidentally left behind while
// bumping a package. Only enable this if you are really sure you need it.
CAssumeChecksum
// CHostAbstract disables restriction of sandboxed processes from connecting
// to an abstract UNIX socket created by a host process.
//
// This is considered less secure in some systems, but does not introduce
// impurity due to [KindExecNet] being [KnownChecksum]. This flag exists
// to support kernels without Landlock LSM enabled.
CHostAbstract
)
// Cache is a support layer that implementations of [Artifact] can use to store // Cache is a support layer that implementations of [Artifact] can use to store
// cured [Artifact] data in a content addressed fashion. // cured [Artifact] data in a content addressed fashion.
type Cache struct { type Cache struct {
@@ -515,12 +550,8 @@ type Cache struct {
// Directory where all [Cache] related files are placed. // Directory where all [Cache] related files are placed.
base *check.Absolute base *check.Absolute
// Immutable cure options set by [Open].
// Whether to validate [FileArtifact.Cure] for a [KnownChecksum] file. This flags int
// significantly reduces performance.
strict bool
// Maximum size of a dependency graph.
threshold uintptr
// Artifact to [unique.Handle] of identifier cache. // Artifact to [unique.Handle] of identifier cache.
artifact sync.Map artifact sync.Map
@@ -548,24 +579,11 @@ type Cache struct {
unlock func() unlock func()
// Synchronises calls to Close. // Synchronises calls to Close.
closeOnce sync.Once closeOnce sync.Once
// Whether EnterExec has not yet returned.
inExec atomic.Bool
} }
// IsStrict returns whether the [Cache] strictly verifies checksums.
func (c *Cache) IsStrict() bool { return c.strict }
// SetStrict sets whether the [Cache] strictly verifies checksums, even when
// the implementation promises to validate them internally. This significantly
// reduces performance and is not recommended outside of testing.
//
// This method is not safe for concurrent use with any other method.
func (c *Cache) SetStrict(strict bool) { c.strict = strict }
// SetThreshold imposes a maximum size on the dependency graph, checked on every
// call to Cure. The zero value disables this check entirely.
//
// This method is not safe for concurrent use with any other method.
func (c *Cache) SetThreshold(threshold uintptr) { c.threshold = threshold }
// extIdent is a [Kind] concatenated with [ID]. // extIdent is a [Kind] concatenated with [ID].
type extIdent [wordSize + len(ID{})]byte type extIdent [wordSize + len(ID{})]byte
@@ -880,7 +898,7 @@ func (c *Cache) Scrub(checks int) error {
se.DanglingIdentifiers = append(se.DanglingIdentifiers, *want) se.DanglingIdentifiers = append(se.DanglingIdentifiers, *want)
seMu.Unlock() seMu.Unlock()
return false return false
} else if err = Decode(got, path.Base(linkname)); err != nil { } else if err = Decode(got, filepath.Base(linkname)); err != nil {
seMu.Lock() seMu.Lock()
lnp := dir.Append(linkname) lnp := dir.Append(linkname)
se.Errs[lnp.Handle()] = append(se.Errs[lnp.Handle()], err) se.Errs[lnp.Handle()] = append(se.Errs[lnp.Handle()], err)
@@ -1045,7 +1063,7 @@ func (c *Cache) finaliseIdent(
// [FileArtifact] to the filesystem. If err is nil, the caller is responsible // [FileArtifact] to the filesystem. If err is nil, the caller is responsible
// for closing the resulting [io.ReadCloser]. // for closing the resulting [io.ReadCloser].
func (c *Cache) openFile(f FileArtifact) (r io.ReadCloser, err error) { func (c *Cache) openFile(f FileArtifact) (r io.ReadCloser, err error) {
if kc, ok := f.(KnownChecksum); ok { if kc, ok := f.(KnownChecksum); c.flags&CAssumeChecksum != 0 && ok {
c.checksumMu.RLock() c.checksumMu.RLock()
r, err = os.Open(c.base.Append( r, err = os.Open(c.base.Append(
dirChecksum, dirChecksum,
@@ -1216,14 +1234,6 @@ func (e InvalidArtifactError) Error() string {
return "artifact " + Encode(e) + " cannot be cured" return "artifact " + Encode(e) + " cannot be cured"
} }
// DependencyError refers to an artifact with a dependency tree larger than the
// threshold specified by a previous call to [Cache.SetThreshold].
type DependencyError struct{ A Artifact }
func (e DependencyError) Error() string {
return "artifact has too many dependencies"
}
// Cure cures the [Artifact] and returns its pathname and [Checksum]. Direct // Cure cures the [Artifact] and returns its pathname and [Checksum]. Direct
// calls to Cure are not subject to the cures limit. // calls to Cure are not subject to the cures limit.
func (c *Cache) Cure(a Artifact) ( func (c *Cache) Cure(a Artifact) (
@@ -1239,18 +1249,6 @@ func (c *Cache) Cure(a Artifact) (
default: default:
} }
if c.threshold > 0 {
var n uintptr
for range Flood(a) {
if n == c.threshold {
err = DependencyError{a}
return
}
n++
}
c.msg.Verbosef("visited %d artifacts", n)
}
return c.cure(a, true) return c.cure(a, true)
} }
@@ -1474,7 +1472,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
return return
} }
buf := c.getIdentBuf() buf := c.getIdentBuf()
err = Decode((*Checksum)(buf[:]), path.Base(name)) err = Decode((*Checksum)(buf[:]), filepath.Base(name))
if err == nil { if err == nil {
checksum = unique.Make(Checksum(buf[:])) checksum = unique.Make(Checksum(buf[:]))
} }
@@ -1508,6 +1506,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
checksums, checksums,
) )
if c.flags&CAssumeChecksum != 0 {
c.checksumMu.RLock() c.checksumMu.RLock()
checksumFi, err = os.Stat(checksumPathname.String()) checksumFi, err = os.Stat(checksumPathname.String())
c.checksumMu.RUnlock() c.checksumMu.RUnlock()
@@ -1520,6 +1519,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
checksumFi, err = nil, nil checksumFi, err = nil, nil
} }
} }
}
if c.msg.IsVerbose() { if c.msg.IsVerbose() {
rn := reportName(a, id) rn := reportName(a, id)
@@ -1573,7 +1573,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
} }
r, err = f.Cure(&RContext{common{c}}) r, err = f.Cure(&RContext{common{c}})
if err == nil { if err == nil {
if checksumPathname == nil || c.IsStrict() { if checksumPathname == nil || c.flags&CValidateKnown != 0 {
h := sha512.New384() h := sha512.New384()
hbw := c.getWriter(h) hbw := c.getWriter(h)
_, err = io.Copy(w, io.TeeReader(r, hbw)) _, err = io.Copy(w, io.TeeReader(r, hbw))
@@ -1590,7 +1590,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
if checksumPathname == nil { if checksumPathname == nil {
checksum = unique.Make(Checksum(buf[:])) checksum = unique.Make(Checksum(buf[:]))
checksums = Encode(Checksum(buf[:])) checksums = Encode(Checksum(buf[:]))
} else if c.IsStrict() { } else if c.flags&CValidateKnown != 0 {
if got := Checksum(buf[:]); got != checksum.Value() { if got := Checksum(buf[:]); got != checksum.Value() {
err = &ChecksumMismatchError{ err = &ChecksumMismatchError{
Got: got, Got: got,
@@ -1828,10 +1828,10 @@ func (c *Cache) Close() {
func Open( func Open(
ctx context.Context, ctx context.Context,
msg message.Msg, msg message.Msg,
cures int, flags, cures int,
base *check.Absolute, base *check.Absolute,
) (*Cache, error) { ) (*Cache, error) {
return open(ctx, msg, cures, base, true) return open(ctx, msg, flags, cures, base, true)
} }
// open implements Open but allows omitting the [lockedfile] lock when called // open implements Open but allows omitting the [lockedfile] lock when called
@@ -1839,7 +1839,7 @@ func Open(
func open( func open(
ctx context.Context, ctx context.Context,
msg message.Msg, msg message.Msg,
cures int, flags, cures int,
base *check.Absolute, base *check.Absolute,
lock bool, lock bool,
) (*Cache, error) { ) (*Cache, error) {
@@ -1861,6 +1861,7 @@ func open(
c := Cache{ c := Cache{
cures: make(chan struct{}, cures), cures: make(chan struct{}, cures),
flags: flags,
msg: msg, msg: msg,
base: base, base: base,
@@ -1890,3 +1891,33 @@ func open(
return &c, nil return &c, nil
} }
// Collected is returned by [Collect.Cure] to indicate a successful collection.
type Collected struct{}
// Error returns a constant string to satisfy error, but should never be seen
// by the user.
func (Collected) Error() string { return "artifacts successfully collected" }
// IsCollected returns whether the underlying error contains that of the result
// of curing a [Collect] helper.
func IsCollected(err error) bool { return errors.As(err, new(Collected)) }
// Collect implements [pkg.FloodArtifact] to concurrently cure multiple
// [pkg.Artifact]. It returns [Collected].
type Collect []Artifact
// Cure returns [Collected].
func (*Collect) Cure(*FContext) error { return Collected{} }
// Kind returns the hardcoded [pkg.Kind] value.
func (*Collect) Kind() Kind { return kindCollection }
// Params is a noop: dependencies are already represented in the header.
func (*Collect) Params(*IContext) {}
// Dependencies returns [Collect] as is.
func (c *Collect) Dependencies() []Artifact { return *c }
// IsExclusive returns false: Cure is a noop.
func (*Collect) IsExclusive() bool { return false }

View File

@@ -24,6 +24,8 @@ import (
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/internal/info"
"hakurei.app/internal/landlock"
"hakurei.app/internal/pkg" "hakurei.app/internal/pkg"
"hakurei.app/internal/stub" "hakurei.app/internal/stub"
"hakurei.app/message" "hakurei.app/message"
@@ -33,7 +35,7 @@ import (
func unsafeOpen( func unsafeOpen(
ctx context.Context, ctx context.Context,
msg message.Msg, msg message.Msg,
cures int, flags, cures int,
base *check.Absolute, base *check.Absolute,
lock bool, lock bool,
) (*pkg.Cache, error) ) (*pkg.Cache, error)
@@ -228,7 +230,7 @@ func TestIdent(t *testing.T) {
var cache *pkg.Cache var cache *pkg.Cache
if a, err := check.NewAbs(t.TempDir()); err != nil { if a, err := check.NewAbs(t.TempDir()); err != nil {
t.Fatal(err) t.Fatal(err)
} else if cache, err = pkg.Open(t.Context(), msg, 0, a); err != nil { } else if cache, err = pkg.Open(t.Context(), msg, 0, 0, a); err != nil {
t.Fatal(err) t.Fatal(err)
} }
t.Cleanup(cache.Close) t.Cleanup(cache.Close)
@@ -252,6 +254,7 @@ func TestIdent(t *testing.T) {
// on test completion. // on test completion.
type cacheTestCase struct { type cacheTestCase struct {
name string name string
flags int
early func(t *testing.T, base *check.Absolute) early func(t *testing.T, base *check.Absolute)
f func(t *testing.T, base *check.Absolute, c *pkg.Cache) f func(t *testing.T, base *check.Absolute, c *pkg.Cache)
want pkg.Checksum want pkg.Checksum
@@ -288,8 +291,20 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
msg := message.New(log.New(os.Stderr, "cache: ", 0)) msg := message.New(log.New(os.Stderr, "cache: ", 0))
msg.SwapVerbose(testing.Verbose()) msg.SwapVerbose(testing.Verbose())
flags := tc.flags
if info.CanDegrade {
if _, err := landlock.GetABI(); err != nil {
if !errors.Is(err, syscall.ENOSYS) {
t.Fatalf("LandlockGetABI: error = %v", err)
}
flags |= pkg.CHostAbstract
t.Log("Landlock LSM is unavailable, setting CHostAbstract")
}
}
var scrubFunc func() error // scrub after hashing var scrubFunc func() error // scrub after hashing
if c, err := pkg.Open(t.Context(), msg, 1<<4, base); err != nil { if c, err := pkg.Open(t.Context(), msg, flags, 1<<4, base); err != nil {
t.Fatalf("Open: error = %v", err) t.Fatalf("Open: error = %v", err)
} else { } else {
t.Cleanup(c.Close) t.Cleanup(c.Close)
@@ -468,9 +483,7 @@ func TestCache(t *testing.T) {
}() }()
testCases := []cacheTestCase{ testCases := []cacheTestCase{
{"file", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"file", pkg.CValidateKnown | pkg.CAssumeChecksum, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
identifier := (pkg.ID)(bytes.Repeat([]byte{ identifier := (pkg.ID)(bytes.Repeat([]byte{
0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f, 0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f,
}, 8)) }, 8))
@@ -593,7 +606,7 @@ func TestCache(t *testing.T) {
if c0, err := unsafeOpen( if c0, err := unsafeOpen(
t.Context(), t.Context(),
message.New(nil), message.New(nil),
0, base, false, 0, 0, base, false,
); err != nil { ); err != nil {
t.Fatalf("open: error = %v", err) t.Fatalf("open: error = %v", err)
} else { } else {
@@ -627,7 +640,7 @@ func TestCache(t *testing.T) {
} }
}, pkg.MustDecode("St9rlE-mGZ5gXwiv_hzQ_B8bZP-UUvSNmf4nHUZzCMOumb6hKnheZSe0dmnuc4Q2")}, }, pkg.MustDecode("St9rlE-mGZ5gXwiv_hzQ_B8bZP-UUvSNmf4nHUZzCMOumb6hKnheZSe0dmnuc4Q2")},
{"directory", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"directory", pkg.CAssumeChecksum, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
id := pkg.MustDecode( id := pkg.MustDecode(
"HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY", "HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY",
) )
@@ -804,9 +817,7 @@ func TestCache(t *testing.T) {
}) })
}, pkg.MustDecode("WVpvsVqVKg9Nsh744x57h51AuWUoUR2nnh8Md-EYBQpk6ziyTuUn6PLtF2e0Eu_d")}, }, pkg.MustDecode("WVpvsVqVKg9Nsh744x57h51AuWUoUR2nnh8Md-EYBQpk6ziyTuUn6PLtF2e0Eu_d")},
{"pending", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"pending", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
c.SetStrict(true)
wantErr := stub.UniqueError(0xcafe) wantErr := stub.UniqueError(0xcafe)
n, ready := make(chan struct{}), make(chan struct{}) n, ready := make(chan struct{}), make(chan struct{})
go func() { go func() {
@@ -876,7 +887,54 @@ func TestCache(t *testing.T) {
<-wCureDone <-wCureDone
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C")}, }, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C")},
{"scrub", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"no assume checksum", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
makeGarbage := func(work *check.Absolute, wantErr error) error {
if err := os.Mkdir(work.String(), 0700); err != nil {
return err
}
if err := os.WriteFile(work.Append(
"check",
).String(), nil, 0400); err != nil {
return err
}
return wantErr
}
wantChecksum := pkg.MustDecode("Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")
cureMany(t, c, []cureStep{
{"create", overrideChecksum{wantChecksum, overrideIdent{pkg.ID{0xff, 0}, &stubArtifact{
kind: pkg.KindTar,
cure: func(t *pkg.TContext) error {
return makeGarbage(t.GetWorkDir(), nil)
},
}}}, base.Append(
"identifier",
pkg.Encode(pkg.ID{0xff, 0}),
), wantChecksum, nil},
{"reject", overrideChecksum{wantChecksum, overrideIdent{pkg.ID{0xfe, 1}, &stubArtifact{
kind: pkg.KindTar,
cure: func(t *pkg.TContext) error {
return makeGarbage(t.GetWorkDir(), stub.UniqueError(0xbad))
},
}}}, nil, pkg.Checksum{}, stub.UniqueError(0xbad)},
{"match", overrideChecksum{wantChecksum, overrideIdent{pkg.ID{0xff, 1}, &stubArtifact{
kind: pkg.KindTar,
cure: func(t *pkg.TContext) error {
return makeGarbage(t.GetWorkDir(), nil)
},
}}}, base.Append(
"identifier",
pkg.Encode(pkg.ID{0xff, 1}),
), wantChecksum, nil},
})
}, pkg.MustDecode("OC290t23aimNo2Rp2pPwan5GI2KRLRdOwYxXQMD9jw0QROgHnNXWodoWdV0hwu2w")},
{"scrub", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
cureMany(t, c, []cureStep{ cureMany(t, c, []cureStep{
{"bad measured file", newStubFile( {"bad measured file", newStubFile(
pkg.KindHTTPGet, pkg.KindHTTPGet,
@@ -1182,7 +1240,7 @@ func (a earlyFailureF) Cure(*pkg.FContext) error {
func TestDependencyCureErrorEarly(t *testing.T) { func TestDependencyCureErrorEarly(t *testing.T) {
checkWithCache(t, []cacheTestCase{ checkWithCache(t, []cacheTestCase{
{"early", nil, func(t *testing.T, _ *check.Absolute, c *pkg.Cache) { {"early", 0, nil, func(t *testing.T, _ *check.Absolute, c *pkg.Cache) {
_, _, err := c.Cure(earlyFailureF(8)) _, _, err := c.Cure(earlyFailureF(8))
if !errors.Is(err, stub.UniqueError(0xcafe)) { if !errors.Is(err, stub.UniqueError(0xcafe)) {
t.Fatalf("Cure: error = %v", err) t.Fatalf("Cure: error = %v", err)
@@ -1205,7 +1263,7 @@ func TestNew(t *testing.T) {
if _, err := pkg.Open( if _, err := pkg.Open(
t.Context(), t.Context(),
message.New(nil), message.New(nil),
0, check.MustAbs(container.Nonexistent), 0, 0, check.MustAbs(container.Nonexistent),
); !reflect.DeepEqual(err, wantErr) { ); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Open: error = %#v, want %#v", err, wantErr) t.Errorf("Open: error = %#v, want %#v", err, wantErr)
} }
@@ -1233,7 +1291,7 @@ func TestNew(t *testing.T) {
if _, err := pkg.Open( if _, err := pkg.Open(
t.Context(), t.Context(),
message.New(nil), message.New(nil),
0, tempDir.Append("cache"), 0, 0, tempDir.Append("cache"),
); !reflect.DeepEqual(err, wantErr) { ); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Open: error = %#v, want %#v", err, wantErr) t.Errorf("Open: error = %#v, want %#v", err, wantErr)
} }

View File

@@ -10,7 +10,7 @@ import (
"io/fs" "io/fs"
"net/http" "net/http"
"os" "os"
"path" "path/filepath"
) )
const ( const (
@@ -169,7 +169,7 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
} }
if typeflag >= '0' && typeflag <= '9' && typeflag != tar.TypeDir { if typeflag >= '0' && typeflag <= '9' && typeflag != tar.TypeDir {
if err = root.MkdirAll(path.Dir(header.Name), 0700); err != nil { if err = root.MkdirAll(filepath.Dir(header.Name), 0700); err != nil {
return return
} }
} }

View File

@@ -21,7 +21,7 @@ func TestTar(t *testing.T) {
t.Parallel() t.Parallel()
checkWithCache(t, []cacheTestCase{ checkWithCache(t, []cacheTestCase{
{"http", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"http", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
checkTarHTTP(t, base, c, fstest.MapFS{ checkTarHTTP(t, base, c, fstest.MapFS{
".": {Mode: fs.ModeDir | 0700}, ".": {Mode: fs.ModeDir | 0700},
@@ -42,7 +42,7 @@ func TestTar(t *testing.T) {
)) ))
}, pkg.MustDecode("NQTlc466JmSVLIyWklm_u8_g95jEEb98PxJU-kjwxLpfdjwMWJq0G8ze9R4Vo1Vu")}, }, pkg.MustDecode("NQTlc466JmSVLIyWklm_u8_g95jEEb98PxJU-kjwxLpfdjwMWJq0G8ze9R4Vo1Vu")},
{"http expand", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { {"http expand", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
checkTarHTTP(t, base, c, fstest.MapFS{ checkTarHTTP(t, base, c, fstest.MapFS{
".": {Mode: fs.ModeDir | 0700}, ".": {Mode: fs.ModeDir | 0700},

View File

@@ -7,7 +7,7 @@ import (
"log" "log"
"net" "net"
"os" "os"
"path" "path/filepath"
"reflect" "reflect"
"slices" "slices"
"strings" "strings"
@@ -68,7 +68,7 @@ func main() {
if got, err := os.Executable(); err != nil { if got, err := os.Executable(); err != nil {
log.Fatalf("Executable: error = %v", err) log.Fatalf("Executable: error = %v", err)
} else { } else {
iftPath = path.Join(path.Dir(path.Dir(got)), "ift") iftPath = filepath.Join(filepath.Dir(filepath.Dir(got)), "ift")
if got != wantExec { if got != wantExec {
switch got { switch got {
@@ -161,7 +161,7 @@ func main() {
} }
} }
if !layers { if !layers {
if path.Base(lowerdir) != checksumEmptyDir { if filepath.Base(lowerdir) != checksumEmptyDir {
log.Fatal("unexpected artifact checksum") log.Fatal("unexpected artifact checksum")
} }
} else { } else {
@@ -187,8 +187,8 @@ func main() {
} }
if len(lowerdirs) != 2 || if len(lowerdirs) != 2 ||
path.Base(lowerdirs[0]) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" || filepath.Base(lowerdirs[0]) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" ||
path.Base(lowerdirs[1]) != "nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK" { filepath.Base(lowerdirs[1]) != "nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK" {
log.Fatalf("unexpected lowerdirs %s", strings.Join(lowerdirs, ", ")) log.Fatalf("unexpected lowerdirs %s", strings.Join(lowerdirs, ", "))
} }
} }
@@ -202,12 +202,12 @@ func main() {
} }
next() next()
if path.Base(m.Root) != "OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb" { if filepath.Base(m.Root) != "OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb" {
log.Fatal("unexpected file artifact checksum") log.Fatal("unexpected file artifact checksum")
} }
next() next()
if path.Base(m.Root) != checksumEmptyDir { if filepath.Base(m.Root) != checksumEmptyDir {
log.Fatal("unexpected artifact checksum") log.Fatal("unexpected artifact checksum")
} }
} }
@@ -226,13 +226,13 @@ func main() {
log.Fatal("unexpected work mount entry") log.Fatal("unexpected work mount entry")
} }
} else { } else {
if path.Base(m.Root) != ident || m.Target != "/work" { if filepath.Base(m.Root) != ident || m.Target != "/work" {
log.Fatal("unexpected work mount entry") log.Fatal("unexpected work mount entry")
} }
} }
next() next()
if path.Base(m.Root) != ident || m.Target != "/tmp" { if filepath.Base(m.Root) != ident || m.Target != "/tmp" {
log.Fatal("unexpected temp mount entry") log.Fatal("unexpected temp mount entry")
} }

View File

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

View File

@@ -47,8 +47,10 @@ const (
Bison Bison
Bzip2 Bzip2
CMake CMake
Connman
Coreutils Coreutils
Curl Curl
DBus
DTC DTC
Diffutils Diffutils
Elfutils Elfutils
@@ -62,23 +64,32 @@ const (
GenInitCPIO GenInitCPIO
Gettext Gettext
Git Git
GnuTLS
Go Go
Gperf Gperf
Grep Grep
Gzip Gzip
Hakurei Hakurei
HakureiDist HakureiDist
IPTables
Kmod Kmod
LibXau LibXau
Libbsd
Libcap Libcap
Libev
Libexpat Libexpat
Libiconv
Libpsl
Libffi Libffi
Libgd Libgd
Libtool Libiconv
Libmd
Libmnl
Libnftnl
Libpsl
Libseccomp Libseccomp
Libtasn1
Libtool
Libucontext Libucontext
Libunistring
Libxml2 Libxml2
Libxslt Libxslt
M4 M4
@@ -95,6 +106,7 @@ const (
Nettle Nettle
Ninja Ninja
OpenSSL OpenSSL
P11Kit
PCRE2 PCRE2
Parallel Parallel
Patch Patch
@@ -119,6 +131,7 @@ const (
PythonPygments PythonPygments
QEMU QEMU
Rdfind Rdfind
Readline
Rsync Rsync
Sed Sed
Setuptools Setuptools
@@ -153,6 +166,9 @@ const (
// stages only. This preset and its direct output must never be exposed. // stages only. This preset and its direct output must never be exposed.
gcc gcc
// nettle3 is an older version of [Nettle].
nettle3
// Stage0 is a tarball containing all compile-time dependencies of artifacts // Stage0 is a tarball containing all compile-time dependencies of artifacts
// part of the [Std] toolchain. // part of the [Std] toolchain.
Stage0 Stage0
@@ -291,6 +307,17 @@ var (
artifactsOnce [_toolchainEnd][len(artifactsM)]sync.Once artifactsOnce [_toolchainEnd][len(artifactsM)]sync.Once
) )
// zero zeros the value pointed to by p.
func zero[T any](p *T) { var v T; *p = v }
// DropCaches arranges for all cached [pkg.Artifact] to be freed some time after
// it returns. Must not be used concurrently with any other function from this
// package.
func DropCaches() {
zero(&artifacts)
zero(&artifactsOnce)
}
// GetMetadata returns [Metadata] of a [PArtifact]. // GetMetadata returns [Metadata] of a [PArtifact].
func GetMetadata(p PArtifact) *Metadata { return &artifactsM[p] } func GetMetadata(p PArtifact) *Metadata { return &artifactsM[p] }

View File

@@ -19,6 +19,18 @@ func TestLoad(t *testing.T) {
} }
} }
func BenchmarkAll(b *testing.B) {
for b.Loop() {
for i := range rosa.PresetEnd {
rosa.Std.Load(rosa.PArtifact(i))
}
b.StopTimer()
rosa.DropCaches()
b.StartTimer()
}
}
func TestResolveName(t *testing.T) { func TestResolveName(t *testing.T) {
t.Parallel() t.Parallel()

View File

@@ -1,7 +1,7 @@
package rosa package rosa
import ( import (
"path" "path/filepath"
"slices" "slices"
"strings" "strings"
@@ -10,8 +10,8 @@ import (
func (t Toolchain) newCMake() (pkg.Artifact, string) { func (t Toolchain) newCMake() (pkg.Artifact, string) {
const ( const (
version = "4.2.3" version = "4.3.1"
checksum = "Y4uYGnLrDQX78UdzH7fMzfok46Nt_1taDIHSmqgboU1yFi6f0iAXBDegMCu4eS-J" checksum = "RHpzZiM1kJ5bwLjo9CpXSeHJJg3hTtV9QxBYpQoYwKFtRh5YhGWpShrqZCSOzQN6"
) )
return t.NewPackage("cmake", version, pkg.NewHTTPGetTar( return t.NewPackage("cmake", version, pkg.NewHTTPGetTar(
nil, "https://github.com/Kitware/CMake/releases/download/"+ nil, "https://github.com/Kitware/CMake/releases/download/"+
@@ -25,7 +25,7 @@ func (t Toolchain) newCMake() (pkg.Artifact, string) {
// expected to be writable in the copy made during bootstrap // expected to be writable in the copy made during bootstrap
Chmod: true, Chmod: true,
Patches: [][2]string{ Patches: []KV{
{"bootstrap-test-no-openssl", `diff --git a/Tests/BootstrapTest.cmake b/Tests/BootstrapTest.cmake {"bootstrap-test-no-openssl", `diff --git a/Tests/BootstrapTest.cmake b/Tests/BootstrapTest.cmake
index 137de78bc1..b4da52e664 100644 index 137de78bc1..b4da52e664 100644
--- a/Tests/BootstrapTest.cmake --- a/Tests/BootstrapTest.cmake
@@ -88,7 +88,7 @@ index 2ead810437..f85cbb8b1c 100644
OmitDefaults: true, OmitDefaults: true,
ConfigureName: "/usr/src/cmake/bootstrap", ConfigureName: "/usr/src/cmake/bootstrap",
Configure: [][2]string{ Configure: []KV{
{"prefix", "/system"}, {"prefix", "/system"},
{"parallel", `"$(nproc)"`}, {"parallel", `"$(nproc)"`},
{"--"}, {"--"},
@@ -125,7 +125,7 @@ type CMakeHelper struct {
Append []string Append []string
// CMake CACHE entries. // CMake CACHE entries.
Cache [][2]string Cache []KV
// Runs after install. // Runs after install.
Script string Script string
@@ -144,11 +144,11 @@ func (attr *CMakeHelper) name(name, version string) string {
} }
// extra returns a hardcoded slice of [CMake] and [Ninja]. // extra returns a hardcoded slice of [CMake] and [Ninja].
func (attr *CMakeHelper) extra(int) []PArtifact { func (attr *CMakeHelper) extra(int) P {
if attr != nil && attr.Make { if attr != nil && attr.Make {
return []PArtifact{CMake, Make} return P{CMake, Make}
} }
return []PArtifact{CMake, Ninja} return P{CMake, Ninja}
} }
// wantsChmod returns false. // wantsChmod returns false.
@@ -170,7 +170,7 @@ func (*CMakeHelper) wantsDir() string { return "/cure/" }
func (attr *CMakeHelper) script(name string) string { func (attr *CMakeHelper) script(name string) string {
if attr == nil { if attr == nil {
attr = &CMakeHelper{ attr = &CMakeHelper{
Cache: [][2]string{ Cache: []KV{
{"CMAKE_BUILD_TYPE", "Release"}, {"CMAKE_BUILD_TYPE", "Release"},
}, },
} }
@@ -200,7 +200,7 @@ cmake -G ` + generate + ` \
} }
}), " \\\n\t") + ` \ }), " \\\n\t") + ` \
-DCMAKE_INSTALL_PREFIX=/system \ -DCMAKE_INSTALL_PREFIX=/system \
'/usr/src/` + name + `/` + path.Join(attr.Append...) + `' '/usr/src/` + name + `/` + filepath.Join(attr.Append...) + `'
cmake --build .` + jobs + ` cmake --build .` + jobs + `
cmake --install . --prefix=/work/system cmake --install . --prefix=/work/system
` + attr.Script ` + attr.Script

109
internal/rosa/connman.go Normal file
View File

@@ -0,0 +1,109 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newConnman() (pkg.Artifact, string) {
const (
version = "2.0"
checksum = "MhVTdJOhndnZn2SWd8URKo_Pj7Zvc14tntEbrVOf9L3yVWJvpb3v3Q6104tWJgtW"
)
return t.NewPackage("connman", version, pkg.NewHTTPGetTar(
nil, "https://git.kernel.org/pub/scm/network/connman/connman.git/"+
"snapshot/connman-"+version+".tar.gz",
mustDecode(checksum),
pkg.TarGzip,
), &PackageAttr{
Patches: []KV{
{"alpine-musl-res", `musl does not implement res_ninit
--- a/gweb/gresolv.c
+++ b/gweb/gresolv.c
@@ -877,8 +877,6 @@
resolv->index = index;
resolv->nameserver_list = NULL;
- res_ninit(&resolv->res);
-
return resolv;
}
@@ -918,8 +916,6 @@
flush_nameservers(resolv);
- res_nclose(&resolv->res);
-
g_free(resolv);
}
@@ -1022,24 +1018,19 @@
debug(resolv, "hostname %s", hostname);
if (!resolv->nameserver_list) {
- int i;
-
- for (i = 0; i < resolv->res.nscount; i++) {
- char buf[100];
- int family = resolv->res.nsaddr_list[i].sin_family;
- void *sa_addr = &resolv->res.nsaddr_list[i].sin_addr;
-
- if (family != AF_INET &&
- resolv->res._u._ext.nsaddrs[i]) {
- family = AF_INET6;
- sa_addr = &resolv->res._u._ext.nsaddrs[i]->sin6_addr;
+ FILE *f = fopen("/etc/resolv.conf", "r");
+ if (f) {
+ char line[256], *s;
+ int i;
+ while (fgets(line, sizeof(line), f)) {
+ if (strncmp(line, "nameserver", 10) || !isspace(line[10]))
+ continue;
+ for (s = &line[11]; isspace(s[0]); s++);
+ for (i = 0; s[i] && !isspace(s[i]); i++);
+ s[i] = 0;
+ g_resolv_add_nameserver(resolv, s, 53, 0);
}
-
- if (family != AF_INET && family != AF_INET6)
- continue;
-
- if (inet_ntop(family, sa_addr, buf, sizeof(buf)))
- g_resolv_add_nameserver(resolv, buf, 53, 0);
+ fclose(f);
}
if (!resolv->nameserver_list)
`},
},
}, &MakeHelper{
Generate: "./bootstrap",
},
Automake,
Libtool,
PkgConfig,
DBus,
IPTables,
GnuTLS,
Readline,
KernelHeaders,
), version
}
func init() {
artifactsM[Connman] = Metadata{
f: Toolchain.newConnman,
Name: "connman",
Description: "a daemon for managing Internet connections",
Website: "https://git.kernel.org/pub/scm/network/connman/connman.git/",
Dependencies: P{
DBus,
IPTables,
GnuTLS,
Readline,
},
ID: 337,
}
}

View File

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

46
internal/rosa/dbus.go Normal file
View File

@@ -0,0 +1,46 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newDBus() (pkg.Artifact, string) {
const (
version = "1.16.2"
checksum = "INwOuNdrDG7XW5ilW_vn8JSxEa444rRNc5ho97i84I1CNF09OmcFcV-gzbF4uCyg"
)
return t.NewPackage("dbus", version, pkg.NewHTTPGetTar(
nil, "https://gitlab.freedesktop.org/dbus/dbus/-/archive/"+
"dbus-"+version+"/dbus-dbus-"+version+".tar.bz2",
mustDecode(checksum),
pkg.TarBzip2,
), &PackageAttr{
// OSError: [Errno 30] Read-only file system: '/usr/src/dbus/subprojects/packagecache'
Writable: true,
// PermissionError: [Errno 13] Permission denied: '/usr/src/dbus/subprojects/packagecache'
Chmod: true,
}, &MesonHelper{
Setup: []KV{
{"Depoll", "enabled"},
{"Dinotify", "enabled"},
{"Dx11_autolaunch", "disabled"},
},
},
GLib,
Libexpat,
), version
}
func init() {
artifactsM[DBus] = Metadata{
f: Toolchain.newDBus,
Name: "dbus",
Description: "a message bus system",
Website: "https://www.freedesktop.org/wiki/Software/dbus/",
Dependencies: P{
GLib,
Libexpat,
},
ID: 5356,
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ func (t Toolchain) newFuse() (pkg.Artifact, string) {
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, pkg.TarGzip,
), nil, &MesonHelper{ ), nil, &MesonHelper{
Setup: [][2]string{ Setup: []KV{
{"Ddefault_library", "both"}, {"Ddefault_library", "both"},
{"Dtests", "true"}, {"Dtests", "true"},
{"Duseroot", "false"}, {"Duseroot", "false"},

View File

@@ -1,6 +1,11 @@
package rosa package rosa
import "hakurei.app/internal/pkg" import (
"path"
"strings"
"hakurei.app/internal/pkg"
)
func (t Toolchain) newGit() (pkg.Artifact, string) { func (t Toolchain) newGit() (pkg.Artifact, string) {
const ( const (
@@ -87,17 +92,23 @@ func init() {
// NewViaGit returns a [pkg.Artifact] for cloning a git repository. // NewViaGit returns a [pkg.Artifact] for cloning a git repository.
func (t Toolchain) NewViaGit( func (t Toolchain) NewViaGit(
name, url, rev string, url, rev string,
checksum pkg.Checksum, checksum pkg.Checksum,
) pkg.Artifact { ) pkg.Artifact {
return t.New(name+"-"+rev, 0, t.AppendPresets(nil, return t.New(strings.TrimSuffix(
path.Base(url),
".git",
)+"-src-"+path.Base(rev), 0, t.AppendPresets(nil,
NSSCACert, NSSCACert,
Git, Git,
), &checksum, nil, ` ), &checksum, nil, `
git \ git \
-c advice.detachedHead=false \ -c advice.detachedHead=false \
clone \ clone \
--depth=1 \
--revision=`+rev+` \ --revision=`+rev+` \
--shallow-submodules \
--recurse-submodules \
`+url+` \ `+url+` \
/work /work
rm -rf /work/.git rm -rf /work/.git

View File

@@ -1,6 +1,10 @@
package rosa package rosa
import "hakurei.app/internal/pkg" import (
"runtime"
"hakurei.app/internal/pkg"
)
func (t Toolchain) newM4() (pkg.Artifact, string) { func (t Toolchain) newM4() (pkg.Artifact, string) {
const ( const (
@@ -88,8 +92,8 @@ func init() {
func (t Toolchain) newAutoconf() (pkg.Artifact, string) { func (t Toolchain) newAutoconf() (pkg.Artifact, string) {
const ( const (
version = "2.72" version = "2.73"
checksum = "-c5blYkC-xLDer3TWEqJTyh1RLbOd1c5dnRLKsDnIrg_wWNOLBpaqMY8FvmUFJ33" checksum = "yGabDTeOfaCUB0JX-h3REYLYzMzvpDwFmFFzHNR7QilChCUNE4hR6q7nma4viDYg"
) )
return t.NewPackage("autoconf", version, pkg.NewHTTPGetTar( return t.NewPackage("autoconf", version, pkg.NewHTTPGetTar(
nil, "https://ftpmirror.gnu.org/gnu/autoconf/autoconf-"+version+".tar.gz", nil, "https://ftpmirror.gnu.org/gnu/autoconf/autoconf-"+version+".tar.gz",
@@ -351,7 +355,7 @@ func (t Toolchain) newBash() (pkg.Artifact, string) {
Flag: TEarly, Flag: TEarly,
}, &MakeHelper{ }, &MakeHelper{
Script: "ln -s bash /work/system/bin/sh\n", Script: "ln -s bash /work/system/bin/sh\n",
Configure: [][2]string{ Configure: []KV{
{"without-bash-malloc"}, {"without-bash-malloc"},
}, },
}), version }), version
@@ -390,7 +394,7 @@ test_disable 'int main(){return 0;}' gnulib-tests/test-fchownat.c
test_disable 'int main(){return 0;}' gnulib-tests/test-lchown.c test_disable 'int main(){return 0;}' gnulib-tests/test-lchown.c
`, `,
Patches: [][2]string{ Patches: []KV{
{"tests-fix-job-control", `From 21d287324aa43aa3a31f39619ade0deac7fd6013 Mon Sep 17 00:00:00 2001 {"tests-fix-job-control", `From 21d287324aa43aa3a31f39619ade0deac7fd6013 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <P@draigBrady.com> From: =?UTF-8?q?P=C3=A1draig=20Brady?= <P@draigBrady.com>
Date: Tue, 24 Feb 2026 15:44:41 +0000 Date: Tue, 24 Feb 2026 15:44:41 +0000
@@ -485,7 +489,7 @@ index 9a395416b..fbb043312 100755
Flag: TEarly, Flag: TEarly,
}, &MakeHelper{ }, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
{"enable-single-binary", "symlinks"}, {"enable-single-binary", "symlinks"},
}, },
}, },
@@ -720,7 +724,7 @@ func (t Toolchain) newTar() (pkg.Artifact, string) {
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, pkg.TarGzip,
), nil, &MakeHelper{ ), nil, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
{"disable-acl"}, {"disable-acl"},
{"without-posix-acls"}, {"without-posix-acls"},
{"without-xattrs"}, {"without-xattrs"},
@@ -754,8 +758,8 @@ func init() {
func (t Toolchain) newParallel() (pkg.Artifact, string) { func (t Toolchain) newParallel() (pkg.Artifact, string) {
const ( const (
version = "20260222" version = "20260322"
checksum = "4wxjMi3G2zMxr9hvLcIn6D7_12A3e5UNObeTPhzn7mDAYwsZApmmkxfGPyllQQ7E" checksum = "gHoPmFkOO62ev4xW59HqyMlodhjp8LvTsBOwsVKHUUdfrt7KwB8koXmSVqQ4VOrB"
) )
return t.NewPackage("parallel", version, pkg.NewHTTPGetTar( return t.NewPackage("parallel", version, pkg.NewHTTPGetTar(
nil, "https://ftpmirror.gnu.org/gnu/parallel/parallel-"+version+".tar.bz2", nil, "https://ftpmirror.gnu.org/gnu/parallel/parallel-"+version+".tar.bz2",
@@ -781,6 +785,278 @@ func init() {
} }
} }
func (t Toolchain) newLibunistring() (pkg.Artifact, string) {
const (
version = "1.4.2"
checksum = "iW9BbfLoVlXjWoLTZ4AekQSu4cFBnLcZ4W8OHWbv0AhJNgD3j65_zqaLMzFKylg2"
)
return t.NewPackage("libunistring", version, pkg.NewHTTPGetTar(
nil, "https://ftp.gnu.org/gnu/libunistring/libunistring-"+version+".tar.gz",
mustDecode(checksum),
pkg.TarGzip,
), &PackageAttr{
Writable: true,
ScriptEarly: `
test_disable() { chmod +w "$2" && echo "$1" > "$2"; }
test_disable '#!/bin/sh' tests/test-c32ispunct.sh
test_disable 'int main(){return 0;}' tests/test-c32ispunct.c
`,
}, (*MakeHelper)(nil),
Diffutils,
), version
}
func init() {
artifactsM[Libunistring] = Metadata{
f: Toolchain.newLibunistring,
Name: "libunistring",
Description: "provides functions for manipulating Unicode strings",
Website: "https://www.gnu.org/software/libunistring/",
ID: 1747,
}
}
func (t Toolchain) newLibtasn1() (pkg.Artifact, string) {
const (
version = "4.21.0"
checksum = "9DYI3UYbfYLy8JsKUcY6f0irskbfL0fHZA91Q-JEOA3kiUwpodyjemRsYRjUpjuq"
)
return t.NewPackage("libtasn1", version, pkg.NewHTTPGetTar(
nil, "https://ftpmirror.gnu.org/gnu/libtasn1/libtasn1-"+version+".tar.gz",
mustDecode(checksum),
pkg.TarGzip,
), nil, (*MakeHelper)(nil)), version
}
func init() {
artifactsM[Libtasn1] = Metadata{
f: Toolchain.newLibtasn1,
Name: "libtasn1",
Description: "the ASN.1 library used by GnuTLS, p11-kit and some other packages",
Website: "https://www.gnu.org/software/libtasn1/",
ID: 1734,
}
}
func (t Toolchain) newReadline() (pkg.Artifact, string) {
const (
version = "8.3"
checksum = "r-lcGRJq_MvvBpOq47Z2Y1OI2iqrmtcqhTLVXR0xWo37ZpC2uT_md7gKq5o_qTMV"
)
return t.NewPackage("readline", version, pkg.NewHTTPGetTar(
nil, "https://ftp.gnu.org/gnu/readline/readline-"+version+".tar.gz",
mustDecode(checksum),
pkg.TarGzip,
), nil, &MakeHelper{
Configure: []KV{
{"with-curses"},
{"with-shared-termcap-library"},
},
},
Ncurses,
), version
}
func init() {
artifactsM[Readline] = Metadata{
f: Toolchain.newReadline,
Name: "readline",
Description: "provides a set of functions for use by applications that allow users to edit command lines as they are typed in",
Website: "https://tiswww.cwru.edu/php/chet/readline/rltop.html",
Dependencies: P{
Ncurses,
},
ID: 4173,
}
}
func (t Toolchain) newGnuTLS() (pkg.Artifact, string) {
const (
version = "3.8.12"
checksum = "VPdP-nRydQQRJcnma-YA7CJYA_kzTJ2rb3QFeP6D27emSyInJ8sQ-Wzn518I38dl"
)
var configureExtra []KV
switch runtime.GOARCH {
case "arm64":
configureExtra = []KV{
{"disable-hardware-acceleration"},
}
}
return t.NewPackage("gnutls", version, t.NewViaGit(
"https://gitlab.com/gnutls/gnutls.git",
"refs/tags/"+version,
mustDecode(checksum),
), &PackageAttr{
Patches: []KV{
{"bootstrap-remove-gtk-doc", `diff --git a/bootstrap.conf b/bootstrap.conf
index 1c3cc61e6..32bae9387 100644
--- a/bootstrap.conf
+++ b/bootstrap.conf
@@ -50,7 +50,6 @@ bison 2.4
gettext 0.17
git 1.4.4
gperf -
-gtkdocize -
perl 5.5
wget -
"
diff --git a/configure.ac b/configure.ac
index 5057536e5..731558a15 100644
--- a/configure.ac
+++ b/configure.ac
@@ -403,11 +403,6 @@ if test "$enable_fuzzer_target" != "no";then
AC_DEFINE([FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION], 1, [Enable fuzzer target -not for production])
fi
-dnl
-dnl check for gtk-doc
-dnl
-GTK_DOC_CHECK([1.14],[--flavour no-tmpl])
-
AM_GNU_GETTEXT([external])
AM_GNU_GETTEXT_VERSION([0.19])
m4_ifdef([AM_GNU_GET][TEXT_REQUIRE_VERSION],[
diff --git a/doc/Makefile.am b/doc/Makefile.am
index fb1390d70..52f0ad9af 100644
--- a/doc/Makefile.am
+++ b/doc/Makefile.am
@@ -33,9 +33,6 @@ IMAGES = \
pkcs11-vision.png
SUBDIRS = examples scripts credentials latex
-if ENABLE_GTK_DOC
-SUBDIRS += reference
-endif
-include $(top_srcdir)/doc/doc.mk
diff --git a/doc/reference/Makefile.am b/doc/reference/Makefile.am
index f10c8ed3c..b711b58ec 100644
--- a/doc/reference/Makefile.am
+++ b/doc/reference/Makefile.am
@@ -82,13 +82,4 @@ include $(top_srcdir)/gtk-doc.make
# e.g. EXTRA_DIST += version.xml.in
EXTRA_DIST += version.xml.in
-# Comment this out if you want 'make check' to test you doc status
-# and run some sanity checks
-if ENABLE_GTK_DOC
-TESTS_ENVIRONMENT = \
- DOC_MODULE=$(DOC_MODULE) DOC_MAIN_SGML_FILE=$(DOC_MAIN_SGML_FILE) \
- SRCDIR=$(abs_srcdir) BUILDDIR=$(abs_builddir)
-#TESTS = $(GTKDOC_CHECK)
-endif
-
-include $(top_srcdir)/git.mk
`},
{"alpine-tests-certtool", `I think this tests is simply wrong.
When a PIN is given, the program should run in batch mode.
So the question for "Enter password" should _not_ be present.
DO NOT REMOVE UNLESS VERIFIED IT'S NOT ACTUALLY NECESSARY ANYMORE.
--- a/tests/cert-tests/certtool.sh 2019-02-07 07:33:45.960887338 +0000
+++ b/tests/cert-tests/certtool.sh 2019-02-07 07:36:14.550955051 +0000
@@ -49,7 +49,7 @@
#check whether password is being honoured
#some CI runners need GNUTLS_PIN (GNUTLS_PIN=${PASS})
- ${SETSID} "${CERTTOOL}" --generate-self-signed --load-privkey ${TMPFILE1} --template ${srcdir}/templates/template-test.tmpl --ask-pass >${TMPFILE2} 2>&1 <<EOF
+ GNUTLS_PIN=${PASS} ${SETSID} "${CERTTOOL}" --generate-self-signed --load-privkey ${TMPFILE1} --template ${srcdir}/templates/template-test.tmpl --ask-pass >${TMPFILE2} 2>&1 <<EOF
$PASS
EOF
if test $? != 0;then
@@ -59,7 +59,7 @@
fi
grep "Enter password" ${TMPFILE2} >/dev/null 2>&1
- if test $? != 0;then
+ if test $? != 1; then
cat ${TMPFILE2}
echo "No password was asked"
exit 1
`},
{"test-kernel-version-ksh", `diff --git a/tests/scripts/common.sh b/tests/scripts/common.sh
index 1b78b8cf1..350156a86 100644
--- a/tests/scripts/common.sh
+++ b/tests/scripts/common.sh
@@ -279,10 +279,6 @@ kernel_version_check() {
kernel_major=$(echo $kernel_version | cut -d. -f1 2>/dev/null)
kernel_minor=$(echo $kernel_version | cut -d. -f2 2>/dev/null)
- if ! [[ "$kernel_major" =~ ^[0-9]+$ ]] || ! [[ "$kernel_minor" =~ ^[0-9]+$ ]]; then
- return 1
- fi
-
if [ "$kernel_major" -lt "$required_major" ]; then
return 1
fi
`},
},
}, &MakeHelper{
Generate: "./bootstrap --skip-po --no-git --gnulib-srcdir=gnulib",
Configure: append([]KV{
{"disable-doc"},
{"disable-openssl-compatibility"},
{"with-default-trust-store-file", "/system/etc/ssl/certs/ca-bundle.crt"},
{"with-default-trust-store-pkcs11", "pkcs11:"},
{"with-zlib", "link"},
{"with-zstd", "link"},
}, configureExtra...),
},
Gzip,
Automake,
Libtool,
Bison,
Gettext,
Gperf,
PkgConfig,
Python,
Texinfo,
Diffutils,
NSSCACert,
Libev,
Zlib,
Zstd,
P11Kit,
nettle3,
Libunistring,
), version
}
func init() {
artifactsM[GnuTLS] = Metadata{
f: Toolchain.newGnuTLS,
Name: "gnutls",
Description: "a secure communications library implementing the SSL, TLS and DTLS protocols",
Website: "https://gnutls.org",
Dependencies: P{
Zlib,
Zstd,
P11Kit,
nettle3,
Libunistring,
},
ID: 1221,
}
}
func (t Toolchain) newBinutils() (pkg.Artifact, string) { func (t Toolchain) newBinutils() (pkg.Artifact, string) {
const ( const (
version = "2.46.0" version = "2.46.0"
@@ -864,15 +1140,24 @@ func init() {
func (t Toolchain) newMPC() (pkg.Artifact, string) { func (t Toolchain) newMPC() (pkg.Artifact, string) {
const ( const (
version = "1.3.1" version = "1.4.0"
checksum = "o8r8K9R4x7PuRx0-JE3-bC5jZQrtxGV2nkB773aqJ3uaxOiBDCID1gKjPaaDxX4V" checksum = "TbrxLiE3ipQrHz_F3Xzz4zqBAnkMWyjhNwIK6wh9360RZ39xMt8rxfW3LxA9SnvU"
) )
return t.NewPackage("mpc", version, pkg.NewHTTPGetTar( return t.NewPackage("mpc", version, t.NewViaGit(
nil, "https://gcc.gnu.org/pub/gcc/infrastructure/"+ "https://gitlab.inria.fr/mpc/mpc.git",
"mpc-"+version+".tar.gz", "refs/tags/"+version,
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, ), &PackageAttr{
), nil, (*MakeHelper)(nil), // does not find mpc-impl.h otherwise
EnterSource: true,
}, &MakeHelper{
InPlace: true,
Generate: "autoreconf -vfi",
},
Automake,
Libtool,
Texinfo,
MPFR, MPFR,
), version ), version
} }
@@ -903,7 +1188,7 @@ func (t Toolchain) newGCC() (pkg.Artifact, string) {
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, pkg.TarGzip,
), &PackageAttr{ ), &PackageAttr{
Patches: [][2]string{ Patches: []KV{
{"musl-off64_t-loff_t", `diff --git a/libgo/sysinfo.c b/libgo/sysinfo.c {"musl-off64_t-loff_t", `diff --git a/libgo/sysinfo.c b/libgo/sysinfo.c
index 180f5c31d74..44d7ea73f7d 100644 index 180f5c31d74..44d7ea73f7d 100644
--- a/libgo/sysinfo.c --- a/libgo/sysinfo.c
@@ -1062,7 +1347,7 @@ ln -s system/lib /work/
// it also saturates the CPU for a consequential amount of time. // it also saturates the CPU for a consequential amount of time.
Flag: TExclusive, Flag: TExclusive,
}, &MakeHelper{ }, &MakeHelper{
Configure: [][2]string{ Configure: []KV{
{"disable-multilib"}, {"disable-multilib"},
{"with-multilib-list", `""`}, {"with-multilib-list", `""`},
{"enable-default-pie"}, {"enable-default-pie"},

View File

@@ -73,7 +73,7 @@ func (t Toolchain) newGoLatest() (pkg.Artifact, string) {
case "amd64": case "amd64":
bootstrapExtra = append(bootstrapExtra, t.newGoBootstrap()) bootstrapExtra = append(bootstrapExtra, t.newGoBootstrap())
case "arm64": case "arm64", "riscv64":
bootstrapEnv = append(bootstrapEnv, "GOROOT_BOOTSTRAP=/system") bootstrapEnv = append(bootstrapEnv, "GOROOT_BOOTSTRAP=/system")
bootstrapExtra = t.AppendPresets(bootstrapExtra, gcc) bootstrapExtra = t.AppendPresets(bootstrapExtra, gcc)
finalEnv = append(finalEnv, "CGO_ENABLED=0") finalEnv = append(finalEnv, "CGO_ENABLED=0")
@@ -135,13 +135,14 @@ sed -i \
cmd/link/internal/`+runtime.GOARCH+`/obj.go cmd/link/internal/`+runtime.GOARCH+`/obj.go
rm \ rm \
os/root_unix_test.go os/root_unix_test.go \
net/smtp/smtp_test.go
`, go123, `, go123,
) )
const ( const (
version = "1.26.1" version = "1.26.2"
checksum = "DdC5Ea-aCYPUHNObQh_09uWU0vn4e-8Ben850Vq-5OoamDRrXhuYI4YQ_BOFgaT0" checksum = "v-6BE89_1g3xYf-9oIYpJKFXlo3xKHYJj2_VGkaUq8ZVkIVQmLwrto-xGG03OISH"
) )
return t.newGo( return t.newGo(
version, version,

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