434 Commits

Author SHA1 Message Date
mae
b255f07b0f Merge remote-tracking branch 'origin/pkgserver' into pkgserver 2026-03-05 02:06:27 -06:00
mae
dec4cdd068 cmd/pkgserver: replace favicon 2026-03-05 02:06:07 -06:00
mae
73c620ecd5 cmd/pkgserver: pagination 2026-03-05 02:06:07 -06:00
mae
69467a1542 cmd/pkgserver: basic web ui 2026-03-05 02:06:07 -06:00
ebdf9dcecc cmd/mbf: preset status command
This exposes the new OpenStatus cache method.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-05 16:59:47 +09:00
8ea2a56d5b internal/pkg: expose status file
This is useful for external tooling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-05 16:58:52 +09:00
159a45c027 internal/rosa: export preset bounds
These are useful for external tooling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-05 16:34:25 +09:00
mae
1ae6a35bc8 cmd/pkgserver: replace favicon 2026-03-05 01:12:17 -06:00
0eb2bfa12e internal/rosa/zlib: 1.3.1 to 1.3.2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-05 15:49:18 +09:00
e19a98244a internal/rosa: zlib use stable archive url
These do not get removed on new release.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-05 15:46:10 +09:00
mae
9ef5b52b85 cmd/pkgserver: pagination 2026-03-05 00:32:25 -06:00
mae
f93158cb3c cmd/pkgserver: basic web ui 2026-03-04 22:50:58 -06:00
7e2f13fa1b internal/rosa: cure checks
This cures all presets if a cache directory is supplied and verbose is set.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-05 00:47:39 +09:00
97448e2104 internal/rosa/squashfs: enter correct directory
This was missed during the make helper migration.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-05 00:43:50 +09:00
a87ad28b8b internal/pkg: scrub for dangling status
These cause build to fail to start.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-05 00:39:15 +09:00
883d4ee4af internal/pkg: return writer after sync
This fixes a use-after-free.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-05 00:23:24 +09:00
d2c6d486b0 internal/rosa: provide package metadata
This had to be done out-of-band because there was no way to efficiently represent these within Artifact.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-05 00:20:27 +09:00
6fdd800b2b internal/pkg: check filtered error
This avoids filtering some unrelated os.ErrExist.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-04 17:43:17 +09:00
94e3debc63 internal/pkg: write per-artifact logs
This is currently only used by execArtifact. A later patch will add additional logging facilities.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-04 17:27:54 +09:00
ea87664a75 internal/pkg: cancel on scanner error
This avoids discarding output thus appearing unresponsive.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-03 22:20:43 +09:00
04d9984da0 internal/rosa/meson: migrate to helper interface
This change also removes some unused options.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-02 14:07:16 +09:00
145ccd1c92 remove .github
This is no longer needed after discontinuation of the Microsoft GitHub mirror.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-02 13:46:24 +09:00
c5089cad78 cmd: remove hpkg
This proof-of-concept was abandoned long ago. Its test suite is flaky, heavy on I/O and does not increase test coverage. This change fully removes hpkg and supporting code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-02 13:42:49 +09:00
c83905f311 internal/rosa/cmake: enable check
CMake was packaged very early, before the current infrastructure existed to support this. This change patches out broken tests and enables the test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-02 13:12:20 +09:00
b7cc14f296 internal/rosa/cmake: 4.2.1 to 4.2.3
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-02 13:09:21 +09:00
57e1e5141d internal/rosa/ninja: remove cmake dependency
This does not actually depend on cmake. This is left over from very early on.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-02 13:08:18 +09:00
1440195c3f internal/rosa/llvm: pass patches via helper
This was missed while migrating LLVM to the new interface.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-02 01:30:14 +09:00
cc60e0d15d internal/rosa/make: migrate to helper interface
This also updates all affected artifacts to use new behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-02 01:24:50 +09:00
9deaf853f0 internal/rosa/cmake: migrate to helper interface
This change also removes some unused options.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-01 23:50:22 +09:00
2baa9df133 internal/rosa: general helper abstraction
This greatly increases code sharing and makes implementations far simpler.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-01 23:39:43 +09:00
51d3df2419 internal/rosa/make: split build and check
Doing these together breaks far too many buggy makefiles.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-01 14:41:34 +09:00
1d0fcf3a75 internal/rosa/perl: migrate to make helper
This uses the new configure helper behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-01 10:45:25 +09:00
e92971e0c2 internal/rosa/make: alternative configure script
This enables using the configure helper with non-autotools configure scripts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-01 10:42:20 +09:00
6159c74e96 internal/rosa/toybox: migrate to make helper
A previous change caused world rebuild, so it is a good time to do this.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-01 10:02:36 +09:00
2a34a269d0 internal/rosa: stricter cure-script options
This change also moves .cure-script out of /system/bin.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-28 23:29:22 +09:00
ef130adb27 internal/rosa/kernel: early serial
Having serial driver before initramfs is helpful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-28 23:22:18 +09:00
5694e528e6 cmd/mbf: use standalone musl in container
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-28 23:12:13 +09:00
b4e82e68a7 internal/rosa/images: initramfs via gen_init_cpio
This is much cleaner than hacking around the cpio tool.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-28 23:04:58 +09:00
d041fee791 internal/rosa: export musl
This can be useful externally.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-28 22:49:54 +09:00
cefd02e960 internal/rosa: gen_init_cpio artifact
This works much better than hacking around the toybox cpio implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-28 21:19:03 +09:00
ad8f799703 container/std: rename seccomp types
Aliases will be kept until 0.4.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-28 20:48:30 +09:00
c74c269b66 container: use /proc/self/exe directly
This is a more reliable form of pathname to self and also cheaper than os.Executable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-28 20:44:44 +09:00
4b0cce4db5 ldd: treat nil pathname as self
This is a helpful shortcut for examining a test program's ldd output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-28 20:37:01 +09:00
cd9b534d6b container: improve documentation
This change removes inconsistencies collected over time in this package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-28 20:18:30 +09:00
84e6922f30 cmd/mbf: optionally set SCHED_IDLE
None of the other supported policies are applicable here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-26 16:50:35 +09:00
c16725a679 internal/pkg: set container scheduling policy
This is not as necessary as it was for nix, since internal/pkg only unblocks exclusive artifacts one at a time. Still, this is useful when running alongside an unprivileged music player which cannot set itself to a higher priority.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-26 16:35:23 +09:00
a6160cd410 container: set scheduling policy
This is thread-directed so cannot be done externally. The glibc wrapper exposes this behaviour so most multithreaded programs using this is straight up incorrect.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-26 16:29:47 +09:00
826347fe1f internal/rosa: expose standalone musl
This is useful in the system image and might also be used elsewhere.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-26 14:21:32 +09:00
085eaed7ba cmd/earlyinit: early /dev/ and io setup
This establishes an environment where devtmpfs is mounted, and if the kernel fails to set up console, 1 and 2 is pointed at /dev/kmsg.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-25 16:37:42 +09:00
37d368a7f9 internal/rosa: initramfs artifact
This constructs a single-program initramfs and populates /dev/null so the runtime does not throw if the kernel fails to set up console.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-25 16:31:52 +09:00
2aeac7f582 internal/rosa: fakeroot artifact
XSLT is untamable and extremely unpleasant to work with. This patches out the broken docs for now in the interest of getting some work done.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-25 01:20:31 +09:00
2b93631f52 cmd/mbf: use stage2 musl when possible
This avoids pulling in the stage3 toolchain when it is not requested.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-25 01:05:49 +09:00
b3749aaf0b internal/rosa/kernel: arm64 configuration
These new dependencies do not apply to amd64, but adding them anyway in case they are needed some day.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-24 23:57:28 +09:00
c8bb88cced internal/rosa: libxslt artifact
For building documentation that cannot be turned off.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-24 23:41:54 +09:00
f7f80f95b9 internal/rosa/perl: various perl module artifacts
This change includes helpers for both Makefile.PL and Build.PL as well as various modules.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-24 23:09:55 +09:00
6ea6c794fb internal/rosa/gnu: build single-binary coreutils
This enables more fine-grained toybox replacements.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-24 16:15:15 +09:00
6c2da4c4b2 internal/rosa: libcap artifact
Required by fakeroot. Quite refreshing to package a non-autotools project.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-24 16:02:54 +09:00
90f915a708 internal/rosa/kernel: disable DEBUG_STACK_USAGE
This is no longer needed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-24 14:10:37 +09:00
a5fea4686e internal/rosa: make toolchain optional
The final Rosa OS image does not need the toolchain.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-23 18:07:38 +09:00
ae8c365c0f internal/rosa/hakurei: optionally use embedded source
This builds hakurei in Rosa OS between releases.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-23 17:56:16 +09:00
485db515f7 internal/pkg/ir: raise string limit to 16 MiB
A string holds "current" hakurei source code. For now the compressed tarball is 4.9 MiB long.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-23 15:12:19 +09:00
ec7ee0789e internal/rosa/fuse: fix init script path
The default value is quite misleading.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-23 14:39:36 +09:00
42c93a57a4 internal/rosa: fix patches
Turns out alacritty clobbers output. It turns tabs into spaces and also removes whitespace-only lines for some reason.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-23 02:12:09 +09:00
b1b14810ac internal/rosa/kernel: increase audio powersave timeout
This feature is incredibly annoying as some amplifiers take time to wake up, and causes a non-insignificant amount of audio to be dropped.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-22 21:28:39 +09:00
de117ef365 internal/rosa: ncurses artifact
For running menuconfig.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-22 18:07:24 +09:00
5e4bf23e0c internal/rosa/musl: migrate to make helper
This is much cleaner and eliminates the early ugliness.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-22 15:53:41 +09:00
d4519e2075 internal/rosa/make: expose --host
This should be set alongside --build.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-22 15:14:09 +09:00
7f1e4cf43c internal/rosa: kernel artifact
The configuration still wants some cleanup, but this works fine as a generic kernel for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-21 23:19:16 +09:00
d021621fba internal/rosa: install kernel headers out-of-tree
This is somewhat cheaper than the implementation with extra artifact and is more friendly to the make helper.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-21 19:16:10 +09:00
56567307ec internal/rosa: gnu tar artifact
Initially, libarchive was going to be used, but its test suite simply does not want to work under musl, not even with libiconv. The ticket last discussing this ceased any activity in 2020.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-21 18:09:51 +09:00
0264a1ef09 internal/rosa: libiconv artifact
For software that assumes glibc.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-21 14:35:32 +09:00
0123bbee3d internal/rosa: bc artifact
Required by the kernel.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-21 00:30:40 +09:00
771adad603 internal/rosa: texinfo artifact
Yet another wheel reinvented by GNU. Required to shut some GNU programs up.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-21 00:28:21 +09:00
178305cb22 internal/rosa: elfutils artifact
Required by the kernel.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-20 21:51:07 +09:00
c2456e252c internal/rosa: musl-obstack artifact
Yet another nonstandard glibc extension used by elfutils.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-20 21:34:59 +09:00
273068b90c internal/rosa: musl-fts artifact
Another nonstandard glibc extension used by elfutils.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-20 21:29:09 +09:00
16b20e1d34 internal/rosa: argp-standalone artifact
Nonstandard glibc extension used by elfutils.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-20 21:19:34 +09:00
b983917a6e internal/rosa: expose kernel source
This also removes the unused kernel helper.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-20 18:45:42 +09:00
e1b8f40add cmd/mbf: cache dir via environment
This is much less cumbersome than dragging the flag around all the time.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-19 15:32:25 +09:00
6df0d37c5a cmd/mbf: Rosa OS container helper
This sets up a Rosa OS container with its shell as the initial process.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-19 15:12:16 +09:00
1619b06541 internal/pkg: export layer promotion
This is a useful helper for external tooling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-19 15:06:10 +09:00
e335d99c6b internal/pkg: export seccomp presets
This is useful for external tooling providing an execArtifact-like environment.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-19 14:30:31 +09:00
d888d09b6d cmd/mbf: explicit help command
Not having this command is counterintuitive.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-19 13:55:05 +09:00
54176e7315 internal/rosa: use LTS kernel
For out-of-tree modules.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-19 01:22:08 +09:00
3bfe99d3d8 internal/lockedfile: keep objects alive while stopping cleanups
Fixes https://go.dev/issues/74780.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-18 20:00:32 +09:00
149dfbb6af internal/rosa: tamago toolchain artifact
Currently used by the (wip) bootloader, might not make it into the final OS.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-18 19:26:42 +09:00
58801b44d4 internal/rosa: util-linux artifact
This stuff will likely be implemented natively in the final system. For now, it is useful for debugging.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-18 14:21:59 +09:00
e065bbf792 internal/rosa: procps artifact
Generally pretty useful, and required by util-linux test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-18 14:07:36 +09:00
a883e57e7d internal/rosa: qemu artifact
This is still a quite minimal build. More features will be enabled as dependencies become available. The powerpc failure will be investigated if it is ever needed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-17 17:34:25 +09:00
ef9bd8ecbf internal/rosa/go: 1.25.7 to 1.26.0
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-17 14:40:24 +09:00
a40527dcb2 internal/pkg/ir: document reason for avoiding ident cache
This got brought up earlier today as a potential optimisation. This change documents why it is not viable, and hopefully clears up some performance implications of using IRDecoder, namely that its decoding costs do not amortise.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-17 14:38:42 +09:00
88d9a6163e container/initplace: return nil for createTemp error injection
This matches os package behaviour, and avoids adding the cleanup.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-17 14:16:54 +09:00
47860b0387 internal/rosa/python: enable bzip2 and xz
This is required by qemu test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-17 02:39:11 +09:00
50c9da8b6d internal/rosa/python: enable openssl
This is required by qemu test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-17 02:01:57 +09:00
16966043c7 internal/rosa: dtc artifact
Required by qemu.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-17 00:54:34 +09:00
a3515a6ef5 internal/rosa: bison artifact
Required by dtc, which is required by qemu.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-17 00:32:04 +09:00
7f05baab28 internal/rosa: flex artifact
Required by dtc, which is required by qemu.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-16 23:55:36 +09:00
d4d5e631ae internal/rosa: glib artifact
Unfortunately required by many programs, even non-gtk ones.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-16 19:01:02 +09:00
1df3bcc3b9 nix: mount tmpfs on /tmp
This hopefully eliminates spurious test failures caused by /tmp running out of space.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-16 18:22:29 +09:00
1809b53e52 internal/rosa/wayland: build-only tests patch
This patch last had any discussion eight months ago and is still not merged.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-16 13:55:34 +09:00
67b2914c94 internal/rosa: meson helper
This is used by quite a few projects.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-16 13:49:13 +09:00
74dee11822 internal/rosa/cmake: optional variant string
This improves consistency with other helpers and removes the usually unnecessary variant suffix.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-16 12:44:40 +09:00
a58c9258cc internal/rosa/pcre2: downgrade to 10.43
Latest release breaks assumptions made by GLib.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-16 01:51:38 +09:00
710b164c91 internal/pkg: allow devel syscalls
This is required by the GLib test suite, and possibly others.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-16 01:46:28 +09:00
93911d6015 internal/rosa: pcre2 artifact
Required by GLib.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-16 01:20:52 +09:00
bb097536d4 internal/rosa: remove libcxxabi hack
This was caused by stack overflow which was resolved many commits ago.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-15 21:59:09 +09:00
49b6526a38 internal/rosa: remove redundant meson flags
These have no effect.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-15 20:18:34 +09:00
f9c31df94d internal/rosa: fixed-size toolchain enum
This fits in an inlined uint32 IR value.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-15 00:58:14 +09:00
4f570cc5c9 internal/pkg: expose extra methods to file
This is useful for FileArtifact processing another stream.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-15 00:45:37 +09:00
5828631e79 internal/pkg: split off context common
For making these methods available to RContext.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-15 00:39:24 +09:00
4f9f4875d7 internal/rosa/openssl: scale jobs based on cpu count
The hardcoded value of 256 causes test failures due to excessive load on some machines. Twice the cpu count appears to almost saturate all cpus without causing spurious failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-13 03:34:08 +09:00
d49e654482 internal/rosa: kmod artifact
Required by the kernel.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-13 03:15:27 +09:00
b746e352e5 internal/rosa/zstd: fix libdir
CMake implicitly changes it to lib64 which is not supported.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-13 03:14:30 +09:00
c620d88dce update README document
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-13 01:38:50 +09:00
7cd14b8865 internal/rosa: squashfs-tools artifact
The Makefile is very poorly written, so had to be configured through the environment.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-12 02:33:33 +09:00
3e18a4b397 internal/rosa: zstd artifact
Optional dependency of many programs, and generally useful to have around.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-12 01:48:21 +09:00
1791b604b5 internal/rosa/make: configurable configure and install
This makes the helper useful for non-autotools build systems.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-12 01:19:40 +09:00
59ff6db7ec internal/rosa: toolchain type methods
This improves readability for toolchain-specific checks.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-11 18:18:11 +09:00
430e099556 internal/rosa/stage0: add arm64 tarball
This took far longer to complete because the aarch64 development machine is much slower.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-11 17:57:54 +09:00
17b64bb42c internal/pkg: skip resolved cure errors
This significantly improves error resolution performance.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-11 17:50:18 +09:00
dbb89dfb0f internal/pkg: buffer tar reader
This significantly improves performance and is a good assumption since the primary use case of FileArtifact is over the network.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-11 02:08:44 +09:00
de06ea2be4 internal/pkg: read buffer free list
Reader has a non-insignificant buffer that is worth saving as well.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-11 02:02:46 +09:00
1ef7bedfb5 internal/rosa/toybox: do not assume bash location
For compatibility with Gentoo stage3 as bootstrap seed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-11 01:36:41 +09:00
05a828c474 internal/pkg: validate tar pathnames
TContext no longer validates FileArtifact ahead of time, validation outcome is instead determined after consuming the reader to EOF. All data must therefore be treated as untrusted input until the reader is closed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-11 00:40:54 +09:00
0061d11f93 internal/rosa: use self-hosted stage0
This removes the bootstrap dependency on Gentoo stage3 tarball.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-10 22:31:08 +09:00
fb101a02f2 internal/rosa: self-host stage0 tarball
This replaces gentoo stage3 tarballs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-10 21:22:46 +09:00
3dbd67d113 internal/rosa: consistent stage0 paths
This makes using the gentoo stage3 as our stage0 compatible with Rosa OS stage0 tarballs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-10 19:28:47 +09:00
f511f0a9e9 internal/rosa: bzip2 artifact
For creating the stage0 tarball. Might be replaced by a custom artifact at some point.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-09 02:26:52 +09:00
47995137b3 internal/rosa/perl: skip installing manpages
Perl manpages ignore prefix and gets installed to /. This change does not use the configure script because it is completely broken and specifying either "none" or a single space character (undocumented) has no effect.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-09 01:50:52 +09:00
e1b8607101 internal/rosa: rename stage0 toolchain
This is stage0 relative to Rosa OS, and stage3 relative to the toolchain it is compiled on (Gentoo in this case). Referring to the toolchain itself as stage3 is counterintuitive and misleading.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-09 00:55:07 +09:00
3d3bd45b95 internal/rosa/hakurei: 0.3.4 to 0.3.5
This removes all backport patches.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-09 00:27:45 +09:00
9fb0b2452e release: 0.3.5
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-09 00:11:20 +09:00
a3e87dd0ef container: ignore uninterpreted source
These can be set to anything by the distribution.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-08 19:39:39 +09:00
90a38c0708 container: strip host-dependent opts in test cases
This change also improves plumbing for stripping options.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-08 19:35:20 +09:00
39cc8caa93 container: add riscv64 constants
This target is unlikely to become viable any time soon.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-08 19:03:47 +09:00
c4f64f7606 dist: include target in filename
Backport patch will be removed in the next release.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-08 16:03:41 +09:00
a9e2a5e59f internal/rosa/go: 1.25.6 to 1.25.7
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-08 15:42:58 +09:00
9fb0722cdf internal/rosa/go: alternative bootstrap path
For targets where the bootstrap toolchain is not available.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-08 15:04:03 +09:00
2f3e323c46 internal/rosa/gnu: gcc toolchain artifact
This toolchain is hacked to pieces. It works well enough to bootstrap Go, though.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-08 01:00:15 +09:00
1fc9c3200f internal/rosa: libucontext artifact
Required by GCC on musl.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-07 22:33:12 +09:00
096a25ad3a cmd/mbf: dump IR of artifact presets
This exposes IR outside test cases, useful for verifying correctness of alternative IR emitters.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-07 17:21:43 +09:00
ffd2f979fb internal/pkg: skip duplicate early
This significantly increases IR generation performance.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-07 17:11:41 +09:00
31a8cc9b5c internal/rosa/gnu: binutils artifact
Appears to be required by GCC? It complains with stuff installed by LLVM.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-07 14:02:23 +09:00
bb3f60fc74 internal/rosa/gnu: gmp, mpfr, mpc artifacts
Required by GCC.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-07 13:55:40 +09:00
697c91e04d internal/rosa/cmake: expose earlier build script
This allows for more flexible build setups.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-07 13:23:13 +09:00
3f7b8b4332 internal/rosa/git: git clone helper
For obtaining sources of projects that stubbornly refuse to provide release tarballs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-06 21:10:59 +09:00
fa94155f42 internal/rosa/etc: resolv.conf
Required by programs that download from the internet in measured execArtifact.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-06 21:04:59 +09:00
233bd163fb internal/rosa/git: disable flaky test
This fails intermittently.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-06 20:45:52 +09:00
f9b69c94bc internal/rosa/ssl: prefix CA paths
This makes prefixes consistent with everything else since this will end up in the final Rosa OS image.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-06 20:41:58 +09:00
68aefa6d59 internal/rosa/openssl: fix paths
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-06 19:53:51 +09:00
159fd55dbb internal/rosa/ssl: fix dependencies
These used to be provided by busybox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-05 19:12:48 +09:00
ce6b3ff53b internal/rosa: unzip artifact
Because the zip format is too awful and cannot be streamed anyway, supporting it natively comes with no benefit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-05 19:10:32 +09:00
30afa0e2ab internal/rosa/git: compile with http support
This should be able to fetch repositories deterministically.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-05 18:51:02 +09:00
9b751de078 internal/rosa/gnu: fix test suite flags
This sets the correct flag and also avoids changing ident per system.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-05 18:29:47 +09:00
d77ad3bb6e internal/rosa: curl artifact
Required for http support in git.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-05 18:15:16 +09:00
0142fc90b0 internal/rosa/make: post-configure script
Required for some projects with broken build scripts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-05 18:13:48 +09:00
3c9f7cfcd0 internal/rosa: libpsl artifact
Required by curl.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-05 18:06:33 +09:00
a3526b3ceb internal/rosa: openssl artifact
Optional for many programs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-05 18:03:18 +09:00
6ad21e2288 internal/rosa: register custom artifacts
This also encodes extra information for iana-etc.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-05 17:50:48 +09:00
27e2e3f996 internal/rosa/llvm: drop git dependency
This was added quite early and has no effect. Remove it to avoid compiling git early.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-05 17:44:58 +09:00
e0c720681b internal/pkg: standardise artifact IR
This should hopefully provide good separation between the artifact curing backend implementation and the (still work in progress) language. Making the IR parseable also guarantees uniqueness of the representation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-05 08:24:09 +09:00
f982b13a59 internal/pkg: improve error resolution
This was taking way too long for early failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-03 10:01:44 +09:00
443911ada1 internal/rosa: use stage3 mirror
These get taken down periodically and causes way too many rebuilds when they are taken down. Use mirror until a more elegant solution is available.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-02 23:43:34 +09:00
d7a3706db3 internal/rosa/x: regenerate build system
These come with 16-year-old scripts that do not understand aarch64 or really anything else relevant to Rosa OS.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-02 19:57:39 +09:00
3226dc44dc internal/rosa/gnu: libtool artifact
Required when generating autotools build systems.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-02 19:52:08 +09:00
9f98d12ad8 internal/rosa/gnu: automake artifact
This is very expensive. Avoid.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-02 18:49:18 +09:00
550e83dda9 internal/rosa/gnu: grep artifact
Some GNU software do not like the grep in toybox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-02 18:38:01 +09:00
7877b4e627 cmd/mbf: print extra stage3 information
This includes ident of all three stages and the matching checksum if check is passing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-02 18:33:16 +09:00
47ce6f5bd0 internal/rosa/llvm: conditionally add Rosa OS paths
This change also moves rpath flags to a more appropriate method.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-02-02 07:29:35 +09:00
48f4ccba33 internal/rosa/llvm: add rosa vendor
This cleans up checks specific to Rosa OS, and fixes stack overflow in llvm under certain conditions.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-31 22:47:13 +09:00
c31884bee4 internal/rosa: disable broken tests
These fail when running as users with supplementary groups, since they are unmapped in the container. This was not the case in the development container where all groups were dropped, so the failure was missed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-31 14:12:40 +09:00
f8661ad479 internal/rosa/hakurei: backport test case fix
This patch will be removed in the next release.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-31 12:00:21 +09:00
536f0cbae6 internal/rosa/gnu: gettext 0.26 to 1.0
This now requires kernel headers for some reason.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-31 11:41:07 +09:00
8d872ff1cd internal/rosa: fetch from gnu mirror
GNU infrastructure is extraordinarily flaky and fetching from it killed the server too many times.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-31 11:26:48 +09:00
bf14a412e4 container: fix host-dependent test cases
These are not fully controlled by hakurei and may change depending on host configuration.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-31 10:59:56 +09:00
8b4576bc5f internal/rosa: migrate to make helper
This migrates artifacts that the helper cannot produce an identical instance of.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-31 08:55:33 +09:00
29ebc52e26 internal/rosa/hakurei: suffix variants
This makes log output more useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-31 05:03:15 +09:00
5f81aac0e2 internal/rosa: make helper
This change only migrates artifacts that remain unchanged under the helper, so this change should not cause any rebuilds.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-31 05:01:22 +09:00
47490823be internal/rosa: improve cmake interface
This should make the call site look better for new artifacts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-31 02:42:01 +09:00
1ac8ca7a80 internal/rosa: isolate make implementation
This will come with a helper eventually.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-31 02:33:14 +09:00
fd8b2fd522 internal/rosa: fix up dependencies
These are no longer provided by the (incomplete) toybox implementations, so they need to be specified explicitly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-30 03:11:16 +09:00
20a8519044 internal/rosa/mksh: also build lksh
This is better suited for /system/bin/sh. Full mksh is still included, installed at /system/bin/mksh.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-30 00:54:30 +09:00
8c4fd00c50 internal/rosa/ninja: build in $TMPDIR
This used to build in /work/system/bin/ and unfortunately leaves its garbage there. This behaviour is from very early stages of this package, and was never fixed. This change updates it to use the "$(mktemp -d)" convention that every other artifact uses.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-30 00:36:43 +09:00
bc3dd6fbb0 internal/rosa: chmod via patch helper
This works around the zfs overlay mount overhead and significantly reduces I/O in general.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-30 00:29:44 +09:00
616ed29edf internal/rosa: early toybox variant
This is a variant of toybox with unfinished tools enabled, for artifacts that will end up in a dependency loop without them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-30 00:24:14 +09:00
9d9b7294a4 internal/rosa: flags for toolchain-dependent artifact
This is much cleaner to add extra flags to.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 20:44:43 +09:00
6c1e2f10a7 internal/rosa: remove busybox artifact
This is no longer used and its implementation is unacceptably shaky.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 19:58:51 +09:00
abf96d2283 internal/rosa: replace busybox with toybox
The busybox artifact does not run on aarch64, and the workarounds required for it to compile successfully on x86_64 is unacceptably shaky. This change fully replaces it with toybox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 19:51:18 +09:00
6c90e879da internal/rosa/llvm: enable asan
This is required by test suite of latest toybox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 18:30:37 +09:00
d1b404dc3a internal/rosa: findutils artifact
Required by llvm test suite, compiler-rt sanitisers-related tests fail on toybox xargs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 18:24:01 +09:00
744e4e0632 internal/rosa: sed artifact
Required by various GNU programs as they are not happy with toybox sed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 18:17:18 +09:00
85eda49b2b internal/rosa: xz artifact
Wanted to avoid this as much as possible. Unfortunately newer versions of GNU findutils only come in xz and is required for llvm compiler-rt sanitisers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 18:06:17 +09:00
b26bc05bb0 internal/rosa: remove unused receiver
This returns the preset itself, it is up to the caller to load the underlying artifact.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 17:52:50 +09:00
2d63ea8fee internal/rosa: gzip artifact
Toybox does not implement this, and it is used by many programs, including toybox itself.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 17:46:30 +09:00
dd4326418c internal/rosa: toybox artifact
This compiles surprisingly quickly and required no workarounds, unlike busybox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 17:44:30 +09:00
79c0106ea0 internal/rosa: replace busybox dash with mksh
Toybox does not provide a shell, mksh fills that gap.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 02:00:32 +09:00
536db533de internal/rosa: install bash as sh
This works around software relying on bashisms even when explicitly invoking sh.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 01:25:44 +09:00
07927006a8 internal/pkg: set User-Agent header
Avoid living under the default user agent and be at the mercy of some IDS.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 01:17:35 +09:00
77ea27b038 internal/rosa: mksh artifact
This provides a shell, as part of the effort to replace busybox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-29 00:51:32 +09:00
e76bc6a13a internal/rosa: resolve preset by name
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-28 20:57:51 +09:00
cc403c96d8 internal/rosa: remove busybox patch
This allows different versions of busybox to be attempted, to find one that works on arm.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-28 01:24:56 +09:00
66118ba941 internal/rosa: gawk artifact
Replaces broken awk in busybox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-28 01:22:42 +09:00
823ba08dbc internal/rosa: use patch helper
This is significantly cleaner and runs somewhat faster.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-28 00:58:34 +09:00
660835151e internal/rosa: disable busybox SHA1_HWACCEL
This also pretties up the build output and sets correct linker path.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-28 00:51:09 +09:00
53e6df7e81 internal/rosa: remove uname
This does not change ident based on target.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 23:37:29 +09:00
bd80327a8f internal/rosa: add arm64 strings
This enables building on arm64.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 21:12:39 +09:00
41f9aebbb7 internal/pkg: allow multiarch
The armv8l busybox binary release needs this to run correctly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 21:07:18 +09:00
a2a0e36802 internal/rosa: cross-platform stage3
The stage3 binary seed is arch-specific.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 20:58:31 +09:00
fbe93fc771 internal/rosa/busybox: cross-platform binary
The initial binary seed is arch-specific.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 20:54:11 +09:00
968d8dbaf1 internal/pkg: encode checksum in ident
This also rearranges the ident ir to be more predictable, and avoids an obvious and somewhat easy to get into inconsistent state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 20:18:34 +09:00
f1758a6fa8 internal/rosa: nss artifacts
Not used by anything for now, but will be part of Rosa OS.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 08:17:58 +09:00
88aaa4497c internal/rosa/hakurei: dist tarball
The patch will be removed in the next release.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 07:34:45 +09:00
b7ea68de35 internal/rosa/hakurei: isolate hakurei helper
For creating the dist tarball, which runs in the same environment.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 07:28:13 +09:00
67e453f5c4 dist: run tests
This used to be impossible due to nix jank which has been addressed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 07:00:39 +09:00
67092c835a internal/rosa/hakurei: v0.3.3 to v0.3.4
This now contains the sharefs program which pulls in fuse.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 05:40:53 +09:00
18918d9a0d internal/rosa: fuse artifact
Required by hakurei for sharefs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 05:34:42 +09:00
380ca4e022 internal/rosa: pytest artifact
Required by libfuse. This pulls in many dependencies.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 05:20:37 +09:00
887aef8514 internal/rosa: python pip helper
Fuse requires pytest which depends on many packages. This helper eases the pain of packaging them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 05:14:59 +09:00
d61faa09eb release: 0.3.4
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 03:56:06 +09:00
50153788ef internal/rosa: hakurei artifact
This does not yet have fuse from staging. Everything else works perfectly, though.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 02:24:49 +09:00
c84fe63217 internal/rosa: various X artifacts
Required by xcb which is required by hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 02:02:49 +09:00
eb67e5e0a8 internal/pkg: exclusive artifacts
This alleviates scheduler overhead when curing many artifacts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 01:23:50 +09:00
948afe33e5 internal/rosa/acl: use patch helper
This is significantly less ugly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-27 00:30:50 +09:00
76c657177d internal/rosa: patch ignore whitespace
This makes it work better with patches emitted by git.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 21:56:36 +09:00
4356f978aa internal/rosa: kernel patching
The side effect of this is to work around zfs performance issue with chmod on overlay mount.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 21:20:52 +09:00
4f17dad645 internal/rosa: isolate patching helper
This is useful outside llvm as well.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 21:00:29 +09:00
68b7d41c65 internal/rosa: parallel autoconf tests
These take forever and run sequentially by default for some reason.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 19:52:59 +09:00
e48f303e38 internal/rosa: parallel perl tests
This is found in the github action, the test target does not appear to support parallelisation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 19:45:50 +09:00
f1fd406b82 internal/rosa: link libc ldd
Musl appears to implement this behaviour but does not install the symlink by default.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 08:00:03 +09:00
53b1de3395 internal/rosa: enable static on various artifacts
This is implicitly enabled sometimes, but better to be explicit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 07:56:14 +09:00
92dcadbf27 internal/acl: connect getfacl stderr
This shows whatever failure is happening in the cure container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 07:51:16 +09:00
0bd6a18326 internal/rosa: acl artifact
Required by hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 07:38:56 +09:00
67d592c337 internal/pkg: close gzip reader on success
The Close method panics otherwise.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 07:06:38 +09:00
fdc8a8419b internal/rosa: static libwayland
Required by hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 06:49:08 +09:00
122cfbf63a internal/rosa: run wayland tests
Broken test is disabled for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 06:39:45 +09:00
504f5d28fe internal/rosa: libseccomp artifact
Required by hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 05:28:36 +09:00
3eadd5c580 internal/rosa: gperf artifact
Required by libseccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 05:25:39 +09:00
4d29333807 internal/rosa: wayland-protocols artifact
Required by hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 05:13:30 +09:00
e1533fa4c6 internal/rosa: wayland artifact
Required by hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 05:10:35 +09:00
9a74d5273d internal/rosa: libgd artifact
Required by graphviz which is required by wayland.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 04:20:11 +09:00
2abc8c454e internal/pkg: absolute hard link
This cannot be relative since the curing process is not in the temp directory.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 04:03:05 +09:00
fecb963e85 internal/rosa: libxml2 artifact
Required by wayland. Release tarball is xz only, unfortunately.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 03:47:42 +09:00
cd9da57f20 internal/rosa: libexpat artifact
Required by wayland.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 03:15:25 +09:00
c6a95f5a6a internal/rosa: meson artifact
Required by wayland and pipewire.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 03:03:21 +09:00
228489371d internal/rosa: setuptools artifact
Apparently the only way to install python stuff offline.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 02:28:47 +09:00
490471d22b cmd/mbf: verbose by default
It usually does not make sense to use this without verbose.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 02:12:56 +09:00
763d2572fe internal/rosa: pkg-config artifact
Used by hakurei and many other programs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 01:26:54 +09:00
bb1b6beb87 internal/rosa: name suffix by toolchain
This makes output more useful during bootstrap.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 00:57:03 +09:00
3224a7da63 cmd/mbf: disable threshold by default
This is not very useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-26 00:05:59 +09:00
8a86cf74ee internal/rosa/go: symlink executables
This avoids having to fix up $PATH for every artifact.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-25 23:59:08 +09:00
e34a59e332 internal/rosa/go: run toolchain tests
LLVM patches and a TMPDIR backed by tmpfs fixed most tests. Broken tests in older versions are disabled.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-25 21:21:53 +09:00
861801597d internal/pkg: expose response body
This uses the new measured reader provided by Cache. This should make httpArtifact zero-copy.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-25 16:10:34 +09:00
334578fdde internal/pkg: expose underlying reader
This will be fully implemented in httpArtifact in a future commit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-25 14:48:25 +09:00
20790af71e internal/rosa: lazy initialise all artifacts
This improves performance, though not as drastically as lazy initialising llvm.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-25 01:43:18 +09:00
43b8a40fc0 internal/rosa: lazy initialise llvm
This significantly improves performance.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-25 00:29:46 +09:00
87c3059214 internal/rosa: run perl tests
A broken test with unexplainable failure is disabled.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-24 18:58:09 +09:00
6956dfc31a internal/pkg: block on implementation entry
This avoids blocking while not in Cure method of the implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-24 16:02:50 +09:00
d9ebaf20f8 internal/rosa: stage3 special case helper
This makes it cleaner to specify non-stage3 and stage3-exclusive dependencies.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-24 12:23:35 +09:00
acee0b3632 internal/pkg: increase output buffer size
This avoids truncating unreasonably long lines from llvm.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-24 11:45:44 +09:00
5e55a796df internal/rosa: gnu patch artifact
This is more robust than the busybox implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-24 11:32:27 +09:00
f6eaf76ec9 internal/rosa: patch library paths
This removes the need for reference LDFLAGS in the standard toolchain.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-24 11:22:25 +09:00
5c127a7035 internal/rosa: patch header search paths
This removes the need for reference CFLAGS in the standard toolchain.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-23 01:56:52 +09:00
8a26521f5b internal/rosa/go: run bootstrap toolchain tests
The objdump test will be re-enabled after fixing llvm search paths.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-22 07:05:48 +09:00
0fd4556e38 internal/rosa/llvm: fix broken test patch
Both stage1 and stage2 passes at this point.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-22 06:42:04 +09:00
50b82dcf82 internal/rosa/gnu: coreutils artifact
Required by llvm unit and regression tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-22 05:42:23 +09:00
20a8d30821 internal/rosa/busybox: link /usr/bin/env
This is required by many scripts which uses bash but still pretends to be portable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-22 04:21:06 +09:00
cdf2e4a2fb internal/rosa: bash artifact
Required by llvm unit and regression tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-22 04:06:48 +09:00
dcb8a6ea06 internal/rosa: fix toolchain layer order
This allows extras to override toolchain artifacts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-22 03:37:46 +09:00
094a62ba9d internal/rosa: diffutils artifact
LLVM tests are not happy with busybox diff.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-22 03:00:59 +09:00
6420b6e6e8 internal/rosa: libffi artifact
Required by python during llvm test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-22 02:47:47 +09:00
d7d058fdc5 internal/rosa/gnu: disable broken tests
These are documented as broken via comments yet not disabled on musl for some reason.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 23:31:45 +09:00
84795b5d9f internal/rosa/git: add dependencies
These are required outside the stage3 toolchain.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 22:14:48 +09:00
f84d30deed internal/rosa/gnu: run checks
Checks are not run for gettext for now since it contains broken tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 22:07:08 +09:00
77821feb8b internal/rosa: gettext artifact
Compile time dependency of git.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 21:52:50 +09:00
eb1060f395 internal/rosa: autoconf artifact
Required by git to reconfigure some options.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 21:44:49 +09:00
0e08254595 internal/rosa: m4 artifact
Autotools dependency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 21:42:25 +09:00
349d8693bf internal/rosa: perl artifact
This runs without tests for now, will be enabled after some toolchain patches.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 21:33:12 +09:00
e88ae87e50 internal/rosa/llvm: run unit and regression tests
Two tests are marked expected to fail for Rosa OS.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 08:25:36 +09:00
7cd4aa838c internal/rosa/llvm: patch source tree
A few patches are required for disabling broken tests and changing default search paths.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 08:25:17 +09:00
641942a4e3 internal/rosa/cmake: chmod entire source tree
This works around builds that traverse out of the appended pathname.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 07:26:36 +09:00
b6a66acfe4 internal/rosa: git artifact
This is required by the clang unit and regression tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 07:00:14 +09:00
b72dc43bc3 internal/pkg: report dependency graph size
This is an interesting value to know when profiling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 05:35:28 +09:00
8e59ff98b5 internal/rosa: include iana-etc
This is used by some programs and will likely end up in the Rosa OS system image anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 05:28:41 +09:00
f06d7fd387 cmd/mbf: expose some artifacts for curing
This will remain until dist is successfully bootstrapped.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 05:18:08 +09:00
ba75587132 internal/pkg: allow user namespace creation
No good reason to filter this in the execArtifact container, and the extended filter breaks certain programs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-21 04:49:25 +09:00
9a06ce2db0 internal/rosa: bootstrap go toolchain
This runs without tests for now. Will be fixed in a later commit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-20 07:14:11 +09:00
3ec15bcdf1 internal/rosa/cmake: use hardcoded build directory
This eliminates some nondeterminism. Still getting 3-stage non-determinism in runtimes and clang, though.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 22:51:34 +09:00
d933234784 internal/pkg: make checksum available to cure
This enables deduplication by value as implemented in execArtifact.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 21:29:56 +09:00
1c49c75f95 cmd/mbf: toolchain 3-stage non-determinism check
This unfortunately fails right now. Requires further investigation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 04:40:44 +09:00
6a01a55d7e internal/rosa: parallel cmake bootstrap
This takes a very long time otherwise.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 03:21:29 +09:00
b14964a66d internal/rosa: standard toolchain via 2-stage bootstrap
This implements the 2-stage bootstrap build without clumping the stages together in the cmake target.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 02:57:28 +09:00
ff98c9ded9 internal/rosa: llvm bootstrap artifacts
This bootstraps the LLVM toolchain across multiple artifacts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 02:48:44 +09:00
7f3d1d6375 internal/rosa: llvm artifact abstraction
The llvm bootstrap is multi-stage by nature, and cannot be completed in a single artifact.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 02:29:06 +09:00
3a4f20b759 internal/rosa: cmake abstraction
This is a helper for generating cure script for a cmake-based project.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 02:20:37 +09:00
21858ecfe4 internal/rosa: ninja artifact
Generated by cmake, recommended format for llvm toolchain.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 02:08:17 +09:00
574a64aa85 internal/rosa: cpython artifact
Dependency of llvm build scripts, also an optional cure dependency of ninja.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 01:58:53 +09:00
85d27229fd internal/rosa: zlib artifact
Dependency of llvm build scripts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 01:48:27 +09:00
83fb80d710 internal/rosa: cmake artifact
This is required for compiling the toolchain and many other programs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 01:36:38 +09:00
fe6dc62ebf internal/rosa: musl libc artifact
This will likely be included in Rosa OS. The installation is modified to be entirely contained in prefix.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 01:25:41 +09:00
823f9c76a7 internal/rosa: busybox from source
This will be part of the standard toolchain.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 01:12:47 +09:00
2df913999b internal/rosa: kernel headers
This is required by the toolchain and many other programs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 01:03:19 +09:00
52c959bd6a internal/rosa: minimal rsync artifact
For installing kernel headers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 00:58:34 +09:00
d258dea0bf internal/rosa: bootstrap on gentoo stage3
This contains a fully working musl+llvm toolchain and many build systems in a pretty small package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 00:51:49 +09:00
dc96302111 internal/rosa: GNU make artifact
This compiles GNU make from source. This is unfortunately required by many programs, but is a cure dependency only.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 00:40:25 +09:00
88e9a143d6 internal/rosa: toolchain abstraction
This provides a clean and easy to use API over toolchains. A toolchain is an opaque set of artifacts and environment fixups. Exported toolchains should be functionally indistinguishable from each other.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 00:28:16 +09:00
8d06c0235b internal/rosa: busybox binary artifact
This installs a statically linked busybox binary distribution for decompressing the gentoo stage3 tarball, since there is no native xz implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-19 00:07:56 +09:00
4155adc16a internal/rosa: static etc artifact
This places configuration files with hardcoded content in /etc to silence test suites expecting them to be present.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-18 23:51:45 +09:00
2a9525c77a cmd/mbf: command handling
This tool is a frontend for bootstrapping hakurei via internal/pkg. Named mbf for now for "maiden's best friend" as a tribute to the DOOM source port.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-18 22:19:19 +09:00
efc90c3221 internal/pkg: deduplicate dependency errors
This significantly simplifies error reporting for caller.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-17 14:41:00 +09:00
610ee13ab3 internal/pkg: lock on-filesystem cache
Any fine-grained file-based locking here significantly hurts performance and is not part of the use case of the package. This change guarantees exclusive access to prevent inconsistent state on the filesystem.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-16 18:12:51 +09:00
5936e6a4aa internal/pkg: parallelise scrub
This significantly improves scrubbing performance. Since the cache directory structure is friendly to simultaneous access, this is possible without synchronisation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-16 02:47:59 +09:00
3499a82785 internal/pkg: cache computed identifiers
This eliminates duplicate identifier computations. The new implementation also significantly reduces allocations while computing identifier for a large dependency tree.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-15 23:30:43 +09:00
088d35e4e6 internal/pkg: optional dependency graph size limit
This provides a quick check against cyclic dependencies without hurting cure performance.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-14 18:25:46 +09:00
1667df9c43 internal/pkg: zero atime and mtime
This is significantly more practical than keeping track of them in directory flattening format and setting this in every non-artifact implementation. Only tarArtifact can have meaningful deterministic checksums that are not zero and zeroing them still keeps autotools happy.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-13 01:30:30 +09:00
156dd767ef internal/pkg: remove typeflag promotion loop
Expanding this enables sharing of code common between types.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-13 00:33:13 +09:00
5fe166a4a7 internal/pkg: exec prefix verbose output
This proxies program output through msg with a name and fd prefix. This also avoids introducing additional information to the container via process stdout/stderr.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-12 22:15:01 +09:00
41a8d03dd2 internal/pkg: cure completion verbose messages
This reports cure completions to the user.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-12 21:56:00 +09:00
610572d0e6 internal/pkg: optionally named static file
These are generally for generating configuration files or build scripts, naming them is quite useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-12 04:34:50 +09:00
29951c5174 internal/pkg: caller-supplied reporting name for exec
This does not have a reasonable way of inferring the underlying name. For zero value it falls back to base of executable pathname.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-12 04:17:47 +09:00
91c3594dee internal/pkg: append user-facing name in messages
This makes verbose messages much more useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-12 03:53:19 +09:00
7ccc2fc5ec internal/pkg: exec with specific timeout
This change also updates the documentation of NewExec.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-11 17:46:04 +09:00
63e137856e internal/pkg: do not discard the result of compact
This result was mistakenly unused resulting in incorrect identifiers for artifacts with duplicate dependencies.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-11 04:40:24 +09:00
e1e46504a1 container/check: return error backed by string type
The struct turned out not necessary during initial implementation but was not unwrapped into its single string field. This change replaces it with the underlying string and removes the indirection.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-11 04:23:55 +09:00
ec9343ebd6 container/check: intern absolute pathnames
This improves performance in heavy users like internal/pkg.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-11 04:18:11 +09:00
423808ac76 nix: use package from module in default
This makes overriding hakurei easier. Also avoids building hakurei twice since nix does that for some reason.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-11 03:50:08 +09:00
2494ede106 container/init: configure interface lo
This enables loopback networking when owning the net namespace.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-11 03:36:20 +09:00
da3848b92f internal/pkg: compare interfaces for host net
An upcoming improvement in the container init makes the current host net check return the same result for both cases. This change

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-11 00:30:32 +09:00
34cb4ebd3b internal/pkg: pass context to file cure
This removes the left over embedded contexts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-09 05:31:38 +09:00
f712466714 internal/pkg: move dependency flooding to cache
This imposes a hard upper limit to concurrency during dependency satisfaction and moves all dependency-related code out of individual implementations of Artifact. This change also includes ctx and msg as part of Cache.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-09 05:20:34 +09:00
f2430b5f5e internal/pkg: use short wait delay
The cure is condemned at the point of cancellation and all of its state is destroyed by the deferred cleanup, so it makes little sense to wait for it much.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-08 18:58:51 +09:00
863e6f5db6 internal/pkg: use correct artifact count
This updates buffer sizes and counters to use correct total artifact count.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-08 08:45:37 +09:00
23df2ab999 internal/pkg: place ephemeral upperdir in tmp
This enables the use of directories made writable this way as scratch space.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-08 08:23:16 +09:00
7bd4d7d0e6 internal/pkg: support explicit overlay mount
This removes all but the /work/ auto overlay behaviour and enables much greater flexibility. This also renames ExecContainerPath to ExecPath so it is easier to type.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-08 07:55:09 +09:00
b3c30bcc51 internal/pkg: set container WaitDelay
This prevents a container from blocking forever after context is canceled.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-08 06:23:57 +09:00
38059db835 internal/pkg: make tar temporary directory writable
This allows it to be renamed to work directory.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-08 04:32:14 +09:00
409fd3149e internal/pkg: reserve kind range
This is useful for custom implementations of Artifact.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-08 01:21:51 +09:00
4eea136308 internal/pkg: do not connect stdin
This introduces external state when verbose.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-07 23:17:58 +09:00
c86ff02d8d internal/pkg: tar optional file
This allows tar to take a single-file directory Artifact as input.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-07 22:16:26 +09:00
e8dda70c41 internal/pkg: return reader for files
This improves efficiency for cache hits.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-07 21:36:47 +09:00
7ea4e8b643 internal/pkg: support tarball compressed via bzip2
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-07 20:18:28 +09:00
5eefebcb48 internal/pkg: reject entry types disallowed in the cache
These are not encoded in the format, they are rejected here to serve as a check for cache since checksum is computed for every directory.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-07 03:22:44 +09:00
8e08e8f518 internal/pkg: automatic overlay mount on work
This directly submits the upperdir to cache. It is primarily used in bootstrapping where tools are limited and should not be used unless there is a very good reason to.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-07 03:14:33 +09:00
54da6ce03d internal/pkg: respect mount order for overlay temp
Setting it up after everything else prevents covering files in /tmp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-07 01:03:49 +09:00
3a21ba1bca internal/pkg: implement file artifact
This is an Artifact implementing File, backed by a constant, caller-supplied byte slice.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-07 00:39:29 +09:00
45301559bf internal/pkg: fail on empty output directory
This works around the fact that execArtifact always creates the work directory when setting up the bind mount.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-07 00:04:32 +09:00
0df87ab111 internal/pkg: automatic overlay mount on tmp
This sets up the last Artifact to target /tmp as a writable overlay mount backed by the host side temp directory. This is useful for an Artifact containing source code to be built for another Artifact for example.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 23:45:08 +09:00
aa0a949cef internal/pkg: do not clear execute bit
Only write should be cleared here, clearing execute causes execArtifact to be unable to start anything since no Artifact is able to produce an executable file.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 22:31:44 +09:00
ce0064384d internal/pkg: automatic overlay mount on root
This makes it possible to use an Artifact as root without arranging for directory creation in the Artifact ahead of time.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 22:19:47 +09:00
53d80f4b66 internal/pkg/testdata: check network
This validates hostNet state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 21:32:10 +09:00
156096ac98 internal/pkg: known checksum exec artifact
This optionally attaches an output checksum to an execArtifact and enables host networking for the resulting container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 20:58:34 +09:00
ceb75538cf internal/pkg: update http checksum signature
This was using the old pre-KnownChecksum function signature. It did not affect correctness since httpArtifact performs internal validation to avoid the strict mode vfs roundtrip, but it prevented content-addressed cache hits.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 18:24:31 +09:00
0741a614ed internal/pkg: relocate testtool workaround
This can be reused in other test cases.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 18:06:56 +09:00
e7e9b4caea internal/pkg: exec nil path check during cure
This results in os.ErrInvalid instead of a panic, which hopefully improves user experience.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 17:46:12 +09:00
f6d32e482a internal/pkg: ensure parent for non-directory entries
This works around streams containing out of order entries.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 05:55:24 +09:00
79adf217f4 internal/pkg: implement exec artifact
This runs a program in a container environment. Artifacts can be made available to the container, they are cured concurrently and mounted in order.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 05:05:34 +09:00
8efffd72f4 internal/pkg: destroy temp during deferred cleanup
This avoids missing the cleanup when cure returns an error.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 04:24:54 +09:00
86ad8b72aa internal/pkg: expose cure through cure context
This allows a curing Artifact to cure Artifact it depends on.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 01:04:06 +09:00
e91049c3c5 internal/pkg: pass cure context as single value
This cleans up the function signature and makes backwards compatible API changes possible.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-06 00:56:49 +09:00
3d4d32932d internal/pkg: verify checksum after uneventful scrub
This checks that scrub did not condemn any entry without reporting it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-05 22:54:16 +09:00
0ab6c13c77 internal/pkg: consistency check for on-disk cache
This change adds a method to check on-disk cache consistency and destroy inconsistent entries as they are encountered. This primarily helps verify artifact implementation correctness, but can also repair a cache that got into an inconsistent state from curing a misbehaving artifact, without having to destroy the entire cache.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-05 05:30:29 +09:00
834cb0d40b internal/pkg: override "." for directory checksum
This makes the checksum consistent with the final resting state of artifact directories without incurring the cost of an extra pair of chown syscalls.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-05 04:00:13 +09:00
7548a627e5 internal/pkg: delete stale done channels
There is no reason to keep these around.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-05 02:12:34 +09:00
b98d27f773 internal/pkg: expand single directory tarball
This enables much cleaner use of their output without giving up any meaningful data.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-05 01:43:23 +09:00
f3aa31e401 internal/pkg: temporary scratch space for cure
This allows for more flexibility during implementation. The use case that required this was for expanding single directory tarballs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-05 01:39:18 +09:00
4da26681b5 internal/pkg: compute http identifier from url
The previous implementation exposes arbitrary user input to the cache as an identifier, which is highly error-prone and can cause the cache to enter an inconsistent state if the user is not careful. This change replaces the implementation to compute identifier late, using url string as params.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-05 00:43:21 +09:00
4897b0259e internal/pkg: improve artifact interface
This moves all cache I/O code to Cache. Artifact now only contains methods for constructing their actual contents.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-05 00:01:23 +09:00
d6e4f85864 internal/pkg: ignore typeflag 'g'
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-04 12:46:56 +09:00
3eb927823f internal/pkg: create symlinks for files
These are much easier to handle than hard links and should be just as transparent for this use case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-04 01:48:53 +09:00
d76b9d04b8 internal/pkg: implement tar artifact
This is useful for unpacking tarballs downloaded from the internet.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-04 01:34:30 +09:00
fa93476896 internal/pkg: override working directory perms
This must be writable to enable renaming, and the final result is conventionally read-only alongside the entire directory contents. This change overrides the permission bits as part of Store.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-04 00:55:52 +09:00
bd0ef086b1 internal/pkg: enable cache access during store
This is still not ideal as it makes entry into Store sequential. This will be improved after more usage code is written.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-04 00:39:14 +09:00
05202cf994 internal/pkg: pass context in request wrapper
This method is for the most common use case, and in actual use there will always be an associated context.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-03 23:53:52 +09:00
40081e7a06 internal/pkg: implement caching for directories
This works on any directories and should be robust against any bad state the artifact curing process might have failed at.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-03 22:54:46 +09:00
863d3dcf9f internal/pkg: wrap checksum string encoding
This wraps base64.URLEncoding.EncodeToString for cleaner call site.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-03 22:03:25 +09:00
8ad9909065 internal/pkg: compute identifier from deps
This provides infrastructure for computing a deterministic identifier based on current artifact kind, opaque parameters data, and optional dependency kind and identifiers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-03 21:26:25 +09:00
deda16da38 internal/pkg: create work directory
This is used for artifacts that cure into directories.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-03 20:56:11 +09:00
55465c6e72 internal/pkg: optionally validate flat pathnames
This makes the decoder safe against untrusted input without hurting performance for a trusted stream. This should still not be called against untrusted input though.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-03 18:59:18 +09:00
ce249d23f1 internal/pkg: implement http artifact
This is useful for downloading source tarballs from the internet.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-03 15:29:58 +09:00
dd5d792d14 go: 1.25
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-03 15:25:28 +09:00
d15d2ec2bd internal/pkg: relocate cache test helper
This is useful for other tests that need a cache instance.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-02 16:08:43 +09:00
3078c41ce7 internal/pkg: encode entry in custom format
The fact that Gob serialisation is deterministic is an implementation detail. This change replaces Gob with a simple custom format.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-02 15:39:42 +09:00
e9de5d3aca internal/pkg: implement caching for files
This change contains primitives for validating and caching single-file artifacts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-02 12:57:19 +09:00
993afde840 dist: install sharefs
This also removes the deprecated hpkg program.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-01-02 00:57:51 +09:00
c9cd16fd2a cmd/sharefs: prepare directory early
This change also checks against filesystem daemon running as root early.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-27 23:17:02 +09:00
e42ea32dbe nix: configure sharefs via fileSystems
Turns out this did not work because in the vm test harness, virtualisation.fileSystems completely and silently overrides fileSystems, causing its contents to not even be evaluated anymore. This is not documented as far as I can tell, and is not obvious by any stretch of the imagination. The current hack is cargo culted from nix-community/impermanence and hopefully lasts until this project fully replaces nix.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-27 23:14:08 +09:00
e7982b4ee9 cmd/sharefs: create directory as root
This optional behaviour is required on NixOS as it is otherwise impossible to set this up: systemd.mounts breaks startup order somehow even though my unit looks identical to generated ones, fileSystems does not support any kind of initialisation or ordering other than against other mount points.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-27 22:14:33 +09:00
ef1ebf12d9 cmd/sharefs: handle mount -t fuse.sharefs
This should have been handled in a custom option parsing function, but that much extra complexity is unnecessary for this edge case. Honestly I do not know why libfuse does not handle this itself.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-27 20:49:27 +09:00
775a9f57c9 cmd/sharefs: check option parsing behaviour
This change makes it possible to check parseOpts behaviour as part of Go tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-27 17:33:12 +09:00
2f8ca83376 cmd/sharefs: containerise filesystem daemon
This replaces the forking daemonise libfuse function which prevents Go callbacks from calling into the runtime. This also enforces least privilege on the daemon process.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-27 10:16:35 +09:00
3d720ada92 container: optionally allow orphan
This is required for the typical daemonise use case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-27 09:12:02 +09:00
2e5362e536 cmd/sharefs: opaque setup state
This allows unrestricted use of the type system and prepares setup code for cross-process initialisation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-27 04:14:00 +09:00
6d3bd27220 cmd/sharefs: expand fuse_main
This change should not change behaviour other than making output more consistent.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-27 02:30:28 +09:00
a27305cb4a cmd/sharefs: improve help message
This improves consistency with the fuse_main help message.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-27 02:20:41 +09:00
0e476c5e5b cmd/sharefs: allocate sharefs_private early
This also removes global state used by sharefs_init.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-26 08:08:41 +09:00
54712e0426 nix: set noatime on sharefs
Could improve performance, atime is not useful for this filesystem anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-26 05:34:05 +09:00
b77c1ecfdb cmd/sharefs/test: check option handling
This verifies behaviour related to setuid/setgid when starting as root.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-26 05:28:45 +09:00
dce5839a79 nix: do not restart sharefs
This avoids disrupting running containers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-26 04:12:14 +09:00
d597592e1f cmd/sharefs: rename fuse-helper to fuse-operations
This is not really just library wrapper functions, but instead implements the callbacks, so fuse-operations makes more sense.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-26 03:19:32 +09:00
056f5b12d4 cmd/sharefs: move translate_pathname body to macro wrapper
This is never called directly anywhere and it is simple enough to be included in the macro. This avoids passing the pointer around and dereferencing errno location, resulting in over 5% increase in throughput on the clang build. No change in the gcc build though.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-26 02:10:59 +09:00
da2bb546ba cmd/sharefs: remove readlink
This filesystem does not support symbolic links, so readlink is not useful, and unreachable in this case because of the check in getattr.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-25 06:00:58 +09:00
7bfbd59810 cmd/sharefs: implement shared filesystem
This is for passing files between applications, similar to android /sdcard.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-25 05:13:02 +09:00
ea815a59e8 nix: disable source fortification in devShell
This generates warnings when compiling without optimisation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-21 02:22:28 +09:00
28a8dc67d2 internal/pipewire: raise Core::Sync timeout
Hopefully relieves spurious failures on a very overloaded system.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-19 00:49:33 +09:00
ec49c63c5f internal/pipewire: EPOLL_CTL_ADD instead of EPOLL_CTL_MOD
Implementation is no longer tied down by the limitations of SyscallConn.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-19 00:43:44 +09:00
5a50bf80ee internal/pipewire: hold socket fd directly
The interface provided by net is not used here and is a leftover from a previous implementation. This change removes it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-19 00:28:24 +09:00
ce06b7b663 internal/pipewire: inform conn of blocking intent
The interface does not expose underlying kernel notification mechanisms. This change removes the need to poll in situations were the next call might block.

