3 Commits

Author SHA1 Message Date
mae
d89715c20b cmd/irdump: formatted disassembly 2026-02-08 21:06:22 +09:00
mae
c7ba7f2a31 cmd/irdump: basic disassembler 2026-02-08 21:06:22 +09:00
mae
5db302110f cmd/irdump: create cli 2026-02-08 21:06:22 +09:00
178 changed files with 4080 additions and 35375 deletions

View File

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

5
.github/workflows/README vendored Normal file
View File

@@ -0,0 +1,5 @@
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.

46
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
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-**

48
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
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

1
.gitignore vendored
View File

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

187
README.md
View File

@@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<a href="https://git.gensokyo.uk/rosa/hakurei"> <a href="https://git.gensokyo.uk/security/hakurei">
<picture> <picture>
<img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari"> <img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari">
</picture> </picture>
@@ -8,58 +8,171 @@
<p align="center"> <p align="center">
<a href="https://pkg.go.dev/hakurei.app"><img src="https://pkg.go.dev/badge/hakurei.app.svg" alt="Go Reference" /></a> <a href="https://pkg.go.dev/hakurei.app"><img src="https://pkg.go.dev/badge/hakurei.app.svg" alt="Go Reference" /></a>
<a href="https://git.gensokyo.uk/rosa/hakurei/actions"><img src="https://git.gensokyo.uk/rosa/hakurei/actions/workflows/test.yml/badge.svg?branch=staging&style=flat-square" alt="Gitea Workflow Status" /></a> <a href="https://git.gensokyo.uk/security/hakurei/actions"><img src="https://git.gensokyo.uk/security/hakurei/actions/workflows/test.yml/badge.svg?branch=staging&style=flat-square" alt="Gitea Workflow Status" /></a>
<br/> <br/>
<a href="https://git.gensokyo.uk/rosa/hakurei/releases"><img src="https://img.shields.io/gitea/v/release/rosa/hakurei?gitea_url=https%3A%2F%2Fgit.gensokyo.uk&color=purple" alt="Release" /></a> <a href="https://git.gensokyo.uk/security/hakurei/releases"><img src="https://img.shields.io/gitea/v/release/security/hakurei?gitea_url=https%3A%2F%2Fgit.gensokyo.uk&color=purple" alt="Release" /></a>
<a href="https://goreportcard.com/report/hakurei.app"><img src="https://goreportcard.com/badge/hakurei.app" alt="Go Report Card" /></a> <a href="https://goreportcard.com/report/hakurei.app"><img src="https://goreportcard.com/badge/hakurei.app" alt="Go Report Card" /></a>
<a href="https://hakurei.app"><img src="https://img.shields.io/website?url=https%3A%2F%2Fhakurei.app" alt="Website" /></a> <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 desktop applications as dedicated Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
subordinate users on the Linux kernel. It implements the application container It implements the application container of [planterette (WIP)](https://git.gensokyo.uk/security/planterette),
of [planterette (WIP)](https://git.gensokyo.uk/rosa/planterette), a a self-contained Android-like package manager with modern security features.
self-contained Android-like package manager with modern security features.
Interaction with hakurei happens entirely through structures described by ## NixOS Module usage
package [hst](https://pkg.go.dev/hakurei.app/hst). No native API is available
due to internal details of uid isolation.
## Notable Packages The NixOS module currently requires home-manager to configure subordinate users. Full module documentation can be found [here](options.md).
Package [container](https://pkg.go.dev/hakurei.app/container) is general purpose To use the module, import it into your configuration with
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).
Package [internal/pkg](https://pkg.go.dev/hakurei.app/internal/pkg) provides ```nix
infrastructure for hermetic builds. This replaces the legacy nix-based testing {
framework and serves as the build system of Rosa OS, currently developed under inputs = {
package [internal/rosa](https://pkg.go.dev/hakurei.app/internal/rosa). nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
## Dependencies hakurei = {
url = "git+https://git.gensokyo.uk/security/hakurei";
`container` depends on: # Optional but recommended to limit the size of your system closure.
inputs.nixpkgs.follows = "nixpkgs";
};
};
- [libseccomp](https://github.com/seccomp/libseccomp) to generate BPF programs. outputs = { self, nixpkgs, hakurei, ... }:
{
nixosConfigurations.hakurei = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
hakurei.nixosModules.hakurei
];
};
};
}
```
`cmd/hakurei` depends on: This adds the `environment.hakurei` option:
- [acl](https://savannah.nongnu.org/projects/acl/) to export sockets to ```nix
subordinate users. { pkgs, ... }:
- [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;
};
- [fuse](https://github.com/libfuse/libfuse) to implement the filesystem. commonPaths = [
{
src = "/sdcard";
write = true;
}
];
New dependencies will generally not be added. Patches adding new dependencies extraHomeConfig = {
are very likely to be rejected. home.stateVersion = "23.05";
};
## NixOS Module (deprecated) apps = {
"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 = { };
};
};
};
The NixOS module is in maintenance mode and will be removed once planterette is "org.claws_mail.Claws-Mail" = {
feature-complete. Full module documentation can be found [here](options.md). name = "claws-mail";
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;
};
};
};
};
}
```

View File

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

View File

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

View File

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

7
cmd/hpkg/README Normal file
View File

@@ -0,0 +1,7 @@
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.

173
cmd/hpkg/app.go Normal file
View File

@@ -0,0 +1,173 @@
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
}
}

256
cmd/hpkg/build.nix Normal file
View File

@@ -0,0 +1,256 @@
{
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"
'';
}

335
cmd/hpkg/main.go Normal file
View File

@@ -0,0 +1,335 @@
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")
}

117
cmd/hpkg/paths.go Normal file
View File

@@ -0,0 +1,117 @@
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}},
}...)
}

61
cmd/hpkg/proc.go Normal file
View File

@@ -0,0 +1,61 @@
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

@@ -0,0 +1,62 @@
{ 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";
};
};
}

34
cmd/hpkg/test/default.nix Normal file
View File

@@ -0,0 +1,34 @@
{
testers,
callPackage,
system,
self,
}:
let
buildPackage = self.buildPackage.${system};
in
testers.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;
}

48
cmd/hpkg/test/foot.nix Normal file
View File

@@ -0,0 +1,48 @@
{
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 "$@"
'';
}

110
cmd/hpkg/test/test.py Normal file
View File

@@ -0,0 +1,110 @@
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"))

130
cmd/hpkg/with.go Normal file
View File

@@ -0,0 +1,130 @@
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)
}

76
cmd/irdump/main.go Normal file
View File

@@ -0,0 +1,76 @@
package main
import (
"errors"
"log"
"os"
"hakurei.app/command"
"hakurei.app/internal/pkg"
)
func main() {
log.SetFlags(0)
log.SetPrefix("irdump: ")
var (
flagOutput string
flagReal bool
flagHeader bool
flagForce bool
flagRaw bool
)
c := command.New(os.Stderr, log.Printf, "irdump", func(args []string) (err error) {
var input *os.File
if len(args) != 1 {
return errors.New("irdump requires 1 argument")
}
if input, err = os.Open(args[0]); err != nil {
return
}
defer input.Close()
var output *os.File
if flagOutput == "" {
output = os.Stdout
} else {
defer output.Close()
if output, err = os.Create(flagOutput); err != nil {
return
}
}
var out string
if out, err = pkg.Disassemble(input, flagReal, flagHeader, flagForce, flagRaw); err != nil {
return
}
if _, err = output.WriteString(out); err != nil {
return
}
return
}).Flag(
&flagOutput,
"o", command.StringFlag(""),
"Output file for asm (leave empty for stdout)",
).Flag(
&flagReal,
"r", command.BoolFlag(false),
"skip label generation; idents print real value",
).Flag(
&flagHeader,
"H", command.BoolFlag(false),
"display artifact headers",
).Flag(
&flagForce,
"f", command.BoolFlag(false),
"force display (skip validations)",
).Flag(
&flagRaw,
"R", command.BoolFlag(false),
"don't format output",
)
c.MustParse(os.Args[1:], func(err error) {
log.Fatal(err)
})
}

View File

@@ -4,26 +4,17 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall" "syscall"
"time"
"unique" "unique"
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/internal/pkg" "hakurei.app/internal/pkg"
"hakurei.app/internal/rosa" "hakurei.app/internal/rosa"
"hakurei.app/message" "hakurei.app/message"
@@ -60,16 +51,10 @@ func main() {
flagCures int flagCures int
flagBase string flagBase string
flagTShift int flagTShift int
flagIdle bool
) )
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) { c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) {
msg.SwapVerbose(!flagQuiet) msg.SwapVerbose(!flagQuiet)
flagBase = os.ExpandEnv(flagBase)
if flagBase == "" {
flagBase = "cache"
}
var base *check.Absolute var base *check.Absolute
if flagBase, err = filepath.Abs(flagBase); err != nil { if flagBase, err = filepath.Abs(flagBase); err != nil {
return return
@@ -85,11 +70,6 @@ func main() {
cache.SetThreshold(1 << flagTShift) cache.SetThreshold(1 << flagTShift)
} }
} }
if flagIdle {
pkg.SetSchedIdle = true
}
return return
}).Flag( }).Flag(
&flagQuiet, &flagQuiet,
@@ -101,16 +81,12 @@ func main() {
"Maximum number of dependencies to cure at any given time", "Maximum number of dependencies to cure at any given time",
).Flag( ).Flag(
&flagBase, &flagBase,
"d", command.StringFlag("$MBF_CACHE_DIR"), "d", command.StringFlag("cache"),
"Directory to store cured artifacts", "Directory to store cured artifacts",
).Flag( ).Flag(
&flagTShift, &flagTShift,
"tshift", command.IntFlag(-1), "tshift", command.IntFlag(-1),
"Dependency graph size exponent, to the power of 2", "Dependency graph size exponent, to the power of 2",
).Flag(
&flagIdle,
"sched-idle", command.BoolFlag(false),
"Set SCHED_IDLE scheduling policy",
) )
{ {
@@ -133,245 +109,13 @@ func main() {
) )
} }
{
var (
flagStatus bool
flagReport string
)
c.NewCommand(
"info",
"Display out-of-band metadata of an artifact",
func(args []string) (err error) {
if len(args) == 0 {
return errors.New("info requires at least 1 argument")
}
var r *rosa.Report
if flagReport != "" {
if r, err = rosa.OpenReport(flagReport); err != nil {
return err
}
defer func() {
if closeErr := r.Close(); err == nil {
err = closeErr
}
}()
defer r.HandleAccess(&err)()
}
for i, name := range args {
if p, ok := rosa.ResolveName(name); !ok {
return fmt.Errorf("unknown artifact %q", name)
} else {
var suffix string
if version := rosa.Std.Version(p); version != rosa.Unversioned {
suffix += "-" + version
}
fmt.Println("name : " + name + suffix)
meta := rosa.GetMetadata(p)
fmt.Println("description : " + meta.Description)
if meta.Website != "" {
fmt.Println("website : " +
strings.TrimSuffix(meta.Website, "/"))
}
if len(meta.Dependencies) > 0 {
fmt.Print("depends on :")
for _, d := range meta.Dependencies {
s := rosa.GetMetadata(d).Name
if version := rosa.Std.Version(d); version != rosa.Unversioned {
s += "-" + version
}
fmt.Print(" " + s)
}
fmt.Println()
}
const statusPrefix = "status : "
if flagStatus {
if r == nil {
var f io.ReadSeekCloser
f, err = cache.OpenStatus(rosa.Std.Load(p))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Println(
statusPrefix + "not yet cured",
)
} else {
return
}
} else {
fmt.Print(statusPrefix)
_, err = io.Copy(os.Stdout, f)
if err = errors.Join(err, f.Close()); err != nil {
return
}
}
} else {
status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p)))
if status == nil {
fmt.Println(
statusPrefix + "not in report",
)
} else {
fmt.Println("size :", n)
fmt.Print(statusPrefix)
if _, err = os.Stdout.Write(status); err != nil {
return
}
}
}
}
if i != len(args)-1 {
fmt.Println()
}
}
}
return nil
},
).
Flag(
&flagStatus,
"status", command.BoolFlag(false),
"Display cure status if available",
).
Flag(
&flagReport,
"report", command.StringFlag(""),
"Load cure status from this report file instead of cache",
)
}
c.NewCommand(
"report",
"Generate an artifact cure report for the current cache",
func(args []string) (err error) {
var w *os.File
switch len(args) {
case 0:
w = os.Stdout
case 1:
if w, err = os.OpenFile(
args[0],
os.O_CREATE|os.O_EXCL|syscall.O_WRONLY,
0400,
); err != nil {
return
}
defer func() {
closeErr := w.Close()
if err == nil {
err = closeErr
}
}()
default:
return errors.New("report requires 1 argument")
}
if container.Isatty(int(w.Fd())) {
return errors.New("output appears to be a terminal")
}
return rosa.WriteReport(msg, w, cache)
},
)
{
var flagJobs int
c.NewCommand("updates", command.UsageInternal, func([]string) error {
var (
errsMu sync.Mutex
errs []error
n atomic.Uint64
)
w := make(chan rosa.PArtifact)
var wg sync.WaitGroup
for range max(flagJobs, 1) {
wg.Go(func() {
for p := range w {
meta := rosa.GetMetadata(p)
if meta.ID == 0 {
continue
}
v, err := meta.GetVersions(ctx)
if err != nil {
errsMu.Lock()
errs = append(errs, err)
errsMu.Unlock()
continue
}
if current, latest :=
rosa.Std.Version(p),
meta.GetLatest(v); current != latest {
n.Add(1)
log.Printf("%s %s < %s", meta.Name, current, latest)
continue
}
msg.Verbosef("%s is up to date", meta.Name)
}
})
}
done:
for i := range rosa.PresetEnd {
select {
case w <- rosa.PArtifact(i):
break
case <-ctx.Done():
break done
}
}
close(w)
wg.Wait()
if v := n.Load(); v > 0 {
errs = append(errs, errors.New(strconv.Itoa(int(v))+
" package(s) are out of date"))
}
return errors.Join(errs...)
}).
Flag(
&flagJobs,
"j", command.IntFlag(32),
"Maximum number of simultaneous connections",
)
}
{
var (
flagGentoo string
flagChecksum string
flagStage0 bool
)
c.NewCommand( c.NewCommand(
"stage3", "stage3",
"Check for toolchain 3-stage non-determinism", "Check for toolchain 3-stage non-determinism",
func(args []string) (err error) { func(args []string) (err error) {
t := rosa.Std _, _, _, stage1 := (rosa.Std - 2).NewLLVM()
if flagGentoo != "" { _, _, _, stage2 := (rosa.Std - 1).NewLLVM()
t -= 3 // magic number to discourage misuse _, _, _, stage3 := rosa.Std.NewLLVM()
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 ( var (
pathname *check.Absolute pathname *check.Absolute
checksum [2]unique.Handle[pkg.Checksum] checksum [2]unique.Handle[pkg.Checksum]
@@ -402,40 +146,13 @@ func main() {
"("+pkg.Encode(checksum[0].Value())+")", "("+pkg.Encode(checksum[0].Value())+")",
) )
} }
if flagStage0 {
if pathname, _, err = cache.Cure(
t.Load(rosa.Stage0),
); err != nil {
return err
}
log.Println(pathname)
}
return 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 ( var (
flagDump string flagDump string
flagExport string
) )
c.NewCommand( c.NewCommand(
"cure", "cure",
@@ -445,37 +162,13 @@ func main() {
return errors.New("cure requires 1 argument") return errors.New("cure requires 1 argument")
} }
if p, ok := rosa.ResolveName(args[0]); !ok { if p, ok := rosa.ResolveName(args[0]); !ok {
return fmt.Errorf("unknown artifact %q", args[0]) return fmt.Errorf("unsupported artifact %q", args[0])
} else if flagDump == "" { } else if flagDump == "" {
pathname, _, err := cache.Cure(rosa.Std.Load(p)) pathname, _, err := cache.Cure(rosa.Std.Load(p))
if err != nil { if err == nil {
return err
}
log.Println(pathname) log.Println(pathname)
if flagExport != "" {
msg.Verbosef("exporting %s to %s...", args[0], flagExport)
var f *os.File
if f, err = os.OpenFile(
flagExport,
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
0400,
); err != nil {
return err
} else if _, err = pkg.Flatten(
os.DirFS(pathname.String()),
".",
f,
); err != nil {
_ = f.Close()
return err
} else if err = f.Close(); err != nil {
return err
} }
} return err
return nil
} else { } else {
f, err := os.OpenFile( f, err := os.OpenFile(
flagDump, flagDump,
@@ -499,173 +192,13 @@ func main() {
&flagDump, &flagDump,
"dump", command.StringFlag(""), "dump", command.StringFlag(""),
"Write IR to specified pathname and terminate", "Write IR to specified pathname and terminate",
).
Flag(
&flagExport,
"export", command.StringFlag(""),
"Export cured artifact to specified pathname",
) )
} }
{
var (
flagNet bool
flagSession bool
flagWithToolchain bool
)
c.NewCommand(
"shell",
"Interactive shell in the specified Rosa OS environment",
func(args []string) error {
presets := make([]rosa.PArtifact, len(args))
for i, arg := range args {
p, ok := rosa.ResolveName(arg)
if !ok {
return fmt.Errorf("unknown artifact %q", arg)
}
presets[i] = p
}
root := make(rosa.Collect, 0, 6+len(args))
root = rosa.Std.AppendPresets(root, presets...)
if flagWithToolchain {
musl, compilerRT, runtimes, clang := (rosa.Std - 1).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),
)
if _, _, err := cache.Cure(&root); err == nil {
return errors.New("unreachable")
} else if !errors.Is(err, rosa.Collected{}) {
return err
}
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) { c.MustParse(os.Args[1:], func(err error) {
if cache != nil { if cache != nil {
cache.Close() cache.Close()
} }
if w, ok := err.(interface{ Unwrap() []error }); !ok {
log.Fatal(err) log.Fatal(err)
} else {
errs := w.Unwrap()
for i, e := range errs {
if i == len(errs)-1 {
log.Fatal(e)
}
log.Println(e)
}
}
}) })
} }

View File

@@ -33,7 +33,6 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/helper/proc" "hakurei.app/internal/helper/proc"
@@ -442,7 +441,12 @@ func _main(s ...string) (exitCode int) {
// keep fuse_parse_cmdline happy in the container // keep fuse_parse_cmdline happy in the container
z.Tmpfs(check.MustAbs(container.Nonexistent), 1<<10, 0755) z.Tmpfs(check.MustAbs(container.Nonexistent), 1<<10, 0755)
z.Path = fhs.AbsProcSelfExe if a, err := check.NewAbs(container.MustExecutable(msg)); err != nil {
log.Println(err)
return 5
} else {
z.Path = a
}
z.Args = s z.Args = s
z.ForwardCancel = true z.ForwardCancel = true
z.SeccompPresets |= std.PresetStrict z.SeccompPresets |= std.PresetStrict

View File

@@ -10,7 +10,8 @@ import (
func init() { gob.Register(new(AutoEtcOp)) } func init() { gob.Register(new(AutoEtcOp)) }
// Etc is a helper for appending [AutoEtcOp] to [Ops]. // Etc appends an [Op] that 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.
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)
@@ -19,9 +20,6 @@ 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,15 +11,13 @@ import (
func init() { gob.Register(new(AutoRootOp)) } func init() { gob.Register(new(AutoRootOp)) }
// Root is a helper for appending [AutoRootOp] to [Ops]. // Root appends an [Op] that 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.
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

@@ -50,16 +50,10 @@ 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 { func capBoundingSetDrop(cap uintptr) error { return Prctl(syscall.PR_CAPBSET_DROP, cap, 0) }
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 { func capAmbientClearAll() error { return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0) }
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 { func capAmbientRaise(cap uintptr) error { return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap) }
return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap)
}

View File

@@ -11,8 +11,7 @@ const (
SpecialOverlayPath = ":" SpecialOverlayPath = ":"
) )
// EscapeOverlayDataSegment escapes a string for formatting into the data // EscapeOverlayDataSegment escapes a string for formatting into the data argument of an overlay mount call.
// 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,5 +1,4 @@
// Package container implements unprivileged Linux containers with built-in // Package container implements unprivileged Linux containers with built-in support for syscall filtering.
// support for syscall filtering.
package container package container
import ( import (
@@ -38,34 +37,24 @@ type (
Container struct { Container struct {
// Whether the container init should stay alive after its parent terminates. // Whether the container init should stay alive after its parent terminates.
AllowOrphan bool AllowOrphan bool
// Whether to set SchedPolicy and SchedPriority via sched_setscheduler(2).
SetScheduler bool
// Scheduling policy to set via sched_setscheduler(2).
SchedPolicy std.SchedPolicy
// Scheduling priority to set via sched_setscheduler(2). The zero value
// implies the minimum value supported by the current SchedPolicy.
SchedPriority std.Int
// Cgroup fd, nil to disable. // Cgroup fd, nil to disable.
Cgroup *int Cgroup *int
// ExtraFiles passed through to initial process in the container, with // ExtraFiles passed through to initial process in the container,
// behaviour identical to its [exec.Cmd] counterpart. // with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File ExtraFiles []*os.File
// Write end of a pipe connected to the init to deliver [Params]. // param pipe for shim and init
setup *os.File setup *os.File
// Cancels the context passed to the underlying cmd. // cancels cmd
cancel context.CancelFunc cancel context.CancelFunc
// Closed after Wait returns. Keeps the spawning thread alive. // closed after Wait returns
wait chan struct{} wait chan struct{}
Stdin io.Reader Stdin io.Reader
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
// Custom cancellation behaviour for the underlying [exec.Cmd]. Must
// deliver [CancelSignal] before returning.
Cancel func(cmd *exec.Cmd) error Cancel func(cmd *exec.Cmd) error
// Copied to the underlying [exec.Cmd].
WaitDelay time.Duration WaitDelay time.Duration
cmd *exec.Cmd cmd *exec.Cmd
@@ -294,11 +283,7 @@ 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{ return &StartError{true, "set up params stream", err, false, false}
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)}
@@ -310,16 +295,10 @@ func (p *Container) Start() error {
runtime.LockOSThread() runtime.LockOSThread()
p.wait = make(chan struct{}) p.wait = make(chan struct{})
// setup depending on per-thread state must happen here done <- func() error { // setup depending on per-thread state must happen here
done <- func() error { // PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread
// 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{ return &StartError{true, "prctl(PR_SET_NO_NEW_PRIVS)", err, false, false}
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
@@ -331,40 +310,28 @@ 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 // landlock can be skipped here as it restricts access to resources
// to resources already covered by namespaces (pid) // already covered by namespaces (pid)
goto landlockOut goto landlockOut
} }
return &StartError{Step: "get landlock ABI", Err: err} return &StartError{false, "get landlock ABI", err, false, false}
} 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{ return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
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{ return &StartError{true, "create landlock ruleset", err, false, false}
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{ return &StartError{true, "enforce landlock ruleset", err, false, false}
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)
@@ -375,52 +342,9 @@ func (p *Container) Start() error {
landlockOut: landlockOut:
} }
// sched_setscheduler: thread-directed but acts on all processes
// created from the calling thread
if p.SetScheduler {
if p.SchedPolicy < 0 || p.SchedPolicy > std.SCHED_LAST {
return &StartError{
Fatal: false,
Step: "set scheduling policy",
Err: EINVAL,
}
}
var param schedParam
if priority, err := p.SchedPolicy.GetPriorityMin(); err != nil {
return &StartError{
Fatal: true,
Step: "get minimum priority",
Err: err,
}
} else {
param.priority = max(priority, p.SchedPriority)
}
p.msg.Verbosef(
"setting scheduling policy %s priority %d",
p.SchedPolicy, param.priority,
)
if err := schedSetscheduler(
0, // calling thread
p.SchedPolicy,
&param,
); err != nil {
return &StartError{
Fatal: true,
Step: "set scheduling policy",
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{ return &StartError{false, "start container init", err, false, true}
Step: "start container init",
Err: err,
Passthrough: true,
}
} }
return nil return nil
}() }()
@@ -432,7 +356,6 @@ 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 {
@@ -442,21 +365,12 @@ 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{ return &StartError{true, "set init pipe deadline", err, false, true}
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{ return &StartError{false, "invalid executable pathname", EINVAL, true, false}
Step: "invalid executable pathname",
Err: EINVAL,
Origin: true,
}
} }
// do not transmit nil // do not transmit nil
@@ -481,8 +395,7 @@ func (p *Container) Serve() error {
return err return err
} }
// Wait blocks until the container init process to exit and releases any // Wait waits for the container init process to exit and releases any resources associated with the [Container].
// 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
@@ -527,13 +440,11 @@ func (p *Container) StderrPipe() (r io.ReadCloser, err error) {
} }
func (p *Container) String() string { func (p *Container) String() string {
return fmt.Sprintf( return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
"argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x", p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets),
)
} }
// ProcessState returns the address of os.ProcessState held by the underlying [exec.Cmd]. // ProcessState returns the address to 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
@@ -541,8 +452,7 @@ func (p *Container) ProcessState() *os.ProcessState {
return p.cmd.ProcessState return p.cmd.ProcessState
} }
// New returns the address to a new instance of [Container]. This value requires // New returns the address to a new instance of [Container] that requires further initialisation before use.
// 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)
@@ -551,18 +461,12 @@ 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, fhs.ProcSelfExe) p.cmd = exec.CommandContext(c, MustExecutable(msg))
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( func NewCommand(ctx context.Context, msg message.Msg, pathname *check.Absolute, name string, args ...string) *Container {
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

@@ -773,13 +773,14 @@ 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(fhs.AbsProcSelfExe, absHelperInnerPath, 0) c.Bind(executable, absHelperInnerPath, 0)
// in case test has cgo enabled // in case test has cgo enabled
if entries, err := ldd.Resolve(ctx, msg, nil); err != nil { if entries, err := ldd.Resolve(ctx, msg, executable); 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,8 +21,7 @@ type osFile interface {
fs.File fs.File
} }
// syscallDispatcher provides methods that make state-dependent system calls as // syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
// 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,

View File

@@ -238,11 +238,8 @@ 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 // check happens in Close, and cleanup is not guaranteed to run, so relying on it for sloppy implementations will cause sporadic test results
// on it for sloppy implementations will cause sporadic test results f.cleanup = runtime.AddCleanup(f, func(name string) { f.t.Fatalf("checkedOsFile %s became unreachable without a call to Close", name) }, f.name)
f.cleanup = runtime.AddCleanup(f, func(name string) {
panic("checkedOsFile " + name + " became unreachable without a call to Close")
}, name)
return f return f
} }

View File

@@ -43,8 +43,7 @@ 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 // While this is usable for pointer errors, such use should be avoided as nil check is omitted.
// 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

@@ -28,9 +28,6 @@ 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

@@ -42,8 +42,6 @@ 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,8 +9,7 @@ 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 // Run points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar.
// 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.
@@ -18,12 +17,10 @@ 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 // UsrBin points to binaries and executables for user commands that shall appear in the $PATH search path.
// appear in the $PATH search path.
UsrBin = Usr + "bin/" UsrBin = Usr + "bin/"
// Var points to persistent, variable system data. Writable during normal // Var points to persistent, variable system data. Writable during normal system operation.
// system operation.
Var = "/var/" Var = "/var/"
// VarLib points to persistent system data. // VarLib points to persistent system data.
VarLib = Var + "lib/" VarLib = Var + "lib/"
@@ -32,20 +29,12 @@ 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 // DevShm is the place for POSIX shared memory segments, as created via shm_open(3).
// shm_open(3).
DevShm = "/dev/shm/" DevShm = "/dev/shm/"
// Proc points to a virtual kernel file system exposing the process list and // Proc points to a virtual kernel file system exposing the process list and other functionality.
// other functionality.
Proc = "/proc/" Proc = "/proc/"
// ProcSys points to a hierarchy below /proc/ that exposes a number of // ProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
// kernel tunables.
ProcSys = Proc + "sys/" ProcSys = Proc + "sys/"
// ProcSelf resolves to the process's own /proc/pid directory. // Sys points to a virtual kernel file system exposing discovered devices and other functionality.
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 It should be noted that none of this should become relevant at any point since the resulting
since the resulting intermediate root tmpfs should be effectively anonymous. */ 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 // setupEnv is the name of the environment variable holding the string representation of
// representation of the read end file descriptor of the setup params pipe. // 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,8 +59,7 @@ 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 // prefix returns a log message prefix, and whether this Op prints no identifying message on its own.
// identifying message on its own.
prefix() (string, bool) prefix() (string, bool)
Is(op Op) bool Is(op Op) bool
@@ -72,11 +71,9 @@ type (
setupState struct { setupState struct {
nonrepeatable uintptr nonrepeatable uintptr
// Whether early reaping has concluded. Must only be accessed in the // Whether early reaping has concluded. Must only be accessed in the wait4 loop.
// wait4 loop.
processConcluded bool processConcluded bool
// Process to syscall.WaitStatus populated in the wait4 loop. Freed // Process to syscall.WaitStatus populated in the wait4 loop. Freed after early reaping concludes.
// 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
@@ -219,10 +216,9 @@ 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 this step is mostly for gathering information that would otherwise be difficult to obtain
difficult to obtain via library functions after pivot_root, and via library functions after pivot_root, and implementations are expected to avoid changing
implementations are expected to avoid changing the state of the mount the state of the mount namespace */
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)
@@ -262,10 +258,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. This /* apply is called right after pivot_root and entering the new root;
step sets up the container filesystem, and implementations are expected to this step sets up the container filesystem, and implementations are expected to keep the host root
keep the host root and sysroot mount points intact but otherwise can do and sysroot mount points intact but otherwise can do whatever they need to;
whatever they need to. Calling chdir is allowed but discouraged. */ 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

@@ -12,16 +12,14 @@ import (
func init() { gob.Register(new(BindMountOp)) } func init() { gob.Register(new(BindMountOp)) }
// Bind is a helper for appending [BindMountOp] to [Ops]. // Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target].
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 creates a bind mount from host path Source to container path Target. // BindMountOp bind mounts host path Source on 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,7 +24,8 @@ const (
daemonTimeout = 5 * time.Second daemonTimeout = 5 * time.Second
) )
// Daemon is a helper for appending [DaemonOp] to [Ops]. // Daemon appends an [Op] that starts a daemon in the container and blocks until
// [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,9 +19,7 @@ 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 is a helper for appending [MkdirOp] to [Ops]. // Mkdir appends an [Op] that creates a directory in the container filesystem.
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,11 +54,8 @@ func (e *OverlayArgumentError) Error() string {
} }
} }
// Overlay is a helper for appending [MountOverlayOp] to [Ops]. // Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target].
func (f *Ops) Overlay( func (f *Ops) Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) *Ops {
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,
@@ -68,12 +65,13 @@ func (f *Ops) Overlay(
return f return f
} }
// OverlayEphemeral appends a [MountOverlayOp] with an ephemeral upperdir and workdir. // OverlayEphemeral appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]
// 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 a readonly [MountOverlayOp]. // OverlayReadonly appends an [Op] that mounts the overlay pseudo filesystem readonly on [MountOverlayOp.Target]
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...)
} }
@@ -84,34 +82,25 @@ 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]. // formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early
//
// 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], an // If Work is nil and Upper holds the special value [fhs.AbsRoot],
// ephemeral upperdir and workdir will be set up. // an ephemeral upperdir and workdir will be set up.
// //
// If both Work and Upper are nil, upperdir and workdir is omitted and the // If both Work and Upper are nil, upperdir and workdir is omitted and the overlay is mounted readonly.
// overlay is mounted readonly.
Upper *check.Absolute Upper *check.Absolute
// Formatted for [OptionOverlayUpperdir]. // formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early
//
// 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]. // formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early
//
// 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

@@ -16,7 +16,7 @@ const (
func init() { gob.Register(new(TmpfileOp)) } func init() { gob.Register(new(TmpfileOp)) }
// Place is a helper for appending [TmpfileOp] to [Ops]. // Place appends an [Op] that places a file in container path [TmpfileOp.Path] containing [TmpfileOp.Data].
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.*"}, (*checkedOsFile)(nil), stub.UniqueError(5)), call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, 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.Close", sampleDataString, stub.UniqueError(3)), nil), call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", 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.ensureFile", sampleDataString, nil), nil), call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", 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.bindMount", sampleDataString, nil), nil), call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", 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.bindMount", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, stub.UniqueError(1)), call("bindMount", stub.ExpectArgs{"tmp.32768", "/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.remove", sampleDataString, nil), nil), call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", 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.remove", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil), call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil),
call("remove", stub.ExpectArgs{"tmp.remove"}, nil, stub.UniqueError(0)), call("remove", stub.ExpectArgs{"tmp.32768"}, 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.success", sampleDataString, nil), nil), call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", 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.success", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil), call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil),
call("remove", stub.ExpectArgs{"tmp.success"}, nil, nil), call("remove", stub.ExpectArgs{"tmp.32768"}, nil, nil),
}, nil}, }, nil},
}) })

View File

@@ -10,7 +10,7 @@ import (
func init() { gob.Register(new(MountProcOp)) } func init() { gob.Register(new(MountProcOp)) }
// Proc is a helper for appending [MountProcOp] to [Ops]. // Proc appends an [Op] that mounts a private instance of proc.
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 is a helper for appending [RemountOp] to [Ops]. // Remount appends an [Op] that applies [RemountOp.Flags] on container path [RemountOp.Target].
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

@@ -38,7 +38,6 @@ 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:
@@ -117,7 +116,6 @@ 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:
@@ -154,7 +152,6 @@ 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:
@@ -187,12 +184,10 @@ 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 // Bitmask of scopes restricting a Landlock domain from accessing outside resources (e.g. IPCs).
// 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"
@@ -213,7 +208,6 @@ 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
@@ -222,13 +216,10 @@ func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
size = unsafe.Sizeof(*rulesetAttr) size = unsafe.Sizeof(*rulesetAttr)
} }
rulesetFd, _, errno := syscall.Syscall( rulesetFd, _, errno := syscall.Syscall(std.SYS_LANDLOCK_CREATE_RULESET, pointer, size, flags)
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
} }
@@ -239,19 +230,12 @@ 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( r, _, errno := syscall.Syscall(std.SYS_LANDLOCK_RESTRICT_SELF, uintptr(rulesetFd), flags, 0)
std.SYS_LANDLOCK_RESTRICT_SELF,
uintptr(rulesetFd),
flags,
0,
)
if r != 0 { if r != 0 {
return errno return errno
} }

View File

@@ -99,7 +99,7 @@ done:
} }
if m.Header.Type == NLMSG_ERROR { if m.Header.Type == NLMSG_ERROR {
if len(m.Data) >= 4 { if len(m.Data) >= 4 {
errno := Errno(-std.Int(binary.NativeEndian.Uint32(m.Data))) errno := Errno(-std.ScmpInt(binary.NativeEndian.Uint32(m.Data)))
if errno == 0 { if errno == 0 {
return nil return nil
} }

View File

@@ -15,10 +15,7 @@ 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,22 +88,18 @@ var resPrefix = [...]string{
7: "seccomp_load failed", 7: "seccomp_load failed",
} }
// cbAllocateBuffer is the function signature for the function handle passed to // cbAllocateBuffer is the function signature for the function handle passed to hakurei_export_filter
// hakurei_scmp_make_filter which allocates the buffer that the resulting bpf // which allocates the buffer that the resulting bpf program is copied into, and writes its slice header
// program is copied into, and writes its slice header to a value held by the caller. // 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 // makeFilter generates a bpf program from a slice of [std.NativeRule] and writes the resulting byte slice to p.
// writes the resulting byte slice to p. The filter is installed to the current // The filter is installed to the current process if p is nil.
// 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
@@ -174,8 +170,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 // Load generates a bpf program from a slice of [std.NativeRule] and enforces it on the current process.
// on the current process. Errors returned by libseccomp is wrapped in [LibraryError]. // 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

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

View File

@@ -7,28 +7,24 @@ 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 Int ScmpSyscall ScmpInt
// ScmpErrno represents an errno value passed to libseccomp via [NativeRule.Errno]. // ScmpErrno represents an errno value passed to libseccomp via [NativeRule.Errno].
ScmpErrno Int ScmpErrno ScmpInt
// ScmpCompare is equivalent to enum scmp_compare; // ScmpCompare is equivalent to enum scmp_compare;
ScmpCompare Uint ScmpCompare ScmpUint
// 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 Uint `json:"arg"` Arg ScmpUint `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

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