This is made cumbersome by the SyscallConn interface left over from a previous implementation, it will be replaced in a later commit as the current implementation does not make use of any net.Conn methods other than Close.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-19 00:00:33 +09:00
08bdc68f3a internal/pipewire: sendmsg/recvmsg errors are fatal
When returned wrapped as a syscall error, these are impossible to recover from, so wrap them as a fatal error.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-18 23:33:12 +09:00
8cb0b433b2 release: 0.3.3
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-15 20:34:45 +09:00
767f1844d2 test: check shim private dir cleanup
This asserts that no shim private dir was left behind after all containers terminate.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-15 20:30:19 +09:00
54610aaddc internal/outcome: expose pipewire via pipewire-pulse
This no longer exposes the pipewire socket to the container, and instead mediates access via pipewire-pulse. This makes insecure parts of the protocol inaccessible as explained in the doc comment in hst.

Closes #29.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-15 12:57:06 +09:00
2e80660169 internal/outcome: look up pipewire-pulse path
This is for setting up the pipewire-pulse container in shim, for #29.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-15 12:38:39 +09:00
d0a3c6a2f3 internal/outcome: optional shim private dir
This is a private work directory owned by the specific shim. Useful for sockets owned by this instance of the shim and requires no direct assistance from the priv-side process.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-15 12:32:46 +09:00
0c0e3d6fc2 hst: add direct hardware option
This is unfortunately the only possible setup to securely expose PipeWire to the container. Further explanation explained in the doc comment and #29.

This will be implemented in a future commit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-15 12:29:32 +09:00
fae910a1ad container: sync stubbed wait4 loop after notify
This ensures consistent state observed by wait4 loop when running against stub.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 10:22:48 +09:00
178c8bc28b internal/pipewire: handle SecurityContext::Create error
This method can result in an error targeting it, so it is handled here. This change also causes a call to Create to also Core::Sync, as it should have done.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 09:41:28 +09:00
30dcab0734 internal/pipewire: SecurityContext as destructible
This proxy can be destroyed by sending a Core::Destroy targeting it. This change implements the Destroy method by embedding destructible.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 09:31:50 +09:00
0ea051062b internal/pipewire: reorder context struct
This change reorders and groups struct elements. This improves readability since this struct holds a lot of state loosely related to each other.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 09:26:30 +09:00
b0f2ab6fff internal/pipewire: implement Core::Destroy
This change also implements pending destructible check on Sync. Destruction method should always be implemented as a wrapper of destructible.destroy.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 09:20:58 +09:00
00a5bdf006 internal/pipewire: do not emit None for spa_dict
Turns out the PipeWire server does not expect a value of type None here at all.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 09:06:44 +09:00
a27dfdc058 internal/pipewire: implement Core::CreateObject
Nothing uses this right now, this would have to be called by wrapper methods on Registry that would search the objects

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 08:10:57 +09:00
6d0d9cecd1 internal/pipewire: handle nil spa_dict correctly
This now marshals into a value of type None when the slice is nil, and correctly unmarshals from type None.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 07:12:00 +09:00
17248d7d61 internal/pipewire: unmarshal nil pointer correctly
This now calls unmarshalCheckTypeBounds to advance to the next message. Additionally, handling for None value is relocated to a function for reuse by other types.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 06:58:53 +09:00
41e5628c67 internal/pipewire: return correct size for nil spa_dict
A nil spa_dict results in a None type value being sent over the wire.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 06:07:46 +09:00
ffbec828e1 internal/pipewire: move Core wrapper methods under Core
These do not belong under Context, and is an early implementation limitation that carried over.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 05:37:21 +09:00
de0467a65e internal/pipewire: treat noAck violation as fatal
Receiving this event indicates something has gone terribly wrong somehow, and ignoring Core::BoundProps causes inconsistent state anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-13 22:30:06 +09:00
b5999b8814 internal/pipewire: implement Core::RemoveId
This is emitted by the server when a proxy id is removed for any reason. Currently, the only path for this to be emitted is when a global object is destroyed while some proxy is still bound to it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-13 21:50:32 +09:00
ebc67bb8ad nix: update flake lock
NixOS 25.11 introduces a crash in cage and an intermittent crash in foot.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-12 08:39:55 +09:00
e60ff660f6 internal/pipewire: treat unknown opcode as fatal
Skipping events can cause local state to diverge from the server.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-11 04:22:03 +09:00
47db461546 internal/pipewire: generic Core::Error handling
This flushes message buffer before queueing the event expecting the error. Since this is quite useful and relatively complex, it is relocated to a method of Context.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-11 04:07:55 +09:00
0a3fe5f907 internal/pipewire: export Registry::Destroy
This handles the error returned by Sync.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-11 03:34:33 +09:00
b72d502f1c internal/outcome: populate instance metadata for PipeWire
These have similar semantics to equivalent Wayland security-context-v1 fields.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-10 03:01:30 +09:00
f8b3db3f66 internal/pipewire: cleaner error message for unsupported type
The error string itself is descriptive enough, so use it as the error message directly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-10 01:51:06 +09:00
0e2fb1788f internal/pipewire: implement Registry::Destroy
This requires error handling infrastructure in Core that does not yet exist, so it is not exported for now. It has been manually tested via linkname against PipeWire.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-10 01:25:30 +09:00
d8417e2927 internal/pipewire: implement Registry::GlobalRemove
This is emitted by PipeWire when a global object disappears, because PipeWire insists that all clients that had called Core::GetRegistry must constantly sync its local registry state with the remote.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-10 01:00:03 +09:00
222 changed files with 45409 additions and 2176 deletions

View File

@@ -72,20 +72,20 @@ jobs:
path: result/* path: result/*
retention-days: 1 retention-days: 1
hpkg: sharefs:
name: Hpkg name: ShareFS
runs-on: nix runs-on: nix
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Run NixOS test - name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hpkg run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sharefs
- name: Upload test output - name: Upload test output
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: "hpkg-vm-output" name: "sharefs-vm-output"
path: result/* path: result/*
retention-days: 1 retention-days: 1
@@ -96,7 +96,7 @@ jobs:
- race - race
- sandbox - sandbox
- sandbox-race - sandbox-race
- hpkg - sharefs
runs-on: nix runs-on: nix
steps: steps:
- name: Checkout - name: Checkout

View File

@@ -1,5 +0,0 @@
DO NOT ADD NEW ACTIONS HERE
This port is solely for releasing to the github mirror and serves no purpose during development.
All development happens at https://git.gensokyo.uk/security/hakurei. If you wish to contribute,
request for an account on git.gensokyo.uk.

View File

@@ -1,46 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
name: Create release
runs-on: ubuntu-latest
permissions:
packages: write
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v32
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Restore and cache Nix store
uses: nix-community/cache-nix-action@v6
with:
primary-key: build-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
restore-prefixes-first-match: build-${{ runner.os }}-
gc-max-store-size-linux: 1G
purge: true
purge-prefixes: build-${{ runner.os }}-
purge-created: 60
purge-primary-key: never
- name: Build for release
run: nix build --print-out-paths --print-build-logs .#dist
- name: Release
uses: softprops/action-gh-release@v2
with:
files: |-
result/hakurei-**

View File

@@ -1,48 +0,0 @@
name: Test
on:
- push
jobs:
dist:
name: Create distribution
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v32
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Restore and cache Nix store
uses: nix-community/cache-nix-action@v6
with:
primary-key: build-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
restore-prefixes-first-match: build-${{ runner.os }}-
gc-max-store-size-linux: 1G
purge: true
purge-prefixes: build-${{ runner.os }}-
purge-created: 60
purge-primary-key: never
- name: Build for test
id: build-test
run: >-
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
nix build --print-out-paths --print-build-logs .#dist &&
mv package.nix.old package.nix &&
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
- name: Upload test build
uses: actions/upload-artifact@v4
with:
name: "hakurei-${{ steps.build-test.outputs.rev }}"
path: result/*
retention-days: 1

2
.gitignore vendored
View File

@@ -27,6 +27,8 @@ go.work.sum
# go generate # go generate
/cmd/hakurei/LICENSE /cmd/hakurei/LICENSE
/internal/pkg/testdata/testtool
/internal/rosa/hakurei_current.tar.gz
# release # release
/dist/hakurei-* /dist/hakurei-*

181
README.md
View File

@@ -15,164 +15,51 @@
<a href="https://hakurei.app"><img src="https://img.shields.io/website?url=https%3A%2F%2Fhakurei.app" alt="Website" /></a> <a href="https://hakurei.app"><img src="https://img.shields.io/website?url=https%3A%2F%2Fhakurei.app" alt="Website" /></a>
</p> </p>
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel. Hakurei is a tool for running sandboxed desktop applications as dedicated
It implements the application container of [planterette (WIP)](https://git.gensokyo.uk/security/planterette), subordinate users on the Linux kernel. It implements the application container
a self-contained Android-like package manager with modern security features. of [planterette (WIP)](https://git.gensokyo.uk/security/planterette), a
self-contained Android-like package manager with modern security features.
## NixOS Module usage Interaction with hakurei happens entirely through structures described by
package [hst](https://pkg.go.dev/hakurei.app/hst). No native API is available
due to internal details of uid isolation.
The NixOS module currently requires home-manager to configure subordinate users. Full module documentation can be found [here](options.md). ## Notable Packages
To use the module, import it into your configuration with Package [container](https://pkg.go.dev/hakurei.app/container) is general purpose
container tooling. It is used by the hakurei shim process running as the target
subordinate user to set up the application container. It has a single dependency,
[libseccomp](https://github.com/seccomp/libseccomp), to create BPF programs
for the [system call filter](https://www.kernel.org/doc/html/latest/userspace-api/seccomp_filter.html).
```nix Package [internal/pkg](https://pkg.go.dev/hakurei.app/internal/pkg) provides
{ infrastructure for hermetic builds. This replaces the legacy nix-based testing
inputs = { framework and serves as the build system of Rosa OS, currently developed under
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; package [internal/rosa](https://pkg.go.dev/hakurei.app/internal/rosa).
hakurei = { ## Dependencies
url = "git+https://git.gensokyo.uk/security/hakurei";
# Optional but recommended to limit the size of your system closure. `container` depends on:
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, hakurei, ... }: - [libseccomp](https://github.com/seccomp/libseccomp) to generate BPF programs.
{
nixosConfigurations.hakurei = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
hakurei.nixosModules.hakurei
];
};
};
}
```
This adds the `environment.hakurei` option: `cmd/hakurei` depends on:
```nix - [acl](https://savannah.nongnu.org/projects/acl/) to export sockets to
{ pkgs, ... }: subordinate users.
- [wayland](https://gitlab.freedesktop.org/wayland/wayland) to set up
[security-context-v1](https://wayland.app/protocols/security-context-v1).
- [xcb](https://xcb.freedesktop.org/) to grant and revoke subordinate users
access to the X server.
{ `cmd/sharefs` depends on:
environment.hakurei = {
enable = true;
stateDir = "/var/lib/hakurei";
users = {
alice = 0;
nixos = 10;
};
commonPaths = [ - [fuse](https://github.com/libfuse/libfuse) to implement the filesystem.
{
src = "/sdcard";
write = true;
}
];
extraHomeConfig = { New dependencies will generally not be added. Patches adding new dependencies
home.stateVersion = "23.05"; are very likely to be rejected.
};
apps = { ## NixOS Module (deprecated)
"org.chromium.Chromium" = {
name = "chromium";
identity = 1;
packages = [ pkgs.chromium ];
userns = true;
mapRealUid = true;
dbus = {
system = {
filter = true;
talk = [
"org.bluez"
"org.freedesktop.Avahi"
"org.freedesktop.UPower"
];
};
session =
f:
f {
talk = [
"org.freedesktop.FileManager1"
"org.freedesktop.Notifications"
"org.freedesktop.ScreenSaver"
"org.freedesktop.secrets"
"org.kde.kwalletd5"
"org.kde.kwalletd6"
];
own = [
"org.chromium.Chromium.*"
"org.mpris.MediaPlayer2.org.chromium.Chromium.*"
"org.mpris.MediaPlayer2.chromium.*"
];
call = { };
broadcast = { };
};
};
};
"org.claws_mail.Claws-Mail" = { The NixOS module is in maintenance mode and will be removed once planterette is
name = "claws-mail"; feature-complete. Full module documentation can be found [here](options.md).
identity = 2;
packages = [ pkgs.claws-mail ];
gpu = false;
capability.pulse = false;
};
"org.weechat" = {
name = "weechat";
identity = 3;
shareUid = true;
packages = [ pkgs.weechat ];
capability = {
wayland = false;
x11 = false;
dbus = true;
pulse = false;
};
};
"dev.vencord.Vesktop" = {
name = "discord";
identity = 3;
shareUid = true;
packages = [ pkgs.vesktop ];
share = pkgs.vesktop;
command = "vesktop --ozone-platform-hint=wayland";
userns = true;
mapRealUid = true;
capability.x11 = true;
dbus = {
session =
f:
f {
talk = [ "org.kde.StatusNotifierWatcher" ];
own = [ ];
call = { };
broadcast = { };
};
system.filter = true;
};
};
"io.looking-glass" = {
name = "looking-glass-client";
identity = 4;
useCommonPaths = false;
groups = [ "plugdev" ];
extraPaths = [
{
src = "/dev/shm/looking-glass";
write = true;
}
];
extraConfig = {
programs.looking-glass-client.enable = true;
};
};
};
};
}
```

58
cmd/earlyinit/main.go Normal file
View File

@@ -0,0 +1,58 @@
package main
import (
"log"
"os"
"runtime"
. "syscall"
)
func main() {
runtime.LockOSThread()
log.SetFlags(0)
log.SetPrefix("earlyinit: ")
if err := Mount(
"devtmpfs",
"/dev/",
"devtmpfs",
MS_NOSUID|MS_NOEXEC,
"",
); err != nil {
log.Fatalf("cannot mount devtmpfs: %v", err)
}
// The kernel might be unable to set up the console. When that happens,
// printk is called with "Warning: unable to open an initial console."
// and the init runs with no files. The checkfds runtime function
// populates 0-2 by opening /dev/null for them.
//
// This check replaces 1 and 2 with /dev/kmsg to improve the chance
// of output being visible to the user.
if fi, err := os.Stdout.Stat(); err == nil {
if stat, ok := fi.Sys().(*Stat_t); ok {
if stat.Rdev == 0x103 {
var fd int
if fd, err = Open(
"/dev/kmsg",
O_WRONLY|O_CLOEXEC,
0,
); err != nil {
log.Fatalf("cannot open kmsg: %v", err)
}
if err = Dup3(fd, Stdout, 0); err != nil {
log.Fatalf("cannot open stdout: %v", err)
}
if err = Dup3(fd, Stderr, 0); err != nil {
log.Fatalf("cannot open stderr: %v", err)
}
if err = Close(fd); err != nil {
log.Printf("cannot close kmsg: %v", err)
}
}
}
}
}

View File

@@ -14,7 +14,6 @@ import (
_ "unsafe" // for go:linkname _ "unsafe" // for go:linkname
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/container/fhs" "hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
@@ -187,14 +186,6 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
}}) }})
} }
// start pipewire-pulse: this most likely exists on host if PipeWire is available
if flagPulse {
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSDaemon{
Target: fhs.AbsRunUser.Append(strconv.Itoa(container.OverflowUid(msg)), "pulse/native"),
Exec: shell, Args: []string{"-lc", "exec pipewire-pulse"},
}})
}
config.Container.Filesystem = append(config.Container.Filesystem, config.Container.Filesystem = append(config.Container.Filesystem,
// opportunistically bind kvm // opportunistically bind kvm
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{ hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{

View File

@@ -1,7 +0,0 @@
This program is a proof of concept and is now deprecated. It is only kept
around for API demonstration purposes and to make the most out of the test
suite.
This program is replaced by planterette, which can be found at
https://git.gensokyo.uk/security/planterette. Development effort should be
focused there instead.

View File

@@ -1,173 +0,0 @@
package main
import (
"encoding/json"
"log"
"os"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
)
type appInfo struct {
Name string `json:"name"`
Version string `json:"version"`
// passed through to [hst.Config]
ID string `json:"id"`
// passed through to [hst.Config]
Identity int `json:"identity"`
// passed through to [hst.Config]
Groups []string `json:"groups,omitempty"`
// passed through to [hst.Config]
Devel bool `json:"devel,omitempty"`
// passed through to [hst.Config]
Userns bool `json:"userns,omitempty"`
// passed through to [hst.Config]
HostNet bool `json:"net,omitempty"`
// passed through to [hst.Config]
HostAbstract bool `json:"abstract,omitempty"`
// passed through to [hst.Config]
Device bool `json:"dev,omitempty"`
// passed through to [hst.Config]
Tty bool `json:"tty,omitempty"`
// passed through to [hst.Config]
MapRealUID bool `json:"map_real_uid,omitempty"`
// passed through to [hst.Config]
DirectWayland bool `json:"direct_wayland,omitempty"`
// passed through to [hst.Config]
SystemBus *hst.BusConfig `json:"system_bus,omitempty"`
// passed through to [hst.Config]
SessionBus *hst.BusConfig `json:"session_bus,omitempty"`
// passed through to [hst.Config]
Enablements *hst.Enablements `json:"enablements,omitempty"`
// passed through to [hst.Config]
Multiarch bool `json:"multiarch,omitempty"`
// passed through to [hst.Config]
Bluetooth bool `json:"bluetooth,omitempty"`
// allow gpu access within sandbox
GPU bool `json:"gpu"`
// store path to nixGL mesa wrappers
Mesa string `json:"mesa,omitempty"`
// store path to nixGL source
NixGL string `json:"nix_gl,omitempty"`
// store path to activate-and-exec script
Launcher *check.Absolute `json:"launcher"`
// store path to /run/current-system
CurrentSystem *check.Absolute `json:"current_system"`
// store path to home-manager activation package
ActivationPackage string `json:"activation_package"`
}
func (app *appInfo) toHst(pathSet *appPathSet, pathname *check.Absolute, argv []string, flagDropShell bool) *hst.Config {
config := &hst.Config{
ID: app.ID,
Enablements: app.Enablements,
SystemBus: app.SystemBus,
SessionBus: app.SessionBus,
DirectWayland: app.DirectWayland,
Identity: app.Identity,
Groups: app.Groups,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name),
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsPrivateTmp.Append("app")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Path: pathname,
Args: argv,
},
ExtraPerms: []hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
}
if app.Devel {
config.Container.Flags |= hst.FDevel
}
if app.Userns {
config.Container.Flags |= hst.FUserns
}
if app.HostNet {
config.Container.Flags |= hst.FHostNet
}
if app.HostAbstract {
config.Container.Flags |= hst.FHostAbstract
}
if app.Device {
config.Container.Flags |= hst.FDevice
}
if app.Tty || flagDropShell {
config.Container.Flags |= hst.FTty
}
if app.MapRealUID {
config.Container.Flags |= hst.FMapRealUID
}
if app.Multiarch {
config.Container.Flags |= hst.FMultiarch
}
config.Container.Flags |= hst.FShareRuntime | hst.FShareTmpdir
return config
}
func loadAppInfo(name string, beforeFail func()) *appInfo {
bundle := new(appInfo)
if f, err := os.Open(name); err != nil {
beforeFail()
log.Fatalf("cannot open bundle: %v", err)
} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
beforeFail()
log.Fatalf("cannot parse bundle metadata: %v", err)
} else if err = f.Close(); err != nil {
log.Printf("cannot close bundle metadata: %v", err)
// not fatal
}
if bundle.ID == "" {
beforeFail()
log.Fatal("application identifier must not be empty")
}
if bundle.Launcher == nil {
beforeFail()
log.Fatal("launcher must not be empty")
}
if bundle.CurrentSystem == nil {
beforeFail()
log.Fatal("current-system must not be empty")
}
return bundle
}
func formatHostname(name string) string {
if h, err := os.Hostname(); err != nil {
log.Printf("cannot get hostname: %v", err)
return "hakurei-" + name
} else {
return h + "-" + name
}
}

View File

@@ -1,256 +0,0 @@
{
nixpkgsFor,
system,
nixpkgs,
home-manager,
}:
{
lib,
stdenv,
closureInfo,
writeScript,
runtimeShell,
writeText,
symlinkJoin,
vmTools,
runCommand,
fetchFromGitHub,
zstd,
nix,
sqlite,
name ? throw "name is required",
version ? throw "version is required",
pname ? "${name}-${version}",
modules ? [ ],
nixosModules ? [ ],
script ? ''
exec "$SHELL" "$@"
'',
id ? name,
identity ? throw "identity is required",
groups ? [ ],
userns ? false,
net ? true,
dev ? false,
no_new_session ? false,
map_real_uid ? false,
direct_wayland ? false,
system_bus ? null,
session_bus ? null,
allow_wayland ? true,
allow_x11 ? false,
allow_dbus ? true,
allow_audio ? true,
gpu ? allow_wayland || allow_x11,
}:
let
inherit (lib) optionals;
homeManagerConfiguration = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgsFor.${system};
modules = modules ++ [
{
home = {
username = "hakurei";
homeDirectory = "/data/data/${id}";
stateVersion = "22.11";
};
}
];
};
launcher = writeScript "hakurei-${pname}" ''
#!${runtimeShell} -el
${script}
'';
extraNixOSConfig =
{ pkgs, ... }:
{
environment = {
etc.nixpkgs.source = nixpkgs.outPath;
systemPackages = [ pkgs.nix ];
};
imports = nixosModules;
};
nixos = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
extraNixOSConfig
{ nix.settings.experimental-features = [ "flakes" ]; }
{ nix.settings.experimental-features = [ "nix-command" ]; }
{ boot.isContainer = true; }
{ system.stateVersion = "22.11"; }
];
};
etc = vmTools.runInLinuxVM (
runCommand "etc" { } ''
mkdir -p /etc
${nixos.config.system.build.etcActivationCommands}
# remove unused files
rm -rf /etc/sudoers
mkdir -p $out
tar -C /etc -cf "$out/etc.tar" .
''
);
extendSessionDefault = id: ext: {
filter = true;
talk = [ "org.freedesktop.Notifications" ] ++ ext.talk;
own =
(optionals (id != null) [
"${id}.*"
"org.mpris.MediaPlayer2.${id}.*"
])
++ ext.own;
inherit (ext) call broadcast;
};
nixGL = fetchFromGitHub {
owner = "nix-community";
repo = "nixGL";
rev = "310f8e49a149e4c9ea52f1adf70cdc768ec53f8a";
hash = "sha256-lnzZQYG0+EXl/6NkGpyIz+FEOc/DSEG57AP1VsdeNrM=";
};
mesaWrappers =
let
isIntelX86Platform = system == "x86_64-linux";
nixGLPackages = import (nixGL + "/default.nix") {
pkgs = nixpkgs.legacyPackages.${system};
enable32bits = isIntelX86Platform;
enableIntelX86Extensions = isIntelX86Platform;
};
in
symlinkJoin {
name = "nixGL-mesa";
paths = with nixGLPackages; [
nixGLIntel
nixVulkanIntel
];
};
info = builtins.toJSON {
inherit
name
version
id
identity
launcher
groups
userns
net
dev
no_new_session
map_real_uid
direct_wayland
system_bus
gpu
;
session_bus =
if session_bus != null then
(session_bus (extendSessionDefault id))
else
(extendSessionDefault id {
talk = [ ];
own = [ ];
call = { };
broadcast = { };
});
enablements = {
wayland = allow_wayland;
x11 = allow_x11;
dbus = allow_dbus;
pipewire = allow_audio;
};
mesa = if gpu then mesaWrappers else null;
nix_gl = if gpu then nixGL else null;
current_system = nixos.config.system.build.toplevel;
activation_package = homeManagerConfiguration.activationPackage;
};
in
stdenv.mkDerivation {
name = "${pname}.pkg";
inherit version;
__structuredAttrs = true;
nativeBuildInputs = [
zstd
nix
sqlite
];
buildCommand = ''
NIX_ROOT="$(mktemp -d)"
export USER="nobody"
# create bootstrap store
bootstrapClosureInfo="${
closureInfo {
rootPaths = [
nix
nixos.config.system.build.toplevel
];
}
}"
echo "copying bootstrap store paths..."
mkdir -p "$NIX_ROOT/nix/store"
xargs -n 1 -a "$bootstrapClosureInfo/store-paths" cp -at "$NIX_ROOT/nix/store/"
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --load-db < "$bootstrapClosureInfo/registration"
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --optimise
sqlite3 "$NIX_ROOT/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
chmod -R +r "$NIX_ROOT/nix/var"
# create binary cache
closureInfo="${
closureInfo {
rootPaths = [
homeManagerConfiguration.activationPackage
launcher
]
++ optionals gpu [
mesaWrappers
nixGL
];
}
}"
echo "copying application paths..."
TMP_STORE="$(mktemp -d)"
mkdir -p "$TMP_STORE/nix/store"
xargs -n 1 -a "$closureInfo/store-paths" cp -at "$TMP_STORE/nix/store/"
NIX_REMOTE="local?root=$TMP_STORE" nix-store --load-db < "$closureInfo/registration"
sqlite3 "$TMP_STORE/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
NIX_REMOTE="local?root=$TMP_STORE" nix --offline --extra-experimental-features nix-command \
--verbose --log-format raw-with-logs \
copy --all --no-check-sigs --to \
"file://$NIX_ROOT/res?compression=zstd&compression-level=19&parallel-compression=true"
# package /etc
mkdir -p "$NIX_ROOT/etc"
tar -C "$NIX_ROOT/etc" -xf "${etc}/etc.tar"
# write metadata
cp "${writeText "bundle.json" info}" "$NIX_ROOT/bundle.json"
# create an intermediate file to improve zstd performance
INTER="$(mktemp)"
tar -C "$NIX_ROOT" -cf "$INTER" .
zstd -T0 -19 -fo "$out" "$INTER"
'';
}

View File

@@ -1,335 +0,0 @@
package main
import (
"context"
"encoding/json"
"errors"
"log"
"os"
"os/signal"
"path"
"syscall"
"hakurei.app/command"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/message"
)
var (
errSuccess = errors.New("success")
)
func main() {
log.SetPrefix("hpkg: ")
log.SetFlags(0)
msg := message.New(log.Default())
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)
}
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
var (
flagVerbose bool
flagDropShell bool
)
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { msg.SwapVerbose(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
{
var (
flagDropShellActivate bool
)
c.NewCommand("install", "Install an application from its package", func(args []string) error {
if len(args) != 1 {
log.Println("invalid argument")
return syscall.EINVAL
}
pkgPath := args[0]
if !path.IsAbs(pkgPath) {
if dir, err := os.Getwd(); err != nil {
log.Printf("cannot get current directory: %v", err)
return err
} else {
pkgPath = path.Join(dir, pkgPath)
}
}
/*
Look up paths to programs started by hpkg.
This is done here to ease error handling as cleanup is not yet required.
*/
var (
_ = lookPath("zstd")
tar = lookPath("tar")
chmod = lookPath("chmod")
rm = lookPath("rm")
)
/*
Extract package and set up for cleanup.
*/
var workDir *check.Absolute
if p, err := os.MkdirTemp("", "hpkg.*"); err != nil {
log.Printf("cannot create temporary directory: %v", err)
return err
} else if workDir, err = check.NewAbs(p); err != nil {
log.Printf("invalid temporary directory: %v", err)
return err
}
cleanup := func() {
// should be faster than a native implementation
mustRun(msg, chmod, "-R", "+w", workDir.String())
mustRun(msg, rm, "-rf", workDir.String())
}
beforeRunFail.Store(&cleanup)
mustRun(msg, tar, "-C", workDir.String(), "-xf", pkgPath)
/*
Parse bundle and app metadata, do pre-install checks.
*/
bundle := loadAppInfo(path.Join(workDir.String(), "bundle.json"), cleanup)
pathSet := pathSetByApp(bundle.ID)
a := bundle
if s, err := os.Stat(pathSet.metaPath.String()); err != nil {
if !os.IsNotExist(err) {
cleanup()
log.Printf("cannot access %q: %v", pathSet.metaPath, err)
return err
}
// did not modify app, clean installation condition met later
} else if s.IsDir() {
cleanup()
log.Printf("metadata path %q is not a file", pathSet.metaPath)
return syscall.EBADMSG
} else {
a = loadAppInfo(pathSet.metaPath.String(), cleanup)
if a.ID != bundle.ID {
cleanup()
log.Printf("app %q claims to have identifier %q",
bundle.ID, a.ID)
return syscall.EBADE
}
// sec: should verify credentials
}
if a != bundle {
// do not try to re-install
if a.NixGL == bundle.NixGL &&
a.CurrentSystem == bundle.CurrentSystem &&
a.Launcher == bundle.Launcher &&
a.ActivationPackage == bundle.ActivationPackage {
cleanup()
log.Printf("package %q is identical to local application %q",
pkgPath, a.ID)
return errSuccess
}
// identity determines uid
if a.Identity != bundle.Identity {
cleanup()
log.Printf("package %q identity %d differs from installed %d",
pkgPath, bundle.Identity, a.Identity)
return syscall.EBADE
}
// sec: should compare version string
msg.Verbosef("installing application %q version %q over local %q",
bundle.ID, bundle.Version, a.Version)
} else {
msg.Verbosef("application %q clean installation", bundle.ID)
// sec: should install credentials
}
/*
Setup steps for files owned by the target user.
*/
withCacheDir(ctx, msg, "install", []string{
// export inner bundle path in the environment
"export BUNDLE=" + hst.PrivateTmp + "/bundle",
// replace inner /etc
"mkdir -p etc",
"chmod -R +w etc",
"rm -rf etc",
"cp -dRf $BUNDLE/etc etc",
// replace inner /nix
"mkdir -p nix",
"chmod -R +w nix",
"rm -rf nix",
"cp -dRf /nix nix",
// copy from binary cache
"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
// deduplicate nix store
"nix store --offline --store $PWD optimise",
// make cache directory world-readable for autoetc
"chmod 0755 .",
}, workDir, bundle, pathSet, flagDropShell, cleanup)
if bundle.GPU {
withCacheDir(ctx, msg, "mesa-wrappers", []string{
// link nixGL mesa wrappers
"mkdir -p nix/.nixGL",
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
}, workDir, bundle, pathSet, false, cleanup)
}
/*
Activate home-manager generation.
*/
withNixDaemon(ctx, msg, "activate", []string{
// clean up broken links
"mkdir -p .local/state/{nix,home-manager}",
"chmod -R +w .local/state/{nix,home-manager}",
"rm -rf .local/state/{nix,home-manager}",
// run activation script
bundle.ActivationPackage + "/activate",
}, false, func(config *hst.Config) *hst.Config { return config },
bundle, pathSet, flagDropShellActivate, cleanup)
/*
Installation complete. Write metadata to block re-installs or downgrades.
*/
// serialise metadata to ensure consistency
if f, err := os.OpenFile(pathSet.metaPath.String()+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
cleanup()
log.Printf("cannot create metadata file: %v", err)
return err
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
cleanup()
log.Printf("cannot write metadata: %v", err)
return err
} else if err = f.Close(); err != nil {
log.Printf("cannot close metadata file: %v", err)
// not fatal
}
if err := os.Rename(pathSet.metaPath.String()+"~", pathSet.metaPath.String()); err != nil {
cleanup()
log.Printf("cannot rename metadata file: %v", err)
return err
}
cleanup()
return errSuccess
}).
Flag(&flagDropShellActivate, "s", command.BoolFlag(false), "Drop to a shell on activation")
}
{
var (
flagDropShellNixGL bool
flagAutoDrivers bool
)
c.NewCommand("start", "Start an application", func(args []string) error {
if len(args) < 1 {
log.Println("invalid argument")
return syscall.EINVAL
}
/*
Parse app metadata.
*/
id := args[0]
pathSet := pathSetByApp(id)
a := loadAppInfo(pathSet.metaPath.String(), func() {})
if a.ID != id {
log.Printf("app %q claims to have identifier %q", id, a.ID)
return syscall.EBADE
}
/*
Prepare nixGL.
*/
if a.GPU && flagAutoDrivers {
withNixDaemon(ctx, msg, "nix-gl", []string{
"mkdir -p /nix/.nixGL/auto",
"rm -rf /nix/.nixGL/auto",
"export NIXPKGS_ALLOW_UNFREE=1",
"nix build --impure " +
"--out-link /nix/.nixGL/auto/opengl " +
"--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + a.NixGL,
"nix build --impure " +
"--out-link /nix/.nixGL/auto/vulkan " +
"--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + a.NixGL + "#nixVulkanNvidia",
}, true, func(config *hst.Config) *hst.Config {
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices"), Optional: true}},
}...)
appendGPUFilesystem(config)
return config
}, a, pathSet, flagDropShellNixGL, func() {})
}
/*
Create app configuration.
*/
pathname := a.Launcher
argv := make([]string, 1, len(args))
if flagDropShell {
pathname = pathShell
argv[0] = bash
} else {
argv[0] = a.Launcher.String()
}
argv = append(argv, args[1:]...)
config := a.toHst(pathSet, pathname, argv, flagDropShell)
/*
Expose GPU devices.
*/
if a.GPU {
config.Container.Filesystem = append(config.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsPrivateTmp.Append("nixGL")}})
appendGPUFilesystem(config)
}
/*
Spawn app.
*/
mustRunApp(ctx, msg, config, func() {})
return errSuccess
}).
Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
Flag(&flagAutoDrivers, "auto-drivers", command.BoolFlag(false), "Attempt automatic opengl driver detection")
}
c.MustParse(os.Args[1:], func(err error) {
msg.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
msg.BeforeExit()
os.Exit(0)
}
})
log.Fatal("unreachable")
}

View File

@@ -1,117 +0,0 @@
package main
import (
"log"
"os"
"os/exec"
"strconv"
"sync/atomic"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/message"
)
const bash = "bash"
var (
dataHome *check.Absolute
)
func init() {
// dataHome
if a, err := check.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil {
dataHome = a
} else {
dataHome = fhs.AbsVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid()))
}
}
var (
pathBin = fhs.AbsRoot.Append("bin")
pathNix = check.MustAbs("/nix/")
pathNixStore = pathNix.Append("store/")
pathCurrentSystem = fhs.AbsRun.Append("current-system")
pathSwBin = pathCurrentSystem.Append("sw/bin/")
pathShell = pathSwBin.Append(bash)
pathData = check.MustAbs("/data")
pathDataData = pathData.Append("data")
)
func lookPath(file string) string {
if p, err := exec.LookPath(file); err != nil {
log.Fatalf("%s: command not found", file)
return ""
} else {
return p
}
}
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(msg message.Msg, name string, arg ...string) {
msg.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
if f := beforeRunFail.Swap(nil); f != nil {
(*f)()
}
log.Fatalf("%s: %v", name, err)
}
}
type appPathSet struct {
// ${dataHome}/${id}
baseDir *check.Absolute
// ${baseDir}/app
metaPath *check.Absolute
// ${baseDir}/files
homeDir *check.Absolute
// ${baseDir}/cache
cacheDir *check.Absolute
// ${baseDir}/cache/nix
nixPath *check.Absolute
}
func pathSetByApp(id string) *appPathSet {
pathSet := new(appPathSet)
pathSet.baseDir = dataHome.Append(id)
pathSet.metaPath = pathSet.baseDir.Append("app")
pathSet.homeDir = pathSet.baseDir.Append("files")
pathSet.cacheDir = pathSet.baseDir.Append("cache")
pathSet.nixPath = pathSet.cacheDir.Append("nix")
return pathSet
}
func appendGPUFilesystem(config *hst.Config) {
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("dri"), Device: true, Optional: true}},
// mali
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("mali"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("mali0"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("umplock"), Device: true, Optional: true}},
// nvidia
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidiactl"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-modeset"), Device: true, Optional: true}},
// nvidia OpenCL/CUDA
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-uvm"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-uvm-tools"), Device: true, Optional: true}},
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia1"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia2"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia3"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia4"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia5"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia6"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia7"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia8"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia9"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia10"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia11"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia12"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia13"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia14"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia15"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia16"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia17"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia18"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia19"), Device: true, Optional: true}},
}...)
}

View File

@@ -1,61 +0,0 @@
package main
import (
"context"
"encoding/json"
"errors"
"io"
"log"
"os"
"os/exec"
"hakurei.app/hst"
"hakurei.app/internal/info"
"hakurei.app/message"
)
var hakureiPathVal = info.MustHakureiPath().String()
func mustRunApp(ctx context.Context, msg message.Msg, config *hst.Config, beforeFail func()) {
var (
cmd *exec.Cmd
st io.WriteCloser
)
if r, w, err := os.Pipe(); err != nil {
beforeFail()
log.Fatalf("cannot pipe: %v", err)
} else {
if msg.IsVerbose() {
cmd = exec.CommandContext(ctx, hakureiPathVal, "-v", "app", "3")
} else {
cmd = exec.CommandContext(ctx, hakureiPathVal, "app", "3")
}
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.ExtraFiles = []*os.File{r}
st = w
}
go func() {
if err := json.NewEncoder(st).Encode(config); err != nil {
beforeFail()
log.Fatalf("cannot send configuration: %v", err)
}
}()
if err := cmd.Start(); err != nil {
beforeFail()
log.Fatalf("cannot start hakurei: %v", err)
}
if err := cmd.Wait(); err != nil {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
beforeFail()
msg.BeforeExit()
os.Exit(exitError.ExitCode())
} else {
beforeFail()
log.Fatalf("cannot wait: %v", err)
}
}
}

View File

@@ -1,62 +0,0 @@
{ pkgs, ... }:
{
users.users = {
alice = {
isNormalUser = true;
description = "Alice Foobar";
password = "foobar";
uid = 1000;
};
};
home-manager.users.alice.home.stateVersion = "24.11";
# Automatically login on tty1 as a normal user:
services.getty.autologinUser = "alice";
environment = {
variables = {
SWAYSOCK = "/tmp/sway-ipc.sock";
WLR_RENDERER = "pixman";
};
};
# Automatically configure and start Sway when logging in on tty1:
programs.bash.loginShellInit = ''
if [ "$(tty)" = "/dev/tty1" ]; then
set -e
mkdir -p ~/.config/sway
(sed s/Mod4/Mod1/ /etc/sway/config &&
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
sway --validate
systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
fi
'';
programs.sway.enable = true;
virtualisation = {
diskSize = 6 * 1024;
qemu.options = [
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
"-vga none -device virtio-gpu-pci"
# Increase zstd performance:
"-smp 8"
];
};
environment.hakurei = {
enable = true;
stateDir = "/var/lib/hakurei";
users.alice = 0;
extraHomeConfig = {
home.stateVersion = "23.05";
};
};
}

View File

@@ -1,34 +0,0 @@
{
nixosTest,
callPackage,
system,
self,
}:
let
buildPackage = self.buildPackage.${system};
in
nixosTest {
name = "hpkg";
nodes.machine = {
environment.etc = {
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
};
imports = [
./configuration.nix
self.nixosModules.hakurei
self.inputs.home-manager.nixosModules.home-manager
];
};
# adapted from nixos sway integration tests
# testScriptWithTypes:49: error: Cannot call function of unknown type
# (machine.succeed if succeed else machine.execute)(
# ^
# Found 1 error in 1 file (checked 1 source file)
skipTypeCheck = true;
testScript = builtins.readFile ./test.py;
}

View File

@@ -1,48 +0,0 @@
{
lib,
buildPackage,
foot,
wayland-utils,
inconsolata,
}:
buildPackage {
name = "foot";
inherit (foot) version;
identity = 2;
id = "org.codeberg.dnkl.foot";
modules = [
{
home.packages = [
foot
# For wayland-info:
wayland-utils
];
}
];
nixosModules = [
{
# To help with OCR:
environment.etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
main = {
font = "inconsolata:size=14";
};
colors = rec {
foreground = "000000";
background = "ffffff";
regular2 = foreground;
};
};
fonts.packages = [ inconsolata ];
}
];
script = ''
exec foot "$@"
'';
}

View File

@@ -1,110 +0,0 @@
import json
import shlex
q = shlex.quote
NODE_GROUPS = ["nodes", "floating_nodes"]
def swaymsg(command: str = "", succeed=True, type="command"):
assert command != "" or type != "command", "Must specify command or type"
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
with machine.nested(
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
):
ret = (machine.succeed if succeed else machine.execute)(
f"su - alice -c {shell}"
)
# execute also returns a status code, but disregard.
if not succeed:
_, ret = ret
if not succeed and not ret:
return None
parsed = json.loads(ret)
return parsed
def walk(tree):
yield tree
for group in NODE_GROUPS:
for node in tree.get(group, []):
yield from walk(node)
def wait_for_window(pattern):
def func(last_chance):
nodes = (node["name"] for node in walk(swaymsg(type="get_tree")))
if last_chance:
nodes = list(nodes)
machine.log(f"Last call! Current list of windows: {nodes}")
return any(pattern in name for name in nodes)
retry(func)
def collect_state_ui(name):
swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'")
machine.copy_from_vm(f"/tmp/{name}.ps", "")
swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'")
machine.copy_from_vm(f"/tmp/{name}.json", "")
machine.screenshot(name)
def check_state(name, enablements):
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
if len(instances) != 1:
raise Exception(f"unexpected state length {len(instances)}")
instance = instances[0]
if len(instance['container']['args']) != 1 or not (instance['container']['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (instance['container']['args'][0]):
raise Exception(f"unexpected args {instance['container']['args']}")
if instance['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['enablements']}")
start_all()
machine.wait_for_unit("multi-user.target")
# To check hakurei's version:
print(machine.succeed("sudo -u alice -i hakurei version"))
# Wait for Sway to complete startup:
machine.wait_for_file("/run/user/1000/wayland-1")
machine.wait_for_file("/tmp/sway-ipc.sock")
# Prepare hpkg directory:
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
# Install hpkg app:
swaymsg("exec hpkg -v install /etc/foot.pkg && touch /tmp/hpkg-install-ok")
machine.wait_for_file("/tmp/hpkg-install-ok")
# Start app (foot) with Wayland enablement:
swaymsg("exec hpkg -v start org.codeberg.dnkl.foot")
wait_for_window("hakurei@machine-foot")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
collect_state_ui("app_wayland")
check_state("foot", {"wayland": True, "dbus": True, "pipewire": True})
# Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002"))
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot")
# Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002")
# Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")
# Print hakurei share and rundir contents:
print(machine.succeed("find /tmp/hakurei.0 "
+ "-path '/tmp/hakurei.0/runtime/*/*' -prune -o "
+ "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
+ "-print"))
print(machine.fail("ls /run/user/1000/hakurei"))

View File

@@ -1,130 +0,0 @@
package main
import (
"context"
"os"
"strings"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/message"
)
func withNixDaemon(
ctx context.Context,
msg message.Msg,
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
flags := hst.FMultiarch | hst.FUserns // nix sandbox requires userns
if net {
flags |= hst.FHostNet
}
if dropShell {
flags |= hst.FTty
}
mustRunAppDropShell(ctx, msg, updateConfig(&hst.Config{
ID: app.ID,
ExtraPerms: []hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Path: pathShell,
Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon
"nix-daemon --store / & " +
// wait for socket to appear
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
// create directory so nix stops complaining
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
strings.Join(command, " && ") +
// terminate nix-daemon
" && pkill nix-daemon",
},
Flags: flags,
},
}), dropShell, beforeFail)
}
func withCacheDir(
ctx context.Context,
msg message.Msg,
action string, command []string, workDir *check.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
flags := hst.FMultiarch
if dropShell {
flags |= hst.FTty
}
mustRunAppDropShell(ctx, msg, &hst.Config{
ID: app.ID,
ExtraPerms: []hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
{Path: workDir, Execute: true},
},
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: workDir.Append(fhs.Etc), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsPrivateTmp.Append("bundle")}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID, "cache"), Source: pathSet.cacheDir, Write: true, Ensure: true}},
},
Username: "nixos",
Shell: pathShell,
Home: pathDataData.Append(app.ID, "cache"),
Path: pathShell,
Args: []string{bash, "-lc", strings.Join(command, " && ")},
Flags: flags,
},
}, dropShell, beforeFail)
}
func mustRunAppDropShell(ctx context.Context, msg message.Msg, config *hst.Config, dropShell bool, beforeFail func()) {
if dropShell {
if config.Container != nil {
config.Container.Args = []string{bash, "-l"}
}
mustRunApp(ctx, msg, config, beforeFail)
beforeFail()
msg.BeforeExit()
os.Exit(0)
}
mustRunApp(ctx, msg, config, beforeFail)
}

431
cmd/mbf/main.go Normal file
View File