View File

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

View File

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

View File

@@ -3,8 +3,6 @@ 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.
@@ -43,37 +41,6 @@ func Isatty(fd int) bool {
return r == 0 return r == 0
} }
// 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 int, policy std.SchedPolicy, param *schedParam) error {
if _, _, errno := Syscall(
SYS_SCHED_SETSCHEDULER,
uintptr(tid),
uintptr(policy),
uintptr(unsafe.Pointer(param)),
); errno != 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,8 +2,6 @@ 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,7 +24,6 @@ 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
@@ -52,8 +51,7 @@ func (e *DecoderError) Error() string {
} }
type ( type (
// A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from // A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream.
// an input stream.
MountInfoDecoder struct { MountInfoDecoder struct {
s *bufio.Scanner s *bufio.Scanner
m *MountInfo m *MountInfo
@@ -74,16 +72,13 @@ 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 // parent ID: the ID of the parent mount (or of self for the root of this mount namespace's mount tree).
// 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: the pathname of the directory in the filesystem which forms the root of this mount.
// root of this mount.
Root string `json:"root"` Root string `json:"root"`
// mount point: the pathname of the mount point relative to the // mount point: the pathname of the mount point relative to the process's root directory.
// 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"`
@@ -131,8 +126,7 @@ 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 // The decoder introduces its own buffering and may read data from r beyond the mountinfo entries requested.
// 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)}
} }
@@ -277,8 +271,6 @@ 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) &&
@@ -292,8 +284,6 @@ 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,7 +6,6 @@ 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 {
@@ -28,7 +27,6 @@ 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

4
dist/release.sh vendored
View File

@@ -13,7 +13,7 @@ echo
echo '# Building hakurei.' echo '# Building hakurei.'
go generate ./... go generate ./...
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w
-buildid= -linkmode external -extldflags=-static -buildid= -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
@@ -21,7 +21,7 @@ go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w
echo echo
echo '# Testing hakurei.' echo '# Testing hakurei.'
go test -ldflags='-buildid= -linkmode external -extldflags=-static' ./... go test -ldflags='-buildid= -extldflags=-static' ./...
echo echo
echo '# Creating distribution.' echo '# Creating distribution.'

12
flake.lock generated
View File

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

View File

@@ -29,6 +29,20 @@
{ {
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
@@ -57,6 +71,8 @@
sharefs = callPackage ./cmd/sharefs/test { inherit system self; }; sharefs = callPackage ./cmd/sharefs/test { inherit system self; };
hpkg = callPackage ./cmd/hpkg/test { inherit system self; };
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } '' formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
cd ${./.} cd ${./.}
@@ -99,7 +115,7 @@
hakurei = pkgs.pkgsStatic.callPackage ./package.nix { hakurei = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs) inherit (pkgs)
# passthru.buildInputs # passthru.buildInputs
go_1_26 go
clang clang
# nativeBuildInputs # nativeBuildInputs
@@ -111,6 +127,11 @@
glibc glibc
xdg-dbus-proxy xdg-dbus-proxy
# hpkg
zstd
gnutar
coreutils
# for check # for check
util-linux util-linux
nettools nettools
@@ -182,7 +203,7 @@
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.stdenv.hostPlatform.system}.hakurei.override { package = self.packages.${pkgs.stdenv.hostPlatform.system}.hakurei.override {
buildGo126Module = previousArgs: pkgs.pkgsStatic.buildGo126Module (previousArgs // { doCheck = false; }); buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; });
}; };
in in
{ {
@@ -198,7 +219,7 @@
./test/interactive/trace.nix ./test/interactive/trace.nix
self.nixosModules.hakurei self.nixosModules.hakurei
home-manager.nixosModules.home-manager self.inputs.home-manager.nixosModules.home-manager
]; ];
}; };
in in

2
go.mod
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,17 +11,15 @@ import (
type Enablement byte type Enablement byte
const ( const (
// EWayland exposes a Wayland pathname socket via security-context-v1. // EWayland exposes a wayland pathname socket via security-context-v1.
EWayland Enablement = 1 << iota EWayland Enablement = 1 << iota
// EX11 adds the target user via X11 ChangeHosts and exposes the X11 // EX11 adds the target user via X11 ChangeHosts and exposes the X11 pathname socket.
// pathname socket.
EX11 EX11
// EDBus enables the per-container xdg-dbus-proxy daemon. // EDBus enables the per-container xdg-dbus-proxy daemon.
EDBus EDBus
// EPipeWire exposes a pipewire pathname socket via SecurityContext. // EPipeWire exposes a pipewire pathname socket via SecurityContext.
EPipeWire EPipeWire
// EPulse copies the PulseAudio cookie to [hst.PrivateTmp] and exposes the // EPulse copies the PulseAudio cookie to [hst.PrivateTmp] and exposes the PulseAudio socket.
// PulseAudio socket.
EPulse EPulse
// EM is a noop. // EM is a noop.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@
package filelock package filelock
import ( import (
"errors"
"io/fs" "io/fs"
) )
@@ -73,3 +74,10 @@ 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/fhs" "hakurei.app/container"
"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(), fhs.ProcSelfExe, "-test.run=^$") cmd := testexec.CommandContext(t, t.Context(), container.MustExecutable(nil), "-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,11 +94,6 @@ 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
} }

View File

@@ -15,7 +15,7 @@ import (
"testing" "testing"
"time" "time"
"hakurei.app/container/fhs" "hakurei.app/container"
"hakurei.app/internal/lockedfile" "hakurei.app/internal/lockedfile"
"hakurei.app/internal/lockedfile/internal/testexec" "hakurei.app/internal/lockedfile/internal/testexec"
) )
@@ -215,7 +215,7 @@ func TestSpuriousEDEADLK(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
cmd := testexec.CommandContext(t, t.Context(), fhs.ProcSelfExe, "-test.run=^"+t.Name()+"$") cmd := testexec.CommandContext(t, t.Context(), container.MustExecutable(nil), "-test.run=^"+t.Name()+"$")
cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", dirVar, dir)) cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", dirVar, dir))
qDone := make(chan struct{}) qDone := make(chan struct{})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