@@ -0,0 +1,431 @@
package main
import (
"context"
"errors"
"fmt"
"io"
"log"
"os"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"time"
"unique"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
"hakurei.app/message"
)
func main() {
container.TryArgv0(nil)
log.SetFlags(0)
log.SetPrefix("mbf: ")
msg := message.New(log.Default())
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
var cache *pkg.Cache
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer stop()
defer func() {
if cache != nil {
cache.Close()
}
if r := recover(); r != nil {
fmt.Println(r)
log.Fatal("consider scrubbing the on-disk cache")
}
}()
var (
flagQuiet bool
flagCures int
flagBase string
flagTShift int
flagIdle bool
)
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) {
msg.SwapVerbose(!flagQuiet)
flagBase = os.ExpandEnv(flagBase)
if flagBase == "" {
flagBase = "cache"
}
var base *check.Absolute
if flagBase, err = filepath.Abs(flagBase); err != nil {
return
} else if base, err = check.NewAbs(flagBase); err != nil {
return
}
if cache, err = pkg.Open(ctx, msg, flagCures, base); err == nil {
if flagTShift < 0 {
cache.SetThreshold(0)
} else if flagTShift > 31 {
cache.SetThreshold(1 << 31)
} else {
cache.SetThreshold(1 << flagTShift)
}
}
if flagIdle {
pkg.SchedPolicy = container.SCHED_IDLE
}
return
}).Flag(
&flagQuiet,
"q", command.BoolFlag(false),
"Do not print cure messages",
).Flag(
&flagCures,
"cures", command.IntFlag(0),
"Maximum number of dependencies to cure at any given time",
).Flag(
&flagBase,
"d", command.StringFlag("$MBF_CACHE_DIR"),
"Directory to store cured artifacts",
).Flag(
&flagTShift,
"tshift", command.IntFlag(-1),
"Dependency graph size exponent, to the power of 2",
).Flag(
&flagIdle,
"sched-idle", command.BoolFlag(false),
"Set SCHED_IDLE scheduling policy",
)
{
var flagShifts int
c.NewCommand(
"scrub", "Examine the on-disk cache for errors",
func(args []string) error {
if len(args) > 0 {
return errors.New("scrub expects no arguments")
}
if flagShifts < 0 || flagShifts > 31 {
flagShifts = 12
}
return cache.Scrub(runtime.NumCPU() << flagShifts)
},
).Flag(
&flagShifts,
"shift", command.IntFlag(12),
"Scrub parallelism size exponent, to the power of 2",
)
}
{
var (
flagGentoo string
flagChecksum string
flagStage0 bool
)
c.NewCommand(
"stage3",
"Check for toolchain 3-stage non-determinism",
func(args []string) (err error) {
t := rosa.Std
if flagGentoo != "" {
t -= 3 // magic number to discourage misuse
var checksum pkg.Checksum
if len(flagChecksum) != 0 {
if err = pkg.Decode(&checksum, flagChecksum); err != nil {
return
}
}
rosa.SetGentooStage3(flagGentoo, checksum)
}
_, _, _, stage1 := (t - 2).NewLLVM()
_, _, _, stage2 := (t - 1).NewLLVM()
_, _, _, stage3 := t.NewLLVM()
var (
pathname *check.Absolute
checksum [2]unique.Handle[pkg.Checksum]
)
if pathname, _, err = cache.Cure(stage1); err != nil {
return err
}
log.Println("stage1:", pathname)
if pathname, checksum[0], err = cache.Cure(stage2); err != nil {
return err
}
log.Println("stage2:", pathname)
if pathname, checksum[1], err = cache.Cure(stage3); err != nil {
return err
}
log.Println("stage3:", pathname)
if checksum[0] != checksum[1] {
err = &pkg.ChecksumMismatchError{
Got: checksum[0].Value(),
Want: checksum[1].Value(),
}
} else {
log.Println(
"stage2 is identical to stage3",
"("+pkg.Encode(checksum[0].Value())+")",
)
}
if flagStage0 {
if pathname, _, err = cache.Cure(
t.Load(rosa.Stage0),
); err != nil {
return err
}
log.Println(pathname)
}
return
},
).
Flag(
&flagGentoo,
"gentoo", command.StringFlag(""),
"Bootstrap from a Gentoo stage3 tarball",
).
Flag(
&flagChecksum,
"checksum", command.StringFlag(""),
"Checksum of Gentoo stage3 tarball",
).
Flag(
&flagStage0,
"stage0", command.BoolFlag(false),
"Create bootstrap stage0 tarball",
)
}
{
var (
flagDump string
)
c.NewCommand(
"cure",
"Cure the named artifact and show its path",
func(args []string) error {
if len(args) != 1 {
return errors.New("cure requires 1 argument")
}
if p, ok := rosa.ResolveName(args[0]); !ok {
return fmt.Errorf("unknown artifact %q", args[0])
} else if flagDump == "" {
pathname, _, err := cache.Cure(rosa.Std.Load(p))
if err == nil {
log.Println(pathname)
}
return err
} else {
f, err := os.OpenFile(
flagDump,
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
0644,
)
if err != nil {
return err
}
if err = cache.EncodeAll(f, rosa.Std.Load(p)); err != nil {
_ = f.Close()
return err
}
return f.Close()
}
},
).
Flag(
&flagDump,
"dump", command.StringFlag(""),
"Write IR to specified pathname and terminate",
)
}
c.NewCommand(
"status",
"Display the status file of an artifact",
func(args []string) error {
if len(args) != 1 {
return errors.New("status requires 1 argument")
}
if p, ok := rosa.ResolveName(args[0]); !ok {
return fmt.Errorf("unknown artifact %q", args[0])
} else {
r, err := cache.OpenStatus(rosa.Std.Load(p))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return errors.New(args[0] + " was never cured")
}
return err
}
_, err = io.Copy(os.Stdout, r)
return errors.Join(err, r.Close())
}
},
)
{
var (
flagNet bool
flagSession bool
flagWithToolchain bool
)
c.NewCommand(
"shell",
"Interactive shell in the specified Rosa OS environment",
func(args []string) error {
root := make([]pkg.Artifact, 0, 6+len(args))
for _, arg := range args {
p, ok := rosa.ResolveName(arg)
if !ok {
return fmt.Errorf("unknown artifact %q", arg)
}
root = append(root, rosa.Std.Load(p))
}
if flagWithToolchain {
musl, compilerRT, runtimes, clang := rosa.Std.NewLLVM()
root = append(root, musl, compilerRT, runtimes, clang)
} else {
root = append(root, rosa.Std.Load(rosa.Musl))
}
root = append(root,
rosa.Std.Load(rosa.Mksh),
rosa.Std.Load(rosa.Toybox),
)
type cureRes struct {
pathname *check.Absolute
checksum unique.Handle[pkg.Checksum]
}
cured := make(map[pkg.Artifact]cureRes)
for _, a := range root {
pathname, checksum, err := cache.Cure(a)
if err != nil {
return err
}
cured[a] = cureRes{pathname, checksum}
}
layers := pkg.PromoteLayers(root, func(a pkg.Artifact) (
*check.Absolute,
unique.Handle[pkg.Checksum],
) {
res := cured[a]
return res.pathname, res.checksum
}, func(i int, d pkg.Artifact) {
r := pkg.Encode(cache.Ident(d).Value())
if s, ok := d.(fmt.Stringer); ok {
if name := s.String(); name != "" {
r += "-" + name
}
}
msg.Verbosef("promoted layer %d as %s", i, r)
})
z := container.New(ctx, msg)
z.WaitDelay = 3 * time.Second
z.SeccompPresets = pkg.SeccompPresets
z.SeccompFlags |= seccomp.AllowMultiarch
z.ParentPerm = 0700
z.HostNet = flagNet
z.RetainSession = flagSession
z.Hostname = "localhost"
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
var tempdir *check.Absolute
if s, err := filepath.Abs(os.TempDir()); err != nil {
return err
} else if tempdir, err = check.NewAbs(s); err != nil {
return err
}
z.Dir = fhs.AbsRoot
z.Env = []string{
"SHELL=/system/bin/mksh",
"PATH=/system/bin",
"HOME=/",
}
z.Path = rosa.AbsSystem.Append("bin", "mksh")
z.Args = []string{"mksh"}
z.
OverlayEphemeral(fhs.AbsRoot, layers...).
Place(
fhs.AbsEtc.Append("hosts"),
[]byte("127.0.0.1 localhost\n"),
).
Place(
fhs.AbsEtc.Append("passwd"),
[]byte("media_rw:x:1023:1023::/:/system/bin/sh\n"+
"nobody:x:65534:65534::/proc/nonexistent:/system/bin/false\n"),
).
Place(
fhs.AbsEtc.Append("group"),
[]byte("media_rw:x:1023:\nnobody:x:65534:\n"),
).
Bind(tempdir, fhs.AbsTmp, std.BindWritable).
Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
if err := z.Start(); err != nil {
return err
}
if err := z.Serve(); err != nil {
return err
}
return z.Wait()
},
).
Flag(
&flagNet,
"net", command.BoolFlag(false),
"Share host net namespace",
).
Flag(
&flagSession,
"session", command.BoolFlag(false),
"Retain session",
).
Flag(
&flagWithToolchain,
"with-toolchain", command.BoolFlag(false),
"Include the stage3 LLVM toolchain",
)
}
c.Command(
"help",
"Show this help message",
func([]string) error { c.PrintHelp(); return nil },
)
c.MustParse(os.Args[1:], func(err error) {
if cache != nil {
cache.Close()
}
log.Fatal(err)
})
}

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

@@ -0,0 +1,55 @@
package main
import (
"embed"
"fmt"
"net/http"
)
//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc ui/static/index.ts"
//go:embed ui/*
var content embed.FS
func serveWebUI(w http.ResponseWriter, r *http.Request) {
fmt.Printf("serveWebUI: %s\n", r.URL.Path)
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-XSS-Protection", "1")
w.Header().Set("X-Frame-Options", "DENY")
http.ServeFileFS(w, r, content, "ui/index.html")
}
func serveStaticContent(w http.ResponseWriter, r *http.Request) {
fmt.Printf("serveStaticContent: %s\n", r.URL.Path)
switch r.URL.Path {
case "/static/style.css":
darkTheme := r.CookiesNamed("dark_theme")
if len(darkTheme) > 0 && darkTheme[0].Value == "true" {
http.ServeFileFS(w, r, content, "ui/static/dark.css")
} else {
http.ServeFileFS(w, r, content, "ui/static/light.css")
}
break
case "/favicon.ico":
http.ServeFileFS(w, r, content, "ui/static/favicon.ico")
break
case "/static/index.js":
http.ServeFileFS(w, r, content, "ui/static/index.js")
break
default:
http.NotFound(w, r)
}
}
func serveAPI(w http.ResponseWriter, r *http.Request) {
}
func main() {
http.HandleFunc("GET /{$}", serveWebUI)
http.HandleFunc("GET /favicon.ico", serveStaticContent)
http.HandleFunc("GET /static/", serveStaticContent)
http.HandleFunc("GET /api/", serveAPI)
http.ListenAndServe(":8067", nil)
}

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="static/style.css">
<title>Hakurei PkgServer</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="static/index.js"></script>
</head>
<body>
<h1>Hakurei PkgServer</h1>
<table id="pkg-list">
<tr><th>Status</th><th>Name</th><th>Version</th></tr>
</table>
<p>Showing entries <span id="entry-counter"></span>.</p>
<span class="bottom-nav"><a href="javascript:prevPage()">&laquo; Previous</a> <span id="page-number">1</span> <a href="javascript:nextPage()">Next &raquo;</a></span>
<span><label for="count">Entries per page:</label><select name="count" id="count">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select></span>
</body>
<footer>&copy; <a href="https://hakurei.app/">Hakurei</a>. Licensed under the MIT license.</footer>
</html>

View File

View File

@@ -0,0 +1,6 @@
@use 'common';
html {
background-color: #2c2c2c;
color: ghostwhite; }
/*# sourceMappingURL=dark.css.map */

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"mappings": "AAAA,aAAa;AAEb,IAAK;EACH,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,UAAU",
"sources": ["dark.scss"],
"names": [],
"file": "dark.css"
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,67 @@
"use strict";
var PackageEntry = /** @class */ (function () {
function PackageEntry() {
}
return PackageEntry;
}());
var State = /** @class */ (function () {
function State() {
this.entriesPerPage = 10;
this.currentPage = 1;
this.entryIndex = 0;
this.loadedEntries = [];
}
State.prototype.getEntriesPerPage = function () {
return this.entriesPerPage;
};
State.prototype.setEntriesPerPage = function (entriesPerPage) {
this.entriesPerPage = entriesPerPage;
this.updateRange();
};
State.prototype.getCurrentPage = function () {
return this.currentPage;
};
State.prototype.setCurrentPage = function (page) {
this.currentPage = page;
document.getElementById("page-number").innerText = String(this.currentPage);
this.updateRange();
};
State.prototype.getEntryIndex = function () {
return this.entryIndex;
};
State.prototype.setEntryIndex = function (entryIndex) {
this.entryIndex = entryIndex;
this.updateRange();
};
State.prototype.getLoadedEntries = function () {
return this.loadedEntries;
};
State.prototype.getMaxPage = function () {
return this.loadedEntries.length / this.entriesPerPage;
};
State.prototype.updateRange = function () {
var max = Math.min(this.entryIndex + this.entriesPerPage, this.loadedEntries.length);
document.getElementById("entry-counter").innerText = "".concat(this.entryIndex, "-").concat(max, " of ").concat(this.loadedEntries.length);
};
return State;
}());
var STATE;
function prevPage() {
var current = STATE.getCurrentPage();
if (current > 1) {
STATE.setCurrentPage(STATE.getCurrentPage() - 1);
}
}
function nextPage() {
var current = STATE.getCurrentPage();
if (current < STATE.getMaxPage()) {
STATE.setCurrentPage(STATE.getCurrentPage() + 1);
}
}
document.addEventListener("DOMContentLoaded", function () {
STATE = new State();
STATE.updateRange();
document.getElementById("count").addEventListener("change", function (event) {
STATE.setEntriesPerPage(parseInt(event.target.value));
});
});

View File

@@ -0,0 +1,66 @@
"use strict"
class PackageEntry {
}
class State {
entriesPerPage: number = 10
currentPage: number = 1
entryIndex: number = 0
loadedEntries: PackageEntry[] = []
getEntriesPerPage(): number {
return this.entriesPerPage
}
setEntriesPerPage(entriesPerPage: number) {
this.entriesPerPage = entriesPerPage
this.updateRange()
}
getCurrentPage(): number {
return this.currentPage
}
setCurrentPage(page: number) {
this.currentPage = page
document.getElementById("page-number").innerText = String(this.currentPage)
this.updateRange()
}
getEntryIndex(): number {
return this.entryIndex
}
setEntryIndex(entryIndex: number) {
this.entryIndex = entryIndex
this.updateRange()
}
getLoadedEntries(): PackageEntry[] {
return this.loadedEntries
}
getMaxPage(): number {
return this.loadedEntries.length / this.entriesPerPage
}
updateRange() {
let max = Math.min(this.entryIndex + this.entriesPerPage, this.loadedEntries.length)
document.getElementById("entry-counter").innerText = `${this.entryIndex}-${max} of ${this.loadedEntries.length}`
}
}
let STATE: State
function prevPage() {
let current = STATE.getCurrentPage()
if (current > 1) {
STATE.setCurrentPage(STATE.getCurrentPage() - 1)
}
}
function nextPage() {
let current = STATE.getCurrentPage()
if (current < STATE.getMaxPage()) {
STATE.setCurrentPage(STATE.getCurrentPage() + 1)
}
}
document.addEventListener("DOMContentLoaded", () => {
STATE = new State()
STATE.updateRange()
document.getElementById("count").addEventListener("change", (event) => {
STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value))
})
})

View File

@@ -0,0 +1,6 @@
@use 'common';
html {
background-color: #d3d3d3;
color: black; }
/*# sourceMappingURL=light.css.map */

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"mappings": "AAAA,aAAa;AAEb,IAAK;EACH,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,KAAK",
"sources": ["light.scss"],
"names": [],
"file": "light.css"
}

View File

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

View File

@@ -0,0 +1,282 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* O_DIRECT */
#endif
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
/* TODO(ophestra): remove after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
#include <sys/syscall.h>
#include "fuse-operations.h"
/* MUST_TRANSLATE_PATHNAME translates a userspace pathname to a relative pathname;
* the resulting address points to a constant string or part of pathname, it is never heap allocated. */
#define MUST_TRANSLATE_PATHNAME(pathname) \
do { \
if (pathname == NULL) \
return -EINVAL; \
while (*pathname == '/') \
pathname++; \
if (*pathname == '\0') \
pathname = "."; \
} while (0)
/* GET_CONTEXT_PRIV obtains fuse context and private data for the calling thread. */
#define GET_CONTEXT_PRIV(ctx, priv) \
do { \
ctx = fuse_get_context(); \
priv = ctx->private_data; \
} while (0)
/* impl_getattr modifies a struct stat from the kernel to present to userspace;
* impl_getattr returns a negative errno style error code. */
static int impl_getattr(struct fuse_context *ctx, struct stat *statbuf) {
/* allowlist of permitted types */
if (!S_ISDIR(statbuf->st_mode) && !S_ISREG(statbuf->st_mode) && !S_ISLNK(statbuf->st_mode)) {
return -ENOTRECOVERABLE; /* returning an errno causes all operations on the file to return EIO */
}
#define OVERRIDE_PERM(v) (statbuf->st_mode & ~0777) | (v & 0777)
if (S_ISDIR(statbuf->st_mode))
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_DIR);
else if (S_ISREG(statbuf->st_mode))
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_REG);
else
statbuf->st_mode = 0; /* should always be symlink in this case */
statbuf->st_uid = ctx->uid;
statbuf->st_gid = SHAREFS_MEDIA_RW_ID;
statbuf->st_ctim = statbuf->st_mtim;
statbuf->st_nlink = 1;
return 0;
}
/* fuse_operations implementation */
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi) {
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
(void)fi;
if (fstatat(priv->dirfd, pathname, statbuf, AT_SYMLINK_NOFOLLOW) == -1)
return -errno;
return impl_getattr(ctx, statbuf);
}
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags) {
int fd;
DIR *dp;
struct stat st;
int ret = 0;
struct dirent *de;
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
(void)offset;
(void)fi;
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_DIRECTORY | O_CLOEXEC)) == -1)
return -errno;
if ((dp = fdopendir(fd)) == NULL) {
close(fd);
return -errno;
}
errno = 0; /* for the next readdir call */
while ((de = readdir(dp)) != NULL) {
if (flags & FUSE_READDIR_PLUS) {
if (fstatat(dirfd(dp), de->d_name, &st, AT_SYMLINK_NOFOLLOW) == -1) {
ret = -errno;
break;
}
if ((ret = impl_getattr(ctx, &st)) < 0)
break;
errno = 0;
ret = filler(buf, de->d_name, &st, 0, FUSE_FILL_DIR_PLUS);
} else
ret = filler(buf, de->d_name, NULL, 0, 0);
if (ret != 0) {
ret = errno != 0 ? -errno : -EIO; /* filler */
break;
}
errno = 0; /* for the next readdir call */
}
if (ret == 0 && errno != 0)
ret = -errno; /* readdir */
closedir(dp);
return ret;
}
int sharefs_mkdir(const char *pathname, mode_t mode) {
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
(void)mode;
if (mkdirat(priv->dirfd, pathname, SHAREFS_PERM_DIR) == -1)
return -errno;
return 0;
}
int sharefs_unlink(const char *pathname) {
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
if (unlinkat(priv->dirfd, pathname, 0) == -1)
return -errno;
return 0;
}
int sharefs_rmdir(const char *pathname) {
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
if (unlinkat(priv->dirfd, pathname, AT_REMOVEDIR) == -1)
return -errno;
return 0;
}
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags) {
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(oldpath);
MUST_TRANSLATE_PATHNAME(newpath);
/* TODO(ophestra): replace with wrapper after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
if (syscall(__NR_renameat2, priv->dirfd, oldpath, priv->dirfd, newpath, flags) == -1)
return -errno;
return 0;
}
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi) {
int fd;
int ret;
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
(void)fi;
if ((fd = openat(priv->dirfd, pathname, O_WRONLY | O_CLOEXEC)) == -1)
return -errno;
if ((ret = ftruncate(fd, length)) == -1)
ret = -errno;
close(fd);
return ret;
}
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi) {
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
(void)fi;
if (utimensat(priv->dirfd, pathname, times, AT_SYMLINK_NOFOLLOW) == -1)
return -errno;
return 0;
}
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi) {
int fd;
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
(void)mode;
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS, SHAREFS_PERM_REG)) == -1)
return -errno;
fi->fh = fd;
return 0;
}
int sharefs_open(const char *pathname, struct fuse_file_info *fi) {
int fd;
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS)) == -1)
return -errno;
fi->fh = fd;
return 0;
}
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
int ret;
(void)pathname;
if ((ret = pread(fi->fh, buf, count, offset)) == -1)
return -errno;
return ret;
}
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
int ret;
(void)pathname;
if ((ret = pwrite(fi->fh, buf, count, offset)) == -1)
return -errno;
return ret;
}
int sharefs_statfs(const char *pathname, struct statvfs *statbuf) {
int fd;
int ret;
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_CLOEXEC)) == -1)
return -errno;
if ((ret = fstatvfs(fd, statbuf)) == -1)
ret = -errno;
close(fd);
return ret;
}
int sharefs_release(const char *pathname, struct fuse_file_info *fi) {
(void)pathname;
return close(fi->fh);
}
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi) {
(void)pathname;
if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1)
return -errno;
return 0;
}

View File

@@ -0,0 +1,34 @@
#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12)
#include <fuse.h>
#include <fuse_lowlevel.h> /* for fuse_cmdline_help */
#if (FUSE_VERSION < FUSE_MAKE_VERSION(3, 12))
#error This package requires libfuse >= v3.12
#endif
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
#define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
#define SHAREFS_PERM_REG 0600 /* permission bits for regular files presented to userspace */
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
/* sharefs_private is populated by sharefs_init and contains process-wide context */
struct sharefs_private {
int dirfd; /* source dirfd opened during sharefs_init */
uintptr_t setup; /* cgo handle of opaque setup state */
};
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi);
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags);
int sharefs_mkdir(const char *pathname, mode_t mode);
int sharefs_unlink(const char *pathname);
int sharefs_rmdir(const char *pathname);
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags);
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi);
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi);
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi);
int sharefs_open(const char *pathname, struct fuse_file_info *fi);
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
int sharefs_statfs(const char *pathname, struct statvfs *statbuf);
int sharefs_release(const char *pathname, struct fuse_file_info *fi);
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi);

552
cmd/sharefs/fuse.go Normal file
View File

@@ -0,0 +1,552 @@
package main
/*
#cgo pkg-config: --static fuse3
#include "fuse-operations.h"
#include <stdlib.h>
#include <string.h>
extern void *sharefs_init(struct fuse_conn_info *conn, struct fuse_config *cfg);
extern void sharefs_destroy(void *private_data);
typedef void (*closure)();
static inline struct fuse_opt _FUSE_OPT_END() { return (struct fuse_opt)FUSE_OPT_END; };
*/
import "C"
import (
"context"
"encoding/gob"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"path"
"runtime"
"runtime/cgo"
"strconv"
"syscall"
"unsafe"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/std"
"hakurei.app/hst"
"hakurei.app/internal/helper/proc"
"hakurei.app/internal/info"
"hakurei.app/message"
)
type (
// closure represents a C function pointer.
closure = C.closure
// fuseArgs represents the fuse_args structure.
fuseArgs = C.struct_fuse_args
// setupState holds state used for setup. Its cgo handle is included in
// sharefs_private and considered opaque to non-setup callbacks.
setupState struct {
// Whether sharefs_init failed.
initFailed bool
// Whether to create source directory as root.
mkdir bool
// Open file descriptor to fuse.
Fuse int
// Pathname to open for dirfd.
Source *check.Absolute
// New uid and gid to set by sharefs_init when starting as root.
Setuid, Setgid int
}
)
func init() { gob.Register(new(setupState)) }
// destroySetup invalidates the setup [cgo.Handle] in a sharefs_private structure.
func destroySetup(private_data unsafe.Pointer) (ok bool) {
if private_data == nil {
return false
}
priv := (*C.struct_sharefs_private)(private_data)
if h := cgo.Handle(priv.setup); h != 0 {
priv.setup = 0
h.Delete()
ok = true
}
return
}
//export sharefs_init
func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.Pointer {
ctx := C.fuse_get_context()
priv := (*C.struct_sharefs_private)(ctx.private_data)
setup := cgo.Handle(priv.setup).Value().(*setupState)
if os.Geteuid() == 0 {
log.Println("filesystem daemon must not run as root")
goto fail
}
cfg.use_ino = C.true
cfg.direct_io = C.false
// getattr is context-dependent
cfg.attr_timeout = 0
cfg.entry_timeout = 0
cfg.negative_timeout = 0
// all future filesystem operations happen through this dirfd
if fd, err := syscall.Open(setup.Source.String(), syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC, 0); err != nil {
log.Printf("cannot open %q: %v", setup.Source, err)
goto fail
} else if err = syscall.Fchdir(fd); err != nil {
_ = syscall.Close(fd)
log.Printf("cannot enter %q: %s", setup.Source, err)
goto fail
} else {
priv.dirfd = C.int(fd)
}
return ctx.private_data
fail:
setup.initFailed = true
C.fuse_exit(ctx.fuse)
return nil
}
//export sharefs_destroy
func sharefs_destroy(private_data unsafe.Pointer) {
if private_data != nil {
destroySetup(private_data)
priv := (*C.struct_sharefs_private)(private_data)
if err := syscall.Close(int(priv.dirfd)); err != nil {
log.Printf("cannot close source directory: %v", err)
}
}
}
// showHelp prints the help message.
func showHelp(args *fuseArgs) {
executableName := sharefsName
if args.argc > 0 {
executableName = path.Base(C.GoString(*args.argv))
} else if name, err := os.Executable(); err == nil {
executableName = path.Base(name)
}
fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
fmt.Println("Filesystem options:")
fmt.Println(" -o source=/data/media source directory to be mounted")
fmt.Println(" -o setuid=1023 uid to run as when starting as root")
fmt.Println(" -o setgid=1023 gid to run as when starting as root")
fmt.Println("\nFUSE options:")
C.fuse_cmdline_help()
C.fuse_lib_help(args)
}
// parseOpts parses fuse options via fuse_opt_parse.
func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
var unsafeOpts struct {
// Pathname to writable source directory.
source *C.char
// Whether to create source directory as root.
mkdir C.int
// Decimal string representation of uid to set when running as root.
setuid *C.char
// Decimal string representation of gid to set when running as root.
setgid *C.char
// Decimal string representation of open file descriptor to read setupState from.
// This is an internal detail for containerisation and must not be specified directly.
setup *C.char
}
if C.fuse_opt_parse(args, unsafe.Pointer(&unsafeOpts), &[]C.struct_fuse_opt{
{templ: C.CString("source=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.source)), value: 0},
{templ: C.CString("mkdir"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.mkdir)), value: 1},
{templ: C.CString("setuid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setuid)), value: 0},
{templ: C.CString("setgid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setgid)), value: 0},
{templ: C.CString("setup=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setup)), value: 0},
C._FUSE_OPT_END(),
}[0], nil) == -1 {
return false
}
if unsafeOpts.source != nil {
defer C.free(unsafe.Pointer(unsafeOpts.source))
}
if unsafeOpts.setuid != nil {
defer C.free(unsafe.Pointer(unsafeOpts.setuid))
}
if unsafeOpts.setgid != nil {
defer C.free(unsafe.Pointer(unsafeOpts.setgid))
}
if unsafeOpts.setup != nil {
defer C.free(unsafe.Pointer(unsafeOpts.setup))
if v, err := strconv.Atoi(C.GoString(unsafeOpts.setup)); err != nil || v < 3 {
log.Println("invalid value for option setup")
return false
} else {
r := os.NewFile(uintptr(v), "setup")
defer func() {
if err = r.Close(); err != nil {
log.Println(err)
}
}()
if err = gob.NewDecoder(r).Decode(setup); err != nil {
log.Println(err)
return false
}
}
if setup.Fuse < 3 {
log.Println("invalid file descriptor", setup.Fuse)
return false
}
return true
}
if unsafeOpts.source == nil {
showHelp(args)
return false
} else if a, err := check.NewAbs(C.GoString(unsafeOpts.source)); err != nil {
log.Println(err)
return false
} else {
setup.Source = a
}
setup.mkdir = unsafeOpts.mkdir != 0
if unsafeOpts.setuid == nil {
setup.Setuid = -1
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setuid)); err != nil || v <= 0 {
log.Println("invalid value for option setuid")
return false
} else {
setup.Setuid = v
}
if unsafeOpts.setgid == nil {
setup.Setgid = -1
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setgid)); err != nil || v <= 0 {
log.Println("invalid value for option setgid")
return false
} else {
setup.Setgid = v
}
return true
}
// copyArgs returns a heap allocated copy of an argument slice in fuse_args representation.
func copyArgs(s ...string) fuseArgs {
if len(s) == 0 {
return fuseArgs{argc: 0, argv: nil, allocated: 0}
}
args := unsafe.Slice((**C.char)(C.malloc(C.size_t(uintptr(len(s))*unsafe.Sizeof(s[0])))), len(s))
for i, arg := range s {
args[i] = C.CString(arg)
}
return fuseArgs{argc: C.int(len(s)), argv: &args[0], allocated: 1}
}
// freeArgs frees the contents of argument list.
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }
// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
// The last byte of arg must be 0.
func unsafeAddArgument(args *fuseArgs, arg string) {
C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
}
func _main(s ...string) (exitCode int) {
msg := message.New(log.Default())
container.TryArgv0(msg)
runtime.LockOSThread()
// don't mask creation mode, kernel already did that
syscall.Umask(0)
var pinner runtime.Pinner
defer pinner.Unpin()
args := copyArgs(s...)
defer freeArgs(&args)
// this causes the kernel to enforce access control based on
// struct stat populated by sharefs_getattr
unsafeAddArgument(&args, "-odefault_permissions\x00")
var priv C.struct_sharefs_private
pinner.Pin(&priv)
var setup setupState
priv.setup = C.uintptr_t(cgo.NewHandle(&setup))
defer destroySetup(unsafe.Pointer(&priv))
var opts C.struct_fuse_cmdline_opts
if C.fuse_parse_cmdline(&args, &opts) != 0 {
return 1
}
if opts.mountpoint != nil {
defer C.free(unsafe.Pointer(opts.mountpoint))
}
if opts.show_version != 0 {
fmt.Println("hakurei version", info.Version())
fmt.Println("FUSE library version", C.GoString(C.fuse_pkgversion()))
C.fuse_lowlevel_version()
return 0
}
if opts.show_help != 0 {
showHelp(&args)
return 0
} else if opts.mountpoint == nil {
log.Println("no mountpoint specified")
return 2
} else {
// hack to keep fuse_parse_cmdline happy in the container
mountpoint := C.GoString(opts.mountpoint)
pathnameArg := -1
for i, arg := range s {
if arg == mountpoint {
pathnameArg = i
break
}
}
if pathnameArg < 0 {
log.Println("mountpoint must be absolute")
return 2
}
s[pathnameArg] = container.Nonexistent
}
if !parseOpts(&args, &setup, msg.GetLogger()) {
return 1
}
asRoot := os.Geteuid() == 0
if asRoot {
if setup.Setuid <= 0 || setup.Setgid <= 0 {
log.Println("setuid and setgid must not be 0")
return 1
}
if setup.Fuse >= 3 {
log.Println("filesystem daemon must not run as root")
return 1
}
if setup.mkdir {
if err := os.MkdirAll(setup.Source.String(), 0700); err != nil {
if !errors.Is(err, os.ErrExist) {
log.Println(err)
return 1
}
// skip setup for existing source directory
} else if err = os.Chown(setup.Source.String(), setup.Setuid, setup.Setgid); err != nil {
log.Println(err)
return 1
}
}
} else if setup.Fuse < 3 && (setup.Setuid > 0 || setup.Setgid > 0) {
log.Println("setuid and setgid has no effect when not starting as root")
return 1
} else if setup.mkdir {
log.Println("mkdir has no effect when not starting as root")
return 1
}
op := C.struct_fuse_operations{
init: closure(C.sharefs_init),
destroy: closure(C.sharefs_destroy),
// implemented in fuse-helper.c
getattr: closure(C.sharefs_getattr),
readdir: closure(C.sharefs_readdir),
mkdir: closure(C.sharefs_mkdir),
unlink: closure(C.sharefs_unlink),
rmdir: closure(C.sharefs_rmdir),
rename: closure(C.sharefs_rename),
truncate: closure(C.sharefs_truncate),
utimens: closure(C.sharefs_utimens),
create: closure(C.sharefs_create),
open: closure(C.sharefs_open),
read: closure(C.sharefs_read),
write: closure(C.sharefs_write),
statfs: closure(C.sharefs_statfs),
release: closure(C.sharefs_release),
fsync: closure(C.sharefs_fsync),
}
fuse := C.fuse_new_fn(&args, &op, C.size_t(unsafe.Sizeof(op)), unsafe.Pointer(&priv))
if fuse == nil {
return 3
}
defer C.fuse_destroy(fuse)
se := C.fuse_get_session(fuse)
if setup.Fuse < 3 {
// unconfined, set up mount point and container
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
return 4
}
// unmounted by initial process
defer func() {
if exitCode == 5 {
C.fuse_unmount(fuse)
}
}()
if asRoot {
if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); err != nil {
log.Printf("cannot set gid: %v", err)
return 5
}
if err := syscall.Setgroups(nil); err != nil {
log.Printf("cannot set supplementary groups: %v", err)
return 5
}
if err := syscall.Setresuid(setup.Setuid, setup.Setuid, setup.Setuid); err != nil {
log.Printf("cannot set uid: %v", err)
return 5
}
}
msg.SwapVerbose(opts.debug != 0)
ctx := context.Background()
if opts.foreground != 0 {
c, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer cancel()
ctx = c
}
z := container.New(ctx, msg)
z.AllowOrphan = opts.foreground == 0
z.Env = os.Environ()
// keep fuse_parse_cmdline happy in the container
z.Tmpfs(check.MustAbs(container.Nonexistent), 1<<10, 0755)
z.Path = fhs.AbsProcSelfExe
z.Args = s
z.ForwardCancel = true
z.SeccompPresets |= std.PresetStrict
z.ParentPerm = 0700
z.Bind(setup.Source, setup.Source, std.BindWritable)
if !z.AllowOrphan {
z.WaitDelay = hst.WaitDelayMax
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
}
z.Bind(z.Path, z.Path, 0)
setup.Fuse = int(proc.ExtraFileSlice(&z.ExtraFiles, os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse")))
var setupWriter io.WriteCloser
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
log.Println(err)
return 5
} else {
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
setupWriter = w
}
if err := z.Start(); err != nil {
if m, ok := message.GetMessage(err); ok {
log.Println(m)
} else {
log.Println(err)
}
return 5
}
if err := z.Serve(); err != nil {
if m, ok := message.GetMessage(err); ok {
log.Println(m)
} else {
log.Println(err)
}
return 5
}
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
log.Println(err)
return 5
} else if err = setupWriter.Close(); err != nil {
log.Println(err)
}
if !z.AllowOrphan {
if err := z.Wait(); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) || exitError == nil {
log.Println(err)
return 5
}
switch code := exitError.ExitCode(); syscall.Signal(code & 0x7f) {
case syscall.SIGINT:
case syscall.SIGTERM:
default:
return code
}
}
}
return 0
} else { // confined
C.free(unsafe.Pointer(opts.mountpoint))
// must be heap allocated
opts.mountpoint = C.CString("/dev/fd/" + strconv.Itoa(setup.Fuse))
if err := os.Chdir("/"); err != nil {
log.Println(err)
}
}
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
return 4
}
defer C.fuse_unmount(fuse)
if C.fuse_set_signal_handlers(se) != 0 {
return 6
}
defer C.fuse_remove_signal_handlers(se)
if opts.singlethread != 0 {
if C.fuse_loop(fuse) != 0 {
return 8
}
} else {
loopConfig := C.fuse_loop_cfg_create()
if loopConfig == nil {
return 7
}
defer C.fuse_loop_cfg_destroy(loopConfig)
C.fuse_loop_cfg_set_clone_fd(loopConfig, C.uint(opts.clone_fd))
C.fuse_loop_cfg_set_idle_threads(loopConfig, opts.max_idle_threads)
C.fuse_loop_cfg_set_max_threads(loopConfig, opts.max_threads)
if C.fuse_loop_mt(fuse, loopConfig) != 0 {
return 8
}
}
if setup.initFailed {
return 1
}
return 0
}

113
cmd/sharefs/fuse_test.go Normal file
View File

@@ -0,0 +1,113 @@
package main
import (
"bytes"
"log"
"reflect"
"testing"
"hakurei.app/container/check"
)
func TestParseOpts(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
args []string
want setupState
wantLog string
wantOk bool
}{
{"zero length", []string{}, setupState{}, "", false},
{"not absolute", []string{"sharefs",
"-o", "source=nonexistent",
"-o", "setuid=1023",
"-o", "setgid=1023",
}, setupState{}, "sharefs: path \"nonexistent\" is not absolute\n", false},
{"not specified", []string{"sharefs",
"-o", "setuid=1023",
"-o", "setgid=1023",
}, setupState{}, "", false},
{"invalid setuid", []string{"sharefs",
"-o", "source=/proc/nonexistent",
"-o", "setuid=ff",
"-o", "setgid=1023",
}, setupState{
Source: check.MustAbs("/proc/nonexistent"),
}, "sharefs: invalid value for option setuid\n", false},
{"invalid setgid", []string{"sharefs",
"-o", "source=/proc/nonexistent",
"-o", "setuid=1023",
"-o", "setgid=ff",
}, setupState{
Source: check.MustAbs("/proc/nonexistent"),
Setuid: 1023,
}, "sharefs: invalid value for option setgid\n", false},
{"simple", []string{"sharefs",
"-o", "source=/proc/nonexistent",
}, setupState{
Source: check.MustAbs("/proc/nonexistent"),
Setuid: -1,
Setgid: -1,
}, "", true},
{"root", []string{"sharefs",
"-o", "source=/proc/nonexistent",
"-o", "setuid=1023",
"-o", "setgid=1023",
}, setupState{
Source: check.MustAbs("/proc/nonexistent"),
Setuid: 1023,
Setgid: 1023,
}, "", true},
{"setuid", []string{"sharefs",
"-o", "source=/proc/nonexistent",
"-o", "setuid=1023",
}, setupState{
Source: check.MustAbs("/proc/nonexistent"),
Setuid: 1023,
Setgid: -1,
}, "", true},
{"setgid", []string{"sharefs",
"-o", "source=/proc/nonexistent",
"-o", "setgid=1023",
}, setupState{
Source: check.MustAbs("/proc/nonexistent"),
Setuid: -1,
Setgid: 1023,
}, "", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var (
got setupState
buf bytes.Buffer
)
args := copyArgs(tc.args...)
defer freeArgs(&args)
unsafeAddArgument(&args, "-odefault_permissions\x00")
if ok := parseOpts(&args, &got, log.New(&buf, "sharefs: ", 0)); ok != tc.wantOk {
t.Errorf("parseOpts: ok = %v, want %v", ok, tc.wantOk)
}
if !reflect.DeepEqual(&got, &tc.want) {
t.Errorf("parseOpts: setup = %#v, want %#v", got, tc.want)
}
if buf.String() != tc.wantLog {
t.Errorf("parseOpts: log =\n%s\nwant\n%s", buf.String(), tc.wantLog)
}
})
}
}

31
cmd/sharefs/main.go Normal file
View File

@@ -0,0 +1,31 @@
package main
import (
"log"
"os"
"slices"
)
// sharefsName is the prefix used by log.std in the sharefs process.
const sharefsName = "sharefs"
// handleMountArgs returns an alternative, libfuse-compatible args slice for
// args passed by mount -t fuse.sharefs [options] sharefs <mountpoint>.
//
// In this case, args always has a length of 5 with index 0 being what comes
// after "fuse." in the filesystem type, 1 is the uninterpreted string passed
// to mount (sharefsName is used as the magic string to enable this hack),
// 2 is passed through to libfuse as mountpoint, and 3 is always "-o".
func handleMountArgs(args []string) []string {
if len(args) == 5 && args[1] == sharefsName && args[3] == "-o" {
return []string{sharefsName, args[2], "-o", args[4]}
}
return slices.Clone(args)
}
func main() {
log.SetFlags(0)
log.SetPrefix(sharefsName + ": ")
os.Exit(_main(handleMountArgs(os.Args)...))
}

29
cmd/sharefs/main_test.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import (
"slices"
"testing"
)
func TestHandleMountArgs(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
args []string
want []string
}{
{"nil", nil, nil},
{"passthrough", []string{"sharefs", "-V"}, []string{"sharefs", "-V"}},
{"replace", []string{"/sbin/sharefs", "sharefs", "/sdcard", "-o", "rw"}, []string{"sharefs", "/sdcard", "-o", "rw"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := handleMountArgs(tc.args); !slices.Equal(got, tc.want) {
t.Errorf("handleMountArgs: %q, want %q", got, tc.want)
}
})
}
}

View File

@@ -0,0 +1,41 @@
{ pkgs, ... }:
{
users.users = {
alice = {
isNormalUser = true;
description = "Alice Foobar";
password = "foobar";
uid = 1000;
};
};
home-manager.users.alice.home.stateVersion = "24.11";
# Automatically login on tty1 as a normal user:
services.getty.autologinUser = "alice";
environment = {
# For benchmarking sharefs:
systemPackages = [ pkgs.fsmark ];
};
virtualisation = {
diskSize = 6 * 1024;
qemu.options = [
# Increase test performance:
"-smp 8"
];
};
environment.hakurei = rec {
enable = true;
stateDir = "/var/lib/hakurei";
sharefs.source = "${stateDir}/sdcard";
users.alice = 0;
extraHomeConfig = {
home.stateVersion = "23.05";
};
};
}

View File

@@ -0,0 +1,44 @@
{
testers,
system,
self,
}:
testers.nixosTest {
name = "sharefs";
nodes.machine =
{ options, pkgs, ... }:
let
fhs =
let
hakurei = options.environment.hakurei.package.default;
in
pkgs.buildFHSEnv {
pname = "hakurei-fhs";
inherit (hakurei) version;
targetPkgs = _: hakurei.targetPkgs;
extraOutputsToInstall = [ "dev" ];
profile = ''
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
'';
};
in
{
environment.systemPackages = [
# For go tests:
(pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" ''
cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei"
${fhs}/bin/hakurei-fhs -c 'CC="clang -O3 -Werror" go test ./...'
'')
];
imports = [
./configuration.nix
self.nixosModules.hakurei
self.inputs.home-manager.nixosModules.home-manager
];
};
testScript = builtins.readFile ./test.py;
}

60
cmd/sharefs/test/test.py Normal file
View File

@@ -0,0 +1,60 @@
start_all()
machine.wait_for_unit("multi-user.target")
# To check sharefs version:
print(machine.succeed("sharefs -V"))
# Make sure sharefs started:
machine.wait_for_unit("sdcard.mount")
machine.succeed("mkdir /mnt")
def check_bad_opts_output(opts, want, source="/etc", privileged=False):
output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"sharefs -f -o source={source},{opts} /mnt 2>&1")
if output != want:
raise Exception(f"unexpected output: {output}")
# Malformed setuid/setgid representation:
check_bad_opts_output("setuid=ff", "sharefs: invalid value for option setuid\n")
check_bad_opts_output("setgid=ff", "sharefs: invalid value for option setgid\n")
# Bounds check for setuid/setgid:
check_bad_opts_output("setuid=0", "sharefs: invalid value for option setuid\n")
check_bad_opts_output("setgid=0", "sharefs: invalid value for option setgid\n")
check_bad_opts_output("setuid=-1", "sharefs: invalid value for option setuid\n")
check_bad_opts_output("setgid=-1", "sharefs: invalid value for option setgid\n")
# Non-root setuid/setgid:
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
check_bad_opts_output("setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
check_bad_opts_output("mkdir", "sharefs: mkdir has no effect when not starting as root\n")
# Starting as root without setuid/setgid:
check_bad_opts_output("allow_other", "sharefs: setuid and setgid must not be 0\n", privileged=True)
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
# Make sure nothing actually got mounted:
machine.fail("umount /mnt")
machine.succeed("rmdir /mnt")
# Unprivileged mount/unmount:
machine.succeed("sudo -u alice -i mkdir /home/alice/{sdcard,persistent}")
machine.succeed("sudo -u alice -i sharefs -o source=/home/alice/persistent /home/alice/sdcard")
machine.succeed("sudo -u alice -i touch /home/alice/sdcard/check")
machine.succeed("sudo -u alice -i umount /home/alice/sdcard")
machine.succeed("sudo -u alice -i rm /home/alice/persistent/check")
machine.succeed("sudo -u alice -i rmdir /home/alice/{sdcard,persistent}")
# Benchmark sharefs:
machine.succeed("fs_mark -v -d /sdcard/fs_mark -l /tmp/fs_log.txt")
machine.copy_from_vm("/tmp/fs_log.txt", "")
# Check permissions:
machine.succeed("sudo -u sharefs touch /var/lib/hakurei/sdcard/fs_mark/.check")
machine.succeed("sudo -u sharefs rm /var/lib/hakurei/sdcard/fs_mark/.check")
machine.succeed("sudo -u alice rm -rf /sdcard/fs_mark")
machine.fail("ls /var/lib/hakurei/sdcard/fs_mark")
# Run hakurei tests on sharefs:
machine.succeed("sudo -u alice -i sharefs-workload-hakurei-tests")

View File

@@ -10,8 +10,7 @@ import (
func init() { gob.Register(new(AutoEtcOp)) } func init() { gob.Register(new(AutoEtcOp)) }
// Etc appends an [Op] that expands host /etc into a toplevel symlink mirror with /etc semantics. // Etc is a helper for appending [AutoEtcOp] to [Ops].
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Etc(host *check.Absolute, prefix string) *Ops { func (f *Ops) Etc(host *check.Absolute, prefix string) *Ops {
e := &AutoEtcOp{prefix} e := &AutoEtcOp{prefix}
f.Mkdir(fhs.AbsEtc, 0755) f.Mkdir(fhs.AbsEtc, 0755)
@@ -20,6 +19,9 @@ func (f *Ops) Etc(host *check.Absolute, prefix string) *Ops {
return f return f
} }
// AutoEtcOp expands host /etc into a toplevel symlink mirror with /etc semantics.
//
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
type AutoEtcOp struct{ Prefix string } type AutoEtcOp struct{ Prefix string }
func (e *AutoEtcOp) Valid() bool { return e != nil } func (e *AutoEtcOp) Valid() bool { return e != nil }

View File

@@ -11,13 +11,15 @@ import (
func init() { gob.Register(new(AutoRootOp)) } func init() { gob.Register(new(AutoRootOp)) }
// Root appends an [Op] that expands a directory into a toplevel bind mount mirror on container root. // Root is a helper for appending [AutoRootOp] to [Ops].
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Root(host *check.Absolute, flags int) *Ops { func (f *Ops) Root(host *check.Absolute, flags int) *Ops {
*f = append(*f, &AutoRootOp{host, flags, nil}) *f = append(*f, &AutoRootOp{host, flags, nil})
return f return f
} }
// AutoRootOp expands a directory into a toplevel bind mount mirror on container root.
//
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
type AutoRootOp struct { type AutoRootOp struct {
Host *check.Absolute Host *check.Absolute
// passed through to bindMount // passed through to bindMount

View File

@@ -202,7 +202,7 @@ func TestIsAutoRootBindable(t *testing.T) {
t.Parallel() t.Parallel()
var msg message.Msg var msg message.Msg
if tc.log { if tc.log {
msg = &kstub{nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{ msg = &kstub{nil, nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),
}})} }})}
} }

View File

@@ -14,6 +14,7 @@ const (
CAP_SYS_ADMIN = 0x15 CAP_SYS_ADMIN = 0x15
CAP_SETPCAP = 0x8 CAP_SETPCAP = 0x8
CAP_NET_ADMIN = 0xc
CAP_DAC_OVERRIDE = 0x1 CAP_DAC_OVERRIDE = 0x1
) )
@@ -49,10 +50,16 @@ func capset(hdrp *capHeader, datap *[2]capData) error {
} }
// capBoundingSetDrop drops a capability from the calling thread's capability bounding set. // capBoundingSetDrop drops a capability from the calling thread's capability bounding set.
func capBoundingSetDrop(cap uintptr) error { return Prctl(syscall.PR_CAPBSET_DROP, cap, 0) } func capBoundingSetDrop(cap uintptr) error {
return Prctl(syscall.PR_CAPBSET_DROP, cap, 0)
}
// capAmbientClearAll clears the ambient capability set of the calling thread. // capAmbientClearAll clears the ambient capability set of the calling thread.
func capAmbientClearAll() error { return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0) } func capAmbientClearAll() error {
return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0)
}
// capAmbientRaise adds to the ambient capability set of the calling thread. // capAmbientRaise adds to the ambient capability set of the calling thread.
func capAmbientRaise(cap uintptr) error { return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap) } func capAmbientRaise(cap uintptr) error {
return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap)
}

View File

@@ -9,46 +9,60 @@ import (
"slices" "slices"
"strings" "strings"
"syscall" "syscall"
"unique"
) )
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname. // AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
type AbsoluteError struct{ Pathname string } type AbsoluteError string
func (e *AbsoluteError) Error() string { return fmt.Sprintf("path %q is not absolute", e.Pathname) } func (e AbsoluteError) Error() string {
func (e *AbsoluteError) Is(target error) bool { return fmt.Sprintf("path %q is not absolute", string(e))
var ce *AbsoluteError }
func (e AbsoluteError) Is(target error) bool {
var ce AbsoluteError
if !errors.As(target, &ce) { if !errors.As(target, &ce) {
return errors.Is(target, syscall.EINVAL) return errors.Is(target, syscall.EINVAL)
} }
return *e == *ce return e == ce
} }
// Absolute holds a pathname checked to be absolute. // Absolute holds a pathname checked to be absolute.
type Absolute struct{ pathname string } type Absolute struct{ pathname unique.Handle[string] }
// ok returns whether [Absolute] is not the zero value.
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
// unsafeAbs returns [check.Absolute] on any string value. // unsafeAbs returns [check.Absolute] on any string value.
func unsafeAbs(pathname string) *Absolute { return &Absolute{pathname} } func unsafeAbs(pathname string) *Absolute {
return &Absolute{unique.Make(pathname)}
}
// String returns the checked pathname.
func (a *Absolute) String() string { func (a *Absolute) String() string {
if a.pathname == "" { if !a.ok() {
panic("attempted use of zero Absolute") panic("attempted use of zero Absolute")
} }
return a.pathname.Value()
}
// Handle returns the underlying [unique.Handle].
func (a *Absolute) Handle() unique.Handle[string] {
return a.pathname return a.pathname
} }
// Is efficiently compares the underlying pathname.
func (a *Absolute) Is(v *Absolute) bool { func (a *Absolute) Is(v *Absolute) bool {
if a == nil && v == nil { if a == nil && v == nil {
return true return true
} }
return a != nil && v != nil && return a.ok() && v.ok() && a.pathname == v.pathname
a.pathname != "" && v.pathname != "" &&
a.pathname == v.pathname
} }
// 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 !path.IsAbs(pathname) {
return nil, &AbsoluteError{pathname} return nil, AbsoluteError(pathname)
} }
return unsafeAbs(pathname), nil return unsafeAbs(pathname), nil
} }
@@ -70,35 +84,49 @@ func (a *Absolute) Append(elem ...string) *Absolute {
// Dir calls [path.Dir] with [Absolute] as its argument. // Dir calls [path.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return unsafeAbs(path.Dir(a.String())) } func (a *Absolute) Dir() *Absolute { return unsafeAbs(path.Dir(a.String())) }
func (a *Absolute) GobEncode() ([]byte, error) { return []byte(a.String()), nil } // GobEncode returns the checked pathname.
func (a *Absolute) GobEncode() ([]byte, error) {
return []byte(a.String()), nil
}
// GobDecode stores data if it represents an absolute pathname.
func (a *Absolute) GobDecode(data []byte) error { func (a *Absolute) GobDecode(data []byte) error {
pathname := string(data) pathname := string(data)
if !path.IsAbs(pathname) { if !path.IsAbs(pathname) {
return &AbsoluteError{pathname} return AbsoluteError(pathname)
} }
a.pathname = pathname a.pathname = unique.Make(pathname)
return nil return nil
} }
func (a *Absolute) MarshalJSON() ([]byte, error) { return json.Marshal(a.String()) } // MarshalJSON returns a JSON representation of the checked pathname.
func (a *Absolute) MarshalJSON() ([]byte, error) {
return json.Marshal(a.String())
}
// UnmarshalJSON stores data if it represents an absolute pathname.
func (a *Absolute) UnmarshalJSON(data []byte) error { func (a *Absolute) UnmarshalJSON(data []byte) error {
var pathname string var pathname string
if err := json.Unmarshal(data, &pathname); err != nil { if err := json.Unmarshal(data, &pathname); err != nil {
return err return err
} }
if !path.IsAbs(pathname) { if !path.IsAbs(pathname) {
return &AbsoluteError{pathname} return AbsoluteError(pathname)
} }
a.pathname = pathname a.pathname = unique.Make(pathname)
return nil 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) {
slices.SortFunc(x, func(a, b *Absolute) int { return strings.Compare(a.String(), b.String()) }) slices.SortFunc(x, func(a, b *Absolute) int {
return strings.Compare(a.String(), b.String())
})
} }
// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute]. // CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
func CompactAbs(s []*Absolute) []*Absolute { func CompactAbs(s []*Absolute) []*Absolute {
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool { return a.String() == b.String() }) return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool {
return a.Is(b)
})
} }

View File