216
internal/pkg/asm.go Normal file
View File

@@ -0,0 +1,216 @@
package pkg
import (
"encoding/binary"
"fmt"
"io"
"strconv"
"strings"
)
type asmOutLine struct {
pos int
word int
kindData int64
valueData []byte
indent int
kind string
value string
}
var spacingLine = asmOutLine{
pos: -1,
kindData: -1,
valueData: nil,
indent: 0,
kind: "",
value: "",
}
func Disassemble(r io.Reader, real bool, showHeader bool, force bool, raw bool) (s string, err error) {
var lines []asmOutLine
sb := new(strings.Builder)
header := true
pos := new(int)
for err == nil {
if header {
var kind uint64
var size uint64
var bsize []byte
p := *pos
if _, kind, err = nextUint64(r, pos); err != nil {
break
}
if bsize, size, err = nextUint64(r, pos); err != nil {
break
}
if showHeader {
lines = append(lines, asmOutLine{p, 8, int64(kind), bsize, 0, "head " + intToKind(kind), ""})
}
for i := 0; uint64(i) < size; i++ {
var did Checksum
var dkind uint64
p := *pos
if _, dkind, err = nextUint64(r, pos); err != nil {
break
}
if _, did, err = nextIdent(r, pos); err != nil {
break
}
if showHeader {
lines = append(lines, asmOutLine{p, 8, int64(dkind), nil, 1, intToKind(dkind), Encode(did)})
}
}
header = false
}
var k uint32
p := *pos
if _, k, err = nextUint32(r, pos); err != nil {
break
}
kind := IRValueKind(k)
switch kind {
case IRKindEnd:
var a uint32
var ba []byte
if ba, a, err = nextUint32(r, pos); err != nil {
break
}
if a&1 != 0 {
var sum Checksum
if _, sum, err = nextIdent(r, pos); err != nil {
break
}
lines = append(lines, asmOutLine{p, 4, int64(kind), ba, 1, "end ", Encode(sum)})
} else {
lines = append(lines, asmOutLine{p, 4, int64(kind), []byte{0, 0, 0, 0}, 1, "end ", ""})
}
lines = append(lines, spacingLine)
header = true
continue
case IRKindIdent:
var a []byte
// discard ancillary
if a, _, err = nextUint32(r, pos); err != nil {
break
}
var sum Checksum
if _, sum, err = nextIdent(r, pos); err != nil {
break
}
lines = append(lines, asmOutLine{p, 4, int64(kind), a, 1, "id ", Encode(sum)})
continue
case IRKindUint32:
var i uint32
var bi []byte
if bi, i, err = nextUint32(r, pos); err != nil {
break
}
lines = append(lines, asmOutLine{p, 4, int64(kind), bi, 1, "int ", strconv.FormatUint(uint64(i), 10)})
case IRKindString:
var l uint32
var bl []byte
if bl, l, err = nextUint32(r, pos); err != nil {
break
}
s := make([]byte, l+(wordSize-(l)%wordSize)%wordSize)
var n int
if n, err = r.Read(s); err != nil {
break
}
*pos = *pos + n
lines = append(lines, asmOutLine{p, 4, int64(kind), bl, 1, "str ", strconv.Quote(string(s[:l]))})
continue
default:
var bi []byte
if bi, _, err = nextUint32(r, pos); err != nil {
break
}
lines = append(lines, asmOutLine{p, 4, int64(kind), bi, 1, "????", ""})
}
}
if err != io.EOF {
return
}
err = nil
for _, line := range lines {
if raw {
if line.pos != -1 {
sb.WriteString(fmt.Sprintf("%s\t%s\n", line.kind, line.value))
}
} else {
if line.pos == -1 {
sb.WriteString("\n")
} else if line.word == 4 {
sb.WriteString(fmt.Sprintf("%06x: %04x %04x%s %s %s\n", line.pos, binary.LittleEndian.AppendUint32(nil, uint32(line.kindData)), line.valueData, headerSpacing(showHeader), line.kind, line.value))
} else {
kind := binary.LittleEndian.AppendUint64(nil, uint64(line.kindData))
value := line.valueData
if len(value) == 8 {
sb.WriteString(fmt.Sprintf("%06x: %04x %04x %04x %04x %s %s\n", line.pos, kind[:4], kind[4:], value[:4], value[4:], line.kind, line.value))
} else {
sb.WriteString(fmt.Sprintf("%06x: %04x %04x %s %s\n", line.pos, kind[:4], kind[4:], line.kind, line.value))
}
}
}
}
return sb.String(), err
}
func nextUint32(r io.Reader, pos *int) ([]byte, uint32, error) {
i := make([]byte, 4)
_, err := r.Read(i)
if err != nil {
return i, 0, err
}
p := *pos + 4
*pos = p
return i, binary.LittleEndian.Uint32(i), nil
}
func nextUint64(r io.Reader, pos *int) ([]byte, uint64, error) {
i := make([]byte, 8)
_, err := r.Read(i)
if err != nil {
return i, 0, err
}
p := *pos + 8
*pos = p
return i, binary.LittleEndian.Uint64(i), nil
}
func nextIdent(r io.Reader, pos *int) ([]byte, Checksum, error) {
i := make([]byte, 48)
if _, err := r.Read(i); err != nil {
return i, Checksum{}, err
}
p := *pos + 48
*pos = p
return i, Checksum(i), nil
}
func intToKind(i uint64) string {
switch Kind(i) {
case KindHTTPGet:
return "http"
case KindTar:
return "tar "
case KindExec:
return "exec"
case KindExecNet:
return "exen"
case KindFile:
return "file"
default:
return fmt.Sprintf("$%d ", i-KindCustomOffset)
}
}
func headerSpacing(showHeader bool) string {
if showHeader {
return " "
}
return ""
}

View File

@@ -23,7 +23,7 @@ import (
"hakurei.app/message" "hakurei.app/message"
) )
// AbsWork is the container pathname [TContext.GetWorkDir] is mounted on. // AbsWork is the container pathname [CureContext.GetWorkDir] is mounted on.
var AbsWork = fhs.AbsRoot.Append("work/") var AbsWork = fhs.AbsRoot.Append("work/")
// ExecPath is a slice of [Artifact] and the [check.Absolute] pathname to make // ExecPath is a slice of [Artifact] and the [check.Absolute] pathname to make
@@ -39,23 +39,22 @@ type ExecPath struct {
W bool W bool
} }
// SetSchedIdle is whether to set [std.SCHED_IDLE] scheduling priority. // layers returns pathnames collected from A deduplicated by checksum.
var SetSchedIdle bool func (p *ExecPath) layers(f *FContext) []*check.Absolute {
msg := f.GetMessage()
// PromoteLayers returns artifacts with identical-by-content layers promoted to layers := make([]*check.Absolute, 0, len(p.A))
// the highest priority instance, as if mounted via [ExecPath]. checksums := make(map[unique.Handle[Checksum]]struct{}, len(p.A))
func PromoteLayers( for i := range p.A {
artifacts []Artifact, d := p.A[len(p.A)-1-i]
getArtifact func(Artifact) (*check.Absolute, unique.Handle[Checksum]), pathname, checksum := f.GetArtifact(d)
report func(i int, d Artifact),
) []*check.Absolute {
layers := make([]*check.Absolute, 0, len(artifacts))
checksums := make(map[unique.Handle[Checksum]]struct{}, len(artifacts))
for i := range artifacts {
d := artifacts[len(artifacts)-1-i]
pathname, checksum := getArtifact(d)
if _, ok := checksums[checksum]; ok { if _, ok := checksums[checksum]; ok {
report(len(artifacts)-1-i, d) if msg.IsVerbose() {
msg.Verbosef(
"promoted layer %d as %s",
len(p.A)-1-i, reportName(d, f.cache.Ident(d)),
)
}
continue continue
} }
checksums[checksum] = struct{}{} checksums[checksum] = struct{}{}
@@ -65,19 +64,6 @@ func PromoteLayers(
return layers return layers
} }
// layers returns pathnames collected from A deduplicated via [PromoteLayers].
func (p *ExecPath) layers(f *FContext) []*check.Absolute {
msg := f.GetMessage()
return PromoteLayers(p.A, f.GetArtifact, func(i int, d Artifact) {
if msg.IsVerbose() {
msg.Verbosef(
"promoted layer %d as %s",
i, reportName(d, f.cache.Ident(d)),
)
}
})
}
// Path returns a populated [ExecPath]. // Path returns a populated [ExecPath].
func Path(pathname *check.Absolute, writable bool, a ...Artifact) ExecPath { func Path(pathname *check.Absolute, writable bool, a ...Artifact) ExecPath {
return ExecPath{pathname, a, writable} return ExecPath{pathname, a, writable}
@@ -361,7 +347,6 @@ const (
// scanVerbose prefixes program output for a verbose [message.Msg]. // scanVerbose prefixes program output for a verbose [message.Msg].
func scanVerbose( func scanVerbose(
msg message.Msg, msg message.Msg,
cancel context.CancelFunc,
done chan<- struct{}, done chan<- struct{},
prefix string, prefix string,
r io.Reader, r io.Reader,
@@ -376,15 +361,10 @@ func scanVerbose(
msg.Verbose(prefix, s.Text()) msg.Verbose(prefix, s.Text())
} }
if err := s.Err(); err != nil && !errors.Is(err, os.ErrClosed) { if err := s.Err(); err != nil && !errors.Is(err, os.ErrClosed) {
cancel()
msg.Verbose("*"+prefix, err) msg.Verbose("*"+prefix, err)
} }
} }
// SeccompPresets is the [seccomp] presets used by exec artifacts.
const SeccompPresets = std.PresetStrict &
^(std.PresetDenyNS | std.PresetDenyDevel)
// cure is like Cure but allows optional host net namespace. This is used for // cure is like Cure but allows optional host net namespace. This is used for
// the [KnownChecksum] variant where networking is allowed. // the [KnownChecksum] variant where networking is allowed.
func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) { func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
@@ -408,23 +388,15 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
z := container.New(ctx, f.GetMessage()) z := container.New(ctx, f.GetMessage())
z.WaitDelay = execWaitDelay z.WaitDelay = execWaitDelay
z.SeccompPresets = SeccompPresets z.SeccompPresets |= std.PresetStrict & ^std.PresetDenyNS
z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompFlags |= seccomp.AllowMultiarch
z.ParentPerm = 0700 z.ParentPerm = 0700
z.HostNet = hostNet z.HostNet = hostNet
z.Hostname = "cure" z.Hostname = "cure"
z.SetScheduler = SetSchedIdle
z.SchedPolicy = std.SCHED_IDLE
if z.HostNet { if z.HostNet {
z.Hostname = "cure-net" z.Hostname = "cure-net"
} }
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1 z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
var status io.Writer
if status, err = f.GetStatusWriter(); err != nil {
return
}
if msg := f.GetMessage(); msg.IsVerbose() { if msg := f.GetMessage(); msg.IsVerbose() {
var stdout, stderr io.ReadCloser var stdout, stderr io.ReadCloser
if stdout, err = z.StdoutPipe(); err != nil { if stdout, err = z.StdoutPipe(); err != nil {
@@ -441,26 +413,10 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
} }
}() }()
brStdout, brStderr := f.cache.getReader(stdout), f.cache.getReader(stderr)
stdoutDone, stderrDone := make(chan struct{}), make(chan struct{}) stdoutDone, stderrDone := make(chan struct{}), make(chan struct{})
go scanVerbose( go scanVerbose(msg, stdoutDone, "("+a.name+":1)", stdout)
msg, cancel, stdoutDone, go scanVerbose(msg, stderrDone, "("+a.name+":2)", stderr)
"("+a.name+":1)", defer func() { <-stdoutDone; <-stderrDone }()
io.TeeReader(brStdout, status),
)
go scanVerbose(
msg, cancel, stderrDone,
"("+a.name+":2)",
io.TeeReader(brStderr, status),
)
defer func() {
<-stdoutDone
<-stderrDone
f.cache.putReader(brStdout)
f.cache.putReader(brStderr)
}()
} else {
z.Stdout, z.Stderr = status, status
} }
z.Dir, z.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args z.Dir, z.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args

View File

@@ -150,7 +150,7 @@ func (i *IContext) WriteUint32(v uint32) {
} }
// irMaxStringLength is the maximum acceptable wire size of [IRKindString]. // irMaxStringLength is the maximum acceptable wire size of [IRKindString].
const irMaxStringLength = 1 << 24 const irMaxStringLength = 1 << 20
// IRStringError is a string value too big to encode in IR. // IRStringError is a string value too big to encode in IR.
type IRStringError string type IRStringError string
@@ -310,13 +310,6 @@ type (
// verbose logging is enabled. Artifacts may only depend on artifacts // verbose logging is enabled. Artifacts may only depend on artifacts
// previously described in the IR stream. // previously described in the IR stream.
// //
// IRDecoder rejects an IR stream on the first decoding error, it does not
// check against nonzero reserved ancillary data or incorrectly ordered or
// redundant unstructured dependencies. An invalid IR stream as such will
// yield [Artifact] values with identifiers disagreeing with those computed
// by IRDecoder. For this reason, IRDecoder does not access the ident cache
// to avoid putting [Cache] into an inconsistent state.
//
// Methods of IRDecoder are not safe for concurrent use. // Methods of IRDecoder are not safe for concurrent use.
IRDecoder struct { IRDecoder struct {
// Address of underlying [Cache], must not be exposed directly. // Address of underlying [Cache], must not be exposed directly.

View File

@@ -28,21 +28,15 @@ import (
"unsafe" "unsafe"
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/internal/info"
"hakurei.app/internal/lockedfile" "hakurei.app/internal/lockedfile"
"hakurei.app/message" "hakurei.app/message"
) )
const (
// programName is the string identifying this build system.
programName = "internal/pkg"
)
type ( type (
// A Checksum is a SHA-384 checksum computed for a cured [Artifact]. // A Checksum is a SHA-384 checksum computed for a cured [Artifact].
Checksum = [sha512.Size384]byte Checksum = [sha512.Size384]byte
// An ID is a unique identifier returned by [KnownIdent.ID]. This value must // An ID is a unique identifier returned by [Artifact.ID]. This value must
// be deterministically determined ahead of time. // be deterministically determined ahead of time.
ID Checksum ID Checksum
) )
@@ -71,75 +65,18 @@ func MustDecode(s string) (checksum Checksum) {
return return
} }
// common holds elements and receives methods shared between different contexts.
type common struct {
// Address of underlying [Cache], should be zeroed or made unusable after
// Cure returns and must not be exposed directly.
cache *Cache
}
// TContext is passed to [TrivialArtifact.Cure] and provides information and // TContext is passed to [TrivialArtifact.Cure] and provides information and
// methods required for curing the [TrivialArtifact]. // methods required for curing the [TrivialArtifact].
// //
// Methods of TContext are safe for concurrent use. TContext is valid // Methods of TContext are safe for concurrent use. TContext is valid
// until [TrivialArtifact.Cure] returns. // until [TrivialArtifact.Cure] returns.
type TContext struct { type TContext struct {
// Address of underlying [Cache], should be zeroed or made unusable after
// [TrivialArtifact.Cure] returns and must not be exposed directly.
cache *Cache
// Populated during [Cache.Cure]. // Populated during [Cache.Cure].
work, temp *check.Absolute work, temp *check.Absolute
// Target [Artifact] encoded identifier.
ids string
// Pathname status was created at.
statusPath *check.Absolute
// File statusHeader and logs are written to.
status *os.File
// Error value during prepareStatus.
statusErr error
common
}
// statusHeader is the header written to all status files in dirStatus.
var statusHeader = func() string {
s := programName
if v := info.Version(); v != info.FallbackVersion {
s += " " + v
}
s += " (" + runtime.GOARCH + ")"
if name, err := os.Hostname(); err == nil {
s += " on " + name
}
s += "\n\n"
return s
}()
// prepareStatus initialises the status file once.
func (t *TContext) prepareStatus() error {
if t.statusPath != nil || t.status != nil {
return t.statusErr
}
t.statusPath = t.cache.base.Append(
dirStatus,
t.ids,
)
if t.status, t.statusErr = os.OpenFile(
t.statusPath.String(),
syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY,
0400,
); t.statusErr != nil {
return t.statusErr
}
_, t.statusErr = t.status.WriteString(statusHeader)
return t.statusErr
}
// GetStatusWriter returns a [io.Writer] for build logs. The caller must not
// seek this writer before the position it was first returned in.
func (t *TContext) GetStatusWriter() (io.Writer, error) {
err := t.prepareStatus()
return t.status, err
} }
// destroy destroys the temporary directory and joins its errors with the error // destroy destroys the temporary directory and joins its errors with the error
@@ -147,15 +84,12 @@ func (t *TContext) GetStatusWriter() (io.Writer, error) {
// directory is removed similarly. [Cache] is responsible for making sure work // directory is removed similarly. [Cache] is responsible for making sure work
// is never left behind for a successful [Cache.Cure]. // is never left behind for a successful [Cache.Cure].
// //
// If implementation had requested status, it is closed with error joined with
// the error referred to by errP. If the error referred to by errP is non-nil,
// the status file is removed from the filesystem.
//
// destroy must be deferred by [Cache.Cure] if [TContext] is passed to any Cure // destroy must be deferred by [Cache.Cure] if [TContext] is passed to any Cure
// implementation. It should not be called prior to that point. // implementation. It should not be called prior to that point.
func (t *TContext) destroy(errP *error) { func (t *TContext) destroy(errP *error) {
if chmodErr, removeErr := removeAll(t.temp); chmodErr != nil || removeErr != nil { if chmodErr, removeErr := removeAll(t.temp); chmodErr != nil || removeErr != nil {
*errP = errors.Join(*errP, chmodErr, removeErr) *errP = errors.Join(*errP, chmodErr, removeErr)
return
} }
if *errP != nil { if *errP != nil {
@@ -163,31 +97,17 @@ func (t *TContext) destroy(errP *error) {
if chmodErr != nil || removeErr != nil { if chmodErr != nil || removeErr != nil {
*errP = errors.Join(*errP, chmodErr, removeErr) *errP = errors.Join(*errP, chmodErr, removeErr)
} else if errors.Is(*errP, os.ErrExist) { } else if errors.Is(*errP, os.ErrExist) {
var linkError *os.LinkError
if errors.As(*errP, &linkError) && linkError != nil &&
linkError.Op == "rename" {
// two artifacts may be backed by the same file // two artifacts may be backed by the same file
*errP = nil *errP = nil
} }
} }
} }
if t.status != nil {
if err := t.status.Close(); err != nil {
*errP = errors.Join(*errP, err)
}
if *errP != nil {
*errP = errors.Join(*errP, os.Remove(t.statusPath.String()))
}
t.status = nil
}
}
// Unwrap returns the underlying [context.Context]. // Unwrap returns the underlying [context.Context].
func (c *common) Unwrap() context.Context { return c.cache.ctx } func (t *TContext) Unwrap() context.Context { return t.cache.ctx }
// GetMessage returns [message.Msg] held by the underlying [Cache]. // GetMessage returns [message.Msg] held by the underlying [Cache].
func (c *common) GetMessage() message.Msg { return c.cache.msg } func (t *TContext) GetMessage() message.Msg { return t.cache.msg }
// GetWorkDir returns a pathname to a directory which [Artifact] is expected to // GetWorkDir returns a pathname to a directory which [Artifact] is expected to
// write its output to. This is not the final resting place of the [Artifact] // write its output to. This is not the final resting place of the [Artifact]
@@ -206,13 +126,13 @@ func (t *TContext) GetTempDir() *check.Absolute { return t.temp }
// If err is nil, the caller must close the resulting [io.ReadCloser] and return // If err is nil, the caller must close the resulting [io.ReadCloser] and return
// its error, if any. Failure to read r to EOF may result in a spurious // its error, if any. Failure to read r to EOF may result in a spurious
// [ChecksumMismatchError], or the underlying implementation may block on Close. // [ChecksumMismatchError], or the underlying implementation may block on Close.
func (c *common) Open(a Artifact) (r io.ReadCloser, err error) { func (t *TContext) Open(a Artifact) (r io.ReadCloser, err error) {
if f, ok := a.(FileArtifact); ok { if f, ok := a.(FileArtifact); ok {
return c.cache.openFile(f) return t.cache.openFile(f)
} }
var pathname *check.Absolute var pathname *check.Absolute
if pathname, _, err = c.cache.Cure(a); err != nil { if pathname, _, err = t.cache.Cure(a); err != nil {
return return
} }
@@ -244,7 +164,7 @@ type FContext struct {
} }
// InvalidLookupError is the identifier of non-dependency [Artifact] looked up // InvalidLookupError is the identifier of non-dependency [Artifact] looked up
// via [FContext.GetArtifact] by a misbehaving [Artifact] implementation. // via [FContext.Pathname] by a misbehaving [Artifact] implementation.
type InvalidLookupError ID type InvalidLookupError ID
func (e InvalidLookupError) Error() string { func (e InvalidLookupError) Error() string {
@@ -271,7 +191,14 @@ func (f *FContext) GetArtifact(a Artifact) (
// //
// Methods of RContext are safe for concurrent use. RContext is valid // Methods of RContext are safe for concurrent use. RContext is valid
// until [FileArtifact.Cure] returns. // until [FileArtifact.Cure] returns.
type RContext struct{ common } type RContext struct {
// Address of underlying [Cache], should be zeroed or made unusable after
// [FileArtifact.Cure] returns and must not be exposed directly.
cache *Cache
}
// Unwrap returns the underlying [context.Context].
func (r *RContext) Unwrap() context.Context { return r.cache.ctx }
// An Artifact is a read-only reference to a piece of data that may be created // An Artifact is a read-only reference to a piece of data that may be created
// deterministically but might not currently be available in memory or on the // deterministically but might not currently be available in memory or on the
@@ -450,9 +377,6 @@ const (
// dirChecksum is the directory name appended to Cache.base for storing // dirChecksum is the directory name appended to Cache.base for storing
// artifacts named after their [Checksum]. // artifacts named after their [Checksum].
dirChecksum = "checksum" dirChecksum = "checksum"
// dirStatus is the directory name appended to Cache.base for storing
// artifact metadata and logs named after their [ID].
dirStatus = "status"
// dirWork is the directory name appended to Cache.base for working // dirWork is the directory name appended to Cache.base for working
// pathnames set up during [Cache.Cure]. // pathnames set up during [Cache.Cure].
@@ -542,7 +466,7 @@ type Cache struct {
// Synchronises entry into exclusive artifacts for the cure method. // Synchronises entry into exclusive artifacts for the cure method.
exclMu sync.Mutex exclMu sync.Mutex
// Buffered I/O free list, must not be accessed directly. // Buffered I/O free list, must not be accessed directly.
brPool, bwPool sync.Pool bufioPool sync.Pool
// Unlocks the on-filesystem cache. Must only be called from Close. // Unlocks the on-filesystem cache. Must only be called from Close.
unlock func() unlock func()
@@ -624,26 +548,6 @@ func (c *Cache) unsafeIdent(a Artifact, encodeKind bool) (
return return
} }
// getReader is like [bufio.NewReader] but for brPool.
func (c *Cache) getReader(r io.Reader) *bufio.Reader {
br := c.brPool.Get().(*bufio.Reader)
br.Reset(r)
return br
}
// putReader adds br to brPool.
func (c *Cache) putReader(br *bufio.Reader) { c.brPool.Put(br) }
// getWriter is like [bufio.NewWriter] but for bwPool.
func (c *Cache) getWriter(w io.Writer) *bufio.Writer {
bw := c.bwPool.Get().(*bufio.Writer)
bw.Reset(w)
return bw
}
// putWriter adds bw to bwPool.
func (c *Cache) putWriter(bw *bufio.Writer) { c.bwPool.Put(bw) }
// A ChecksumMismatchError describes an [Artifact] with unexpected content. // A ChecksumMismatchError describes an [Artifact] with unexpected content.
type ChecksumMismatchError struct { type ChecksumMismatchError struct {
// Actual and expected checksums. // Actual and expected checksums.
@@ -665,9 +569,6 @@ type ScrubError struct {
// Dangling identifier symlinks. This can happen if the content-addressed // Dangling identifier symlinks. This can happen if the content-addressed
// entry was removed while scrubbing due to a checksum mismatch. // entry was removed while scrubbing due to a checksum mismatch.
DanglingIdentifiers []ID DanglingIdentifiers []ID
// Dangling status files. This can happen if a dangling status symlink was
// removed while scrubbing.
DanglingStatus []ID
// Miscellaneous errors, including [os.ReadDir] on checksum and identifier // Miscellaneous errors, including [os.ReadDir] on checksum and identifier
// directories, [Decode] on entry names and [os.RemoveAll] on inconsistent // directories, [Decode] on entry names and [os.RemoveAll] on inconsistent
// entries. // entries.
@@ -719,13 +620,6 @@ func (e *ScrubError) Error() string {
} }
segments = append(segments, s) segments = append(segments, s)
} }
if len(e.DanglingStatus) > 0 {
s := "dangling status:\n"
for _, id := range e.DanglingStatus {
s += Encode(id) + "\n"
}
segments = append(segments, s)
}
if len(e.Errs) > 0 { if len(e.Errs) > 0 {
s := "errors during scrub:\n" s := "errors during scrub:\n"
for pathname, errs := range e.errs { for pathname, errs := range e.errs {
@@ -904,36 +798,6 @@ func (c *Cache) Scrub(checks int) error {
wg.Wait() wg.Wait()
} }
dir = c.base.Append(dirStatus)
if entries, readdirErr := os.ReadDir(dir.String()); readdirErr != nil {
if !errors.Is(readdirErr, os.ErrNotExist) {
addErr(dir, readdirErr)
}
} else {
wg.Add(len(entries))
for _, ent := range entries {
w <- checkEntry{ent, func(ent os.DirEntry, want *Checksum) bool {
got := p.Get().(*Checksum)
defer p.Put(got)
if _, err := os.Stat(c.base.Append(
dirIdentifier,
ent.Name(),
).String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
addErr(dir.Append(ent.Name()), err)
}
seMu.Lock()
se.DanglingStatus = append(se.DanglingStatus, *want)
seMu.Unlock()
return false
}
return true
}}
}
wg.Wait()
}
if len(c.identPending) > 0 { if len(c.identPending) > 0 {
addErr(c.base, errors.New( addErr(c.base, errors.New(
"scrub began with pending artifacts", "scrub began with pending artifacts",
@@ -964,7 +828,6 @@ func (c *Cache) Scrub(checks int) error {
if len(se.ChecksumMismatches) > 0 || if len(se.ChecksumMismatches) > 0 ||
len(se.DanglingIdentifiers) > 0 || len(se.DanglingIdentifiers) > 0 ||
len(se.DanglingStatus) > 0 ||
len(se.Errs) > 0 { len(se.Errs) > 0 {
slices.SortFunc(se.ChecksumMismatches, func(a, b ChecksumMismatchError) int { slices.SortFunc(se.ChecksumMismatches, func(a, b ChecksumMismatchError) int {
return bytes.Compare(a.Want[:], b.Want[:]) return bytes.Compare(a.Want[:], b.Want[:])
@@ -972,9 +835,6 @@ func (c *Cache) Scrub(checks int) error {
slices.SortFunc(se.DanglingIdentifiers, func(a, b ID) int { slices.SortFunc(se.DanglingIdentifiers, func(a, b ID) int {
return bytes.Compare(a[:], b[:]) return bytes.Compare(a[:], b[:])
}) })
slices.SortFunc(se.DanglingStatus, func(a, b ID) int {
return bytes.Compare(a[:], b[:])
})
return &se return &se
} else { } else {
return nil return nil
@@ -1065,17 +925,16 @@ func (c *Cache) openFile(f FileArtifact) (r io.ReadCloser, err error) {
if !errors.Is(err, os.ErrNotExist) { if !errors.Is(err, os.ErrNotExist) {
return return
} }
id := c.Ident(f)
if c.msg.IsVerbose() { if c.msg.IsVerbose() {
rn := reportName(f, id) rn := reportName(f, c.Ident(f))
c.msg.Verbosef("curing %s in memory...", rn) c.msg.Verbosef("curing %s to memory...", rn)
defer func() { defer func() {
if err == nil { if err == nil {
c.msg.Verbosef("opened %s for reading", rn) c.msg.Verbosef("cured %s to memory", rn)
} }
}() }()
} }
return f.Cure(&RContext{common{c}}) return f.Cure(&RContext{c})
} }
return return
} }
@@ -1272,9 +1131,6 @@ type DependencyCureError []*CureError
// unwrapM recursively expands underlying errors into a caller-supplied map. // unwrapM recursively expands underlying errors into a caller-supplied map.
func (e *DependencyCureError) unwrapM(me map[unique.Handle[ID]]*CureError) { func (e *DependencyCureError) unwrapM(me map[unique.Handle[ID]]*CureError) {
for _, err := range *e { for _, err := range *e {
if _, ok := me[err.Ident]; ok {
continue
}
if _e, ok := err.Err.(*DependencyCureError); ok { if _e, ok := err.Err.(*DependencyCureError); ok {
_e.unwrapM(me) _e.unwrapM(me)
continue continue
@@ -1358,6 +1214,13 @@ func (c *Cache) exitCure(a Artifact, curesExempt bool) {
<-c.cures <-c.cures
} }
// getWriter is like [bufio.NewWriter] but for bufioPool.
func (c *Cache) getWriter(w io.Writer) *bufio.Writer {
bw := c.bufioPool.Get().(*bufio.Writer)
bw.Reset(w)
return bw
}
// measuredReader implements [io.ReadCloser] and measures the checksum during // measuredReader implements [io.ReadCloser] and measures the checksum during
// Close. If the underlying reader is not read to EOF, Close blocks until all // Close. If the underlying reader is not read to EOF, Close blocks until all
// remaining data is consumed and validated. // remaining data is consumed and validated.
@@ -1440,6 +1303,9 @@ func (r *RContext) NewMeasuredReader(
return r.cache.newMeasuredReader(rc, checksum) return r.cache.newMeasuredReader(rc, checksum)
} }
// putWriter adds bw to bufioPool.
func (c *Cache) putWriter(bw *bufio.Writer) { c.bufioPool.Put(bw) }
// cure implements Cure without checking the full dependency graph. // cure implements Cure without checking the full dependency graph.
func (c *Cache) cure(a Artifact, curesExempt bool) ( func (c *Cache) cure(a Artifact, curesExempt bool) (
pathname *check.Absolute, pathname *check.Absolute,
@@ -1571,7 +1437,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
if err = c.enterCure(a, curesExempt); err != nil { if err = c.enterCure(a, curesExempt); err != nil {
return return
} }
r, err = f.Cure(&RContext{common{c}}) r, err = f.Cure(&RContext{c})
if err == nil { if err == nil {
if checksumPathname == nil || c.IsStrict() { if checksumPathname == nil || c.IsStrict() {
h := sha512.New384() h := sha512.New384()
@@ -1647,12 +1513,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
return return
} }
t := TContext{ t := TContext{c, c.base.Append(dirWork, ids), c.base.Append(dirTemp, ids)}
c.base.Append(dirWork, ids),
c.base.Append(dirTemp, ids),
ids, nil, nil, nil,
common{c},
}
switch ca := a.(type) { switch ca := a.(type) {
case TrivialArtifact: case TrivialArtifact:
defer t.destroy(&err) defer t.destroy(&err)
@@ -1790,18 +1651,6 @@ func (pending *pendingArtifactDep) cure(c *Cache) {
pending.errsMu.Unlock() pending.errsMu.Unlock()
} }
// OpenStatus attempts to open the status file associated to an [Artifact]. If
// err is nil, the caller must close the resulting reader.
func (c *Cache) OpenStatus(a Artifact) (r io.ReadSeekCloser, err error) {
c.identMu.RLock()
r, err = os.Open(c.base.Append(
dirStatus,
Encode(c.Ident(a).Value())).String(),
)
c.identMu.RUnlock()
return
}
// Close cancels all pending cures and waits for them to clean up. // Close cancels all pending cures and waits for them to clean up.
func (c *Cache) Close() { func (c *Cache) Close() {
c.closeOnce.Do(func() { c.closeOnce.Do(func() {
@@ -1850,7 +1699,6 @@ func open(
for _, name := range []string{ for _, name := range []string{
dirIdentifier, dirIdentifier,
dirChecksum, dirChecksum,
dirStatus,
dirWork, dirWork,
} { } {
if err := os.MkdirAll(base.Append(name).String(), 0700); err != nil && if err := os.MkdirAll(base.Append(name).String(), 0700); err != nil &&
@@ -1865,16 +1713,13 @@ func open(
msg: msg, msg: msg,
base: base, base: base,
identPool: sync.Pool{New: func() any { return new(extIdent) }},
ident: make(map[unique.Handle[ID]]unique.Handle[Checksum]), ident: make(map[unique.Handle[ID]]unique.Handle[Checksum]),
identErr: make(map[unique.Handle[ID]]error), identErr: make(map[unique.Handle[ID]]error),
identPending: make(map[unique.Handle[ID]]<-chan struct{}), identPending: make(map[unique.Handle[ID]]<-chan struct{}),
brPool: sync.Pool{New: func() any { return new(bufio.Reader) }},
bwPool: sync.Pool{New: func() any { return new(bufio.Writer) }},
} }
c.ctx, c.cancel = context.WithCancel(ctx) c.ctx, c.cancel = context.WithCancel(ctx)
c.identPool.New = func() any { return new(extIdent) }
c.bufioPool.New = func() any { return new(bufio.Writer) }
if lock || !testing.Testing() { if lock || !testing.Testing() {
if unlock, err := lockedfile.MutexAt( if unlock, err := lockedfile.MutexAt(

View File

@@ -314,11 +314,6 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
t.Fatal(err) t.Fatal(err)
} }
// destroy non-deterministic status files
if err := os.RemoveAll(base.Append("status").String()); err != nil {
t.Fatal(err)
}
var checksum pkg.Checksum var checksum pkg.Checksum
if err := pkg.HashDir(&checksum, base); err != nil { if err := pkg.HashDir(&checksum, base); err != nil {
t.Fatalf("HashDir: error = %v", err) t.Fatalf("HashDir: error = %v", err)
@@ -387,9 +382,6 @@ func cureMany(t *testing.T, c *pkg.Cache, steps []cureStep) {
} else if step.pathname != ignorePathname && !pathname.Is(step.pathname) { } else if step.pathname != ignorePathname && !pathname.Is(step.pathname) {
t.Fatalf("Cure: pathname = %q, want %q", pathname, step.pathname) t.Fatalf("Cure: pathname = %q, want %q", pathname, step.pathname)
} else if checksum != makeChecksumH(step.checksum) { } else if checksum != makeChecksumH(step.checksum) {
if checksum == (unique.Handle[pkg.Checksum]{}) {
checksum = unique.Make(pkg.Checksum{})
}
t.Fatalf( t.Fatalf(
"Cure: checksum = %s, want %s", "Cure: checksum = %s, want %s",
pkg.Encode(checksum.Value()), pkg.Encode(step.checksum), pkg.Encode(checksum.Value()), pkg.Encode(step.checksum),

View File

@@ -10,7 +10,8 @@ import (
"io/fs" "io/fs"
"net/http" "net/http"
"os" "os"
"path"
"hakurei.app/container/check"
) )
const ( const (
@@ -99,6 +100,7 @@ func (e DisallowedTypeflagError) Error() string {
// Cure cures the [Artifact], producing a directory located at work. // Cure cures the [Artifact], producing a directory located at work.
func (a *tarArtifact) Cure(t *TContext) (err error) { func (a *tarArtifact) Cure(t *TContext) (err error) {
temp := t.GetTempDir()
var tr io.ReadCloser var tr io.ReadCloser
if tr, err = t.Open(a.f); err != nil { if tr, err = t.Open(a.f); err != nil {
return return
@@ -114,9 +116,7 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
err = closeErr err = closeErr
} }
}(tr) }(tr)
br := t.cache.getReader(tr) tr = io.NopCloser(tr)
defer t.cache.putReader(br)
tr = io.NopCloser(br)
switch a.compression { switch a.compression {
case TarUncompressed: case TarUncompressed:
@@ -137,24 +137,14 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
} }
type dirTargetPerm struct { type dirTargetPerm struct {
path string path *check.Absolute
mode fs.FileMode mode fs.FileMode
} }
var madeDirectories []dirTargetPerm var madeDirectories []dirTargetPerm
if err = os.MkdirAll(t.GetTempDir().String(), 0700); err != nil { if err = os.MkdirAll(temp.String(), 0700); err != nil {
return return
} }
var root *os.Root
if root, err = os.OpenRoot(t.GetTempDir().String()); err != nil {
return
}
defer func() {
closeErr := root.Close()
if err == nil {
err = closeErr
}
}()
var header *tar.Header var header *tar.Header
r := tar.NewReader(tr) r := tar.NewReader(tr)
@@ -168,8 +158,9 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
} }
} }
pathname := temp.Append(header.Name)
if typeflag >= '0' && typeflag <= '9' && typeflag != tar.TypeDir { if typeflag >= '0' && typeflag <= '9' && typeflag != tar.TypeDir {
if err = root.MkdirAll(path.Dir(header.Name), 0700); err != nil { if err = os.MkdirAll(pathname.Dir().String(), 0700); err != nil {
return return
} }
} }
@@ -177,8 +168,8 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
switch typeflag { switch typeflag {
case tar.TypeReg: case tar.TypeReg:
var f *os.File var f *os.File
if f, err = root.OpenFile( if f, err = os.OpenFile(
header.Name, pathname.String(),
os.O_CREATE|os.O_EXCL|os.O_WRONLY, os.O_CREATE|os.O_EXCL|os.O_WRONLY,
header.FileInfo().Mode()&0500, header.FileInfo().Mode()&0500,
); err != nil { ); err != nil {
@@ -193,29 +184,26 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
break break
case tar.TypeLink: case tar.TypeLink:
if err = root.Link( if err = os.Link(
header.Linkname, temp.Append(header.Linkname).String(),
header.Name, pathname.String(),
); err != nil { ); err != nil {
return return
} }
break break
case tar.TypeSymlink: case tar.TypeSymlink:
if err = root.Symlink( if err = os.Symlink(header.Linkname, pathname.String()); err != nil {
header.Linkname,
header.Name,
); err != nil {
return return
} }
break break
case tar.TypeDir: case tar.TypeDir:
madeDirectories = append(madeDirectories, dirTargetPerm{ madeDirectories = append(madeDirectories, dirTargetPerm{
path: header.Name, path: pathname,
mode: header.FileInfo().Mode(), mode: header.FileInfo().Mode(),
}) })
if err = root.MkdirAll(header.Name, 0700); err != nil { if err = os.MkdirAll(pathname.String(), 0700); err != nil {
return return
} }
break break
@@ -232,7 +220,7 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
} }
if err == nil { if err == nil {
for _, e := range madeDirectories { for _, e := range madeDirectories {
if err = root.Chmod(e.path, e.mode&0500); err != nil { if err = os.Chmod(e.path.String(), e.mode&0500); err != nil {
return return
} }
} }
@@ -240,7 +228,6 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
return return
} }
temp := t.GetTempDir()
if err = os.Chmod(temp.String(), 0700); err != nil { if err = os.Chmod(temp.String(), 0700); err != nil {
return return
} }

View File

@@ -2,19 +2,18 @@ package rosa
import "hakurei.app/internal/pkg" import "hakurei.app/internal/pkg"
func (t Toolchain) newAttr() (pkg.Artifact, string) { func (t Toolchain) newAttr() pkg.Artifact {
const ( const (
version = "2.5.2" version = "2.5.2"
checksum = "YWEphrz6vg1sUMmHHVr1CRo53pFXRhq_pjN-AlG8UgwZK1y6m7zuDhxqJhD0SV0l" checksum = "YWEphrz6vg1sUMmHHVr1CRo53pFXRhq_pjN-AlG8UgwZK1y6m7zuDhxqJhD0SV0l"
) )
return t.NewPackage("attr", version, pkg.NewHTTPGetTar( return t.NewViaMake("attr", version, t.NewPatchedSource(
"attr", version, pkg.NewHTTPGetTar(
nil, "https://download.savannah.nongnu.org/releases/attr/"+ nil, "https://download.savannah.nongnu.org/releases/attr/"+
"attr-"+version+".tar.gz", "attr-"+version+".tar.gz",
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, pkg.TarGzip,
), &PackageAttr{ ), true, [2]string{"libgen-basename", `From 8a80d895dfd779373363c3a4b62ecce5a549efb2 Mon Sep 17 00:00:00 2001
Patches: [][2]string{
{"libgen-basename", `From 8a80d895dfd779373363c3a4b62ecce5a549efb2 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Sat, 30 Mar 2024 10:17:10 +0100 Date: Sat, 30 Mar 2024 10:17:10 +0100
Subject: tools/attr.c: Add missing libgen.h include for basename(3) Subject: tools/attr.c: Add missing libgen.h include for basename(3)
@@ -39,9 +38,7 @@ index f12e4af..6a3c1e9 100644
#include <attr/attributes.h> #include <attr/attributes.h>
-- --
cgit v1.1`}, cgit v1.1`}, [2]string{"musl-errno", `diff --git a/test/attr.test b/test/attr.test
{"musl-errno", `diff --git a/test/attr.test b/test/attr.test
index 6ce2f9b..e9bde92 100644 index 6ce2f9b..e9bde92 100644
--- a/test/attr.test --- a/test/attr.test
+++ b/test/attr.test +++ b/test/attr.test
@@ -55,56 +52,39 @@ index 6ce2f9b..e9bde92 100644
$ setfattr -n user. -v value f $ setfattr -n user. -v value f
> setfattr: f: Invalid argument > setfattr: f: Invalid argument
`}, `},
}, ), &MakeAttr{
ScriptEarly: ` ScriptEarly: `
ln -s ../../system/bin/perl /usr/bin ln -s ../../system/bin/perl /usr/bin
`, `,
}, (*MakeHelper)(nil), Configure: [][2]string{
Perl, {"enable-static"},
), version },
},
t.Load(Perl),
)
} }
func init() { func init() { artifactsF[Attr] = Toolchain.newAttr }
artifactsM[Attr] = Metadata{
f: Toolchain.newAttr,
Name: "attr", func (t Toolchain) newACL() pkg.Artifact {
Description: "Commands for Manipulating Filesystem Extended Attributes",
Website: "https://savannah.nongnu.org/projects/attr/",
ID: 137,
}
}
func (t Toolchain) newACL() (pkg.Artifact, string) {
const ( const (
version = "2.3.2" version = "2.3.2"
checksum = "-fY5nwH4K8ZHBCRXrzLdguPkqjKI6WIiGu4dBtrZ1o0t6AIU73w8wwJz_UyjIS0P" checksum = "-fY5nwH4K8ZHBCRXrzLdguPkqjKI6WIiGu4dBtrZ1o0t6AIU73w8wwJz_UyjIS0P"
) )
return t.NewPackage("acl", version, pkg.NewHTTPGetTar( return t.NewViaMake("acl", version, pkg.NewHTTPGetTar(
nil, "https://download.savannah.nongnu.org/releases/acl/"+ nil,
"https://download.savannah.nongnu.org/releases/acl/"+
"acl-"+version+".tar.gz", "acl-"+version+".tar.gz",
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, pkg.TarGzip,
), nil, &MakeHelper{ ), &MakeAttr{
Configure: [][2]string{
{"enable-static"},
},
// makes assumptions about uid_map/gid_map // makes assumptions about uid_map/gid_map
SkipCheck: true, SkipCheck: true,
}, },
Attr, t.Load(Attr),
), version )
}
func init() {
artifactsM[ACL] = Metadata{
f: Toolchain.newACL,
Name: "acl",
Description: "Commands for Manipulating POSIX Access Control Lists",
Website: "https://savannah.nongnu.org/projects/acl/",
Dependencies: P{
Attr,
},
ID: 16,
}
} }
func init() { artifactsF[ACL] = Toolchain.newACL }

View File

@@ -1,12 +1,6 @@
package rosa package rosa
import ( import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"sync" "sync"
"hakurei.app/internal/pkg" "hakurei.app/internal/pkg"
@@ -16,50 +10,20 @@ import (
type PArtifact int type PArtifact int
const ( const (
LLVMCompilerRT PArtifact = iota ACL PArtifact = iota
LLVMRuntimes
LLVMClang
// EarlyInit is the Rosa OS init program.
EarlyInit
// ImageSystem is the Rosa OS /system image.
ImageSystem
// ImageInitramfs is the Rosa OS initramfs archive.
ImageInitramfs
// Kernel is the generic Rosa OS Linux kernel.
Kernel
// KernelHeaders is an installation of kernel headers for [Kernel].
KernelHeaders
// KernelSource is a writable kernel source tree installed to [AbsUsrSrc].
KernelSource
// Firmware is firmware blobs for use with the Linux kernel.
Firmware
ACL
ArgpStandalone
Attr Attr
Autoconf Autoconf
Automake Automake
BC
Bash Bash
Binutils Binutils
Bison
Bzip2
CMake CMake
Coreutils Coreutils
Curl Curl
DTC
Diffutils Diffutils
Elfutils
Fakeroot
Findutils Findutils
Flex
Fuse Fuse
GMP
GLib
Gawk Gawk
GenInitCPIO GMP
Gettext Gettext
Git Git
Go Go
@@ -68,11 +32,10 @@ const (
Gzip Gzip
Hakurei Hakurei
HakureiDist HakureiDist
Kmod IniConfig
KernelHeaders
LibXau LibXau
Libcap
Libexpat Libexpat
Libiconv
Libpsl Libpsl
Libffi Libffi
Libgd Libgd
@@ -80,66 +43,31 @@ const (
Libseccomp Libseccomp
Libucontext Libucontext
Libxml2 Libxml2
Libxslt
M4 M4
MPC MPC
MPFR MPFR
Make Make
Meson Meson
Mksh Mksh
MuslFts
MuslObstack
NSS NSS
NSSCACert NSSCACert
Ncurses
Nettle
Ninja Ninja
OpenSSL OpenSSL
PCRE2 Packaging
Parallel
Patch Patch
Perl Perl
PerlLocaleGettext
PerlMIMECharset
PerlModuleBuild
PerlPodParser
PerlSGMLS
PerlTermReadKey
PerlTextCharWidth
PerlTextWrapI18N
PerlUnicodeGCString
PerlYAMLTiny
PkgConfig PkgConfig
Procps Pluggy
PyTest
Pygments
Python Python
PythonCfgv
PythonDiscovery
PythonDistlib
PythonFilelock
PythonIdentify
PythonIniConfig
PythonNodeenv
PythonPackaging
PythonPlatformdirs
PythonPluggy
PythonPreCommit
PythonPyTest
PythonPyYAML
PythonPygments
PythonVirtualenv
QEMU
Rdfind
Rsync Rsync
Sed Sed
Setuptools Setuptools
SquashfsTools
TamaGo
Tar
Texinfo
Toybox Toybox
toyboxEarly toyboxEarly
Unzip Unzip
UtilLinux utilMacros
Wayland Wayland
WaylandProtocols WaylandProtocols
XCB XCB
@@ -147,187 +75,101 @@ const (
Xproto Xproto
XZ XZ
Zlib Zlib
Zstd
// PresetUnexportedStart is the first unexported preset. buildcatrust
PresetUnexportedStart
buildcatrust = iota - 1
utilMacros
// Musl is a standalone libc that does not depend on the toolchain.
Musl
// gcc is a hacked-to-pieces GCC toolchain meant for use in intermediate // gcc is a hacked-to-pieces GCC toolchain meant for use in intermediate
// stages only. This preset and its direct output must never be exposed. // stages only. This preset and its direct output must never be exposed.
gcc gcc
// Stage0 is a tarball containing all compile-time dependencies of artifacts // _presetEnd is the total number of presets and does not denote a preset.
// part of the [Std] toolchain. _presetEnd
Stage0
// PresetEnd is the total number of presets and does not denote a preset.
PresetEnd
) )
// P represents multiple [PArtifact] and is stable through JSON.
type P []PArtifact
// MarshalJSON represents [PArtifact] by their [Metadata.Name].
func (s P) MarshalJSON() ([]byte, error) {
names := make([]string, len(s))
for i, p := range s {
names[i] = GetMetadata(p).Name
}
return json.Marshal(names)
}
// UnmarshalJSON resolves the value created by MarshalJSON back to [P].
func (s *P) UnmarshalJSON(data []byte) error {
var names []string
if err := json.Unmarshal(data, &names); err != nil {
return err
}
*s = make(P, len(names))
for i, name := range names {
if p, ok := ResolveName(name); !ok {
return fmt.Errorf("unknown artifact %q", name)
} else {
(*s)[i] = p
}
}
return nil
}
// Metadata is stage-agnostic information of a [PArtifact] not directly
// representable in the resulting [pkg.Artifact].
type Metadata struct {
f func(t Toolchain) (a pkg.Artifact, version string)
// Unique package name.
Name string `json:"name"`
// Short user-facing description.
Description string `json:"description"`
// Project home page.
Website string `json:"website,omitempty"`
// Runtime dependencies.
Dependencies P `json:"dependencies"`
// Project identifier on [Anitya].
//
// [Anitya]: https://release-monitoring.org/
ID int `json:"-"`
// Optional custom version checking behaviour.
latest func(v *Versions) string
}
// GetLatest returns the latest version described by v.
func (meta *Metadata) GetLatest(v *Versions) string {
if meta.latest != nil {
return meta.latest(v)
}
return v.Latest
}
// Unversioned denotes an unversioned [PArtifact].
const Unversioned = "\x00"
// UnpopulatedIDError is returned by [Metadata.GetLatest] for an instance of
// [Metadata] where ID is not populated.
type UnpopulatedIDError struct{}
func (UnpopulatedIDError) Unwrap() error { return errors.ErrUnsupported }
func (UnpopulatedIDError) Error() string { return "Anitya ID is not populated" }
// Versions are package versions returned by Anitya.
type Versions struct {
// The latest version for the project, as determined by the version sorting algorithm.
Latest string `json:"latest_version"`
// List of all versions that arent flagged as pre-release.
Stable []string `json:"stable_versions"`
// List of all versions stored, sorted from newest to oldest.
All []string `json:"versions"`
}
// getStable returns the first Stable version, or Latest if that is unavailable.
func (v *Versions) getStable() string {
if len(v.Stable) == 0 {
return v.Latest
}
return v.Stable[0]
}
// GetVersions returns versions fetched from Anitya.
func (meta *Metadata) GetVersions(ctx context.Context) (*Versions, error) {
if meta.ID == 0 {
return nil, UnpopulatedIDError{}
}
var resp *http.Response
if req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"https://release-monitoring.org/api/v2/versions/?project_id="+
strconv.Itoa(meta.ID),
nil,
); err != nil {
return nil, err
} else {
req.Header.Set("User-Agent", "Rosa/1.1")
if resp, err = http.DefaultClient.Do(req); err != nil {
return nil, err
}
}
var v Versions
err := json.NewDecoder(resp.Body).Decode(&v)
return &v, errors.Join(err, resp.Body.Close())
}
var ( var (
// artifactsM is an array of [PArtifact] metadata. // artifactsF is an array of functions for the result of [PArtifact].
artifactsM [PresetEnd]Metadata artifactsF [_presetEnd]func(t Toolchain) pkg.Artifact
// artifacts stores the result of Metadata.f. // artifacts stores the result of artifactsF.
artifacts [_toolchainEnd][len(artifactsM)]struct { artifacts [_toolchainEnd][len(artifactsF)]pkg.Artifact
a pkg.Artifact
v string
}
// artifactsOnce is for lazy initialisation of artifacts. // artifactsOnce is for lazy initialisation of artifacts.
artifactsOnce [_toolchainEnd][len(artifactsM)]sync.Once artifactsOnce [_toolchainEnd][len(artifactsF)]sync.Once
) )
// GetMetadata returns [Metadata] of a [PArtifact].
func GetMetadata(p PArtifact) *Metadata { return &artifactsM[p] }
// construct constructs a [pkg.Artifact] corresponding to a [PArtifact] once.
func (t Toolchain) construct(p PArtifact) {
artifactsOnce[t][p].Do(func() {
artifacts[t][p].a, artifacts[t][p].v = artifactsM[p].f(t)
})
}
// Load returns the resulting [pkg.Artifact] of [PArtifact]. // Load returns the resulting [pkg.Artifact] of [PArtifact].
func (t Toolchain) Load(p PArtifact) pkg.Artifact { func (t Toolchain) Load(p PArtifact) pkg.Artifact {
t.construct(p) artifactsOnce[t][p].Do(func() {
return artifacts[t][p].a artifacts[t][p] = artifactsF[p](t)
} })
return artifacts[t][p]
// Version returns the version string of [PArtifact].
func (t Toolchain) Version(p PArtifact) string {
t.construct(p)
return artifacts[t][p].v
} }
// ResolveName returns a [PArtifact] by name. // ResolveName returns a [PArtifact] by name.
func ResolveName(name string) (p PArtifact, ok bool) { func ResolveName(name string) (p PArtifact, ok bool) {
for i := range PresetUnexportedStart { p, ok = map[string]PArtifact{
if name == artifactsM[i].Name { "acl": ACL,
return i, true "attr": Attr,
} "autoconf": Autoconf,
} "automake": Automake,
return 0, false "bash": Bash,
"binutils": Binutils,
"cmake": CMake,
"coreutils": Coreutils,
"curl": Curl,
"diffutils": Diffutils,
"findutils": Findutils,
"fuse": Fuse,
"gawk": Gawk,
"gmp": GMP,
"gettext": Gettext,
"git": Git,
"go": Go,
"gperf": Gperf,
"grep": Grep,
"gzip": Gzip,
"hakurei": Hakurei,
"hakurei-dist": HakureiDist,
"iniconfig": IniConfig,
"kernel-headers": KernelHeaders,
"libXau": LibXau,
"libexpat": Libexpat,
"libpsl": Libpsl,
"libseccomp": Libseccomp,
"libucontext": Libucontext,
"libxml2": Libxml2,
"libffi": Libffi,
"libgd": Libgd,
"libtool": Libtool,
"m4": M4,
"mpc": MPC,
"mpfr": MPFR,
"make": Make,
"meson": Meson,
"mksh": Mksh,
"nss": NSS,
"nss-cacert": NSSCACert,
"ninja": Ninja,
"openssl": OpenSSL,
"packaging": Packaging,
"patch": Patch,
"perl": Perl,
"pkg-config": PkgConfig,
"pluggy": Pluggy,
"pytest": PyTest,
"pygments": Pygments,
"python": Python,
"rsync": Rsync,
"sed": Sed,
"setuptools": Setuptools,
"toybox": Toybox,
"unzip": Unzip,
"wayland": Wayland,
"wayland-protocols": WaylandProtocols,
"xcb": XCB,
"xcb-proto": XCBProto,
"xproto": Xproto,
"xz": XZ,
"zlib": Zlib,
}[name]
return
} }

View File

@@ -1,66 +0,0 @@
package rosa_test
import (
"testing"
"hakurei.app/internal/rosa"
)
func TestLoad(t *testing.T) {
t.Parallel()
for i := range rosa.PresetEnd {
p := rosa.PArtifact(i)
t.Run(rosa.GetMetadata(p).Name, func(t *testing.T) {
t.Parallel()
rosa.Std.Load(p)
})
}
}
func TestResolveName(t *testing.T) {
t.Parallel()
for i := range rosa.PresetUnexportedStart {
p := i
name := rosa.GetMetadata(p).Name
t.Run(name, func(t *testing.T) {
t.Parallel()
if got, ok := rosa.ResolveName(name); !ok {
t.Fatal("ResolveName: ok = false")
} else if got != p {
t.Fatalf("ResolveName: %d, want %d", got, p)
}
})
}
}
func TestResolveNameUnexported(t *testing.T) {
t.Parallel()
for i := rosa.PresetUnexportedStart; i < rosa.PresetEnd; i++ {
p := i
name := rosa.GetMetadata(p).Name
t.Run(name, func(t *testing.T) {
t.Parallel()
if got, ok := rosa.ResolveName(name); ok {
t.Fatalf("ResolveName: resolved unexported preset %d", got)
}
})
}
}
func TestUnique(t *testing.T) {
t.Parallel()
names := make(map[string]struct{})
for i := range rosa.PresetEnd {
name := rosa.GetMetadata(rosa.PArtifact(i)).Name
if _, ok := names[name]; ok {
t.Fatalf("name %s is not unique", name)
}
names[name] = struct{}{}
}
}

View File

@@ -1,36 +0,0 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newArgpStandalone() (pkg.Artifact, string) {
const (
version = "1.3"
checksum = "vtW0VyO2pJ-hPyYmDI2zwSLS8QL0sPAUKC1t3zNYbwN2TmsaE-fADhaVtNd3eNFl"
)
return t.NewPackage("argp-standalone", version, pkg.NewHTTPGetTar(
nil, "http://www.lysator.liu.se/~nisse/misc/"+
"argp-standalone-"+version+".tar.gz",
mustDecode(checksum),
pkg.TarGzip,
), &PackageAttr{
Env: []string{
"CC=cc -std=gnu89 -fPIC",
},
}, &MakeHelper{
Install: `
install -D -m644 /usr/src/argp-standalone/argp.h /work/system/include/argp.h
install -D -m755 libargp.a /work/system/lib/libargp.a
`,
},
Diffutils,
), version
}
func init() {
artifactsM[ArgpStandalone] = Metadata{
f: Toolchain.newArgpStandalone,
Name: "argp-standalone",
Description: "hierarchical argument parsing library broken out from glibc",
Website: "http://www.lysator.liu.se/~nisse/misc/",
}
}

View File

@@ -1,38 +0,0 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newBzip2() (pkg.Artifact, string) {
const (
version = "1.0.8"
checksum = "cTLykcco7boom-s05H1JVsQi1AtChYL84nXkg_92Dm1Xt94Ob_qlMg_-NSguIK-c"
)
return t.NewPackage("bzip2", version, pkg.NewHTTPGetTar(
nil, "https://sourceware.org/pub/bzip2/bzip2-"+version+".tar.gz",
mustDecode(checksum),
pkg.TarGzip,
), &PackageAttr{
Writable: true,
EnterSource: true,
}, &MakeHelper{
// uses source tree as scratch space
SkipConfigure: true,
SkipCheck: true,
InPlace: true,
Make: []string{
"CC=cc",
},
Install: "make PREFIX=/work/system install",
}), version
}
func init() {
artifactsM[Bzip2] = Metadata{
f: Toolchain.newBzip2,
Name: "bzip2",
Description: "a freely available, patent free, high-quality data compressor",
Website: "https://sourceware.org/bzip2/",
ID: 237,
}
}

View File

@@ -1,175 +1,82 @@
package rosa package rosa
import ( import (
"path"
"slices" "slices"
"strings" "strings"
"hakurei.app/container/check"
"hakurei.app/internal/pkg" "hakurei.app/internal/pkg"
) )
func (t Toolchain) newCMake() (pkg.Artifact, string) { func (t Toolchain) newCMake() pkg.Artifact {
const ( const (
version = "4.2.3" version = "4.2.1"
checksum = "Y4uYGnLrDQX78UdzH7fMzfok46Nt_1taDIHSmqgboU1yFi6f0iAXBDegMCu4eS-J" checksum = "Y3OdbMsob6Xk2y1DCME6z4Fryb5_TkFD7knRT8dTNIRtSqbiCJyyDN9AxggN_I75"
) )
return t.NewPackage("cmake", version, pkg.NewHTTPGetTar( return t.New("cmake-"+version, 0, []pkg.Artifact{
t.Load(Make),
t.Load(KernelHeaders),
}, nil, nil, `
cd "$(mktemp -d)"
/usr/src/cmake/bootstrap \
--prefix=/system \
--parallel="$(nproc)" \
-- \
-DCMAKE_USE_OPENSSL=OFF
make "-j$(nproc)"
make DESTDIR=/work install
`, pkg.Path(AbsUsrSrc.Append("cmake"), true, t.NewPatchedSource(
// expected to be writable in the copy made during bootstrap
"cmake", version, pkg.NewHTTPGetTar(
nil, "https://github.com/Kitware/CMake/releases/download/"+ nil, "https://github.com/Kitware/CMake/releases/download/"+
"v"+version+"/cmake-"+version+".tar.gz", "v"+version+"/cmake-"+version+".tar.gz",
mustDecode(checksum), mustDecode(checksum),
pkg.TarGzip, pkg.TarGzip,
), &PackageAttr{ ), false,
// test suite expects writable source tree )))
Writable: true,
// expected to be writable in the copy made during bootstrap
Chmod: true,
Patches: [][2]string{
{"bootstrap-test-no-openssl", `diff --git a/Tests/BootstrapTest.cmake b/Tests/BootstrapTest.cmake
index 137de78bc1..b4da52e664 100644
--- a/Tests/BootstrapTest.cmake
+++ b/Tests/BootstrapTest.cmake
@@ -9,7 +9,7 @@ if(NOT nproc EQUAL 0)
endif()
message(STATUS "running bootstrap: ${bootstrap} ${ninja_arg} ${parallel_arg}")
execute_process(
- COMMAND ${bootstrap} ${ninja_arg} ${parallel_arg}
+ COMMAND ${bootstrap} ${ninja_arg} ${parallel_arg} -- -DCMAKE_USE_OPENSSL=OFF
WORKING_DIRECTORY "${bin_dir}"
RESULT_VARIABLE result
)
`},
{"disable-broken-tests-musl", `diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt
index 2ead810437..f85cbb8b1c 100644
--- a/Tests/CMakeLists.txt
+++ b/Tests/CMakeLists.txt
@@ -384,7 +384,6 @@ if(BUILD_TESTING)
add_subdirectory(CMakeLib)
endif()
add_subdirectory(CMakeOnly)
- add_subdirectory(RunCMake)
add_subdirectory(FindPackageModeMakefileTest)
@@ -528,9 +527,6 @@ if(BUILD_TESTING)
-DCMake_TEST_CUDA:BOOL=${CMake_TEST_CUDA}
-DCMake_INSTALL_NAME_TOOL_BUG:BOOL=${CMake_INSTALL_NAME_TOOL_BUG}
)
- ADD_TEST_MACRO(ExportImport ExportImport)
- set_property(TEST ExportImport APPEND
- PROPERTY LABELS "CUDA")
ADD_TEST_MACRO(Unset Unset)
ADD_TEST_MACRO(PolicyScope PolicyScope)
ADD_TEST_MACRO(EmptyLibrary EmptyLibrary)
@@ -624,7 +620,6 @@ if(BUILD_TESTING)
# run test for BundleUtilities on supported platforms/compilers
if((MSVC OR
MINGW OR
- CMAKE_SYSTEM_NAME MATCHES "Linux" OR
CMAKE_SYSTEM_NAME MATCHES "Darwin")
AND NOT CMAKE_GENERATOR STREQUAL "Watcom WMake")
@@ -3095,10 +3090,6 @@ if(BUILD_TESTING)
"${CMake_SOURCE_DIR}/Tests/CTestTestFdSetSize/test.cmake.in"
"${CMake_BINARY_DIR}/Tests/CTestTestFdSetSize/test.cmake"
@ONLY ESCAPE_QUOTES)
- add_test(CTestTestFdSetSize ${CMAKE_CTEST_COMMAND}
- -S "${CMake_BINARY_DIR}/Tests/CTestTestFdSetSize/test.cmake" -j20 -V --timeout 120
- --output-log "${CMake_BINARY_DIR}/Tests/CTestTestFdSetSize/testOutput.log"
- )
if(CMAKE_TESTS_CDASH_SERVER)
set(regex "^([^:]+)://([^/]+)(.*)$")
`},
},
}, &MakeHelper{
OmitDefaults: true,
ConfigureName: "/usr/src/cmake/bootstrap",
Configure: [][2]string{
{"prefix", "/system"},
{"parallel", `"$(nproc)"`},
{"--"},
{"-DCMAKE_USE_OPENSSL", "OFF"},
{"-DCMake_TEST_NO_NETWORK", "ON"},
},
Check: []string{
"CTEST_OUTPUT_ON_FAILURE=1",
"CTEST_PARALLEL_LEVEL=128",
"test",
},
},
KernelHeaders,
), version
} }
func init() { func init() { artifactsF[CMake] = Toolchain.newCMake }
artifactsM[CMake] = Metadata{
f: Toolchain.newCMake,
Name: "cmake",
Description: "cross-platform, open-source build system",
Website: "https://cmake.org/",
ID: 306,
}
}
// CMakeHelper is the [CMake] build system helper.
type CMakeHelper struct {
// Joined with name with a dash if non-empty.
Variant string
// CMakeAttr holds the project-specific attributes that will be applied to a new
// [pkg.Artifact] compiled via [CMake].
type CMakeAttr struct {
// Path elements joined with source. // Path elements joined with source.
Append []string Append []string
// Use source tree as scratch space.
Writable bool
// CMake CACHE entries. // CMake CACHE entries.
Cache [][2]string Cache [][2]string
// Additional environment variables.
Env []string
// Runs before cmake.
ScriptEarly string
// Runs after cmake, replaces default.
ScriptConfigured string
// Runs after install. // Runs after install.
Script string Script string
// Whether to generate Makefile instead. // Override the default installation prefix [AbsSystem].
Make bool Prefix *check.Absolute
// Passed through to [Toolchain.New].
Paths []pkg.ExecPath
// Passed through to [Toolchain.New].
Flag int
} }
var _ Helper = new(CMakeHelper) // NewViaCMake returns a [pkg.Artifact] for compiling and installing via [CMake].
func (t Toolchain) NewViaCMake(
// name returns its arguments and an optional variant string joined with '-'. name, version, variant string,
func (attr *CMakeHelper) name(name, version string) string { source pkg.Artifact,
if attr != nil && attr.Variant != "" { attr *CMakeAttr,
name += "-" + attr.Variant extra ...pkg.Artifact,
) pkg.Artifact {
if name == "" || version == "" || variant == "" {
panic("names must be non-empty")
} }
return name + "-" + version
}
// extra returns a hardcoded slice of [CMake] and [Ninja].
func (attr *CMakeHelper) extra(int) []PArtifact {
if attr != nil && attr.Make {
return []PArtifact{CMake, Make}
}
return []PArtifact{CMake, Ninja}
}
// wantsChmod returns false.
func (*CMakeHelper) wantsChmod() bool { return false }
// wantsWrite returns false.
func (*CMakeHelper) wantsWrite() bool { return false }
// scriptEarly returns the zero value.
func (*CMakeHelper) scriptEarly() string { return "" }
// createDir returns true.
func (*CMakeHelper) createDir() bool { return true }
// wantsDir returns a hardcoded, deterministic pathname.
func (*CMakeHelper) wantsDir() string { return "/cure/" }
// script generates the cure script.
func (attr *CMakeHelper) script(name string) string {
if attr == nil { if attr == nil {
attr = &CMakeHelper{ attr = &CMakeAttr{
Cache: [][2]string{ Cache: [][2]string{
{"CMAKE_BUILD_TYPE", "Release"}, {"CMAKE_BUILD_TYPE", "Release"},
}, },
@@ -179,19 +86,30 @@ func (attr *CMakeHelper) script(name string) string {
panic("CACHE must be non-empty") panic("CACHE must be non-empty")
} }
generate := "Ninja" scriptConfigured := "cmake --build .\ncmake --install .\n"
jobs := "" if attr.ScriptConfigured != "" {
if attr.Make { scriptConfigured = attr.ScriptConfigured
generate = "'Unix Makefiles'"
jobs += ` "--parallel=$(nproc)"`
} }
return ` prefix := attr.Prefix
cmake -G ` + generate + ` \ if prefix == nil {
prefix = AbsSystem
}
sourcePath := AbsUsrSrc.Append(name)
return t.New(name+"-"+variant+"-"+version, attr.Flag, stage3Concat(t, extra,
t.Load(CMake),
t.Load(Ninja),
), nil, slices.Concat([]string{
"ROSA_SOURCE=" + sourcePath.String(),
"ROSA_CMAKE_SOURCE=" + sourcePath.Append(attr.Append...).String(),
"ROSA_INSTALL_PREFIX=/work" + prefix.String(),
}, attr.Env), attr.ScriptEarly+`
mkdir /cure && cd /cure
cmake -G Ninja \
-DCMAKE_C_COMPILER_TARGET="${ROSA_TRIPLE}" \ -DCMAKE_C_COMPILER_TARGET="${ROSA_TRIPLE}" \
-DCMAKE_CXX_COMPILER_TARGET="${ROSA_TRIPLE}" \ -DCMAKE_CXX_COMPILER_TARGET="${ROSA_TRIPLE}" \
-DCMAKE_ASM_COMPILER_TARGET="${ROSA_TRIPLE}" \ -DCMAKE_ASM_COMPILER_TARGET="${ROSA_TRIPLE}" \
-DCMAKE_INSTALL_LIBDIR=lib \
`+strings.Join(slices.Collect(func(yield func(string) bool) { `+strings.Join(slices.Collect(func(yield func(string) bool) {
for _, v := range attr.Cache { for _, v := range attr.Cache {
if !yield("-D" + v[0] + "=" + v[1]) { if !yield("-D" + v[0] + "=" + v[1]) {
@@ -199,9 +117,9 @@ cmake -G ` + generate + ` \
} }
} }
}), " \\\n\t")+` \ }), " \\\n\t")+` \
-DCMAKE_INSTALL_PREFIX=/system \ -DCMAKE_INSTALL_PREFIX="${ROSA_INSTALL_PREFIX}" \
'/usr/src/` + name + `/` + path.Join(attr.Append...) + `' "${ROSA_CMAKE_SOURCE}"
cmake --build .` + jobs + ` `+scriptConfigured+attr.Script, slices.Concat([]pkg.ExecPath{
cmake --install . --prefix=/work/system pkg.Path(sourcePath, attr.Writable, source),
` + attr.Script }, attr.Paths)...)
} }

View File

@@ -2,68 +2,31 @@ package rosa
import "hakurei.app/internal/pkg" import "hakurei.app/internal/pkg"
func (t Toolchain) newCurl() (pkg.Artifact, string) { func (t Toolchain) newCurl() pkg.Artifact {
const ( const (
version = "8.19.0" version = "8.18.0"
checksum = "YHuVLVVp8q_Y7-JWpID5ReNjq2Zk6t7ArHB6ngQXilp_R5l3cubdxu3UKo-xDByv" checksum = "YpOolP_sx1DIrCEJ3elgVAu0wTLDS-EZMZFvOP0eha7FaLueZUlEpuMwDzJNyi7i"
) )
return t.NewPackage("curl", version, pkg.NewHTTPGetTar( return t.NewViaMake("curl", version, pkg.NewHTTPGetTar(
nil, "https://curl.se/download/curl-"+version+".tar.bz2", nil, "https://curl.se/download/curl-"+version+".tar.bz2",
mustDecode(checksum), mustDecode(checksum),
pkg.TarBzip2, pkg.TarBzip2,
), &PackageAttr{ ), &MakeAttr{
Patches: [][2]string{ Env: []string{
{"test459-misplaced-line-break", `diff --git a/tests/data/test459 b/tests/data/test459 "TFLAGS=-j256",
index 7a2e1db7b3..cc716aa65a 100644
--- a/tests/data/test459
+++ b/tests/data/test459
@@ -54,8 +54,8 @@ Content-Type: application/x-www-form-urlencoded
arg
</protocol>
<stderr mode="text">
-Warning: %LOGDIR/config:1 Option 'data' uses argument with unquoted whitespace.%SP
-Warning: This may cause side-effects. Consider double quotes.
+Warning: %LOGDIR/config:1 Option 'data' uses argument with unquoted%SP
+Warning: whitespace. This may cause side-effects. Consider double quotes.
</stderr>
</verify>
</testcase>
`},
}, },
}, &MakeHelper{
Configure: [][2]string{ Configure: [][2]string{
{"with-openssl"}, {"with-openssl"},
{"with-ca-bundle", "/system/etc/ssl/certs/ca-bundle.crt"}, {"with-ca-bundle", "/system/etc/ssl/certs/ca-bundle.crt"},
},
ScriptConfigured: `
make "-j$(nproc)"
`,
},
t.Load(Perl),
{"disable-smb"}, t.Load(Libpsl),
}, t.Load(OpenSSL),
Check: []string{ )
`TFLAGS="-j$(expr "$(nproc)" '*' 2)"`,
"test-nonflaky",
},
},
Perl,
Python,
PkgConfig,
Diffutils,
Libpsl,
OpenSSL,
), version
}
func init() {
artifactsM[Curl] = Metadata{
f: Toolchain.newCurl,
Name: "curl",
Description: "command line tool and library for transferring data with URLs",
Website: "https://curl.se/",
Dependencies: P{
Libpsl,
OpenSSL,
},
ID: 381,
}
} }
func init() { artifactsF[Curl] = Toolchain.newCurl }

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