@@ -31,8 +31,8 @@ func TestAbsoluteError(t *testing.T) {
}{ }{
{"EINVAL", new(AbsoluteError), syscall.EINVAL, true}, {"EINVAL", new(AbsoluteError), syscall.EINVAL, true},
{"not EINVAL", new(AbsoluteError), syscall.EBADE, false}, {"not EINVAL", new(AbsoluteError), syscall.EBADE, false},
{"ne val", new(AbsoluteError), &AbsoluteError{Pathname: "etc"}, false}, {"ne val", new(AbsoluteError), AbsoluteError("etc"), false},
{"equals", &AbsoluteError{Pathname: "etc"}, &AbsoluteError{Pathname: "etc"}, true}, {"equals", AbsoluteError("etc"), AbsoluteError("etc"), true},
} }
for _, tc := range testCases { for _, tc := range testCases {
@@ -45,7 +45,7 @@ func TestAbsoluteError(t *testing.T) {
t.Parallel() t.Parallel()
want := `path "etc" is not absolute` want := `path "etc" is not absolute`
if got := (&AbsoluteError{Pathname: "etc"}).Error(); got != want { if got := (AbsoluteError("etc")).Error(); got != want {
t.Errorf("Error: %q, want %q", got, want) t.Errorf("Error: %q, want %q", got, want)
} }
}) })
@@ -62,8 +62,8 @@ func TestNewAbs(t *testing.T) {
wantErr error wantErr error
}{ }{
{"good", "/etc", MustAbs("/etc"), nil}, {"good", "/etc", MustAbs("/etc"), nil},
{"not absolute", "etc", nil, &AbsoluteError{Pathname: "etc"}}, {"not absolute", "etc", nil, AbsoluteError("etc")},
{"zero", "", nil, &AbsoluteError{Pathname: ""}}, {"zero", "", nil, AbsoluteError("")},
} }
for _, tc := range testCases { for _, tc := range testCases {
@@ -84,7 +84,7 @@ func TestNewAbs(t *testing.T) {
t.Parallel() t.Parallel()
defer func() { defer func() {
wantPanic := &AbsoluteError{Pathname: "etc"} wantPanic := AbsoluteError("etc")
if r := recover(); !reflect.DeepEqual(r, wantPanic) { if r := recover(); !reflect.DeepEqual(r, wantPanic) {
t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic) t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
@@ -175,7 +175,7 @@ func TestCodecAbsolute(t *testing.T) {
`"/etc"`, `{"val":"/etc","magic":3236757504}`}, `"/etc"`, `{"val":"/etc","magic":3236757504}`},
{"not absolute", nil, {"not absolute", nil,
&AbsoluteError{Pathname: "etc"}, AbsoluteError("etc"),
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc", "\t\x7f\x05\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\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",

View File

@@ -11,7 +11,8 @@ const (
SpecialOverlayPath = ":" SpecialOverlayPath = ":"
) )
// EscapeOverlayDataSegment escapes a string for formatting into the data argument of an overlay mount call. // EscapeOverlayDataSegment escapes a string for formatting into the data
// argument of an overlay mount system call.
func EscapeOverlayDataSegment(s string) string { func EscapeOverlayDataSegment(s string) string {
if s == "" { if s == "" {
return "" return ""

View File

@@ -1,4 +1,5 @@
// Package container implements unprivileged Linux containers with built-in support for syscall filtering. // Package container implements unprivileged Linux containers with built-in
// support for syscall filtering.
package container package container
import ( import (
@@ -35,24 +36,32 @@ type (
// Container represents a container environment being prepared or run. // Container represents a container environment being prepared or run.
// None of [Container] methods are safe for concurrent use. // None of [Container] methods are safe for concurrent use.
Container struct { Container struct {
// Whether the container init should stay alive after its parent terminates.
AllowOrphan bool
// Scheduling policy to set via sched_setscheduler(2). The zero value
// skips this call. Supported policies are [SCHED_BATCH], [SCHED_IDLE].
SchedPolicy int
// Cgroup fd, nil to disable. // Cgroup fd, nil to disable.
Cgroup *int Cgroup *int
// ExtraFiles passed through to initial process in the container, // ExtraFiles passed through to initial process in the container, with
// with behaviour identical to its [exec.Cmd] counterpart. // behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File ExtraFiles []*os.File
// param pipe for shim and init // Write end of a pipe connected to the init to deliver [Params].
setup *os.File setup *os.File
// cancels cmd // Cancels the context passed to the underlying cmd.
cancel context.CancelFunc cancel context.CancelFunc
// closed after Wait returns // Closed after Wait returns. Keeps the spawning thread alive.
wait chan struct{} wait chan struct{}
Stdin io.Reader Stdin io.Reader
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
Cancel func(cmd *exec.Cmd) error // Custom cancellation behaviour for the underlying [exec.Cmd]. Must
// deliver [CancelSignal] before returning.
Cancel func(cmd *exec.Cmd) error
// Copied to the underlying [exec.Cmd].
WaitDelay time.Duration WaitDelay time.Duration
cmd *exec.Cmd cmd *exec.Cmd
@@ -252,8 +261,7 @@ func (p *Container) Start() error {
} }
p.cmd.Dir = fhs.Root p.cmd.Dir = fhs.Root
p.cmd.SysProcAttr = &SysProcAttr{ p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession, Setsid: !p.RetainSession,
Pdeathsig: SIGKILL,
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS | Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP, CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
@@ -262,12 +270,17 @@ func (p *Container) Start() error {
CAP_SYS_ADMIN, CAP_SYS_ADMIN,
// drop capabilities // drop capabilities
CAP_SETPCAP, CAP_SETPCAP,
// bring up loopback interface
CAP_NET_ADMIN,
// overlay access to upperdir and workdir // overlay access to upperdir and workdir
CAP_DAC_OVERRIDE, CAP_DAC_OVERRIDE,
}, },
UseCgroupFD: p.Cgroup != nil, UseCgroupFD: p.Cgroup != nil,
} }
if !p.AllowOrphan {
p.cmd.SysProcAttr.Pdeathsig = SIGKILL
}
if p.cmd.SysProcAttr.UseCgroupFD { if p.cmd.SysProcAttr.UseCgroupFD {
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
} }
@@ -277,7 +290,11 @@ 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 fd, f, err := Setup(&p.cmd.ExtraFiles); err != nil {
return &StartError{true, "set up params stream", err, false, false} return &StartError{
Fatal: true,
Step: "set up params stream",
Err: err,
}
} else { } else {
p.setup = f p.setup = f
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)} p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
@@ -289,10 +306,16 @@ func (p *Container) Start() error {
runtime.LockOSThread() runtime.LockOSThread()
p.wait = make(chan struct{}) p.wait = make(chan struct{})
done <- func() error { // setup depending on per-thread state must happen here // setup depending on per-thread state must happen here
// PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread done <- func() error {
// PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes
// created from the calling thread
if err := SetNoNewPrivs(); err != nil { if err := SetNoNewPrivs(); err != nil {
return &StartError{true, "prctl(PR_SET_NO_NEW_PRIVS)", err, false, false} return &StartError{
Fatal: true,
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
Err: err,
}
} }
// landlock: depends on per-thread state but acts on a process group // landlock: depends on per-thread state but acts on a process group
@@ -304,28 +327,40 @@ func (p *Container) Start() error {
if abi, err := LandlockGetABI(); err != nil { if abi, err := LandlockGetABI(); err != nil {
if p.HostAbstract { if p.HostAbstract {
// landlock can be skipped here as it restricts access to resources // landlock can be skipped here as it restricts access
// already covered by namespaces (pid) // to resources already covered by namespaces (pid)
goto landlockOut goto landlockOut
} }
return &StartError{false, "get landlock ABI", err, false, false} return &StartError{Step: "get landlock ABI", Err: err}
} else if abi < 6 { } else if abi < 6 {
if p.HostAbstract { if p.HostAbstract {
// see above comment // see above comment
goto landlockOut goto landlockOut
} }
return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false} return &StartError{
Step: "kernel too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
Err: ENOSYS,
Origin: true,
}
} else { } else {
p.msg.Verbosef("landlock abi version %d", abi) p.msg.Verbosef("landlock abi version %d", abi)
} }
if rulesetFd, err := rulesetAttr.Create(0); err != nil { if rulesetFd, err := rulesetAttr.Create(0); err != nil {
return &StartError{true, "create landlock ruleset", err, false, false} return &StartError{
Fatal: true,
Step: "create landlock ruleset",
Err: err,
}
} 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 = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd) _ = Close(rulesetFd)
return &StartError{true, "enforce landlock ruleset", err, false, false} return &StartError{
Fatal: true,
Step: "enforce landlock ruleset",
Err: err,
}
} }
if err = Close(rulesetFd); err != nil { if err = Close(rulesetFd); err != nil {
p.msg.Verbosef("cannot close landlock ruleset: %v", err) p.msg.Verbosef("cannot close landlock ruleset: %v", err)
@@ -336,9 +371,30 @@ func (p *Container) Start() error {
landlockOut: landlockOut:
} }
// sched_setscheduler: thread-directed but acts on all processes
// created from the calling thread
if p.SchedPolicy > 0 {
p.msg.Verbosef("setting scheduling policy %d", p.SchedPolicy)
if err := schedSetscheduler(
0, // calling thread
p.SchedPolicy,
&schedParam{0},
); err != nil {
return &StartError{
Fatal: true,
Step: "enforce landlock ruleset",
Err: err,
}
}
}
p.msg.Verbose("starting container init") p.msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil { if err := p.cmd.Start(); err != nil {
return &StartError{false, "start container init", err, false, true} return &StartError{
Step: "start container init",
Err: err,
Passthrough: true,
}
} }
return nil return nil
}() }()
@@ -350,6 +406,7 @@ 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() error {
if p.setup == nil { if p.setup == nil {
@@ -359,12 +416,21 @@ func (p *Container) Serve() error {
setup := p.setup setup := p.setup
p.setup = nil p.setup = nil
if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil { if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil {
return &StartError{true, "set init pipe deadline", err, false, true} return &StartError{
Fatal: true,
Step: "set init pipe deadline",
Err: err,
Passthrough: true,
}
} }
if p.Path == nil { if p.Path == nil {
p.cancel() p.cancel()
return &StartError{false, "invalid executable pathname", EINVAL, true, false} return &StartError{
Step: "invalid executable pathname",
Err: EINVAL,
Origin: true,
}
} }
// do not transmit nil // do not transmit nil
@@ -389,7 +455,8 @@ func (p *Container) Serve() error {
return err return err
} }
// Wait waits for the container init process to exit and releases any resources associated with the [Container]. // Wait blocks until the container init process to exit and releases any
// resources associated with the [Container].
func (p *Container) Wait() error { func (p *Container) Wait() error {
if p.cmd == nil || p.cmd.Process == nil { if p.cmd == nil || p.cmd.Process == nil {
return EINVAL return EINVAL
@@ -434,11 +501,13 @@ func (p *Container) StderrPipe() (r io.ReadCloser, err error) {
} }
func (p *Container) String() string { func (p *Container) String() string {
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x", return fmt.Sprintf(
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets)) "argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets),
)
} }
// ProcessState returns the address to os.ProcessState held by the underlying [exec.Cmd]. // ProcessState returns the address of os.ProcessState held by the underlying [exec.Cmd].
func (p *Container) ProcessState() *os.ProcessState { func (p *Container) ProcessState() *os.ProcessState {
if p.cmd == nil { if p.cmd == nil {
return nil return nil
@@ -446,7 +515,8 @@ func (p *Container) ProcessState() *os.ProcessState {
return p.cmd.ProcessState return p.cmd.ProcessState
} }
// New returns the address to a new instance of [Container] that requires further initialisation before use. // New returns the address to a new instance of [Container]. This value requires
// further initialisation before use.
func New(ctx context.Context, msg message.Msg) *Container { func New(ctx context.Context, msg message.Msg) *Container {
if msg == nil { if msg == nil {
msg = message.New(nil) msg = message.New(nil)
@@ -455,12 +525,18 @@ func New(ctx context.Context, msg message.Msg) *Container {
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}} p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}
c, cancel := context.WithCancel(ctx) c, cancel := context.WithCancel(ctx)
p.cancel = cancel p.cancel = cancel
p.cmd = exec.CommandContext(c, MustExecutable(msg)) p.cmd = exec.CommandContext(c, fhs.ProcSelfExe)
return p return p
} }
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields. // NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
func NewCommand(ctx context.Context, msg message.Msg, pathname *check.Absolute, name string, args ...string) *Container { func NewCommand(
ctx context.Context,
msg message.Msg,
pathname *check.Absolute,
name string,
args ...string,
) *Container {
z := New(ctx, msg) z := New(ctx, msg)
z.Path = pathname z.Path = pathname
z.Args = append([]string{name}, args...) z.Args = append([]string{name}, args...)

View File

@@ -274,13 +274,13 @@ var containerTestCases = []struct {
Dev(check.MustAbs("/dev"), true), Dev(check.MustAbs("/dev"), true),
), ),
earlyMnt( earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", ignore, ignore),
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/null", "/dev/null", ignore, "devtmpfs", ignore, ignore),
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/zero", "/dev/zero", ignore, "devtmpfs", ignore, ignore),
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/full", "/dev/full", ignore, "devtmpfs", ignore, ignore),
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/random", "/dev/random", ignore, "devtmpfs", ignore, ignore),
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/urandom", "/dev/urandom", ignore, "devtmpfs", ignore, ignore),
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/tty", "/dev/tty", ignore, "devtmpfs", ignore, ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
@@ -292,13 +292,13 @@ var containerTestCases = []struct {
Dev(check.MustAbs("/dev"), false), Dev(check.MustAbs("/dev"), false),
), ),
earlyMnt( earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", ignore, ignore),
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/null", "/dev/null", ignore, "devtmpfs", ignore, ignore),
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/zero", "/dev/zero", ignore, "devtmpfs", ignore, ignore),
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/full", "/dev/full", ignore, "devtmpfs", ignore, ignore),
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/random", "/dev/random", ignore, "devtmpfs", ignore, ignore),
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/urandom", "/dev/urandom", ignore, "devtmpfs", ignore, ignore),
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/tty", "/dev/tty", ignore, "devtmpfs", ignore, ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
), ),
@@ -690,11 +690,22 @@ func init() {
return fmt.Errorf("got more than %d entries", len(mnt)) return fmt.Errorf("got more than %d entries", len(mnt))
} }
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags // ugly hack but should be reliable and is less likely to
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime") //false negative than comparing by parsed flags
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime") for _, s := range []string{
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime") "relatime",
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime") "noatime",
} {
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ","+s)
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ","+s)
}
for _, s := range []string{
"seclabel",
"inode64",
} {
cur.FsOptstr = strings.Replace(cur.FsOptstr, ","+s, "", 1)
mnt[i].FsOptstr = strings.Replace(mnt[i].FsOptstr, ","+s, "", 1)
}
if !cur.EqualWithIgnore(mnt[i], "\x00") { if !cur.EqualWithIgnore(mnt[i], "\x00") {
fail = true fail = true
@@ -762,14 +773,13 @@ func TestMain(m *testing.M) {
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) { func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
msg := message.New(nil) msg := message.New(nil)
msg.SwapVerbose(testing.Verbose()) msg.SwapVerbose(testing.Verbose())
executable := check.MustAbs(container.MustExecutable(msg))
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...) c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1") c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(executable, absHelperInnerPath, 0) c.Bind(fhs.AbsProcSelfExe, absHelperInnerPath, 0)
// in case test has cgo enabled // in case test has cgo enabled
if entries, err := ldd.Resolve(ctx, msg, executable); err != nil { if entries, err := ldd.Resolve(ctx, msg, nil); err != nil {
log.Fatalf("ldd: %v", err) log.Fatalf("ldd: %v", err)
} else { } else {
*libPaths = ldd.Path(entries) *libPaths = ldd.Path(entries)

View File

@@ -21,7 +21,8 @@ type osFile interface {
fs.File fs.File
} }
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour. // syscallDispatcher provides methods that make state-dependent system calls as
// part of their behaviour.
type syscallDispatcher interface { type syscallDispatcher interface {
// new starts a goroutine with a new instance of syscallDispatcher. // new starts a goroutine with a new instance of syscallDispatcher.
// A syscallDispatcher must never be used in any goroutine other than the one owning it, // A syscallDispatcher must never be used in any goroutine other than the one owning it,
@@ -61,6 +62,8 @@ type syscallDispatcher interface {
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
// 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(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
@@ -164,6 +167,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) { mustLoopback(msg) }
func (direct) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error { func (direct) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
return seccomp.Load(rules, flags) return seccomp.Load(rules, flags)

View File

@@ -162,7 +162,8 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
t.Parallel() t.Parallel()
wait4signal := make(chan struct{}) wait4signal := make(chan struct{})
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)} lockNotify := make(chan struct{})
k := &kstub{wait4signal, lockNotify, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, lockNotify, s} }, tc.want)}
defer stub.HandleExit(t) defer stub.HandleExit(t)
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) { if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr) t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
@@ -200,8 +201,8 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Helper() t.Helper()
t.Parallel() t.Parallel()
k := &kstub{nil, stub.New(t, k := &kstub{nil, nil, stub.New(t,
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} }, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, nil, s} },
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)}, stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
)} )}
state := &setupState{Params: tc.params, Msg: k} state := &setupState{Params: tc.params, Msg: k}
@@ -237,8 +238,11 @@ func sliceAddr[S any](s []S) *[]S { return &s }
func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile { func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile {
f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr} f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr}
// check happens in Close, and cleanup is not guaranteed to run, so relying on it for sloppy implementations will cause sporadic test results // check happens in Close, and cleanup is not guaranteed to run, so relying
f.cleanup = runtime.AddCleanup(f, func(name string) { f.t.Fatalf("checkedOsFile %s became unreachable without a call to Close", name) }, f.name) // on it for sloppy implementations will cause sporadic test results
f.cleanup = runtime.AddCleanup(f, func(name string) {
panic("checkedOsFile " + name + " became unreachable without a call to Close")
}, name)
return f return f
} }
@@ -322,12 +326,19 @@ const (
type kstub struct { type kstub struct {
wait4signal chan struct{} wait4signal chan struct{}
lockNotify chan struct{}
*stub.Stub[syscallDispatcher] *stub.Stub[syscallDispatcher]
} }
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) } func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") } func (k *kstub) lockOSThread() {
k.Helper()
expect := k.Expects("lockOSThread")
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
<-k.lockNotify
}
}
func (k *kstub) setPtracer(pid uintptr) error { func (k *kstub) setPtracer(pid uintptr) error {
k.Helper() k.Helper()
@@ -457,6 +468,8 @@ 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 (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()
return k.Expects("seccompLoad").Error( return k.Expects("seccompLoad").Error(
@@ -472,6 +485,10 @@ func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
k.FailNow() k.FailNow()
} }
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
defer close(k.lockNotify)
}
// export channel for external instrumentation // export channel for external instrumentation
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil { if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
chanf(c) chanf(c)

View File

@@ -18,7 +18,7 @@ func messageFromError(err error) (m string, ok bool) {
if m, ok = messagePrefixP[os.PathError]("cannot ", err); ok { if m, ok = messagePrefixP[os.PathError]("cannot ", err); ok {
return return
} }
if m, ok = messagePrefixP[check.AbsoluteError](zeroString, err); ok { if m, ok = messagePrefix[check.AbsoluteError](zeroString, err); ok {
return return
} }
if m, ok = messagePrefix[OpRepeatError](zeroString, err); ok { if m, ok = messagePrefix[OpRepeatError](zeroString, err); ok {
@@ -43,7 +43,8 @@ func messageFromError(err error) (m string, ok bool) {
} }
// messagePrefix checks and prefixes the error message of a non-pointer error. // messagePrefix checks and prefixes the error message of a non-pointer error.
// While this is usable for pointer errors, such use should be avoided as nil check is omitted. // While this is usable for pointer errors, such use should be avoided as nil
// check is omitted.
func messagePrefix[T error](prefix string, err error) (string, bool) { func messagePrefix[T error](prefix string, err error) (string, bool) {
var targetError T var targetError T
if errors.As(err, &targetError) { if errors.As(err, &targetError) {

View File

@@ -37,7 +37,7 @@ func TestMessageFromError(t *testing.T) {
Err: stub.UniqueError(0xdeadbeef), Err: stub.UniqueError(0xdeadbeef),
}, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true}, }, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true},
{"absolute", &check.AbsoluteError{Pathname: "etc/mtab"}, {"absolute", check.AbsoluteError("etc/mtab"),
`path "etc/mtab" is not absolute`, true}, `path "etc/mtab" is not absolute`, true},
{"repeat", OpRepeatError("autoetc"), {"repeat", OpRepeatError("autoetc"),

View File

@@ -28,6 +28,9 @@ func copyExecutable(msg message.Msg) {
} }
} }
// MustExecutable calls [os.Executable] and terminates the process on error.
//
// Deprecated: This is no longer used and will be removed in 0.4.
func MustExecutable(msg message.Msg) string { func MustExecutable(msg message.Msg) string {
executableOnce.Do(func() { copyExecutable(msg) }) executableOnce.Do(func() { copyExecutable(msg) })
return executable return executable

View File

@@ -26,6 +26,8 @@ var (
// AbsRunUser is [RunUser] as [check.Absolute]. // AbsRunUser is [RunUser] as [check.Absolute].
AbsRunUser = unsafeAbs(RunUser) AbsRunUser = unsafeAbs(RunUser)
// AbsUsr is [Usr] as [check.Absolute].
AbsUsr = unsafeAbs(Usr)
// AbsUsrBin is [UsrBin] as [check.Absolute]. // AbsUsrBin is [UsrBin] as [check.Absolute].
AbsUsrBin = unsafeAbs(UsrBin) AbsUsrBin = unsafeAbs(UsrBin)
@@ -40,6 +42,8 @@ var (
AbsDevShm = unsafeAbs(DevShm) AbsDevShm = unsafeAbs(DevShm)
// AbsProc is [Proc] as [check.Absolute]. // AbsProc is [Proc] as [check.Absolute].
AbsProc = unsafeAbs(Proc) AbsProc = unsafeAbs(Proc)
// AbsProcSelfExe is [ProcSelfExe] as [check.Absolute].
AbsProcSelfExe = unsafeAbs(ProcSelfExe)
// AbsSys is [Sys] as [check.Absolute]. // AbsSys is [Sys] as [check.Absolute].
AbsSys = unsafeAbs(Sys) AbsSys = unsafeAbs(Sys)
) )

View File

@@ -9,7 +9,8 @@ const (
// Tmp points to the place for small temporary files. // Tmp points to the place for small temporary files.
Tmp = "/tmp/" Tmp = "/tmp/"
// Run points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar. // Run points to a "tmpfs" file system for system packages to place runtime
// data, socket files, and similar.
Run = "/run/" Run = "/run/"
// RunUser points to a directory containing per-user runtime directories, // RunUser points to a directory containing per-user runtime directories,
// each usually individually mounted "tmpfs" instances. // each usually individually mounted "tmpfs" instances.
@@ -17,10 +18,12 @@ const (
// Usr points to vendor-supplied operating system resources. // Usr points to vendor-supplied operating system resources.
Usr = "/usr/" Usr = "/usr/"
// UsrBin points to binaries and executables for user commands that shall appear in the $PATH search path. // UsrBin points to binaries and executables for user commands that shall
// appear in the $PATH search path.
UsrBin = Usr + "bin/" UsrBin = Usr + "bin/"
// Var points to persistent, variable system data. Writable during normal system operation. // Var points to persistent, variable system data. Writable during normal
// system operation.
Var = "/var/" Var = "/var/"
// VarLib points to persistent system data. // VarLib points to persistent system data.
VarLib = Var + "lib/" VarLib = Var + "lib/"
@@ -29,12 +32,20 @@ const (
// Dev points to the root directory for device nodes. // Dev points to the root directory for device nodes.
Dev = "/dev/" Dev = "/dev/"
// DevShm is the place for POSIX shared memory segments, as created via shm_open(3). // DevShm is the place for POSIX shared memory segments, as created via
// shm_open(3).
DevShm = "/dev/shm/" DevShm = "/dev/shm/"
// Proc points to a virtual kernel file system exposing the process list and other functionality. // Proc points to a virtual kernel file system exposing the process list and
// other functionality.
Proc = "/proc/" Proc = "/proc/"
// ProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables. // ProcSys points to a hierarchy below /proc/ that exposes a number of
// kernel tunables.
ProcSys = Proc + "sys/" ProcSys = Proc + "sys/"
// Sys points to a virtual kernel file system exposing discovered devices and other functionality. // ProcSelf resolves to the process's own /proc/pid directory.
ProcSelf = Proc + "self/"
// ProcSelfExe is a symbolic link to program pathname.
ProcSelfExe = ProcSelf + "exe"
// Sys points to a virtual kernel file system exposing discovered devices
// and other functionality.
Sys = "/sys/" Sys = "/sys/"
) )

View File

@@ -33,12 +33,12 @@ const (
- This path is only accessible by init and root: - This path is only accessible by init and root:
The container init sets SUID_DUMP_DISABLE and terminates if that fails. The container init sets SUID_DUMP_DISABLE and terminates if that fails.
It should be noted that none of this should become relevant at any point since the resulting It should be noted that none of this should become relevant at any point
intermediate root tmpfs should be effectively anonymous. */ since the resulting intermediate root tmpfs should be effectively anonymous. */
intermediateHostPath = fhs.Proc + "self/fd" intermediateHostPath = fhs.Proc + "self/fd"
// setupEnv is the name of the environment variable holding the string representation of // setupEnv is the name of the environment variable holding the string
// the read end file descriptor of the setup params pipe. // representation of the read end file descriptor of the setup params pipe.
setupEnv = "HAKUREI_SETUP" setupEnv = "HAKUREI_SETUP"
// exitUnexpectedWait4 is the exit code if wait4 returns an unexpected errno. // exitUnexpectedWait4 is the exit code if wait4 returns an unexpected errno.
@@ -59,7 +59,8 @@ type (
// late is called right before starting the initial process. // late is called right before starting the initial process.
late(state *setupState, k syscallDispatcher) error late(state *setupState, k syscallDispatcher) error
// prefix returns a log message prefix, and whether this Op prints no identifying message on its own. // prefix returns a log message prefix, and whether this Op prints no
// identifying message on its own.
prefix() (string, bool) prefix() (string, bool)
Is(op Op) bool Is(op Op) bool
@@ -71,9 +72,11 @@ type (
setupState struct { setupState struct {
nonrepeatable uintptr nonrepeatable uintptr
// Whether early reaping has concluded. Must only be accessed in the wait4 loop. // Whether early reaping has concluded. Must only be accessed in the
// wait4 loop.
processConcluded bool processConcluded bool
// Process to syscall.WaitStatus populated in the wait4 loop. Freed after early reaping concludes. // Process to syscall.WaitStatus populated in the wait4 loop. Freed
// after early reaping concludes.
process map[int]WaitStatus process map[int]WaitStatus
// Synchronises access to process. // Synchronises access to process.
processMu sync.RWMutex processMu sync.RWMutex
@@ -170,6 +173,10 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
offsetSetup = int(setupFd + 1) offsetSetup = int(setupFd + 1)
} }
if !params.HostNet {
k.mustLoopback(msg)
}
// 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
if err := k.setDumpable(SUID_DUMP_USER); err != nil { if err := k.setDumpable(SUID_DUMP_USER); err != nil {
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err) k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
@@ -212,9 +219,10 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
defer cancel() defer cancel()
/* early is called right before pivot_root into intermediate root; /* early is called right before pivot_root into intermediate root;
this step is mostly for gathering information that would otherwise be difficult to obtain this step is mostly for gathering information that would otherwise be
via library functions after pivot_root, and implementations are expected to avoid changing difficult to obtain via library functions after pivot_root, and
the state of the mount namespace */ implementations are expected to avoid changing the state of the mount
namespace */
for i, op := range *params.Ops { for i, op := range *params.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)
@@ -254,10 +262,10 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot enter intermediate root: %v", err) k.fatalf(msg, "cannot enter intermediate root: %v", err)
} }
/* apply is called right after pivot_root and entering the new root; /* apply is called right after pivot_root and entering the new root. This
this step sets up the container filesystem, and implementations are expected to keep the host root step sets up the container filesystem, and implementations are expected to
and sysroot mount points intact but otherwise can do whatever they need to; keep the host root and sysroot mount points intact but otherwise can do
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 *params.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 {

View File

@@ -1992,7 +1992,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */ /* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{ Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour // this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD), call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
@@ -2075,7 +2075,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(10)}}, nil, nil), call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(10)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbose", stub.ExpectArgs{[]any{"forwarding context cancellation"}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{"forwarding context cancellation"}}, nil, nil),
// magicWait4Signal as ret causes wait4 stub to unblock // magicWait4Signal as ret causes wait4 stub to unblock
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", os.Interrupt}, magicWait4Signal, stub.UniqueError(9)), call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", os.Interrupt}, magicWait4Signal, stub.UniqueError(9)),
@@ -2090,7 +2090,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */ /* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{ Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered // magicWait4Signal as args[4] causes this to block until simulated signal is delivered
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil), call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
@@ -2175,7 +2175,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil), call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbosef", stub.ExpectArgs{"got %s, forwarding to initial process", []any{"quit"}}, nil, nil), call("verbosef", stub.ExpectArgs{"got %s, forwarding to initial process", []any{"quit"}}, nil, nil),
// magicWait4Signal as ret causes wait4 stub to unblock // magicWait4Signal as ret causes wait4 stub to unblock
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", syscall.SIGQUIT}, magicWait4Signal, stub.UniqueError(0xfe)), call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", syscall.SIGQUIT}, magicWait4Signal, stub.UniqueError(0xfe)),
@@ -2190,7 +2190,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */ /* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{ Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered // magicWait4Signal as args[4] causes this to block until simulated signal is delivered
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil), call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
@@ -2275,7 +2275,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil), call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil), call("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil),
call("beforeExit", stub.ExpectArgs{}, nil, nil), call("beforeExit", stub.ExpectArgs{}, nil, nil),
call("exit", stub.ExpectArgs{0}, nil, nil), call("exit", stub.ExpectArgs{0}, nil, nil),
@@ -2283,7 +2283,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */ /* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{ Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour // this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD), call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
@@ -2366,7 +2366,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(5)}}, nil, nil), call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(5)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil), call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
@@ -2377,7 +2377,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */ /* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{ Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil}, 0xbad, nil), call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil}, 0xbad, nil),
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour // this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
@@ -2461,7 +2461,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(3)}}, nil, nil), call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(3)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil), call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
@@ -2471,7 +2471,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */ /* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{ Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR), call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR), call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
@@ -2599,7 +2599,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil), call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbosef", stub.ExpectArgs{"initial process exited with code %d", []any{1}}, nil, nil), call("verbosef", stub.ExpectArgs{"initial process exited with code %d", []any{1}}, nil, nil),
@@ -2609,7 +2609,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */ /* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{ Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR), call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR), call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
@@ -2741,7 +2741,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(0)}}, nil, nil), call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(0)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/bin/zsh")}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/bin/zsh")}}, nil, nil),
call("start", stub.ExpectArgs{"/bin/zsh", []string{"zsh", "-c", "exec vim"}, []string{"DISPLAY=:0"}, "/.hakurei"}, &os.Process{Pid: 0xcafe}, nil), call("start", stub.ExpectArgs{"/bin/zsh", []string{"zsh", "-c", "exec vim"}, []string{"DISPLAY=:0"}, "/.hakurei"}, &os.Process{Pid: 0xcafe}, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
@@ -2752,7 +2752,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */ /* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{ Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR), call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR), call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),

View File

@@ -12,14 +12,16 @@ import (
func init() { gob.Register(new(BindMountOp)) } func init() { gob.Register(new(BindMountOp)) }
// Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target]. // Bind is a helper for appending [BindMountOp] to [Ops].
func (f *Ops) Bind(source, target *check.Absolute, flags int) *Ops { func (f *Ops) Bind(source, target *check.Absolute, flags int) *Ops {
*f = append(*f, &BindMountOp{nil, source, target, flags}) *f = append(*f, &BindMountOp{nil, source, target, flags})
return f return f
} }
// BindMountOp bind mounts host path Source on container path Target. // BindMountOp creates a bind mount from host path Source to container path Target.
// Note that Flags uses bits declared in this package and should not be set with constants in [syscall]. //
// Note that Flags uses bits declared in the [std] package and should not be set
// with constants in [syscall].
type BindMountOp struct { type BindMountOp struct {
sourceFinal, Source, Target *check.Absolute sourceFinal, Source, Target *check.Absolute

View File

@@ -24,8 +24,7 @@ const (
daemonTimeout = 5 * time.Second daemonTimeout = 5 * time.Second
) )
// Daemon appends an [Op] that starts a daemon in the container and blocks until // Daemon is a helper for appending [DaemonOp] to [Ops].
// [DaemonOp.Target] appears.
func (f *Ops) Daemon(target, path *check.Absolute, args ...string) *Ops { func (f *Ops) Daemon(target, path *check.Absolute, args ...string) *Ops {
*f = append(*f, &DaemonOp{target, path, args}) *f = append(*f, &DaemonOp{target, path, args})
return f return f

View File

@@ -19,7 +19,9 @@ func (f *Ops) Dev(target *check.Absolute, mqueue bool) *Ops {
} }
// DevWritable appends an [Op] that mounts a writable subset of host /dev. // DevWritable appends an [Op] that mounts a writable subset of host /dev.
// There is usually no good reason to write to /dev, so this should always be followed by a [RemountOp]. //
// There is usually no good reason to write to /dev, so this should always be
// followed by a [RemountOp].
func (f *Ops) DevWritable(target *check.Absolute, mqueue bool) *Ops { func (f *Ops) DevWritable(target *check.Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{target, mqueue, true}) *f = append(*f, &MountDevOp{target, mqueue, true})
return f return f

View File

@@ -10,7 +10,7 @@ import (
func init() { gob.Register(new(MkdirOp)) } func init() { gob.Register(new(MkdirOp)) }
// Mkdir appends an [Op] that creates a directory in the container filesystem. // Mkdir is a helper for appending [MkdirOp] to [Ops].
func (f *Ops) Mkdir(name *check.Absolute, perm os.FileMode) *Ops { func (f *Ops) Mkdir(name *check.Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{name, perm}) *f = append(*f, &MkdirOp{name, perm})
return f return f

View File

@@ -54,8 +54,11 @@ func (e *OverlayArgumentError) Error() string {
} }
} }
// Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]. // Overlay is a helper for appending [MountOverlayOp] to [Ops].
func (f *Ops) Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) *Ops { func (f *Ops) Overlay(
target, state, work *check.Absolute,
layers ...*check.Absolute,
) *Ops {
*f = append(*f, &MountOverlayOp{ *f = append(*f, &MountOverlayOp{
Target: target, Target: target,
Lower: layers, Lower: layers,
@@ -65,13 +68,12 @@ func (f *Ops) Overlay(target, state, work *check.Absolute, layers ...*check.Abso
return f return f
} }
// OverlayEphemeral appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target] // OverlayEphemeral appends a [MountOverlayOp] with an ephemeral upperdir and workdir.
// with an ephemeral upperdir and workdir.
func (f *Ops) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) *Ops { func (f *Ops) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) *Ops {
return f.Overlay(target, fhs.AbsRoot, nil, layers...) return f.Overlay(target, fhs.AbsRoot, nil, layers...)
} }
// OverlayReadonly appends an [Op] that mounts the overlay pseudo filesystem readonly on [MountOverlayOp.Target] // OverlayReadonly appends a readonly [MountOverlayOp].
func (f *Ops) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) *Ops { func (f *Ops) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) *Ops {
return f.Overlay(target, nil, nil, layers...) return f.Overlay(target, nil, nil, layers...)
} }
@@ -82,25 +84,34 @@ type MountOverlayOp struct {
// Any filesystem, does not need to be on a writable filesystem. // Any filesystem, does not need to be on a writable filesystem.
Lower []*check.Absolute Lower []*check.Absolute
// formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early // Formatted for [OptionOverlayLowerdir].
//
// Resolved, prefixed and escaped during early.
lower []string lower []string
// The upperdir is normally on a writable filesystem. // The upperdir is normally on a writable filesystem.
// //
// If Work is nil and Upper holds the special value [fhs.AbsRoot], // If Work is nil and Upper holds the special value [fhs.AbsRoot], an
// an ephemeral upperdir and workdir will be set up. // ephemeral upperdir and workdir will be set up.
// //
// If both Work and Upper are nil, upperdir and workdir is omitted and the overlay is mounted readonly. // If both Work and Upper are nil, upperdir and workdir is omitted and the
// overlay is mounted readonly.
Upper *check.Absolute Upper *check.Absolute
// formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early // Formatted for [OptionOverlayUpperdir].
//
// Resolved, prefixed and escaped during early.
upper string upper string
// The workdir needs to be an empty directory on the same filesystem as upperdir. // The workdir needs to be an empty directory on the same filesystem as upperdir.
Work *check.Absolute Work *check.Absolute
// formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early // Formatted for [OptionOverlayWorkdir].
//
// Resolved, prefixed and escaped during early.
work string work string
ephemeral bool ephemeral bool
// used internally for mounting to the intermediate root // Used internally for mounting to the intermediate root.
noPrefix bool noPrefix bool
} }

View File

@@ -312,7 +312,10 @@ func TestMountOverlayOp(t *testing.T) {
}, },
}}, }},
{"ephemeral", new(Ops).OverlayEphemeral(check.MustAbs("/nix/store"), check.MustAbs("/mnt-root/nix/.ro-store")), Ops{ {"ephemeral", new(Ops).OverlayEphemeral(
check.MustAbs("/nix/store"),
check.MustAbs("/mnt-root/nix/.ro-store"),
), Ops{
&MountOverlayOp{ &MountOverlayOp{
Target: check.MustAbs("/nix/store"), Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
@@ -320,7 +323,10 @@ func TestMountOverlayOp(t *testing.T) {
}, },
}}, }},
{"readonly", new(Ops).OverlayReadonly(check.MustAbs("/nix/store"), check.MustAbs("/mnt-root/nix/.ro-store")), Ops{ {"readonly", new(Ops).OverlayReadonly(
check.MustAbs("/nix/store"),
check.MustAbs("/mnt-root/nix/.ro-store"),
), Ops{
&MountOverlayOp{ &MountOverlayOp{
Target: check.MustAbs("/nix/store"), Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},

View File

@@ -16,7 +16,7 @@ const (
func init() { gob.Register(new(TmpfileOp)) } func init() { gob.Register(new(TmpfileOp)) }
// Place appends an [Op] that places a file in container path [TmpfileOp.Path] containing [TmpfileOp.Data]. // Place is a helper for appending [TmpfileOp] to [Ops].
func (f *Ops) Place(name *check.Absolute, data []byte) *Ops { func (f *Ops) Place(name *check.Absolute, data []byte) *Ops {
*f = append(*f, &TmpfileOp{name, data}) *f = append(*f, &TmpfileOp{name, data})
return f return f

View File

@@ -21,7 +21,7 @@ func TestTmpfileOp(t *testing.T) {
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), stub.UniqueError(5)), call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, (*checkedOsFile)(nil), stub.UniqueError(5)),
}, stub.UniqueError(5)}, }, stub.UniqueError(5)},
{"Write", &Params{ParentPerm: 0700}, &TmpfileOp{ {"Write", &Params{ParentPerm: 0700}, &TmpfileOp{
@@ -35,14 +35,14 @@ func TestTmpfileOp(t *testing.T) {
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, stub.UniqueError(3)), nil), call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.Close", sampleDataString, stub.UniqueError(3)), nil),
}, stub.UniqueError(3)}, }, stub.UniqueError(3)},
{"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{ {"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil), call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.ensureFile", sampleDataString, nil), nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, stub.UniqueError(2)), call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, stub.UniqueError(2)),
}, stub.UniqueError(2)}, }, stub.UniqueError(2)},
@@ -50,29 +50,29 @@ func TestTmpfileOp(t *testing.T) {
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil), call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.bindMount", sampleDataString, nil), nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil), call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, stub.UniqueError(1)), call("bindMount", stub.ExpectArgs{"tmp.bindMount", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, stub.UniqueError(1)),
}, stub.UniqueError(1)}, }, stub.UniqueError(1)},
{"remove", &Params{ParentPerm: 0700}, &TmpfileOp{ {"remove", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil), call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.remove", sampleDataString, nil), nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil), call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil), call("bindMount", stub.ExpectArgs{"tmp.remove", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil),
call("remove", stub.ExpectArgs{"tmp.32768"}, nil, stub.UniqueError(0)), call("remove", stub.ExpectArgs{"tmp.remove"}, nil, stub.UniqueError(0)),
}, stub.UniqueError(0)}, }, stub.UniqueError(0)},
{"success", &Params{ParentPerm: 0700}, &TmpfileOp{ {"success", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil), call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.success", sampleDataString, nil), nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil), call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil), call("bindMount", stub.ExpectArgs{"tmp.success", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil),
call("remove", stub.ExpectArgs{"tmp.32768"}, nil, nil), call("remove", stub.ExpectArgs{"tmp.success"}, nil, nil),
}, nil}, }, nil},
}) })

View File

@@ -10,7 +10,7 @@ import (
func init() { gob.Register(new(MountProcOp)) } func init() { gob.Register(new(MountProcOp)) }
// Proc appends an [Op] that mounts a private instance of proc. // Proc is a helper for appending [MountProcOp] to [Ops].
func (f *Ops) Proc(target *check.Absolute) *Ops { func (f *Ops) Proc(target *check.Absolute) *Ops {
*f = append(*f, &MountProcOp{target}) *f = append(*f, &MountProcOp{target})
return f return f

View File

@@ -9,7 +9,7 @@ import (
func init() { gob.Register(new(RemountOp)) } func init() { gob.Register(new(RemountOp)) }
// Remount appends an [Op] that applies [RemountOp.Flags] on container path [RemountOp.Target]. // Remount is a helper for appending [RemountOp] to [Ops].
func (f *Ops) Remount(target *check.Absolute, flags uintptr) *Ops { func (f *Ops) Remount(target *check.Absolute, flags uintptr) *Ops {
*f = append(*f, &RemountOp{target, flags}) *f = append(*f, &RemountOp{target, flags})
return f return f

View File

@@ -31,7 +31,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 !path.IsAbs(l.LinkName) {
return &check.AbsoluteError{Pathname: 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 {
return err return err

View File

@@ -23,7 +23,7 @@ func TestSymlinkOp(t *testing.T) {
Target: check.MustAbs("/etc/mtab"), Target: check.MustAbs("/etc/mtab"),
LinkName: "etc/mtab", LinkName: "etc/mtab",
Dereference: true, Dereference: true,
}, nil, &check.AbsoluteError{Pathname: "etc/mtab"}, nil, nil}, }, nil, check.AbsoluteError("etc/mtab"), nil, nil},
{"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{ {"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: check.MustAbs("/etc/mtab"), Target: check.MustAbs("/etc/mtab"),

View File

@@ -38,6 +38,7 @@ const (
_LANDLOCK_ACCESS_FS_DELIM _LANDLOCK_ACCESS_FS_DELIM
) )
// String returns a space-separated string of [LandlockAccessFS] flags.
func (f LandlockAccessFS) String() string { func (f LandlockAccessFS) String() string {
switch f { switch f {
case LANDLOCK_ACCESS_FS_EXECUTE: case LANDLOCK_ACCESS_FS_EXECUTE:
@@ -116,6 +117,7 @@ const (
_LANDLOCK_ACCESS_NET_DELIM _LANDLOCK_ACCESS_NET_DELIM
) )
// String returns a space-separated string of [LandlockAccessNet] flags.
func (f LandlockAccessNet) String() string { func (f LandlockAccessNet) String() string {
switch f { switch f {
case LANDLOCK_ACCESS_NET_BIND_TCP: case LANDLOCK_ACCESS_NET_BIND_TCP:
@@ -152,6 +154,7 @@ const (
_LANDLOCK_SCOPE_DELIM _LANDLOCK_SCOPE_DELIM
) )
// String returns a space-separated string of [LandlockScope] flags.
func (f LandlockScope) String() string { func (f LandlockScope) String() string {
switch f { switch f {
case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET: case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET:
@@ -184,10 +187,12 @@ type RulesetAttr struct {
HandledAccessFS LandlockAccessFS HandledAccessFS LandlockAccessFS
// Bitmask of handled network actions. // Bitmask of handled network actions.
HandledAccessNet LandlockAccessNet HandledAccessNet LandlockAccessNet
// Bitmask of scopes restricting a Landlock domain from accessing outside resources (e.g. IPCs). // Bitmask of scopes restricting a Landlock domain from accessing outside
// resources (e.g. IPCs).
Scoped LandlockScope Scoped LandlockScope
} }
// String returns a user-facing description of [RulesetAttr].
func (rulesetAttr *RulesetAttr) String() string { func (rulesetAttr *RulesetAttr) String() string {
if rulesetAttr == nil { if rulesetAttr == nil {
return "NULL" return "NULL"
@@ -208,6 +213,7 @@ func (rulesetAttr *RulesetAttr) String() string {
return strings.Join(elems, ", ") return strings.Join(elems, ", ")
} }
// Create loads the ruleset into the kernel.
func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) { func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
var pointer, size uintptr var pointer, size uintptr
// NULL needed for abi version // NULL needed for abi version
@@ -216,10 +222,13 @@ func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
size = unsafe.Sizeof(*rulesetAttr) size = unsafe.Sizeof(*rulesetAttr)
} }
rulesetFd, _, errno := syscall.Syscall(std.SYS_LANDLOCK_CREATE_RULESET, pointer, size, flags) rulesetFd, _, errno := syscall.Syscall(
std.SYS_LANDLOCK_CREATE_RULESET,
pointer, size,
flags,
)
fd = int(rulesetFd) fd = int(rulesetFd)
err = errno err = errno
if fd < 0 { if fd < 0 {
return return
} }
@@ -230,12 +239,19 @@ 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.
func LandlockGetABI() (int, error) { func LandlockGetABI() (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.
func LandlockRestrictSelf(rulesetFd int, flags uintptr) error { func LandlockRestrictSelf(rulesetFd int, flags uintptr) error {
r, _, errno := syscall.Syscall(std.SYS_LANDLOCK_RESTRICT_SELF, uintptr(rulesetFd), flags, 0) r, _, errno := syscall.Syscall(
std.SYS_LANDLOCK_RESTRICT_SELF,
uintptr(rulesetFd),
flags,
0,
)
if r != 0 { if r != 0 {
return errno return errno
} }

269
container/netlink.go Normal file
View File

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

72
container/netlink_test.go Normal file
View File

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

View File

@@ -15,7 +15,10 @@ import (
const ( const (
// Nonexistent is a path that cannot exist. // Nonexistent is a path that cannot exist.
// /proc is chosen because a system with covered /proc is unsupported by this package. //
// This path can never be presented by the kernel if proc is mounted on
// /proc/. This can only exist if parts of /proc/ is covered, or proc is not
// mounted at all. Neither configuration is supported by this package.
Nonexistent = fhs.Proc + "nonexistent" Nonexistent = fhs.Proc + "nonexistent"
hostPath = fhs.Root + hostDir hostPath = fhs.Root + hostDir

View File

@@ -88,18 +88,22 @@ var resPrefix = [...]string{
7: "seccomp_load failed", 7: "seccomp_load failed",
} }
// cbAllocateBuffer is the function signature for the function handle passed to hakurei_export_filter // cbAllocateBuffer is the function signature for the function handle passed to
// which allocates the buffer that the resulting bpf program is copied into, and writes its slice header // hakurei_scmp_make_filter which allocates the buffer that the resulting bpf
// to a value held by the caller. // program is copied into, and writes its slice header to a value held by the caller.
type cbAllocateBuffer = func(len C.size_t) (buf unsafe.Pointer) type cbAllocateBuffer = func(len C.size_t) (buf unsafe.Pointer)
// hakurei_scmp_allocate allocates a buffer of specified size known to the
// runtime through a callback passed in a [cgo.Handle].
//
//export hakurei_scmp_allocate //export hakurei_scmp_allocate
func hakurei_scmp_allocate(f C.uintptr_t, len C.size_t) (buf unsafe.Pointer) { func hakurei_scmp_allocate(f C.uintptr_t, len C.size_t) (buf unsafe.Pointer) {
return cgo.Handle(f).Value().(cbAllocateBuffer)(len) return cgo.Handle(f).Value().(cbAllocateBuffer)(len)
} }
// makeFilter generates a bpf program from a slice of [std.NativeRule] and writes the resulting byte slice to p. // makeFilter generates a bpf program from a slice of [std.NativeRule] and
// The filter is installed to the current process if p is nil. // writes the resulting byte slice to p. The filter is installed to the current
// process if p is nil.
func makeFilter(rules []std.NativeRule, flags ExportFlag, p *[]byte) error { func makeFilter(rules []std.NativeRule, flags ExportFlag, p *[]byte) error {
if len(rules) == 0 { if len(rules) == 0 {
return ErrInvalidRules return ErrInvalidRules
@@ -170,8 +174,8 @@ func Export(rules []std.NativeRule, flags ExportFlag) (data []byte, err error) {
return return
} }
// Load generates a bpf program from a slice of [std.NativeRule] and enforces it on the current process. // Load generates a bpf program from a slice of [std.NativeRule] and enforces it
// Errors returned by libseccomp is wrapped in [LibraryError]. // on the current process. Errors returned by libseccomp is wrapped in [LibraryError].
func Load(rules []std.NativeRule, flags ExportFlag) error { return makeFilter(rules, flags, nil) } func Load(rules []std.NativeRule, flags ExportFlag) error { return makeFilter(rules, flags, nil) }
type ( type (

View File

@@ -0,0 +1,27 @@
package seccomp_test
import (
. "hakurei.app/container/seccomp"
. "hakurei.app/container/std"
)
var bpfExpected = bpfLookup{
{AllowMultiarch | AllowCAN |
AllowBluetooth, PresetExt |
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
PresetLinux32}: toHash(
"a1c4ffa35f4bfbf38061184760b9a09edfcb4964c3b534395e47327b83f3fb61f2f9573ddfcc4772424cc2f5dd12fd32471e6531dbe10e85eda3797dd4fa179f"),
{0, 0}: toHash(
"f3910fd727d087def593e3876c2c6ab9ace71d82ec8cbc992a26223e7bba85e1d7a0b56c5fc6303703f24595825dad8561637edaedd5384b34a6cd080946633c"),
{0, PresetExt}: toHash(
"741438c5e3f11c36c92ae8c5934f13440675c6e719541c2dbffeda79a10081bcfd9ad8314a60c1d1f53db86c8080c13fffa3bbcf7fe753935679b4b902737286"),
{0, PresetStrict}: toHash(
"79e9e464d02405c6d74fd2c771bd72a1311e488221c73a9c32db9270219837c54fccec2f36fe2474895547e60c311514567e2e6cf4e7a7fcf909c1ecd1e254a7"),
{0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel}: toHash(
"3c443715a6c1e557a284862ea8efb70a5d4ecbe67d1226627323e861cd3646fb3e7768ec5b94b93760b7f652cf6916f66e317a4fbf8716d10c3673aa4fc3ae58"),
{0, PresetExt | PresetDenyDevel}: toHash(
"4448a74e8cc75a4ab63799c4f2cc2a5af63e5f4e8e9b8ac15a1873d647dfa67a4c67b39ed466d8dd32abc64136d401879fc6185c9ab00feeaf59ccf4305f8201"),
{0, PresetExt | PresetDenyNS | PresetDenyDevel}: toHash(
"c7c86e793cb7192f5f6c735f372cda27eb43ae1045e587f8eadb64c849520a3280b6570a3d7b601d32cddb38021585a2234db38e506cebfd10aa3d6c75440f17"),
}

View File

@@ -24,8 +24,8 @@ func TestSyscallResolveName(t *testing.T) {
} }
func TestRuleType(t *testing.T) { func TestRuleType(t *testing.T) {
assertKind[std.ScmpUint, scmpUint](t) assertKind[std.Uint, scmpUint](t)
assertKind[std.ScmpInt, scmpInt](t) assertKind[std.Int, scmpInt](t)
assertSize[std.NativeRule, syscallRule](t) assertSize[std.NativeRule, syscallRule](t)
assertKind[std.ScmpDatum, scmpDatum](t) assertKind[std.ScmpDatum, scmpDatum](t)

View File

@@ -12,6 +12,7 @@ my %syscall_cutoff_arch = (
"x86" => 340, "x86" => 340,
"x86_64" => 302, "x86_64" => 302,
"aarch64" => 281, "aarch64" => 281,
"riscv64" => 281,
); );
print <<EOF; print <<EOF;

View File

@@ -7,24 +7,28 @@ import (
type ( type (
// ScmpUint is equivalent to C.uint. // ScmpUint is equivalent to C.uint.
ScmpUint uint32 //
// Deprecated: This type has been renamed to Uint and will be removed in 0.4.
ScmpUint = Uint
// ScmpInt is equivalent to C.int. // ScmpInt is equivalent to C.int.
ScmpInt int32 //
// Deprecated: This type has been renamed to Int and will be removed in 0.4.
ScmpInt = Int
// ScmpSyscall represents a syscall number passed to libseccomp via [NativeRule.Syscall]. // ScmpSyscall represents a syscall number passed to libseccomp via [NativeRule.Syscall].
ScmpSyscall ScmpInt ScmpSyscall Int
// ScmpErrno represents an errno value passed to libseccomp via [NativeRule.Errno]. // ScmpErrno represents an errno value passed to libseccomp via [NativeRule.Errno].
ScmpErrno ScmpInt ScmpErrno Int
// ScmpCompare is equivalent to enum scmp_compare; // ScmpCompare is equivalent to enum scmp_compare;
ScmpCompare ScmpUint ScmpCompare Uint
// ScmpDatum is equivalent to scmp_datum_t. // ScmpDatum is equivalent to scmp_datum_t.
ScmpDatum uint64 ScmpDatum uint64
// ScmpArgCmp is equivalent to struct scmp_arg_cmp. // ScmpArgCmp is equivalent to struct scmp_arg_cmp.
ScmpArgCmp struct { ScmpArgCmp struct {
// argument number, starting at 0 // argument number, starting at 0
Arg ScmpUint `json:"arg"` Arg Uint `json:"arg"`
// the comparison op, e.g. SCMP_CMP_* // the comparison op, e.g. SCMP_CMP_*
Op ScmpCompare `json:"op"` Op ScmpCompare `json:"op"`

View File

@@ -0,0 +1,55 @@
package std
import "syscall"
const (
SYS_NEWFSTATAT = syscall.SYS_FSTATAT
)
var syscallNumExtra = map[string]ScmpSyscall{
"uselib": SNR_USELIB,
"clock_adjtime64": SNR_CLOCK_ADJTIME64,
"clock_settime64": SNR_CLOCK_SETTIME64,
"umount": SNR_UMOUNT,
"chown": SNR_CHOWN,
"chown32": SNR_CHOWN32,
"fchown32": SNR_FCHOWN32,
"lchown": SNR_LCHOWN,
"lchown32": SNR_LCHOWN32,
"setgid32": SNR_SETGID32,
"setgroups32": SNR_SETGROUPS32,
"setregid32": SNR_SETREGID32,
"setresgid32": SNR_SETRESGID32,
"setresuid32": SNR_SETRESUID32,
"setreuid32": SNR_SETREUID32,
"setuid32": SNR_SETUID32,
"modify_ldt": SNR_MODIFY_LDT,
"subpage_prot": SNR_SUBPAGE_PROT,
"switch_endian": SNR_SWITCH_ENDIAN,
"vm86": SNR_VM86,
"vm86old": SNR_VM86OLD,
}
const (
SNR_USELIB ScmpSyscall = __PNR_uselib
SNR_CLOCK_ADJTIME64 ScmpSyscall = __PNR_clock_adjtime64
SNR_CLOCK_SETTIME64 ScmpSyscall = __PNR_clock_settime64
SNR_UMOUNT ScmpSyscall = __PNR_umount
SNR_CHOWN ScmpSyscall = __PNR_chown
SNR_CHOWN32 ScmpSyscall = __PNR_chown32
SNR_FCHOWN32 ScmpSyscall = __PNR_fchown32
SNR_LCHOWN ScmpSyscall = __PNR_lchown
SNR_LCHOWN32 ScmpSyscall = __PNR_lchown32
SNR_SETGID32 ScmpSyscall = __PNR_setgid32
SNR_SETGROUPS32 ScmpSyscall = __PNR_setgroups32
SNR_SETREGID32 ScmpSyscall = __PNR_setregid32
SNR_SETRESGID32 ScmpSyscall = __PNR_setresgid32
SNR_SETRESUID32 ScmpSyscall = __PNR_setresuid32
SNR_SETREUID32 ScmpSyscall = __PNR_setreuid32
SNR_SETUID32 ScmpSyscall = __PNR_setuid32
SNR_MODIFY_LDT ScmpSyscall = __PNR_modify_ldt
SNR_SUBPAGE_PROT ScmpSyscall = __PNR_subpage_prot
SNR_SWITCH_ENDIAN ScmpSyscall = __PNR_switch_endian
SNR_VM86 ScmpSyscall = __PNR_vm86
SNR_VM86OLD ScmpSyscall = __PNR_vm86old
)

View File

@@ -0,0 +1,719 @@
// mksysnum_linux.pl /usr/include/riscv64-linux-gnu/asm/unistd.h
// Code generated by the command above; DO NOT EDIT.
package std
import . "syscall"
var syscallNum = map[string]ScmpSyscall{
"io_setup": SNR_IO_SETUP,
"io_destroy": SNR_IO_DESTROY,
"io_submit": SNR_IO_SUBMIT,
"io_cancel": SNR_IO_CANCEL,
"io_getevents": SNR_IO_GETEVENTS,
"setxattr": SNR_SETXATTR,
"lsetxattr": SNR_LSETXATTR,
"fsetxattr": SNR_FSETXATTR,
"getxattr": SNR_GETXATTR,
"lgetxattr": SNR_LGETXATTR,
"fgetxattr": SNR_FGETXATTR,
"listxattr": SNR_LISTXATTR,
"llistxattr": SNR_LLISTXATTR,
"flistxattr": SNR_FLISTXATTR,
"removexattr": SNR_REMOVEXATTR,
"lremovexattr": SNR_LREMOVEXATTR,
"fremovexattr": SNR_FREMOVEXATTR,
"getcwd": SNR_GETCWD,
"lookup_dcookie": SNR_LOOKUP_DCOOKIE,
"eventfd2": SNR_EVENTFD2,
"epoll_create1": SNR_EPOLL_CREATE1,
"epoll_ctl": SNR_EPOLL_CTL,
"epoll_pwait": SNR_EPOLL_PWAIT,
"dup": SNR_DUP,
"dup3": SNR_DUP3,
"fcntl": SNR_FCNTL,
"inotify_init1": SNR_INOTIFY_INIT1,
"inotify_add_watch": SNR_INOTIFY_ADD_WATCH,
"inotify_rm_watch": SNR_INOTIFY_RM_WATCH,
"ioctl": SNR_IOCTL,
"ioprio_set": SNR_IOPRIO_SET,
"ioprio_get": SNR_IOPRIO_GET,
"flock": SNR_FLOCK,
"mknodat": SNR_MKNODAT,
"mkdirat": SNR_MKDIRAT,
"unlinkat": SNR_UNLINKAT,
"symlinkat": SNR_SYMLINKAT,
"linkat": SNR_LINKAT,
"umount2": SNR_UMOUNT2,
"mount": SNR_MOUNT,
"pivot_root": SNR_PIVOT_ROOT,
"nfsservctl": SNR_NFSSERVCTL,
"statfs": SNR_STATFS,
"fstatfs": SNR_FSTATFS,
"truncate": SNR_TRUNCATE,
"ftruncate": SNR_FTRUNCATE,
"fallocate": SNR_FALLOCATE,
"faccessat": SNR_FACCESSAT,
"chdir": SNR_CHDIR,
"fchdir": SNR_FCHDIR,
"chroot": SNR_CHROOT,
"fchmod": SNR_FCHMOD,
"fchmodat": SNR_FCHMODAT,
"fchownat": SNR_FCHOWNAT,
"fchown": SNR_FCHOWN,
"openat": SNR_OPENAT,
"close": SNR_CLOSE,
"vhangup": SNR_VHANGUP,
"pipe2": SNR_PIPE2,
"quotactl": SNR_QUOTACTL,
"getdents64": SNR_GETDENTS64,
"lseek": SNR_LSEEK,
"read": SNR_READ,
"write": SNR_WRITE,
"readv": SNR_READV,
"writev": SNR_WRITEV,
"pread64": SNR_PREAD64,
"pwrite64": SNR_PWRITE64,
"preadv": SNR_PREADV,
"pwritev": SNR_PWRITEV,
"sendfile": SNR_SENDFILE,
"pselect6": SNR_PSELECT6,
"ppoll": SNR_PPOLL,
"signalfd4": SNR_SIGNALFD4,
"vmsplice": SNR_VMSPLICE,
"splice": SNR_SPLICE,
"tee": SNR_TEE,
"readlinkat": SNR_READLINKAT,
"newfstatat": SNR_NEWFSTATAT,
"fstat": SNR_FSTAT,
"sync": SNR_SYNC,
"fsync": SNR_FSYNC,
"fdatasync": SNR_FDATASYNC,
"sync_file_range": SNR_SYNC_FILE_RANGE,
"timerfd_create": SNR_TIMERFD_CREATE,
"timerfd_settime": SNR_TIMERFD_SETTIME,
"timerfd_gettime": SNR_TIMERFD_GETTIME,
"utimensat": SNR_UTIMENSAT,
"acct": SNR_ACCT,
"capget": SNR_CAPGET,
"capset": SNR_CAPSET,
"personality": SNR_PERSONALITY,
"exit": SNR_EXIT,
"exit_group": SNR_EXIT_GROUP,
"waitid": SNR_WAITID,
"set_tid_address": SNR_SET_TID_ADDRESS,
"unshare": SNR_UNSHARE,
"futex": SNR_FUTEX,
"set_robust_list": SNR_SET_ROBUST_LIST,
"get_robust_list": SNR_GET_ROBUST_LIST,
"nanosleep": SNR_NANOSLEEP,
"getitimer": SNR_GETITIMER,
"setitimer": SNR_SETITIMER,
"kexec_load": SNR_KEXEC_LOAD,
"init_module": SNR_INIT_MODULE,
"delete_module": SNR_DELETE_MODULE,
"timer_create": SNR_TIMER_CREATE,
"timer_gettime": SNR_TIMER_GETTIME,
"timer_getoverrun": SNR_TIMER_GETOVERRUN,
"timer_settime": SNR_TIMER_SETTIME,
"timer_delete": SNR_TIMER_DELETE,
"clock_settime": SNR_CLOCK_SETTIME,
"clock_gettime": SNR_CLOCK_GETTIME,
"clock_getres": SNR_CLOCK_GETRES,
"clock_nanosleep": SNR_CLOCK_NANOSLEEP,
"syslog": SNR_SYSLOG,
"ptrace": SNR_PTRACE,
"sched_setparam": SNR_SCHED_SETPARAM,
"sched_setscheduler": SNR_SCHED_SETSCHEDULER,
"sched_getscheduler": SNR_SCHED_GETSCHEDULER,
"sched_getparam": SNR_SCHED_GETPARAM,
"sched_setaffinity": SNR_SCHED_SETAFFINITY,
"sched_getaffinity": SNR_SCHED_GETAFFINITY,
"sched_yield": SNR_SCHED_YIELD,
"sched_get_priority_max": SNR_SCHED_GET_PRIORITY_MAX,
"sched_get_priority_min": SNR_SCHED_GET_PRIORITY_MIN,
"sched_rr_get_interval": SNR_SCHED_RR_GET_INTERVAL,
"restart_syscall": SNR_RESTART_SYSCALL,
"kill": SNR_KILL,
"tkill": SNR_TKILL,
"tgkill": SNR_TGKILL,
"sigaltstack": SNR_SIGALTSTACK,
"rt_sigsuspend": SNR_RT_SIGSUSPEND,
"rt_sigaction": SNR_RT_SIGACTION,
"rt_sigprocmask": SNR_RT_SIGPROCMASK,
"rt_sigpending": SNR_RT_SIGPENDING,
"rt_sigtimedwait": SNR_RT_SIGTIMEDWAIT,
"rt_sigqueueinfo": SNR_RT_SIGQUEUEINFO,
"rt_sigreturn": SNR_RT_SIGRETURN,
"setpriority": SNR_SETPRIORITY,
"getpriority": SNR_GETPRIORITY,
"reboot": SNR_REBOOT,
"setregid": SNR_SETREGID,
"setgid": SNR_SETGID,
"setreuid": SNR_SETREUID,
"setuid": SNR_SETUID,
"setresuid": SNR_SETRESUID,
"getresuid": SNR_GETRESUID,
"setresgid": SNR_SETRESGID,
"getresgid": SNR_GETRESGID,
"setfsuid": SNR_SETFSUID,
"setfsgid": SNR_SETFSGID,
"times": SNR_TIMES,
"setpgid": SNR_SETPGID,
"getpgid": SNR_GETPGID,
"getsid": SNR_GETSID,
"setsid": SNR_SETSID,
"getgroups": SNR_GETGROUPS,
"setgroups": SNR_SETGROUPS,
"uname": SNR_UNAME,
"sethostname": SNR_SETHOSTNAME,
"setdomainname": SNR_SETDOMAINNAME,
"getrlimit": SNR_GETRLIMIT,
"setrlimit": SNR_SETRLIMIT,
"getrusage": SNR_GETRUSAGE,
"umask": SNR_UMASK,
"prctl": SNR_PRCTL,
"getcpu": SNR_GETCPU,
"gettimeofday": SNR_GETTIMEOFDAY,
"settimeofday": SNR_SETTIMEOFDAY,
"adjtimex": SNR_ADJTIMEX,
"getpid": SNR_GETPID,
"getppid": SNR_GETPPID,
"getuid": SNR_GETUID,
"geteuid": SNR_GETEUID,
"getgid": SNR_GETGID,
"getegid": SNR_GETEGID,
"gettid": SNR_GETTID,
"sysinfo": SNR_SYSINFO,
"mq_open": SNR_MQ_OPEN,
"mq_unlink": SNR_MQ_UNLINK,
"mq_timedsend": SNR_MQ_TIMEDSEND,
"mq_timedreceive": SNR_MQ_TIMEDRECEIVE,
"mq_notify": SNR_MQ_NOTIFY,
"mq_getsetattr": SNR_MQ_GETSETATTR,
"msgget": SNR_MSGGET,
"msgctl": SNR_MSGCTL,
"msgrcv": SNR_MSGRCV,
"msgsnd": SNR_MSGSND,
"semget": SNR_SEMGET,
"semctl": SNR_SEMCTL,
"semtimedop": SNR_SEMTIMEDOP,
"semop": SNR_SEMOP,
"shmget": SNR_SHMGET,
"shmctl": SNR_SHMCTL,
"shmat": SNR_SHMAT,
"shmdt": SNR_SHMDT,
"socket": SNR_SOCKET,
"socketpair": SNR_SOCKETPAIR,
"bind": SNR_BIND,
"listen": SNR_LISTEN,
"accept": SNR_ACCEPT,
"connect": SNR_CONNECT,
"getsockname": SNR_GETSOCKNAME,
"getpeername": SNR_GETPEERNAME,
"sendto": SNR_SENDTO,
"recvfrom": SNR_RECVFROM,
"setsockopt": SNR_SETSOCKOPT,
"getsockopt": SNR_GETSOCKOPT,
"shutdown": SNR_SHUTDOWN,
"sendmsg": SNR_SENDMSG,
"recvmsg": SNR_RECVMSG,
"readahead": SNR_READAHEAD,
"brk": SNR_BRK,
"munmap": SNR_MUNMAP,
"mremap": SNR_MREMAP,
"add_key": SNR_ADD_KEY,
"request_key": SNR_REQUEST_KEY,
"keyctl": SNR_KEYCTL,
"clone": SNR_CLONE,
"execve": SNR_EXECVE,
"mmap": SNR_MMAP,
"fadvise64": SNR_FADVISE64,
"swapon": SNR_SWAPON,
"swapoff": SNR_SWAPOFF,
"mprotect": SNR_MPROTECT,
"msync": SNR_MSYNC,
"mlock": SNR_MLOCK,
"munlock": SNR_MUNLOCK,
"mlockall": SNR_MLOCKALL,
"munlockall": SNR_MUNLOCKALL,
"mincore": SNR_MINCORE,
"madvise": SNR_MADVISE,
"remap_file_pages": SNR_REMAP_FILE_PAGES,
"mbind": SNR_MBIND,
"get_mempolicy": SNR_GET_MEMPOLICY,
"set_mempolicy": SNR_SET_MEMPOLICY,
"migrate_pages": SNR_MIGRATE_PAGES,
"move_pages": SNR_MOVE_PAGES,
"rt_tgsigqueueinfo": SNR_RT_TGSIGQUEUEINFO,
"perf_event_open": SNR_PERF_EVENT_OPEN,
"accept4": SNR_ACCEPT4,
"recvmmsg": SNR_RECVMMSG,
"wait4": SNR_WAIT4,
"prlimit64": SNR_PRLIMIT64,
"fanotify_init": SNR_FANOTIFY_INIT,
"fanotify_mark": SNR_FANOTIFY_MARK,
"name_to_handle_at": SNR_NAME_TO_HANDLE_AT,
"open_by_handle_at": SNR_OPEN_BY_HANDLE_AT,
"clock_adjtime": SNR_CLOCK_ADJTIME,
"syncfs": SNR_SYNCFS,
"setns": SNR_SETNS,
"sendmmsg": SNR_SENDMMSG,
"process_vm_readv": SNR_PROCESS_VM_READV,
"process_vm_writev": SNR_PROCESS_VM_WRITEV,
"kcmp": SNR_KCMP,
"finit_module": SNR_FINIT_MODULE,
"sched_setattr": SNR_SCHED_SETATTR,
"sched_getattr": SNR_SCHED_GETATTR,
"renameat2": SNR_RENAMEAT2,
"seccomp": SNR_SECCOMP,
"getrandom": SNR_GETRANDOM,
"memfd_create": SNR_MEMFD_CREATE,
"bpf": SNR_BPF,
"execveat": SNR_EXECVEAT,
"userfaultfd": SNR_USERFAULTFD,
"membarrier": SNR_MEMBARRIER,
"mlock2": SNR_MLOCK2,
"copy_file_range": SNR_COPY_FILE_RANGE,
"preadv2": SNR_PREADV2,
"pwritev2": SNR_PWRITEV2,
"pkey_mprotect": SNR_PKEY_MPROTECT,
"pkey_alloc": SNR_PKEY_ALLOC,
"pkey_free": SNR_PKEY_FREE,
"statx": SNR_STATX,
"io_pgetevents": SNR_IO_PGETEVENTS,
"rseq": SNR_RSEQ,
"kexec_file_load": SNR_KEXEC_FILE_LOAD,
"pidfd_send_signal": SNR_PIDFD_SEND_SIGNAL,
"io_uring_setup": SNR_IO_URING_SETUP,
"io_uring_enter": SNR_IO_URING_ENTER,
"io_uring_register": SNR_IO_URING_REGISTER,
"open_tree": SNR_OPEN_TREE,
"move_mount": SNR_MOVE_MOUNT,
"fsopen": SNR_FSOPEN,
"fsconfig": SNR_FSCONFIG,
"fsmount": SNR_FSMOUNT,
"fspick": SNR_FSPICK,
"pidfd_open": SNR_PIDFD_OPEN,
"clone3": SNR_CLONE3,
"close_range": SNR_CLOSE_RANGE,
"openat2": SNR_OPENAT2,
"pidfd_getfd": SNR_PIDFD_GETFD,
"faccessat2": SNR_FACCESSAT2,
"process_madvise": SNR_PROCESS_MADVISE,
"epoll_pwait2": SNR_EPOLL_PWAIT2,
"mount_setattr": SNR_MOUNT_SETATTR,
"quotactl_fd": SNR_QUOTACTL_FD,
"landlock_create_ruleset": SNR_LANDLOCK_CREATE_RULESET,
"landlock_add_rule": SNR_LANDLOCK_ADD_RULE,
"landlock_restrict_self": SNR_LANDLOCK_RESTRICT_SELF,
"memfd_secret": SNR_MEMFD_SECRET,
"process_mrelease": SNR_PROCESS_MRELEASE,
"futex_waitv": SNR_FUTEX_WAITV,
"set_mempolicy_home_node": SNR_SET_MEMPOLICY_HOME_NODE,
"cachestat": SNR_CACHESTAT,
"fchmodat2": SNR_FCHMODAT2,
"map_shadow_stack": SNR_MAP_SHADOW_STACK,
"futex_wake": SNR_FUTEX_WAKE,
"futex_wait": SNR_FUTEX_WAIT,
"futex_requeue": SNR_FUTEX_REQUEUE,
"statmount": SNR_STATMOUNT,
"listmount": SNR_LISTMOUNT,
"lsm_get_self_attr": SNR_LSM_GET_SELF_ATTR,
"lsm_set_self_attr": SNR_LSM_SET_SELF_ATTR,
"lsm_list_modules": SNR_LSM_LIST_MODULES,
"mseal": SNR_MSEAL,
"setxattrat": SNR_SETXATTRAT,
"getxattrat": SNR_GETXATTRAT,
"listxattrat": SNR_LISTXATTRAT,
"removexattrat": SNR_REMOVEXATTRAT,
}
const (
SYS_USERFAULTFD = 282
SYS_MEMBARRIER = 283
SYS_MLOCK2 = 284
SYS_COPY_FILE_RANGE = 285
SYS_PREADV2 = 286
SYS_PWRITEV2 = 287
SYS_PKEY_MPROTECT = 288
SYS_PKEY_ALLOC = 289
SYS_PKEY_FREE = 290
SYS_STATX = 291
SYS_IO_PGETEVENTS = 292
SYS_RSEQ = 293
SYS_KEXEC_FILE_LOAD = 294
SYS_PIDFD_SEND_SIGNAL = 424
SYS_IO_URING_SETUP = 425
SYS_IO_URING_ENTER = 426
SYS_IO_URING_REGISTER = 427
SYS_OPEN_TREE = 428
SYS_MOVE_MOUNT = 429
SYS_FSOPEN = 430
SYS_FSCONFIG = 431
SYS_FSMOUNT = 432
SYS_FSPICK = 433
SYS_PIDFD_OPEN = 434
SYS_CLONE3 = 435
SYS_CLOSE_RANGE = 436
SYS_OPENAT2 = 437
SYS_PIDFD_GETFD = 438
SYS_FACCESSAT2 = 439
SYS_PROCESS_MADVISE = 440
SYS_EPOLL_PWAIT2 = 441
SYS_MOUNT_SETATTR = 442
SYS_QUOTACTL_FD = 443
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_MEMFD_SECRET = 447
SYS_PROCESS_MRELEASE = 448
SYS_FUTEX_WAITV = 449
SYS_SET_MEMPOLICY_HOME_NODE = 450
SYS_CACHESTAT = 451
SYS_FCHMODAT2 = 452
SYS_MAP_SHADOW_STACK = 453
SYS_FUTEX_WAKE = 454
SYS_FUTEX_WAIT = 455
SYS_FUTEX_REQUEUE = 456
SYS_STATMOUNT = 457
SYS_LISTMOUNT = 458
SYS_LSM_GET_SELF_ATTR = 459
SYS_LSM_SET_SELF_ATTR = 460
SYS_LSM_LIST_MODULES = 461
SYS_MSEAL = 462
SYS_SETXATTRAT = 463
SYS_GETXATTRAT = 464
SYS_LISTXATTRAT = 465
SYS_REMOVEXATTRAT = 466
SYS_OPEN_TREE_ATTR = 467
SYS_FILE_GETATTR = 468
SYS_FILE_SETATTR = 469
)
const (
SNR_IO_SETUP ScmpSyscall = SYS_IO_SETUP
SNR_IO_DESTROY ScmpSyscall = SYS_IO_DESTROY
SNR_IO_SUBMIT ScmpSyscall = SYS_IO_SUBMIT
SNR_IO_CANCEL ScmpSyscall = SYS_IO_CANCEL
SNR_IO_GETEVENTS ScmpSyscall = SYS_IO_GETEVENTS
SNR_SETXATTR ScmpSyscall = SYS_SETXATTR
SNR_LSETXATTR ScmpSyscall = SYS_LSETXATTR
SNR_FSETXATTR ScmpSyscall = SYS_FSETXATTR
SNR_GETXATTR ScmpSyscall = SYS_GETXATTR
SNR_LGETXATTR ScmpSyscall = SYS_LGETXATTR
SNR_FGETXATTR ScmpSyscall = SYS_FGETXATTR
SNR_LISTXATTR ScmpSyscall = SYS_LISTXATTR
SNR_LLISTXATTR ScmpSyscall = SYS_LLISTXATTR
SNR_FLISTXATTR ScmpSyscall = SYS_FLISTXATTR
SNR_REMOVEXATTR ScmpSyscall = SYS_REMOVEXATTR
SNR_LREMOVEXATTR ScmpSyscall = SYS_LREMOVEXATTR
SNR_FREMOVEXATTR ScmpSyscall = SYS_FREMOVEXATTR
SNR_GETCWD ScmpSyscall = SYS_GETCWD
SNR_LOOKUP_DCOOKIE ScmpSyscall = SYS_LOOKUP_DCOOKIE
SNR_EVENTFD2 ScmpSyscall = SYS_EVENTFD2
SNR_EPOLL_CREATE1 ScmpSyscall = SYS_EPOLL_CREATE1
SNR_EPOLL_CTL ScmpSyscall = SYS_EPOLL_CTL
SNR_EPOLL_PWAIT ScmpSyscall = SYS_EPOLL_PWAIT
SNR_DUP ScmpSyscall = SYS_DUP
SNR_DUP3 ScmpSyscall = SYS_DUP3
SNR_FCNTL ScmpSyscall = SYS_FCNTL
SNR_INOTIFY_INIT1 ScmpSyscall = SYS_INOTIFY_INIT1
SNR_INOTIFY_ADD_WATCH ScmpSyscall = SYS_INOTIFY_ADD_WATCH
SNR_INOTIFY_RM_WATCH ScmpSyscall = SYS_INOTIFY_RM_WATCH
SNR_IOCTL ScmpSyscall = SYS_IOCTL
SNR_IOPRIO_SET ScmpSyscall = SYS_IOPRIO_SET
SNR_IOPRIO_GET ScmpSyscall = SYS_IOPRIO_GET
SNR_FLOCK ScmpSyscall = SYS_FLOCK
SNR_MKNODAT ScmpSyscall = SYS_MKNODAT
SNR_MKDIRAT ScmpSyscall = SYS_MKDIRAT
SNR_UNLINKAT ScmpSyscall = SYS_UNLINKAT
SNR_SYMLINKAT ScmpSyscall = SYS_SYMLINKAT
SNR_LINKAT ScmpSyscall = SYS_LINKAT
SNR_UMOUNT2 ScmpSyscall = SYS_UMOUNT2
SNR_MOUNT ScmpSyscall = SYS_MOUNT
SNR_PIVOT_ROOT ScmpSyscall = SYS_PIVOT_ROOT
SNR_NFSSERVCTL ScmpSyscall = SYS_NFSSERVCTL
SNR_STATFS ScmpSyscall = SYS_STATFS
SNR_FSTATFS ScmpSyscall = SYS_FSTATFS
SNR_TRUNCATE ScmpSyscall = SYS_TRUNCATE
SNR_FTRUNCATE ScmpSyscall = SYS_FTRUNCATE
SNR_FALLOCATE ScmpSyscall = SYS_FALLOCATE
SNR_FACCESSAT ScmpSyscall = SYS_FACCESSAT
SNR_CHDIR ScmpSyscall = SYS_CHDIR
SNR_FCHDIR ScmpSyscall = SYS_FCHDIR
SNR_CHROOT ScmpSyscall = SYS_CHROOT
SNR_FCHMOD ScmpSyscall = SYS_FCHMOD
SNR_FCHMODAT ScmpSyscall = SYS_FCHMODAT
SNR_FCHOWNAT ScmpSyscall = SYS_FCHOWNAT
SNR_FCHOWN ScmpSyscall = SYS_FCHOWN
SNR_OPENAT ScmpSyscall = SYS_OPENAT
SNR_CLOSE ScmpSyscall = SYS_CLOSE
SNR_VHANGUP ScmpSyscall = SYS_VHANGUP
SNR_PIPE2 ScmpSyscall = SYS_PIPE2
SNR_QUOTACTL ScmpSyscall = SYS_QUOTACTL
SNR_GETDENTS64 ScmpSyscall = SYS_GETDENTS64
SNR_LSEEK ScmpSyscall = SYS_LSEEK
SNR_READ ScmpSyscall = SYS_READ
SNR_WRITE ScmpSyscall = SYS_WRITE
SNR_READV ScmpSyscall = SYS_READV
SNR_WRITEV ScmpSyscall = SYS_WRITEV
SNR_PREAD64 ScmpSyscall = SYS_PREAD64
SNR_PWRITE64 ScmpSyscall = SYS_PWRITE64
SNR_PREADV ScmpSyscall = SYS_PREADV
SNR_PWRITEV ScmpSyscall = SYS_PWRITEV
SNR_SENDFILE ScmpSyscall = SYS_SENDFILE
SNR_PSELECT6 ScmpSyscall = SYS_PSELECT6
SNR_PPOLL ScmpSyscall = SYS_PPOLL
SNR_SIGNALFD4 ScmpSyscall = SYS_SIGNALFD4
SNR_VMSPLICE ScmpSyscall = SYS_VMSPLICE
SNR_SPLICE ScmpSyscall = SYS_SPLICE
SNR_TEE ScmpSyscall = SYS_TEE
SNR_READLINKAT ScmpSyscall = SYS_READLINKAT
SNR_NEWFSTATAT ScmpSyscall = SYS_NEWFSTATAT
SNR_FSTAT ScmpSyscall = SYS_FSTAT
SNR_SYNC ScmpSyscall = SYS_SYNC
SNR_FSYNC ScmpSyscall = SYS_FSYNC
SNR_FDATASYNC ScmpSyscall = SYS_FDATASYNC
SNR_SYNC_FILE_RANGE ScmpSyscall = SYS_SYNC_FILE_RANGE
SNR_TIMERFD_CREATE ScmpSyscall = SYS_TIMERFD_CREATE
SNR_TIMERFD_SETTIME ScmpSyscall = SYS_TIMERFD_SETTIME
SNR_TIMERFD_GETTIME ScmpSyscall = SYS_TIMERFD_GETTIME
SNR_UTIMENSAT ScmpSyscall = SYS_UTIMENSAT
SNR_ACCT ScmpSyscall = SYS_ACCT
SNR_CAPGET ScmpSyscall = SYS_CAPGET
SNR_CAPSET ScmpSyscall = SYS_CAPSET
SNR_PERSONALITY ScmpSyscall = SYS_PERSONALITY
SNR_EXIT ScmpSyscall = SYS_EXIT
SNR_EXIT_GROUP ScmpSyscall = SYS_EXIT_GROUP
SNR_WAITID ScmpSyscall = SYS_WAITID
SNR_SET_TID_ADDRESS ScmpSyscall = SYS_SET_TID_ADDRESS
SNR_UNSHARE ScmpSyscall = SYS_UNSHARE
SNR_FUTEX ScmpSyscall = SYS_FUTEX
SNR_SET_ROBUST_LIST ScmpSyscall = SYS_SET_ROBUST_LIST
SNR_GET_ROBUST_LIST ScmpSyscall = SYS_GET_ROBUST_LIST
SNR_NANOSLEEP ScmpSyscall = SYS_NANOSLEEP
SNR_GETITIMER ScmpSyscall = SYS_GETITIMER
SNR_SETITIMER ScmpSyscall = SYS_SETITIMER
SNR_KEXEC_LOAD ScmpSyscall = SYS_KEXEC_LOAD
SNR_INIT_MODULE ScmpSyscall = SYS_INIT_MODULE
SNR_DELETE_MODULE ScmpSyscall = SYS_DELETE_MODULE
SNR_TIMER_CREATE ScmpSyscall = SYS_TIMER_CREATE
SNR_TIMER_GETTIME ScmpSyscall = SYS_TIMER_GETTIME
SNR_TIMER_GETOVERRUN ScmpSyscall = SYS_TIMER_GETOVERRUN
SNR_TIMER_SETTIME ScmpSyscall = SYS_TIMER_SETTIME
SNR_TIMER_DELETE ScmpSyscall = SYS_TIMER_DELETE
SNR_CLOCK_SETTIME ScmpSyscall = SYS_CLOCK_SETTIME
SNR_CLOCK_GETTIME ScmpSyscall = SYS_CLOCK_GETTIME
SNR_CLOCK_GETRES ScmpSyscall = SYS_CLOCK_GETRES
SNR_CLOCK_NANOSLEEP ScmpSyscall = SYS_CLOCK_NANOSLEEP
SNR_SYSLOG ScmpSyscall = SYS_SYSLOG
SNR_PTRACE ScmpSyscall = SYS_PTRACE
SNR_SCHED_SETPARAM ScmpSyscall = SYS_SCHED_SETPARAM
SNR_SCHED_SETSCHEDULER ScmpSyscall = SYS_SCHED_SETSCHEDULER
SNR_SCHED_GETSCHEDULER ScmpSyscall = SYS_SCHED_GETSCHEDULER
SNR_SCHED_GETPARAM ScmpSyscall = SYS_SCHED_GETPARAM
SNR_SCHED_SETAFFINITY ScmpSyscall = SYS_SCHED_SETAFFINITY
SNR_SCHED_GETAFFINITY ScmpSyscall = SYS_SCHED_GETAFFINITY
SNR_SCHED_YIELD ScmpSyscall = SYS_SCHED_YIELD
SNR_SCHED_GET_PRIORITY_MAX ScmpSyscall = SYS_SCHED_GET_PRIORITY_MAX
SNR_SCHED_GET_PRIORITY_MIN ScmpSyscall = SYS_SCHED_GET_PRIORITY_MIN
SNR_SCHED_RR_GET_INTERVAL ScmpSyscall = SYS_SCHED_RR_GET_INTERVAL
SNR_RESTART_SYSCALL ScmpSyscall = SYS_RESTART_SYSCALL
SNR_KILL ScmpSyscall = SYS_KILL
SNR_TKILL ScmpSyscall = SYS_TKILL
SNR_TGKILL ScmpSyscall = SYS_TGKILL
SNR_SIGALTSTACK ScmpSyscall = SYS_SIGALTSTACK
SNR_RT_SIGSUSPEND ScmpSyscall = SYS_RT_SIGSUSPEND
SNR_RT_SIGACTION ScmpSyscall = SYS_RT_SIGACTION
SNR_RT_SIGPROCMASK ScmpSyscall = SYS_RT_SIGPROCMASK
SNR_RT_SIGPENDING ScmpSyscall = SYS_RT_SIGPENDING
SNR_RT_SIGTIMEDWAIT ScmpSyscall = SYS_RT_SIGTIMEDWAIT
SNR_RT_SIGQUEUEINFO ScmpSyscall = SYS_RT_SIGQUEUEINFO
SNR_RT_SIGRETURN ScmpSyscall = SYS_RT_SIGRETURN
SNR_SETPRIORITY ScmpSyscall = SYS_SETPRIORITY
SNR_GETPRIORITY ScmpSyscall = SYS_GETPRIORITY
SNR_REBOOT ScmpSyscall = SYS_REBOOT
SNR_SETREGID ScmpSyscall = SYS_SETREGID
SNR_SETGID ScmpSyscall = SYS_SETGID
SNR_SETREUID ScmpSyscall = SYS_SETREUID
SNR_SETUID ScmpSyscall = SYS_SETUID
SNR_SETRESUID ScmpSyscall = SYS_SETRESUID
SNR_GETRESUID ScmpSyscall = SYS_GETRESUID
SNR_SETRESGID ScmpSyscall = SYS_SETRESGID
SNR_GETRESGID ScmpSyscall = SYS_GETRESGID
SNR_SETFSUID ScmpSyscall = SYS_SETFSUID
SNR_SETFSGID ScmpSyscall = SYS_SETFSGID
SNR_TIMES ScmpSyscall = SYS_TIMES
SNR_SETPGID ScmpSyscall = SYS_SETPGID
SNR_GETPGID ScmpSyscall = SYS_GETPGID
SNR_GETSID ScmpSyscall = SYS_GETSID
SNR_SETSID ScmpSyscall = SYS_SETSID
SNR_GETGROUPS ScmpSyscall = SYS_GETGROUPS
SNR_SETGROUPS ScmpSyscall = SYS_SETGROUPS
SNR_UNAME ScmpSyscall = SYS_UNAME
SNR_SETHOSTNAME ScmpSyscall = SYS_SETHOSTNAME
SNR_SETDOMAINNAME ScmpSyscall = SYS_SETDOMAINNAME
SNR_GETRLIMIT ScmpSyscall = SYS_GETRLIMIT
SNR_SETRLIMIT ScmpSyscall = SYS_SETRLIMIT
SNR_GETRUSAGE ScmpSyscall = SYS_GETRUSAGE
SNR_UMASK ScmpSyscall = SYS_UMASK
SNR_PRCTL ScmpSyscall = SYS_PRCTL
SNR_GETCPU ScmpSyscall = SYS_GETCPU
SNR_GETTIMEOFDAY ScmpSyscall = SYS_GETTIMEOFDAY
SNR_SETTIMEOFDAY ScmpSyscall = SYS_SETTIMEOFDAY
SNR_ADJTIMEX ScmpSyscall = SYS_ADJTIMEX
SNR_GETPID ScmpSyscall = SYS_GETPID
SNR_GETPPID ScmpSyscall = SYS_GETPPID
SNR_GETUID ScmpSyscall = SYS_GETUID
SNR_GETEUID ScmpSyscall = SYS_GETEUID
SNR_GETGID ScmpSyscall = SYS_GETGID
SNR_GETEGID ScmpSyscall = SYS_GETEGID
SNR_GETTID ScmpSyscall = SYS_GETTID
SNR_SYSINFO ScmpSyscall = SYS_SYSINFO
SNR_MQ_OPEN ScmpSyscall = SYS_MQ_OPEN
SNR_MQ_UNLINK ScmpSyscall = SYS_MQ_UNLINK
SNR_MQ_TIMEDSEND ScmpSyscall = SYS_MQ_TIMEDSEND
SNR_MQ_TIMEDRECEIVE ScmpSyscall = SYS_MQ_TIMEDRECEIVE
SNR_MQ_NOTIFY ScmpSyscall = SYS_MQ_NOTIFY
SNR_MQ_GETSETATTR ScmpSyscall = SYS_MQ_GETSETATTR
SNR_MSGGET ScmpSyscall = SYS_MSGGET
SNR_MSGCTL ScmpSyscall = SYS_MSGCTL
SNR_MSGRCV ScmpSyscall = SYS_MSGRCV
SNR_MSGSND ScmpSyscall = SYS_MSGSND
SNR_SEMGET ScmpSyscall = SYS_SEMGET
SNR_SEMCTL ScmpSyscall = SYS_SEMCTL
SNR_SEMTIMEDOP ScmpSyscall = SYS_SEMTIMEDOP
SNR_SEMOP ScmpSyscall = SYS_SEMOP
SNR_SHMGET ScmpSyscall = SYS_SHMGET
SNR_SHMCTL ScmpSyscall = SYS_SHMCTL
SNR_SHMAT ScmpSyscall = SYS_SHMAT
SNR_SHMDT ScmpSyscall = SYS_SHMDT
SNR_SOCKET ScmpSyscall = SYS_SOCKET
SNR_SOCKETPAIR ScmpSyscall = SYS_SOCKETPAIR
SNR_BIND ScmpSyscall = SYS_BIND
SNR_LISTEN ScmpSyscall = SYS_LISTEN
SNR_ACCEPT ScmpSyscall = SYS_ACCEPT
SNR_CONNECT ScmpSyscall = SYS_CONNECT
SNR_GETSOCKNAME ScmpSyscall = SYS_GETSOCKNAME
SNR_GETPEERNAME ScmpSyscall = SYS_GETPEERNAME
SNR_SENDTO ScmpSyscall = SYS_SENDTO
SNR_RECVFROM ScmpSyscall = SYS_RECVFROM
SNR_SETSOCKOPT ScmpSyscall = SYS_SETSOCKOPT
SNR_GETSOCKOPT ScmpSyscall = SYS_GETSOCKOPT
SNR_SHUTDOWN ScmpSyscall = SYS_SHUTDOWN
SNR_SENDMSG ScmpSyscall = SYS_SENDMSG
SNR_RECVMSG ScmpSyscall = SYS_RECVMSG
SNR_READAHEAD ScmpSyscall = SYS_READAHEAD
SNR_BRK ScmpSyscall = SYS_BRK
SNR_MUNMAP ScmpSyscall = SYS_MUNMAP
SNR_MREMAP ScmpSyscall = SYS_MREMAP
SNR_ADD_KEY ScmpSyscall = SYS_ADD_KEY
SNR_REQUEST_KEY ScmpSyscall = SYS_REQUEST_KEY
SNR_KEYCTL ScmpSyscall = SYS_KEYCTL
SNR_CLONE ScmpSyscall = SYS_CLONE
SNR_EXECVE ScmpSyscall = SYS_EXECVE
SNR_MMAP ScmpSyscall = SYS_MMAP
SNR_FADVISE64 ScmpSyscall = SYS_FADVISE64
SNR_SWAPON ScmpSyscall = SYS_SWAPON
SNR_SWAPOFF ScmpSyscall = SYS_SWAPOFF
SNR_MPROTECT ScmpSyscall = SYS_MPROTECT
SNR_MSYNC ScmpSyscall = SYS_MSYNC
SNR_MLOCK ScmpSyscall = SYS_MLOCK
SNR_MUNLOCK ScmpSyscall = SYS_MUNLOCK
SNR_MLOCKALL ScmpSyscall = SYS_MLOCKALL
SNR_MUNLOCKALL ScmpSyscall = SYS_MUNLOCKALL
SNR_MINCORE ScmpSyscall = SYS_MINCORE
SNR_MADVISE ScmpSyscall = SYS_MADVISE
SNR_REMAP_FILE_PAGES ScmpSyscall = SYS_REMAP_FILE_PAGES
SNR_MBIND ScmpSyscall = SYS_MBIND
SNR_GET_MEMPOLICY ScmpSyscall = SYS_GET_MEMPOLICY
SNR_SET_MEMPOLICY ScmpSyscall = SYS_SET_MEMPOLICY
SNR_MIGRATE_PAGES ScmpSyscall = SYS_MIGRATE_PAGES
SNR_MOVE_PAGES ScmpSyscall = SYS_MOVE_PAGES
SNR_RT_TGSIGQUEUEINFO ScmpSyscall = SYS_RT_TGSIGQUEUEINFO
SNR_PERF_EVENT_OPEN ScmpSyscall = SYS_PERF_EVENT_OPEN
SNR_ACCEPT4 ScmpSyscall = SYS_ACCEPT4
SNR_RECVMMSG ScmpSyscall = SYS_RECVMMSG
SNR_WAIT4 ScmpSyscall = SYS_WAIT4
SNR_PRLIMIT64 ScmpSyscall = SYS_PRLIMIT64
SNR_FANOTIFY_INIT ScmpSyscall = SYS_FANOTIFY_INIT
SNR_FANOTIFY_MARK ScmpSyscall = SYS_FANOTIFY_MARK
SNR_NAME_TO_HANDLE_AT ScmpSyscall = SYS_NAME_TO_HANDLE_AT
SNR_OPEN_BY_HANDLE_AT ScmpSyscall = SYS_OPEN_BY_HANDLE_AT
SNR_CLOCK_ADJTIME ScmpSyscall = SYS_CLOCK_ADJTIME
SNR_SYNCFS ScmpSyscall = SYS_SYNCFS
SNR_SETNS ScmpSyscall = SYS_SETNS
SNR_SENDMMSG ScmpSyscall = SYS_SENDMMSG
SNR_PROCESS_VM_READV ScmpSyscall = SYS_PROCESS_VM_READV
SNR_PROCESS_VM_WRITEV ScmpSyscall = SYS_PROCESS_VM_WRITEV
SNR_KCMP ScmpSyscall = SYS_KCMP
SNR_FINIT_MODULE ScmpSyscall = SYS_FINIT_MODULE
SNR_SCHED_SETATTR ScmpSyscall = SYS_SCHED_SETATTR
SNR_SCHED_GETATTR ScmpSyscall = SYS_SCHED_GETATTR
SNR_RENAMEAT2 ScmpSyscall = SYS_RENAMEAT2
SNR_SECCOMP ScmpSyscall = SYS_SECCOMP
SNR_GETRANDOM ScmpSyscall = SYS_GETRANDOM
SNR_MEMFD_CREATE ScmpSyscall = SYS_MEMFD_CREATE
SNR_BPF ScmpSyscall = SYS_BPF
SNR_EXECVEAT ScmpSyscall = SYS_EXECVEAT
SNR_USERFAULTFD ScmpSyscall = SYS_USERFAULTFD
SNR_MEMBARRIER ScmpSyscall = SYS_MEMBARRIER
SNR_MLOCK2 ScmpSyscall = SYS_MLOCK2
SNR_COPY_FILE_RANGE ScmpSyscall = SYS_COPY_FILE_RANGE
SNR_PREADV2 ScmpSyscall = SYS_PREADV2
SNR_PWRITEV2 ScmpSyscall = SYS_PWRITEV2
SNR_PKEY_MPROTECT ScmpSyscall = SYS_PKEY_MPROTECT
SNR_PKEY_ALLOC ScmpSyscall = SYS_PKEY_ALLOC
SNR_PKEY_FREE ScmpSyscall = SYS_PKEY_FREE
SNR_STATX ScmpSyscall = SYS_STATX
SNR_IO_PGETEVENTS ScmpSyscall = SYS_IO_PGETEVENTS
SNR_RSEQ ScmpSyscall = SYS_RSEQ
SNR_KEXEC_FILE_LOAD ScmpSyscall = SYS_KEXEC_FILE_LOAD
SNR_PIDFD_SEND_SIGNAL ScmpSyscall = SYS_PIDFD_SEND_SIGNAL
SNR_IO_URING_SETUP ScmpSyscall = SYS_IO_URING_SETUP
SNR_IO_URING_ENTER ScmpSyscall = SYS_IO_URING_ENTER
SNR_IO_URING_REGISTER ScmpSyscall = SYS_IO_URING_REGISTER
SNR_OPEN_TREE ScmpSyscall = SYS_OPEN_TREE
SNR_MOVE_MOUNT ScmpSyscall = SYS_MOVE_MOUNT
SNR_FSOPEN ScmpSyscall = SYS_FSOPEN
SNR_FSCONFIG ScmpSyscall = SYS_FSCONFIG
SNR_FSMOUNT ScmpSyscall = SYS_FSMOUNT
SNR_FSPICK ScmpSyscall = SYS_FSPICK
SNR_PIDFD_OPEN ScmpSyscall = SYS_PIDFD_OPEN
SNR_CLONE3 ScmpSyscall = SYS_CLONE3
SNR_CLOSE_RANGE ScmpSyscall = SYS_CLOSE_RANGE
SNR_OPENAT2 ScmpSyscall = SYS_OPENAT2
SNR_PIDFD_GETFD ScmpSyscall = SYS_PIDFD_GETFD
SNR_FACCESSAT2 ScmpSyscall = SYS_FACCESSAT2
SNR_PROCESS_MADVISE ScmpSyscall = SYS_PROCESS_MADVISE
SNR_EPOLL_PWAIT2 ScmpSyscall = SYS_EPOLL_PWAIT2
SNR_MOUNT_SETATTR ScmpSyscall = SYS_MOUNT_SETATTR
SNR_QUOTACTL_FD ScmpSyscall = SYS_QUOTACTL_FD
SNR_LANDLOCK_CREATE_RULESET ScmpSyscall = SYS_LANDLOCK_CREATE_RULESET
SNR_LANDLOCK_ADD_RULE ScmpSyscall = SYS_LANDLOCK_ADD_RULE
SNR_LANDLOCK_RESTRICT_SELF ScmpSyscall = SYS_LANDLOCK_RESTRICT_SELF
SNR_MEMFD_SECRET ScmpSyscall = SYS_MEMFD_SECRET
SNR_PROCESS_MRELEASE ScmpSyscall = SYS_PROCESS_MRELEASE
SNR_FUTEX_WAITV ScmpSyscall = SYS_FUTEX_WAITV
SNR_SET_MEMPOLICY_HOME_NODE ScmpSyscall = SYS_SET_MEMPOLICY_HOME_NODE
SNR_CACHESTAT ScmpSyscall = SYS_CACHESTAT
SNR_FCHMODAT2 ScmpSyscall = SYS_FCHMODAT2
SNR_MAP_SHADOW_STACK ScmpSyscall = SYS_MAP_SHADOW_STACK
SNR_FUTEX_WAKE ScmpSyscall = SYS_FUTEX_WAKE
SNR_FUTEX_WAIT ScmpSyscall = SYS_FUTEX_WAIT
SNR_FUTEX_REQUEUE ScmpSyscall = SYS_FUTEX_REQUEUE
SNR_STATMOUNT ScmpSyscall = SYS_STATMOUNT
SNR_LISTMOUNT ScmpSyscall = SYS_LISTMOUNT
SNR_LSM_GET_SELF_ATTR ScmpSyscall = SYS_LSM_GET_SELF_ATTR
SNR_LSM_SET_SELF_ATTR ScmpSyscall = SYS_LSM_SET_SELF_ATTR
SNR_LSM_LIST_MODULES ScmpSyscall = SYS_LSM_LIST_MODULES
SNR_MSEAL ScmpSyscall = SYS_MSEAL
SNR_SETXATTRAT ScmpSyscall = SYS_SETXATTRAT
SNR_GETXATTRAT ScmpSyscall = SYS_GETXATTRAT
SNR_LISTXATTRAT ScmpSyscall = SYS_LISTXATTRAT
SNR_REMOVEXATTRAT ScmpSyscall = SYS_REMOVEXATTRAT
SNR_OPEN_TREE_ATTR ScmpSyscall = SYS_OPEN_TREE_ATTR
SNR_FILE_GETATTR ScmpSyscall = SYS_FILE_GETATTR
SNR_FILE_SETATTR ScmpSyscall = SYS_FILE_SETATTR
)

8
container/std/types.go Normal file
View File

@@ -0,0 +1,8 @@
package std
type (
// Uint is equivalent to C.uint.
Uint uint32
// Int is equivalent to C.int.
Int int32
)

View File

@@ -3,6 +3,8 @@ package container
import ( import (
. "syscall" . "syscall"
"unsafe" "unsafe"
"hakurei.app/container/std"
) )
// Prctl manipulates various aspects of the behavior of the calling thread or process. // Prctl manipulates various aspects of the behavior of the calling thread or process.
@@ -41,6 +43,49 @@ func Isatty(fd int) bool {
return r == 0 return r == 0
} }
// include/uapi/linux/sched.h
const (
SCHED_NORMAL = iota
SCHED_FIFO
SCHED_RR
SCHED_BATCH
_ // SCHED_ISO: reserved but not implemented yet
SCHED_IDLE
SCHED_DEADLINE
SCHED_EXT
)
// schedParam is equivalent to struct sched_param from include/linux/sched.h.
type schedParam struct {
// sched_priority
priority std.Int
}
// schedSetscheduler sets both the scheduling policy and parameters for the
// thread whose ID is specified in tid. If tid equals zero, the scheduling
// policy and parameters of the calling thread will be set.
//
// This function is unexported because it is [very subtle to use correctly]. The
// function signature in libc is misleading: pid actually refers to a thread ID.
// The glibc wrapper for this system call ignores this semantic and exposes
// this counterintuitive behaviour.
//
// This function is only called from the container setup thread. Do not reuse
// this if you do not have something similar in place!
//
// [very subtle to use correctly]: https://www.openwall.com/lists/musl/2016/03/01/4
func schedSetscheduler(tid, policy int, param *schedParam) error {
if r, _, errno := Syscall(
SYS_SCHED_SETSCHEDULER,
uintptr(tid),
uintptr(policy),
uintptr(unsafe.Pointer(param)),
); r < 0 {
return errno
}
return nil
}
// IgnoringEINTR makes a function call and repeats it if it returns an // IgnoringEINTR makes a function call and repeats it if it returns an
// EINTR error. This appears to be required even though we install all // EINTR error. This appears to be required even though we install all
// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846. // signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.

View File

@@ -2,6 +2,8 @@ package vfs
import "strings" import "strings"
// Unmangle reverses mangling of strings done by the kernel. Its behaviour is
// consistent with the equivalent function in util-linux.
func Unmangle(s string) string { func Unmangle(s string) string {
if !strings.ContainsRune(s, '\\') { if !strings.ContainsRune(s, '\\') {
return s return s

View File

@@ -24,6 +24,7 @@ var (
ErrMountInfoSep = errors.New("bad optional fields separator") ErrMountInfoSep = errors.New("bad optional fields separator")
) )
// A DecoderError describes a nonrecoverable error decoding a mountinfo stream.
type DecoderError struct { type DecoderError struct {
Op string Op string
Line int Line int
@@ -51,7 +52,8 @@ func (e *DecoderError) Error() string {
} }
type ( type (
// A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream. // A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from
// an input stream.
MountInfoDecoder struct { MountInfoDecoder struct {
s *bufio.Scanner s *bufio.Scanner
m *MountInfo m *MountInfo
@@ -72,13 +74,16 @@ type (
MountInfoEntry struct { MountInfoEntry struct {
// mount ID: a unique ID for the mount (may be reused after umount(2)). // mount ID: a unique ID for the mount (may be reused after umount(2)).
ID int `json:"id"` ID int `json:"id"`
// parent ID: the ID of the parent mount (or of self for the root of this mount namespace's mount tree). // parent ID: the ID of the parent mount (or of self for the root of
// this mount namespace's mount tree).
Parent int `json:"parent"` Parent int `json:"parent"`
// major:minor: the value of st_dev for files on this filesystem (see stat(2)). // major:minor: the value of st_dev for files on this filesystem (see stat(2)).
Devno DevT `json:"devno"` Devno DevT `json:"devno"`
// root: the pathname of the directory in the filesystem which forms the root of this mount. // root: the pathname of the directory in the filesystem which forms the
// root of this mount.
Root string `json:"root"` Root string `json:"root"`
// mount point: the pathname of the mount point relative to the process's root directory. // mount point: the pathname of the mount point relative to the
// process's root directory.
Target string `json:"target"` Target string `json:"target"`
// mount options: per-mount options (see mount(2)). // mount options: per-mount options (see mount(2)).
VfsOptstr string `json:"vfs_optstr"` VfsOptstr string `json:"vfs_optstr"`
@@ -126,7 +131,8 @@ func (e *MountInfoEntry) Flags() (flags uintptr, unmatched []string) {
// NewMountInfoDecoder returns a new decoder that reads from r. // NewMountInfoDecoder returns a new decoder that reads from r.
// //
// The decoder introduces its own buffering and may read data from r beyond the mountinfo entries requested. // The decoder introduces its own buffering and may read data from r beyond the
// mountinfo entries requested.
func NewMountInfoDecoder(r io.Reader) *MountInfoDecoder { func NewMountInfoDecoder(r io.Reader) *MountInfoDecoder {
return &MountInfoDecoder{s: bufio.NewScanner(r)} return &MountInfoDecoder{s: bufio.NewScanner(r)}
} }
@@ -271,6 +277,8 @@ func parseMountInfoLine(s string, ent *MountInfoEntry) error {
return nil return nil
} }
// EqualWithIgnore compares to [MountInfoEntry] values, ignoring fields that
// compare equal to ignore.
func (e *MountInfoEntry) EqualWithIgnore(want *MountInfoEntry, ignore string) bool { func (e *MountInfoEntry) EqualWithIgnore(want *MountInfoEntry, ignore string) bool {
return (e.ID == want.ID || want.ID == -1) && return (e.ID == want.ID || want.ID == -1) &&
(e.Parent == want.Parent || want.Parent == -1) && (e.Parent == want.Parent || want.Parent == -1) &&
@@ -284,6 +292,8 @@ func (e *MountInfoEntry) EqualWithIgnore(want *MountInfoEntry, ignore string) bo
(e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore) (e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore)
} }
// String returns a user-facing representation of a [MountInfoEntry]. It fits
// roughly into the mountinfo format, but without mangling.
func (e *MountInfoEntry) String() string { func (e *MountInfoEntry) String() string {
return fmt.Sprintf("%d %d %d:%d %s %s %s %s %s %s %s", return fmt.Sprintf("%d %d %d:%d %s %s %s %s %s %s %s",
e.ID, e.Parent, e.Devno[0], e.Devno[1], e.Root, e.Target, e.VfsOptstr, e.ID, e.Parent, e.Devno[0], e.Devno[1], e.Root, e.Target, e.VfsOptstr,

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
) )
// UnfoldTargetError is a pathname that never appeared in a mount hierarchy.
type UnfoldTargetError string type UnfoldTargetError string
func (e UnfoldTargetError) Error() string { func (e UnfoldTargetError) Error() string {
@@ -27,6 +28,7 @@ func (n *MountInfoNode) Collective() iter.Seq[*MountInfoNode] {
return func(yield func(*MountInfoNode) bool) { n.visit(yield) } return func(yield func(*MountInfoNode) bool) { n.visit(yield) }
} }
// visit recursively visits all visible mountinfo nodes.
func (n *MountInfoNode) visit(yield func(*MountInfoNode) bool) bool { func (n *MountInfoNode) visit(yield func(*MountInfoNode) bool) bool {
if !n.Covered && !yield(n) { if !n.Covered && !yield(n) {
return false return false

12
dist/install.sh vendored
View File

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

23
dist/release.sh vendored
View File

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

16
flake.lock generated
View File

@@ -7,32 +7,32 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1756679287, "lastModified": 1765384171,
"narHash": "sha256-Xd1vOeY9ccDf5VtVK12yM0FS6qqvfUop8UQlxEB+gTQ=", "narHash": "sha256-FuFtkJrW1Z7u+3lhzPRau69E0CNjADku1mLQQflUORo=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "07fc025fe10487dd80f2ec694f1cd790e752d0e8", "rev": "44777152652bc9eacf8876976fa72cc77ca8b9d8",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-community", "owner": "nix-community",
"ref": "release-25.05", "ref": "release-25.11",
"repo": "home-manager", "repo": "home-manager",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1757020766, "lastModified": 1765311797,
"narHash": "sha256-PLoSjHRa2bUbi1x9HoXgTx2AiuzNXs54c8omhadyvp0=", "narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "fe83bbdde2ccdc2cb9573aa846abe8363f79a97a", "rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-25.05", "ref": "nixos-25.11",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View File

@@ -2,10 +2,10 @@
description = "hakurei container tool and nixos module"; description = "hakurei container tool and nixos module";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
home-manager = { home-manager = {
url = "github:nix-community/home-manager/release-25.05"; url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
}; };
@@ -29,20 +29,6 @@
{ {
nixosModules.hakurei = import ./nixos.nix self.packages; nixosModules.hakurei = import ./nixos.nix self.packages;
buildPackage = forAllSystems (
system:
nixpkgsFor.${system}.callPackage (
import ./cmd/hpkg/build.nix {
inherit
nixpkgsFor
system
nixpkgs
home-manager
;
}
)
);
checks = forAllSystems ( checks = forAllSystems (
system: system:
let let
@@ -69,7 +55,7 @@
withRace = true; withRace = true;
}; };
hpkg = callPackage ./cmd/hpkg/test { inherit system self; }; sharefs = callPackage ./cmd/sharefs/test { inherit system self; };
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } '' formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
cd ${./.} cd ${./.}
@@ -125,31 +111,38 @@
glibc glibc
xdg-dbus-proxy xdg-dbus-proxy
# hpkg
zstd
gnutar
coreutils
# for check # for check
util-linux util-linux
nettools nettools
; ;
}; };
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; }; hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
sharefs = pkgs.linkFarm "sharefs" {
"bin/sharefs" = "${hakurei}/libexec/sharefs";
"bin/mount.fuse.sharefs" = "${hakurei}/libexec/sharefs";
};
dist = pkgs.runCommand "${hakurei.name}-dist" { buildInputs = hakurei.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } '' dist =
# go requires XDG_CACHE_HOME for the build cache pkgs.runCommand "${hakurei.name}-dist"
export XDG_CACHE_HOME="$(mktemp -d)" {
buildInputs = hakurei.targetPkgs ++ [
pkgs.pkgsStatic.musl
];
}
''
cd $(mktemp -d) \
&& cp -r ${hakurei.src}/. . \
&& chmod +w cmd && cp -r ${hsu.src}/. cmd/hsu/ \
&& chmod -R +w .
# get a different workdir as go does not like /build CC="musl-clang -O3 -Werror -Qunused-arguments" \
cd $(mktemp -d) \ GOCACHE="$(mktemp -d)" \
&& cp -r ${hakurei.src}/. . \ HAKUREI_TEST_SKIP_ACL=1 \
&& chmod +w cmd && cp -r ${hsu.src}/. cmd/hsu/ \ PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \
&& chmod -R +w . DESTDIR="$out" \
HAKUREI_VERSION="v${hakurei.version}" \
export HAKUREI_VERSION="v${hakurei.version}" ./dist/release.sh
CC="clang -O3 -Werror" ./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out '';
'';
} }
); );
@@ -160,7 +153,10 @@
pkgs = nixpkgsFor.${system}; pkgs = nixpkgsFor.${system};
in in
{ {
default = pkgs.mkShell { buildInputs = hakurei.targetPkgs; }; default = pkgs.mkShell {
buildInputs = hakurei.targetPkgs;
hardeningDisable = [ "fortify" ];
};
withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; }; withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
vm = vm =
@@ -185,13 +181,13 @@
hakurei = hakurei =
let let
# this is used for interactive vm testing during development, where tests might be broken # this is used for interactive vm testing during development, where tests might be broken
package = self.packages.${pkgs.system}.hakurei.override { package = self.packages.${pkgs.stdenv.hostPlatform.system}.hakurei.override {
buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; }); buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; });
}; };
in in
{ {
inherit package; inherit package;
hsuPackage = self.packages.${pkgs.system}.hsu.override { hakurei = package; }; hsuPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.hsu.override { hakurei = package; };
}; };
}; };
} }
@@ -202,7 +198,7 @@
./test/interactive/trace.nix ./test/interactive/trace.nix
self.nixosModules.hakurei self.nixosModules.hakurei
self.inputs.home-manager.nixosModules.home-manager home-manager.nixosModules.home-manager
]; ];
}; };
in in

2
go.mod
View File

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

View File

@@ -30,12 +30,46 @@ type Config struct {
// This option is unsupported and most likely enables full control over the Wayland // This option is unsupported and most likely enables full control over the Wayland
// session. Do not set this to true unless you are sure you know what you are doing. // session. Do not set this to true unless you are sure you know what you are doing.
DirectWayland bool `json:"direct_wayland,omitempty"` DirectWayland bool `json:"direct_wayland,omitempty"`
// Direct access to the PipeWire socket established via SecurityContext::Create, no
// attempt is made to start the pipewire-pulse server.
//
// The SecurityContext machinery is fatally flawed, it blindly sets read and execute
// bits on all objects for clients with the lowest achievable privilege level (by
// setting PW_KEY_ACCESS to "restricted"). This enables them to call any method
// targeting any object, and since Registry::Destroy checks for the read and execute bit,
// allows the destruction of any object other than PW_ID_CORE as well. This behaviour
// is implemented separately in media-session and wireplumber, with the wireplumber
// implementation in Lua via an embedded Lua vm. In all known setups, wireplumber is
// in use, and there is no known way to change its behaviour and set permissions
// differently without replacing the Lua script. Also, since PipeWire relies on these
// permissions to work, reducing them is not possible.
//
// Currently, the only other sandboxed use case is flatpak, which is not aware of
// PipeWire and blindly exposes the bare PulseAudio socket to the container (behaves
// like DirectPulse). This socket is backed by the pipewire-pulse compatibility daemon,
// which obtains client pid via the SO_PEERCRED option. The PipeWire daemon, pipewire-pulse
// daemon and the session manager daemon then separately performs the /.flatpak-info hack
// described in https://git.gensokyo.uk/security/hakurei/issues/21. Under such use case,
// since the client has no direct access to PipeWire, insecure parts of the protocol are
// obscured by pipewire-pulse simply not implementing them, and thus hiding the flaws
// described above.
//
// Hakurei does not rely on the /.flatpak-info hack. Instead, a socket is sets up via
// SecurityContext. A pipewire-pulse server connected through it achieves the same
// permissions as flatpak does via the /.flatpak-info hack and is maintained for the
// life of the container.
//
// This option is unsupported and enables a denial-of-service attack as the sandboxed
// client is able to destroy any client object and thus disconnecting them from PipeWire,
// or destroy the SecurityContext object preventing any further container creation.
// Do not set this to true, it is insecure under any configuration.
DirectPipeWire bool `json:"direct_pipewire,omitempty"`
// Direct access to PulseAudio socket, no attempt is made to establish pipewire-pulse // Direct access to PulseAudio socket, no attempt is made to establish pipewire-pulse
// server via a PipeWire socket with a SecurityContext attached and the bare socket // server via a PipeWire socket with a SecurityContext attached and the bare socket
// is made available to the container. // is made available to the container.
// //
// This option is unsupported and enables arbitrary code execution as the PulseAudio // This option is unsupported and enables arbitrary code execution as the PulseAudio
// server. Do not set this to true, this is insecure under any configuration. // server. Do not set this to true, it is insecure under any configuration.
DirectPulse bool `json:"direct_pulse,omitempty"` DirectPulse bool `json:"direct_pulse,omitempty"`
// Extra acl updates to perform before setuid. // Extra acl updates to perform before setuid.

View File

@@ -24,9 +24,8 @@ var (
) )
func TestUpdate(t *testing.T) { func TestUpdate(t *testing.T) {
if os.Getenv("GO_TEST_SKIP_ACL") == "1" { if os.Getenv("HAKUREI_TEST_SKIP_ACL") == "1" {
t.Log("acl test skipped") t.Skip("acl test skipped")
t.SkipNow()
} }
testFilePath := path.Join(t.TempDir(), testFileName) testFilePath := path.Join(t.TempDir(), testFileName)
@@ -143,6 +142,7 @@ func (c *getFAclInvocation) run(name string) error {
} }
c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name) c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name)
c.cmd.Stderr = os.Stderr
scanErr := make(chan error, 1) scanErr := make(chan error, 1)
if p, err := c.cmd.StdoutPipe(); err != nil { if p, err := c.cmd.StdoutPipe(); err != nil {
@@ -254,7 +254,7 @@ func getfacl(t *testing.T, name string) []*getFAclResp {
t.Fatalf("getfacl: error = %v", err) t.Fatalf("getfacl: error = %v", err)
} }
if len(c.pe) != 0 { if len(c.pe) != 0 {
t.Errorf("errors encountered parsing getfacl output\n%s", errors.Join(c.pe...).Error()) t.Errorf("errors encountered parsing getfacl output\n%s", errors.Join(c.pe...))
} }
return c.val return c.val
} }

View File

69
internal/azalea/azalea.go Normal file
View File

@@ -0,0 +1,69 @@
//go:generate gocc -a azalea.bnf
package azalea
import (
"io"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"hakurei.app/container/check"
)
type Parser struct {
Generator
}
func NewParser(gen Generator) *Parser {
return &Parser{
Generator: gen,
}
}
func (p Parser) Initialise() {
}
func (p Parser) Consume(ns string, file io.Reader) error {
return nil
}
// ConsumeDir walks a directory and consumes all Azalea source files within it and all its subdirectories, as long as they end with the .az extension.
func (p Parser) ConsumeDir(dir *check.Absolute) error {
ds := dir.String()
return filepath.WalkDir(ds, func(path string, d fs.DirEntry, err error) (e error) {
if err != nil {
return err
}
if d.IsDir() || !strings.HasSuffix(d.Name(), ".az") {
return
}
rel, e := filepath.Rel(ds, path)
ns := strings.TrimSuffix(rel, ".az")
f, e := os.Open(path)
return p.Consume(ns, f)
})
}
// ConsumeAll consumes all provided readers as Azalea source code, each given the namespace `r%d` where `%d` is the index of the reader in the provided arguments.
func (p Parser) ConsumeAll(in ...io.Reader) error {
for i, r := range in {
err := p.Consume("r"+strconv.FormatInt(int64(i), 10), r)
if err != nil {
return err
}
}
return nil
}
// ConsumeStrings consumes all provided strings as Azalea source code, each given the namespace `s%d` where `%d` is the index of the string in the provided arugments.
func (p Parser) ConsumeStrings(in ...string) error {
for i, s := range in {
err := p.Consume("s"+strconv.FormatInt(int64(i), 10), strings.NewReader(s))
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,36 @@
package azalea
import (
"io"
)
type Generator interface {
Finalise() (error, io.Writer)
}
type JsonGenerator struct {
t any
}
func NewJsonGenerator[T any]() JsonGenerator {
t := new(T)
return JsonGenerator{
t,
}
}
func (j *JsonGenerator) Finalise() (error, io.Writer) {
}
type PkgIRGenerator struct {
}
func NewPkgIRGenerator() PkgIRGenerator {
return PkgIRGenerator{}
}
func (p *PkgIRGenerator) Finalise() (error, io.Writer) {
}

View File

@@ -8,7 +8,6 @@
package filelock package filelock
import ( import (
"errors"
"io/fs" "io/fs"
) )
@@ -74,10 +73,3 @@ func (lt lockType) String() string {
return "Unlock" return "Unlock"
} }
} }
// IsNotSupported returns a boolean indicating whether the error is known to
// report that a function is not supported (possibly for a specific input).
// It is satisfied by errors.ErrUnsupported as well as some syscall errors.
func IsNotSupported(err error) bool {
return errors.Is(err, errors.ErrUnsupported)
}

View File

@@ -14,7 +14,7 @@ import (
"testing" "testing"
"time" "time"
"hakurei.app/container" "hakurei.app/container/fhs"
"hakurei.app/internal/lockedfile/internal/filelock" "hakurei.app/internal/lockedfile/internal/filelock"
"hakurei.app/internal/lockedfile/internal/testexec" "hakurei.app/internal/lockedfile/internal/testexec"
) )
@@ -197,7 +197,7 @@ func TestLockNotDroppedByExecCommand(t *testing.T) {
// Some kinds of file locks are dropped when a duplicated or forked file // Some kinds of file locks are dropped when a duplicated or forked file
// descriptor is unlocked. Double-check that the approach used by os/exec does // descriptor is unlocked. Double-check that the approach used by os/exec does
// not accidentally drop locks. // not accidentally drop locks.
cmd := testexec.CommandContext(t, t.Context(), container.MustExecutable(nil), "-test.run=^$") cmd := testexec.CommandContext(t, t.Context(), fhs.ProcSelfExe, "-test.run=^$")
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
t.Fatalf("exec failed: %v", err) t.Fatalf("exec failed: %v", err)
} }

View File

@@ -94,6 +94,11 @@ func (f *File) Close() error {
err := closeFile(f.osFile.File) err := closeFile(f.osFile.File)
f.cleanup.Stop() f.cleanup.Stop()
// f may be dead at the moment after we access f.cleanup,
// so the cleanup can fire before Stop completes. Keep f
// alive while we call Stop. See the documentation for
// runtime.Cleanup.Stop.
runtime.KeepAlive(f)
return err return err
} }

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