Compare commits
2 Commits
staging
...
2c323cdf5d
| Author | SHA1 | Date | |
|---|---|---|---|
|
2c323cdf5d
|
|||
|
a1e78d1584
|
@@ -72,23 +72,6 @@ jobs:
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
sharefs:
|
||||
name: ShareFS
|
||||
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.sharefs
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "sharefs-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
hpkg:
|
||||
name: Hpkg
|
||||
runs-on: nix
|
||||
@@ -113,7 +96,6 @@ jobs:
|
||||
- race
|
||||
- sandbox
|
||||
- sandbox-race
|
||||
- sharefs
|
||||
- hpkg
|
||||
runs-on: nix
|
||||
steps:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,7 +27,6 @@ go.work.sum
|
||||
|
||||
# go generate
|
||||
/cmd/hakurei/LICENSE
|
||||
/internal/pkg/testdata/testtool
|
||||
|
||||
# release
|
||||
/dist/hakurei-*
|
||||
|
||||
181
README.md
181
README.md
@@ -15,51 +15,164 @@
|
||||
<a href="https://hakurei.app"><img src="https://img.shields.io/website?url=https%3A%2F%2Fhakurei.app" alt="Website" /></a>
|
||||
</p>
|
||||
|
||||
Hakurei is a tool for running sandboxed desktop applications as dedicated
|
||||
subordinate users on the Linux kernel. It implements the application container
|
||||
of [planterette (WIP)](https://git.gensokyo.uk/security/planterette), a
|
||||
self-contained Android-like package manager with modern security features.
|
||||
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
|
||||
It implements the application container of [planterette (WIP)](https://git.gensokyo.uk/security/planterette),
|
||||
a self-contained Android-like package manager with modern security features.
|
||||
|
||||
Interaction with hakurei happens entirely through structures described by
|
||||
package [hst](https://pkg.go.dev/hakurei.app/hst). No native API is available
|
||||
due to internal details of uid isolation.
|
||||
## NixOS Module usage
|
||||
|
||||
## 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
|
||||
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).
|
||||
To use the module, import it into your configuration with
|
||||
|
||||
Package [internal/pkg](https://pkg.go.dev/hakurei.app/internal/pkg) provides
|
||||
infrastructure for hermetic builds. This replaces the legacy nix-based testing
|
||||
framework and serves as the build system of Rosa OS, currently developed under
|
||||
package [internal/rosa](https://pkg.go.dev/hakurei.app/internal/rosa).
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
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
|
||||
subordinate users.
|
||||
- [wayland](https://gitlab.freedesktop.org/wayland/wayland) to set up
|
||||
[security-context-v1](https://wayland.app/protocols/security-context-v1).
|
||||
- [xcb](https://xcb.freedesktop.org/) to grant and revoke subordinate users
|
||||
access to the X server.
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
|
||||
`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
|
||||
are very likely to be rejected.
|
||||
extraHomeConfig = {
|
||||
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
|
||||
feature-complete. Full module documentation can be found [here](options.md).
|
||||
"org.claws_mail.Claws-Mail" = {
|
||||
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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -91,7 +91,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
|
||||
flagPrivateRuntime, flagPrivateTmpdir bool
|
||||
|
||||
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
|
||||
flagWayland, flagX11, flagDBus, flagPulse bool
|
||||
)
|
||||
|
||||
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error {
|
||||
@@ -146,8 +146,8 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
if flagDBus {
|
||||
et |= hst.EDBus
|
||||
}
|
||||
if flagPipeWire || flagPulse {
|
||||
et |= hst.EPipeWire
|
||||
if flagPulse {
|
||||
et |= hst.EPulse
|
||||
}
|
||||
|
||||
config := &hst.Config{
|
||||
@@ -297,10 +297,8 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
"Enable direct connection to X11").
|
||||
Flag(&flagDBus, "dbus", command.BoolFlag(false),
|
||||
"Enable proxied connection to D-Bus").
|
||||
Flag(&flagPipeWire, "pipewire", command.BoolFlag(false),
|
||||
"Enable connection to PipeWire via SecurityContext").
|
||||
Flag(&flagPulse, "pulse", command.BoolFlag(false),
|
||||
"Enable PulseAudio compatibility daemon")
|
||||
"Enable direct connection to PulseAudio")
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ Commands:
|
||||
},
|
||||
{
|
||||
"run", []string{"run", "-h"}, `
|
||||
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS]
|
||||
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] [--pulse] COMMAND [OPTIONS]
|
||||
|
||||
Flags:
|
||||
-X Enable direct connection to X11
|
||||
@@ -58,14 +58,12 @@ Flags:
|
||||
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
|
||||
-mpris
|
||||
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
||||
-pipewire
|
||||
Enable connection to PipeWire via SecurityContext
|
||||
-private-runtime
|
||||
Do not share XDG_RUNTIME_DIR between containers under the same identity
|
||||
-private-tmpdir
|
||||
Do not share TMPDIR between containers under the same identity
|
||||
-pulse
|
||||
Enable PulseAudio compatibility daemon
|
||||
Enable direct connection to PulseAudio
|
||||
-u string
|
||||
Passwd user name within sandbox (default "chronos")
|
||||
-wayland
|
||||
|
||||
@@ -28,12 +28,12 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
return
|
||||
}
|
||||
|
||||
t.Printf("Version:\t%s (libwayland %s)\n", hi.Version, hi.WaylandVersion)
|
||||
t.Printf("User:\t%d\n", hi.User)
|
||||
t.Printf("TempDir:\t%s\n", hi.TempDir)
|
||||
t.Printf("SharePath:\t%s\n", hi.SharePath)
|
||||
t.Printf("RuntimePath:\t%s\n", hi.RuntimePath)
|
||||
t.Printf("RunDirPath:\t%s\n", hi.RunDirPath)
|
||||
t.Printf("Version:\t%s (libwayland %s) (pipewire %s)\n", hi.Version, hi.WaylandVersion, hi.PipeWireVersion)
|
||||
}
|
||||
|
||||
// printShowInstance writes a representation of [hst.State] or [hst.Config] to output.
|
||||
|
||||
@@ -32,7 +32,7 @@ var (
|
||||
PID: 0xbeef,
|
||||
ShimPID: 0xcafe,
|
||||
Config: &hst.Config{
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EPipeWire),
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EPulse),
|
||||
Identity: 1,
|
||||
Container: &hst.ContainerConfig{
|
||||
Shell: check.MustAbs("/bin/sh"),
|
||||
@@ -62,7 +62,7 @@ func TestPrintShowInstance(t *testing.T) {
|
||||
{"nil", nil, nil, false, false, "Error: invalid configuration!\n\n", false},
|
||||
{"config", nil, hst.Template(), false, false, `App
|
||||
Identity: 9 (org.chromium.Chromium)
|
||||
Enablements: wayland, dbus, pipewire
|
||||
Enablements: wayland, dbus, pulseaudio
|
||||
Groups: video, dialout, plugdev
|
||||
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
||||
Home: /data/data/org.chromium.Chromium
|
||||
@@ -159,7 +159,7 @@ Session bus
|
||||
|
||||
App
|
||||
Identity: 9 (org.chromium.Chromium)
|
||||
Enablements: wayland, dbus, pipewire
|
||||
Enablements: wayland, dbus, pulseaudio
|
||||
Groups: video, dialout, plugdev
|
||||
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
||||
Home: /data/data/org.chromium.Chromium
|
||||
@@ -215,7 +215,7 @@ App
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"dbus": true,
|
||||
"pipewire": true
|
||||
"pulse": true
|
||||
},
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
@@ -366,7 +366,7 @@ App
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"dbus": true,
|
||||
"pipewire": true
|
||||
"pulse": true
|
||||
},
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
@@ -564,7 +564,7 @@ func TestPrintPs(t *testing.T) {
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"dbus": true,
|
||||
"pipewire": true
|
||||
"pulse": true
|
||||
},
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
@@ -715,7 +715,7 @@ func TestPrintPs(t *testing.T) {
|
||||
"shim_pid": 51966,
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"pipewire": true
|
||||
"pulse": true
|
||||
},
|
||||
"identity": 1,
|
||||
"groups": null,
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
allow_wayland ? true,
|
||||
allow_x11 ? false,
|
||||
allow_dbus ? true,
|
||||
allow_audio ? true,
|
||||
allow_pulse ? true,
|
||||
gpu ? allow_wayland || allow_x11,
|
||||
}:
|
||||
|
||||
@@ -175,7 +175,7 @@ let
|
||||
wayland = allow_wayland;
|
||||
x11 = allow_x11;
|
||||
dbus = allow_dbus;
|
||||
pipewire = allow_audio;
|
||||
pulse = allow_pulse;
|
||||
};
|
||||
|
||||
mesa = if gpu then mesaWrappers else null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
testers,
|
||||
nixosTest,
|
||||
callPackage,
|
||||
|
||||
system,
|
||||
@@ -8,7 +8,7 @@
|
||||
let
|
||||
buildPackage = self.buildPackage.${system};
|
||||
in
|
||||
testers.nixosTest {
|
||||
nixosTest {
|
||||
name = "hpkg";
|
||||
nodes.machine = {
|
||||
environment.etc = {
|
||||
|
||||
@@ -90,13 +90,13 @@ 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})
|
||||
check_state("foot", {"wayland": True, "dbus": True, "pulse": True})
|
||||
# Verify acl on XDG_RUNTIME_DIR:
|
||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002"))
|
||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | 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")
|
||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10002")
|
||||
|
||||
# Exit Sway and verify process exit status 0:
|
||||
swaymsg("exit", succeed=False)
|
||||
@@ -107,4 +107,4 @@ 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"))
|
||||
print(machine.succeed("find /run/user/1000/hakurei"))
|
||||
|
||||
250
cmd/mbf/main.go
250
cmd/mbf/main.go
@@ -1,250 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unique"
|
||||
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func main() {
|
||||
container.TryArgv0(nil)
|
||||
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("mbf: ")
|
||||
msg := message.New(log.Default())
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
var cache *pkg.Cache
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer stop()
|
||||
defer func() {
|
||||
if cache != nil {
|
||||
cache.Close()
|
||||
}
|
||||
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println(r)
|
||||
log.Fatal("consider scrubbing the on-disk cache")
|
||||
}
|
||||
}()
|
||||
|
||||
var (
|
||||
flagQuiet bool
|
||||
flagCures int
|
||||
flagBase string
|
||||
flagTShift int
|
||||
)
|
||||
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) {
|
||||
msg.SwapVerbose(!flagQuiet)
|
||||
|
||||
var base *check.Absolute
|
||||
if flagBase, err = filepath.Abs(flagBase); err != nil {
|
||||
return
|
||||
} else if base, err = check.NewAbs(flagBase); err != nil {
|
||||
return
|
||||
}
|
||||
if cache, err = pkg.Open(ctx, msg, flagCures, base); err == nil {
|
||||
if flagTShift < 0 {
|
||||
cache.SetThreshold(0)
|
||||
} else if flagTShift > 31 {
|
||||
cache.SetThreshold(1 << 31)
|
||||
} else {
|
||||
cache.SetThreshold(1 << flagTShift)
|
||||
}
|
||||
}
|
||||
return
|
||||
}).Flag(
|
||||
&flagQuiet,
|
||||
"q", command.BoolFlag(false),
|
||||
"Do not print cure messages",
|
||||
).Flag(
|
||||
&flagCures,
|
||||
"cures", command.IntFlag(0),
|
||||
"Maximum number of dependencies to cure at any given time",
|
||||
).Flag(
|
||||
&flagBase,
|
||||
"d", command.StringFlag("cache"),
|
||||
"Directory to store cured artifacts",
|
||||
).Flag(
|
||||
&flagTShift,
|
||||
"tshift", command.IntFlag(-1),
|
||||
"Dependency graph size exponent, to the power of 2",
|
||||
)
|
||||
|
||||
{
|
||||
var flagShifts int
|
||||
c.NewCommand(
|
||||
"scrub", "Examine the on-disk cache for errors",
|
||||
func(args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("scrub expects no arguments")
|
||||
}
|
||||
if flagShifts < 0 || flagShifts > 31 {
|
||||
flagShifts = 12
|
||||
}
|
||||
return cache.Scrub(runtime.NumCPU() << flagShifts)
|
||||
},
|
||||
).Flag(
|
||||
&flagShifts,
|
||||
"shift", command.IntFlag(12),
|
||||
"Scrub parallelism size exponent, to the power of 2",
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
var (
|
||||
flagGentoo string
|
||||
flagChecksum string
|
||||
|
||||
flagStage0 bool
|
||||
)
|
||||
c.NewCommand(
|
||||
"stage3",
|
||||
"Check for toolchain 3-stage non-determinism",
|
||||
func(args []string) (err error) {
|
||||
std := rosa.Std
|
||||
if flagGentoo != "" {
|
||||
std -= 3 // magic number to discourage misuse
|
||||
|
||||
var checksum pkg.Checksum
|
||||
if len(flagChecksum) != 0 {
|
||||
if err = pkg.Decode(&checksum, flagChecksum); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
rosa.SetGentooStage3(flagGentoo, checksum)
|
||||
}
|
||||
|
||||
_, _, _, stage1 := (std - 2).NewLLVM()
|
||||
_, _, _, stage2 := (std - 1).NewLLVM()
|
||||
_, _, _, stage3 := std.NewLLVM()
|
||||
var (
|
||||
pathname *check.Absolute
|
||||
checksum [2]unique.Handle[pkg.Checksum]
|
||||
)
|
||||
|
||||
if pathname, _, err = cache.Cure(stage1); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("stage1:", pathname)
|
||||
|
||||
if pathname, checksum[0], err = cache.Cure(stage2); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("stage2:", pathname)
|
||||
if pathname, checksum[1], err = cache.Cure(stage3); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("stage3:", pathname)
|
||||
|
||||
if checksum[0] != checksum[1] {
|
||||
err = &pkg.ChecksumMismatchError{
|
||||
Got: checksum[0].Value(),
|
||||
Want: checksum[1].Value(),
|
||||
}
|
||||
} else {
|
||||
log.Println(
|
||||
"stage2 is identical to stage3",
|
||||
"("+pkg.Encode(checksum[0].Value())+")",
|
||||
)
|
||||
}
|
||||
|
||||
if flagStage0 {
|
||||
if pathname, _, err = cache.Cure(
|
||||
std.Load(rosa.Stage0),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println(pathname)
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
).
|
||||
Flag(
|
||||
&flagGentoo,
|
||||
"gentoo", command.StringFlag(""),
|
||||
"Bootstrap from a Gentoo stage3 tarball",
|
||||
).
|
||||
Flag(
|
||||
&flagChecksum,
|
||||
"checksum", command.StringFlag(""),
|
||||
"Checksum of Gentoo stage3 tarball",
|
||||
).
|
||||
Flag(
|
||||
&flagStage0,
|
||||
"stage0", command.BoolFlag(false),
|
||||
"Create bootstrap stage0 tarball",
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
var (
|
||||
flagDump string
|
||||
)
|
||||
c.NewCommand(
|
||||
"cure",
|
||||
"Cure the named artifact and show its path",
|
||||
func(args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("cure requires 1 argument")
|
||||
}
|
||||
if p, ok := rosa.ResolveName(args[0]); !ok {
|
||||
return fmt.Errorf("unsupported artifact %q", args[0])
|
||||
} else if flagDump == "" {
|
||||
pathname, _, err := cache.Cure(rosa.Std.Load(p))
|
||||
if err == nil {
|
||||
log.Println(pathname)
|
||||
}
|
||||
return err
|
||||
} else {
|
||||
f, err := os.OpenFile(
|
||||
flagDump,
|
||||
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
|
||||
0644,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = cache.EncodeAll(f, rosa.Std.Load(p)); err != nil {
|
||||
_ = f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
},
|
||||
).
|
||||
Flag(
|
||||
&flagDump,
|
||||
"dump", command.StringFlag(""),
|
||||
"Write IR to specified pathname and terminate",
|
||||
)
|
||||
}
|
||||
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
if cache != nil {
|
||||
cache.Close()
|
||||
}
|
||||
log.Fatal(err)
|
||||
})
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE /* O_DIRECT */
|
||||
#endif
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* TODO(ophestra): remove after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
|
||||
#include <sys/syscall.h>
|
||||
|
||||
#include "fuse-operations.h"
|
||||
|
||||
/* MUST_TRANSLATE_PATHNAME translates a userspace pathname to a relative pathname;
|
||||
* the resulting address points to a constant string or part of pathname, it is never heap allocated. */
|
||||
#define MUST_TRANSLATE_PATHNAME(pathname) \
|
||||
do { \
|
||||
if (pathname == NULL) \
|
||||
return -EINVAL; \
|
||||
while (*pathname == '/') \
|
||||
pathname++; \
|
||||
if (*pathname == '\0') \
|
||||
pathname = "."; \
|
||||
} while (0)
|
||||
|
||||
/* GET_CONTEXT_PRIV obtains fuse context and private data for the calling thread. */
|
||||
#define GET_CONTEXT_PRIV(ctx, priv) \
|
||||
do { \
|
||||
ctx = fuse_get_context(); \
|
||||
priv = ctx->private_data; \
|
||||
} while (0)
|
||||
|
||||
/* impl_getattr modifies a struct stat from the kernel to present to userspace;
|
||||
* impl_getattr returns a negative errno style error code. */
|
||||
static int impl_getattr(struct fuse_context *ctx, struct stat *statbuf) {
|
||||
/* allowlist of permitted types */
|
||||
if (!S_ISDIR(statbuf->st_mode) && !S_ISREG(statbuf->st_mode) && !S_ISLNK(statbuf->st_mode)) {
|
||||
return -ENOTRECOVERABLE; /* returning an errno causes all operations on the file to return EIO */
|
||||
}
|
||||
|
||||
#define OVERRIDE_PERM(v) (statbuf->st_mode & ~0777) | (v & 0777)
|
||||
if (S_ISDIR(statbuf->st_mode))
|
||||
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_DIR);
|
||||
else if (S_ISREG(statbuf->st_mode))
|
||||
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_REG);
|
||||
else
|
||||
statbuf->st_mode = 0; /* should always be symlink in this case */
|
||||
|
||||
statbuf->st_uid = ctx->uid;
|
||||
statbuf->st_gid = SHAREFS_MEDIA_RW_ID;
|
||||
statbuf->st_ctim = statbuf->st_mtim;
|
||||
statbuf->st_nlink = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* fuse_operations implementation */
|
||||
|
||||
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi) {
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
(void)fi;
|
||||
|
||||
if (fstatat(priv->dirfd, pathname, statbuf, AT_SYMLINK_NOFOLLOW) == -1)
|
||||
return -errno;
|
||||
return impl_getattr(ctx, statbuf);
|
||||
}
|
||||
|
||||
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags) {
|
||||
int fd;
|
||||
DIR *dp;
|
||||
struct stat st;
|
||||
int ret = 0;
|
||||
struct dirent *de;
|
||||
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
(void)offset;
|
||||
(void)fi;
|
||||
|
||||
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_DIRECTORY | O_CLOEXEC)) == -1)
|
||||
return -errno;
|
||||
if ((dp = fdopendir(fd)) == NULL) {
|
||||
close(fd);
|
||||
return -errno;
|
||||
}
|
||||
|
||||
errno = 0; /* for the next readdir call */
|
||||
while ((de = readdir(dp)) != NULL) {
|
||||
if (flags & FUSE_READDIR_PLUS) {
|
||||
if (fstatat(dirfd(dp), de->d_name, &st, AT_SYMLINK_NOFOLLOW) == -1) {
|
||||
ret = -errno;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((ret = impl_getattr(ctx, &st)) < 0)
|
||||
break;
|
||||
|
||||
errno = 0;
|
||||
ret = filler(buf, de->d_name, &st, 0, FUSE_FILL_DIR_PLUS);
|
||||
} else
|
||||
ret = filler(buf, de->d_name, NULL, 0, 0);
|
||||
|
||||
if (ret != 0) {
|
||||
ret = errno != 0 ? -errno : -EIO; /* filler */
|
||||
break;
|
||||
}
|
||||
|
||||
errno = 0; /* for the next readdir call */
|
||||
}
|
||||
if (ret == 0 && errno != 0)
|
||||
ret = -errno; /* readdir */
|
||||
|
||||
closedir(dp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sharefs_mkdir(const char *pathname, mode_t mode) {
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
(void)mode;
|
||||
|
||||
if (mkdirat(priv->dirfd, pathname, SHAREFS_PERM_DIR) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_unlink(const char *pathname) {
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
if (unlinkat(priv->dirfd, pathname, 0) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_rmdir(const char *pathname) {
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
if (unlinkat(priv->dirfd, pathname, AT_REMOVEDIR) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags) {
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(oldpath);
|
||||
MUST_TRANSLATE_PATHNAME(newpath);
|
||||
|
||||
/* TODO(ophestra): replace with wrapper after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
|
||||
if (syscall(__NR_renameat2, priv->dirfd, oldpath, priv->dirfd, newpath, flags) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi) {
|
||||
int fd;
|
||||
int ret;
|
||||
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
(void)fi;
|
||||
|
||||
if ((fd = openat(priv->dirfd, pathname, O_WRONLY | O_CLOEXEC)) == -1)
|
||||
return -errno;
|
||||
if ((ret = ftruncate(fd, length)) == -1)
|
||||
ret = -errno;
|
||||
close(fd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi) {
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
(void)fi;
|
||||
|
||||
if (utimensat(priv->dirfd, pathname, times, AT_SYMLINK_NOFOLLOW) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi) {
|
||||
int fd;
|
||||
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
(void)mode;
|
||||
|
||||
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS, SHAREFS_PERM_REG)) == -1)
|
||||
return -errno;
|
||||
fi->fh = fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_open(const char *pathname, struct fuse_file_info *fi) {
|
||||
int fd;
|
||||
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS)) == -1)
|
||||
return -errno;
|
||||
fi->fh = fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
|
||||
int ret;
|
||||
|
||||
(void)pathname;
|
||||
|
||||
if ((ret = pread(fi->fh, buf, count, offset)) == -1)
|
||||
return -errno;
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
|
||||
int ret;
|
||||
|
||||
(void)pathname;
|
||||
|
||||
if ((ret = pwrite(fi->fh, buf, count, offset)) == -1)
|
||||
return -errno;
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sharefs_statfs(const char *pathname, struct statvfs *statbuf) {
|
||||
int fd;
|
||||
int ret;
|
||||
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_CLOEXEC)) == -1)
|
||||
return -errno;
|
||||
if ((ret = fstatvfs(fd, statbuf)) == -1)
|
||||
ret = -errno;
|
||||
close(fd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sharefs_release(const char *pathname, struct fuse_file_info *fi) {
|
||||
(void)pathname;
|
||||
|
||||
return close(fi->fh);
|
||||
}
|
||||
|
||||
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi) {
|
||||
(void)pathname;
|
||||
|
||||
if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12)
|
||||
#include <fuse.h>
|
||||
#include <fuse_lowlevel.h> /* for fuse_cmdline_help */
|
||||
|
||||
#if (FUSE_VERSION < FUSE_MAKE_VERSION(3, 12))
|
||||
#error This package requires libfuse >= v3.12
|
||||
#endif
|
||||
|
||||
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
|
||||
#define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
|
||||
#define SHAREFS_PERM_REG 0600 /* permission bits for regular files presented to userspace */
|
||||
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
|
||||
|
||||
/* sharefs_private is populated by sharefs_init and contains process-wide context */
|
||||
struct sharefs_private {
|
||||
int dirfd; /* source dirfd opened during sharefs_init */
|
||||
uintptr_t setup; /* cgo handle of opaque setup state */
|
||||
};
|
||||
|
||||
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi);
|
||||
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags);
|
||||
int sharefs_mkdir(const char *pathname, mode_t mode);
|
||||
int sharefs_unlink(const char *pathname);
|
||||
int sharefs_rmdir(const char *pathname);
|
||||
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags);
|
||||
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi);
|
||||
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi);
|
||||
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi);
|
||||
int sharefs_open(const char *pathname, struct fuse_file_info *fi);
|
||||
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
|
||||
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
|
||||
int sharefs_statfs(const char *pathname, struct statvfs *statbuf);
|
||||
int sharefs_release(const char *pathname, struct fuse_file_info *fi);
|
||||
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi);
|
||||
@@ -1,556 +0,0 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo pkg-config: --static fuse3
|
||||
|
||||
#include "fuse-operations.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
extern void *sharefs_init(struct fuse_conn_info *conn, struct fuse_config *cfg);
|
||||
extern void sharefs_destroy(void *private_data);
|
||||
|
||||
typedef void (*closure)();
|
||||
static inline struct fuse_opt _FUSE_OPT_END() { return (struct fuse_opt)FUSE_OPT_END; };
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/helper/proc"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
type (
|
||||
// closure represents a C function pointer.
|
||||
closure = C.closure
|
||||
|
||||
// fuseArgs represents the fuse_args structure.
|
||||
fuseArgs = C.struct_fuse_args
|
||||
|
||||
// setupState holds state used for setup. Its cgo handle is included in
|
||||
// sharefs_private and considered opaque to non-setup callbacks.
|
||||
setupState struct {
|
||||
// Whether sharefs_init failed.
|
||||
initFailed bool
|
||||
|
||||
// Whether to create source directory as root.
|
||||
mkdir bool
|
||||
|
||||
// Open file descriptor to fuse.
|
||||
Fuse int
|
||||
|
||||
// Pathname to open for dirfd.
|
||||
Source *check.Absolute
|
||||
// New uid and gid to set by sharefs_init when starting as root.
|
||||
Setuid, Setgid int
|
||||
}
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(setupState)) }
|
||||
|
||||
// destroySetup invalidates the setup [cgo.Handle] in a sharefs_private structure.
|
||||
func destroySetup(private_data unsafe.Pointer) (ok bool) {
|
||||
if private_data == nil {
|
||||
return false
|
||||
}
|
||||
priv := (*C.struct_sharefs_private)(private_data)
|
||||
|
||||
if h := cgo.Handle(priv.setup); h != 0 {
|
||||
priv.setup = 0
|
||||
h.Delete()
|
||||
ok = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//export sharefs_init
|
||||
func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.Pointer {
|
||||
ctx := C.fuse_get_context()
|
||||
priv := (*C.struct_sharefs_private)(ctx.private_data)
|
||||
setup := cgo.Handle(priv.setup).Value().(*setupState)
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
log.Println("filesystem daemon must not run as root")
|
||||
goto fail
|
||||
}
|
||||
|
||||
cfg.use_ino = C.true
|
||||
cfg.direct_io = C.false
|
||||
// getattr is context-dependent
|
||||
cfg.attr_timeout = 0
|
||||
cfg.entry_timeout = 0
|
||||
cfg.negative_timeout = 0
|
||||
|
||||
// all future filesystem operations happen through this dirfd
|
||||
if fd, err := syscall.Open(setup.Source.String(), syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC, 0); err != nil {
|
||||
log.Printf("cannot open %q: %v", setup.Source, err)
|
||||
goto fail
|
||||
} else if err = syscall.Fchdir(fd); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
log.Printf("cannot enter %q: %s", setup.Source, err)
|
||||
goto fail
|
||||
} else {
|
||||
priv.dirfd = C.int(fd)
|
||||
}
|
||||
|
||||
return ctx.private_data
|
||||
|
||||
fail:
|
||||
setup.initFailed = true
|
||||
C.fuse_exit(ctx.fuse)
|
||||
return nil
|
||||
}
|
||||
|
||||
//export sharefs_destroy
|
||||
func sharefs_destroy(private_data unsafe.Pointer) {
|
||||
if private_data != nil {
|
||||
destroySetup(private_data)
|
||||
priv := (*C.struct_sharefs_private)(private_data)
|
||||
|
||||
if err := syscall.Close(int(priv.dirfd)); err != nil {
|
||||
log.Printf("cannot close source directory: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// showHelp prints the help message.
|
||||
func showHelp(args *fuseArgs) {
|
||||
executableName := sharefsName
|
||||
if args.argc > 0 {
|
||||
executableName = path.Base(C.GoString(*args.argv))
|
||||
} else if name, err := os.Executable(); err == nil {
|
||||
executableName = path.Base(name)
|
||||
}
|
||||
|
||||
fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
|
||||
|
||||
fmt.Println("Filesystem options:")
|
||||
fmt.Println(" -o source=/data/media source directory to be mounted")
|
||||
fmt.Println(" -o setuid=1023 uid to run as when starting as root")
|
||||
fmt.Println(" -o setgid=1023 gid to run as when starting as root")
|
||||
|
||||
fmt.Println("\nFUSE options:")
|
||||
C.fuse_cmdline_help()
|
||||
C.fuse_lib_help(args)
|
||||
}
|
||||
|
||||
// parseOpts parses fuse options via fuse_opt_parse.
|
||||
func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
|
||||
var unsafeOpts struct {
|
||||
// Pathname to writable source directory.
|
||||
source *C.char
|
||||
|
||||
// Whether to create source directory as root.
|
||||
mkdir C.int
|
||||
|
||||
// Decimal string representation of uid to set when running as root.
|
||||
setuid *C.char
|
||||
// Decimal string representation of gid to set when running as root.
|
||||
setgid *C.char
|
||||
|
||||
// Decimal string representation of open file descriptor to read setupState from.
|
||||
// This is an internal detail for containerisation and must not be specified directly.
|
||||
setup *C.char
|
||||
}
|
||||
|
||||
if C.fuse_opt_parse(args, unsafe.Pointer(&unsafeOpts), &[]C.struct_fuse_opt{
|
||||
{templ: C.CString("source=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.source)), value: 0},
|
||||
{templ: C.CString("mkdir"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.mkdir)), value: 1},
|
||||
{templ: C.CString("setuid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setuid)), value: 0},
|
||||
{templ: C.CString("setgid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setgid)), value: 0},
|
||||
|
||||
{templ: C.CString("setup=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setup)), value: 0},
|
||||
|
||||
C._FUSE_OPT_END(),
|
||||
}[0], nil) == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
if unsafeOpts.source != nil {
|
||||
defer C.free(unsafe.Pointer(unsafeOpts.source))
|
||||
}
|
||||
if unsafeOpts.setuid != nil {
|
||||
defer C.free(unsafe.Pointer(unsafeOpts.setuid))
|
||||
}
|
||||
if unsafeOpts.setgid != nil {
|
||||
defer C.free(unsafe.Pointer(unsafeOpts.setgid))
|
||||
}
|
||||
|
||||
if unsafeOpts.setup != nil {
|
||||
defer C.free(unsafe.Pointer(unsafeOpts.setup))
|
||||
|
||||
if v, err := strconv.Atoi(C.GoString(unsafeOpts.setup)); err != nil || v < 3 {
|
||||
log.Println("invalid value for option setup")
|
||||
return false
|
||||
} else {
|
||||
r := os.NewFile(uintptr(v), "setup")
|
||||
defer func() {
|
||||
if err = r.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
if err = gob.NewDecoder(r).Decode(setup); err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if setup.Fuse < 3 {
|
||||
log.Println("invalid file descriptor", setup.Fuse)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if unsafeOpts.source == nil {
|
||||
showHelp(args)
|
||||
return false
|
||||
} else if a, err := check.NewAbs(C.GoString(unsafeOpts.source)); err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
} else {
|
||||
setup.Source = a
|
||||
}
|
||||
setup.mkdir = unsafeOpts.mkdir != 0
|
||||
|
||||
if unsafeOpts.setuid == nil {
|
||||
setup.Setuid = -1
|
||||
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setuid)); err != nil || v <= 0 {
|
||||
log.Println("invalid value for option setuid")
|
||||
return false
|
||||
} else {
|
||||
setup.Setuid = v
|
||||
}
|
||||
if unsafeOpts.setgid == nil {
|
||||
setup.Setgid = -1
|
||||
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setgid)); err != nil || v <= 0 {
|
||||
log.Println("invalid value for option setgid")
|
||||
return false
|
||||
} else {
|
||||
setup.Setgid = v
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// copyArgs returns a heap allocated copy of an argument slice in fuse_args representation.
|
||||
func copyArgs(s ...string) fuseArgs {
|
||||
if len(s) == 0 {
|
||||
return fuseArgs{argc: 0, argv: nil, allocated: 0}
|
||||
}
|
||||
args := unsafe.Slice((**C.char)(C.malloc(C.size_t(uintptr(len(s))*unsafe.Sizeof(s[0])))), len(s))
|
||||
for i, arg := range s {
|
||||
args[i] = C.CString(arg)
|
||||
}
|
||||
return fuseArgs{argc: C.int(len(s)), argv: &args[0], allocated: 1}
|
||||
}
|
||||
|
||||
// freeArgs frees the contents of argument list.
|
||||
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }
|
||||
|
||||
// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
|
||||
// The last byte of arg must be 0.
|
||||
func unsafeAddArgument(args *fuseArgs, arg string) {
|
||||
C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
|
||||
}
|
||||
|
||||
func _main(s ...string) (exitCode int) {
|
||||
msg := message.New(log.Default())
|
||||
container.TryArgv0(msg)
|
||||
runtime.LockOSThread()
|
||||
|
||||
// don't mask creation mode, kernel already did that
|
||||
syscall.Umask(0)
|
||||
|
||||
var pinner runtime.Pinner
|
||||
defer pinner.Unpin()
|
||||
|
||||
args := copyArgs(s...)
|
||||
defer freeArgs(&args)
|
||||
|
||||
// this causes the kernel to enforce access control based on
|
||||
// struct stat populated by sharefs_getattr
|
||||
unsafeAddArgument(&args, "-odefault_permissions\x00")
|
||||
|
||||
var priv C.struct_sharefs_private
|
||||
pinner.Pin(&priv)
|
||||
var setup setupState
|
||||
priv.setup = C.uintptr_t(cgo.NewHandle(&setup))
|
||||
defer destroySetup(unsafe.Pointer(&priv))
|
||||
|
||||
var opts C.struct_fuse_cmdline_opts
|
||||
if C.fuse_parse_cmdline(&args, &opts) != 0 {
|
||||
return 1
|
||||
}
|
||||
if opts.mountpoint != nil {
|
||||
defer C.free(unsafe.Pointer(opts.mountpoint))
|
||||
}
|
||||
|
||||
if opts.show_version != 0 {
|
||||
fmt.Println("hakurei version", info.Version())
|
||||
fmt.Println("FUSE library version", C.GoString(C.fuse_pkgversion()))
|
||||
C.fuse_lowlevel_version()
|
||||
return 0
|
||||
}
|
||||
|
||||
if opts.show_help != 0 {
|
||||
showHelp(&args)
|
||||
return 0
|
||||
} else if opts.mountpoint == nil {
|
||||
log.Println("no mountpoint specified")
|
||||
return 2
|
||||
} else {
|
||||
// hack to keep fuse_parse_cmdline happy in the container
|
||||
mountpoint := C.GoString(opts.mountpoint)
|
||||
pathnameArg := -1
|
||||
for i, arg := range s {
|
||||
if arg == mountpoint {
|
||||
pathnameArg = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if pathnameArg < 0 {
|
||||
log.Println("mountpoint must be absolute")
|
||||
return 2
|
||||
}
|
||||
s[pathnameArg] = container.Nonexistent
|
||||
}
|
||||
|
||||
if !parseOpts(&args, &setup, msg.GetLogger()) {
|
||||
return 1
|
||||
}
|
||||
asRoot := os.Geteuid() == 0
|
||||
|
||||
if asRoot {
|
||||
if setup.Setuid <= 0 || setup.Setgid <= 0 {
|
||||
log.Println("setuid and setgid must not be 0")
|
||||
return 1
|
||||
}
|
||||
|
||||
if setup.Fuse >= 3 {
|
||||
log.Println("filesystem daemon must not run as root")
|
||||
return 1
|
||||
}
|
||||
|
||||
if setup.mkdir {
|
||||
if err := os.MkdirAll(setup.Source.String(), 0700); err != nil {
|
||||
if !errors.Is(err, os.ErrExist) {
|
||||
log.Println(err)
|
||||
return 1
|
||||
}
|
||||
// skip setup for existing source directory
|
||||
} else if err = os.Chown(setup.Source.String(), setup.Setuid, setup.Setgid); err != nil {
|
||||
log.Println(err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
} else if setup.Fuse < 3 && (setup.Setuid > 0 || setup.Setgid > 0) {
|
||||
log.Println("setuid and setgid has no effect when not starting as root")
|
||||
return 1
|
||||
} else if setup.mkdir {
|
||||
log.Println("mkdir has no effect when not starting as root")
|
||||
return 1
|
||||
}
|
||||
|
||||
op := C.struct_fuse_operations{
|
||||
init: closure(C.sharefs_init),
|
||||
destroy: closure(C.sharefs_destroy),
|
||||
|
||||
// implemented in fuse-helper.c
|
||||
getattr: closure(C.sharefs_getattr),
|
||||
readdir: closure(C.sharefs_readdir),
|
||||
mkdir: closure(C.sharefs_mkdir),
|
||||
unlink: closure(C.sharefs_unlink),
|
||||
rmdir: closure(C.sharefs_rmdir),
|
||||
rename: closure(C.sharefs_rename),
|
||||
truncate: closure(C.sharefs_truncate),
|
||||
utimens: closure(C.sharefs_utimens),
|
||||
create: closure(C.sharefs_create),
|
||||
open: closure(C.sharefs_open),
|
||||
read: closure(C.sharefs_read),
|
||||
write: closure(C.sharefs_write),
|
||||
statfs: closure(C.sharefs_statfs),
|
||||
release: closure(C.sharefs_release),
|
||||
fsync: closure(C.sharefs_fsync),
|
||||
}
|
||||
|
||||
fuse := C.fuse_new_fn(&args, &op, C.size_t(unsafe.Sizeof(op)), unsafe.Pointer(&priv))
|
||||
if fuse == nil {
|
||||
return 3
|
||||
}
|
||||
defer C.fuse_destroy(fuse)
|
||||
se := C.fuse_get_session(fuse)
|
||||
|
||||
if setup.Fuse < 3 {
|
||||
// unconfined, set up mount point and container
|
||||
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
|
||||
return 4
|
||||
}
|
||||
// unmounted by initial process
|
||||
defer func() {
|
||||
if exitCode == 5 {
|
||||
C.fuse_unmount(fuse)
|
||||
}
|
||||
}()
|
||||
|
||||
if asRoot {
|
||||
if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); err != nil {
|
||||
log.Printf("cannot set gid: %v", err)
|
||||
return 5
|
||||
}
|
||||
if err := syscall.Setgroups(nil); err != nil {
|
||||
log.Printf("cannot set supplementary groups: %v", err)
|
||||
return 5
|
||||
}
|
||||
if err := syscall.Setresuid(setup.Setuid, setup.Setuid, setup.Setuid); err != nil {
|
||||
log.Printf("cannot set uid: %v", err)
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
msg.SwapVerbose(opts.debug != 0)
|
||||
ctx := context.Background()
|
||||
if opts.foreground != 0 {
|
||||
c, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
ctx = c
|
||||
}
|
||||
z := container.New(ctx, msg)
|
||||
z.AllowOrphan = opts.foreground == 0
|
||||
z.Env = os.Environ()
|
||||
|
||||
// keep fuse_parse_cmdline happy in the container
|
||||
z.Tmpfs(check.MustAbs(container.Nonexistent), 1<<10, 0755)
|
||||
|
||||
if a, err := check.NewAbs(container.MustExecutable(msg)); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else {
|
||||
z.Path = a
|
||||
}
|
||||
z.Args = s
|
||||
z.ForwardCancel = true
|
||||
z.SeccompPresets |= std.PresetStrict
|
||||
z.ParentPerm = 0700
|
||||
z.Bind(setup.Source, setup.Source, std.BindWritable)
|
||||
if !z.AllowOrphan {
|
||||
z.WaitDelay = hst.WaitDelayMax
|
||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
}
|
||||
z.Bind(z.Path, z.Path, 0)
|
||||
setup.Fuse = int(proc.ExtraFileSlice(&z.ExtraFiles, os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse")))
|
||||
|
||||
var setupWriter io.WriteCloser
|
||||
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else {
|
||||
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
|
||||
setupWriter = w
|
||||
}
|
||||
|
||||
if err := z.Start(); err != nil {
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
log.Println(m)
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
return 5
|
||||
}
|
||||
if err := z.Serve(); err != nil {
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
log.Println(m)
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
return 5
|
||||
}
|
||||
|
||||
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else if err = setupWriter.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
if !z.AllowOrphan {
|
||||
if err := z.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) || exitError == nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
}
|
||||
switch code := exitError.ExitCode(); syscall.Signal(code & 0x7f) {
|
||||
case syscall.SIGINT:
|
||||
case syscall.SIGTERM:
|
||||
|
||||
default:
|
||||
return code
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
} else { // confined
|
||||
C.free(unsafe.Pointer(opts.mountpoint))
|
||||
// must be heap allocated
|
||||
opts.mountpoint = C.CString("/dev/fd/" + strconv.Itoa(setup.Fuse))
|
||||
|
||||
if err := os.Chdir("/"); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
|
||||
return 4
|
||||
}
|
||||
defer C.fuse_unmount(fuse)
|
||||
|
||||
if C.fuse_set_signal_handlers(se) != 0 {
|
||||
return 6
|
||||
}
|
||||
defer C.fuse_remove_signal_handlers(se)
|
||||
|
||||
if opts.singlethread != 0 {
|
||||
if C.fuse_loop(fuse) != 0 {
|
||||
return 8
|
||||
}
|
||||
} else {
|
||||
loopConfig := C.fuse_loop_cfg_create()
|
||||
if loopConfig == nil {
|
||||
return 7
|
||||
}
|
||||
defer C.fuse_loop_cfg_destroy(loopConfig)
|
||||
|
||||
C.fuse_loop_cfg_set_clone_fd(loopConfig, C.uint(opts.clone_fd))
|
||||
|
||||
C.fuse_loop_cfg_set_idle_threads(loopConfig, opts.max_idle_threads)
|
||||
C.fuse_loop_cfg_set_max_threads(loopConfig, opts.max_threads)
|
||||
if C.fuse_loop_mt(fuse, loopConfig) != 0 {
|
||||
return 8
|
||||
}
|
||||
}
|
||||
|
||||
if setup.initFailed {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
)
|
||||
|
||||
func TestParseOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want setupState
|
||||
wantLog string
|
||||
wantOk bool
|
||||
}{
|
||||
{"zero length", []string{}, setupState{}, "", false},
|
||||
|
||||
{"not absolute", []string{"sharefs",
|
||||
"-o", "source=nonexistent",
|
||||
"-o", "setuid=1023",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{}, "sharefs: path \"nonexistent\" is not absolute\n", false},
|
||||
|
||||
{"not specified", []string{"sharefs",
|
||||
"-o", "setuid=1023",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{}, "", false},
|
||||
|
||||
{"invalid setuid", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setuid=ff",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
}, "sharefs: invalid value for option setuid\n", false},
|
||||
|
||||
{"invalid setgid", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setuid=1023",
|
||||
"-o", "setgid=ff",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: 1023,
|
||||
}, "sharefs: invalid value for option setgid\n", false},
|
||||
|
||||
{"simple", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: -1,
|
||||
Setgid: -1,
|
||||
}, "", true},
|
||||
|
||||
{"root", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setuid=1023",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: 1023,
|
||||
Setgid: 1023,
|
||||
}, "", true},
|
||||
|
||||
{"setuid", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setuid=1023",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: 1023,
|
||||
Setgid: -1,
|
||||
}, "", true},
|
||||
|
||||
{"setgid", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: -1,
|
||||
Setgid: 1023,
|
||||
}, "", true},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
got setupState
|
||||
buf bytes.Buffer
|
||||
)
|
||||
args := copyArgs(tc.args...)
|
||||
defer freeArgs(&args)
|
||||
unsafeAddArgument(&args, "-odefault_permissions\x00")
|
||||
|
||||
if ok := parseOpts(&args, &got, log.New(&buf, "sharefs: ", 0)); ok != tc.wantOk {
|
||||
t.Errorf("parseOpts: ok = %v, want %v", ok, tc.wantOk)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(&got, &tc.want) {
|
||||
t.Errorf("parseOpts: setup = %#v, want %#v", got, tc.want)
|
||||
}
|
||||
|
||||
if buf.String() != tc.wantLog {
|
||||
t.Errorf("parseOpts: log =\n%s\nwant\n%s", buf.String(), tc.wantLog)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// sharefsName is the prefix used by log.std in the sharefs process.
|
||||
const sharefsName = "sharefs"
|
||||
|
||||
// handleMountArgs returns an alternative, libfuse-compatible args slice for
|
||||
// args passed by mount -t fuse.sharefs [options] sharefs <mountpoint>.
|
||||
//
|
||||
// In this case, args always has a length of 5 with index 0 being what comes
|
||||
// after "fuse." in the filesystem type, 1 is the uninterpreted string passed
|
||||
// to mount (sharefsName is used as the magic string to enable this hack),
|
||||
// 2 is passed through to libfuse as mountpoint, and 3 is always "-o".
|
||||
func handleMountArgs(args []string) []string {
|
||||
if len(args) == 5 && args[1] == sharefsName && args[3] == "-o" {
|
||||
return []string{sharefsName, args[2], "-o", args[4]}
|
||||
}
|
||||
return slices.Clone(args)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix(sharefsName + ": ")
|
||||
|
||||
os.Exit(_main(handleMountArgs(os.Args)...))
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandleMountArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{"nil", nil, nil},
|
||||
{"passthrough", []string{"sharefs", "-V"}, []string{"sharefs", "-V"}},
|
||||
{"replace", []string{"/sbin/sharefs", "sharefs", "/sdcard", "-o", "rw"}, []string{"sharefs", "/sdcard", "-o", "rw"}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := handleMountArgs(tc.args); !slices.Equal(got, tc.want) {
|
||||
t.Errorf("handleMountArgs: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
users.users = {
|
||||
alice = {
|
||||
isNormalUser = true;
|
||||
description = "Alice Foobar";
|
||||
password = "foobar";
|
||||
uid = 1000;
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.users.alice.home.stateVersion = "24.11";
|
||||
|
||||
# Automatically login on tty1 as a normal user:
|
||||
services.getty.autologinUser = "alice";
|
||||
|
||||
environment = {
|
||||
# For benchmarking sharefs:
|
||||
systemPackages = [ pkgs.fsmark ];
|
||||
};
|
||||
|
||||
virtualisation = {
|
||||
diskSize = 6 * 1024;
|
||||
|
||||
qemu.options = [
|
||||
# Increase test performance:
|
||||
"-smp 8"
|
||||
];
|
||||
};
|
||||
|
||||
environment.hakurei = rec {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/hakurei";
|
||||
sharefs.source = "${stateDir}/sdcard";
|
||||
users.alice = 0;
|
||||
|
||||
extraHomeConfig = {
|
||||
home.stateVersion = "23.05";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
testers,
|
||||
|
||||
system,
|
||||
self,
|
||||
}:
|
||||
testers.nixosTest {
|
||||
name = "sharefs";
|
||||
nodes.machine =
|
||||
{ options, pkgs, ... }:
|
||||
let
|
||||
fhs =
|
||||
let
|
||||
hakurei = options.environment.hakurei.package.default;
|
||||
in
|
||||
pkgs.buildFHSEnv {
|
||||
pname = "hakurei-fhs";
|
||||
inherit (hakurei) version;
|
||||
targetPkgs = _: hakurei.targetPkgs;
|
||||
extraOutputsToInstall = [ "dev" ];
|
||||
profile = ''
|
||||
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
environment.systemPackages = [
|
||||
# For go tests:
|
||||
(pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" ''
|
||||
cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei"
|
||||
${fhs}/bin/hakurei-fhs -c 'CC="clang -O3 -Werror" go test ./...'
|
||||
'')
|
||||
];
|
||||
|
||||
imports = [
|
||||
./configuration.nix
|
||||
|
||||
self.nixosModules.hakurei
|
||||
self.inputs.home-manager.nixosModules.home-manager
|
||||
];
|
||||
};
|
||||
|
||||
testScript = builtins.readFile ./test.py;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# To check sharefs version:
|
||||
print(machine.succeed("sharefs -V"))
|
||||
|
||||
# Make sure sharefs started:
|
||||
machine.wait_for_unit("sdcard.mount")
|
||||
|
||||
machine.succeed("mkdir /mnt")
|
||||
def check_bad_opts_output(opts, want, source="/etc", privileged=False):
|
||||
output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"sharefs -f -o source={source},{opts} /mnt 2>&1")
|
||||
if output != want:
|
||||
raise Exception(f"unexpected output: {output}")
|
||||
|
||||
# Malformed setuid/setgid representation:
|
||||
check_bad_opts_output("setuid=ff", "sharefs: invalid value for option setuid\n")
|
||||
check_bad_opts_output("setgid=ff", "sharefs: invalid value for option setgid\n")
|
||||
|
||||
# Bounds check for setuid/setgid:
|
||||
check_bad_opts_output("setuid=0", "sharefs: invalid value for option setuid\n")
|
||||
check_bad_opts_output("setgid=0", "sharefs: invalid value for option setgid\n")
|
||||
check_bad_opts_output("setuid=-1", "sharefs: invalid value for option setuid\n")
|
||||
check_bad_opts_output("setgid=-1", "sharefs: invalid value for option setgid\n")
|
||||
|
||||
# Non-root setuid/setgid:
|
||||
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||
check_bad_opts_output("setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||
check_bad_opts_output("mkdir", "sharefs: mkdir has no effect when not starting as root\n")
|
||||
|
||||
# Starting as root without setuid/setgid:
|
||||
check_bad_opts_output("allow_other", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||
|
||||
# Make sure nothing actually got mounted:
|
||||
machine.fail("umount /mnt")
|
||||
machine.succeed("rmdir /mnt")
|
||||
|
||||
# Unprivileged mount/unmount:
|
||||
machine.succeed("sudo -u alice -i mkdir /home/alice/{sdcard,persistent}")
|
||||
machine.succeed("sudo -u alice -i sharefs -o source=/home/alice/persistent /home/alice/sdcard")
|
||||
machine.succeed("sudo -u alice -i touch /home/alice/sdcard/check")
|
||||
machine.succeed("sudo -u alice -i umount /home/alice/sdcard")
|
||||
machine.succeed("sudo -u alice -i rm /home/alice/persistent/check")
|
||||
machine.succeed("sudo -u alice -i rmdir /home/alice/{sdcard,persistent}")
|
||||
|
||||
# Benchmark sharefs:
|
||||
machine.succeed("fs_mark -v -d /sdcard/fs_mark -l /tmp/fs_log.txt")
|
||||
machine.copy_from_vm("/tmp/fs_log.txt", "")
|
||||
|
||||
# Check permissions:
|
||||
machine.succeed("sudo -u sharefs touch /var/lib/hakurei/sdcard/fs_mark/.check")
|
||||
machine.succeed("sudo -u sharefs rm /var/lib/hakurei/sdcard/fs_mark/.check")
|
||||
machine.succeed("sudo -u alice rm -rf /sdcard/fs_mark")
|
||||
machine.fail("ls /var/lib/hakurei/sdcard/fs_mark")
|
||||
|
||||
# Run hakurei tests on sharefs:
|
||||
machine.succeed("sudo -u alice -i sharefs-workload-hakurei-tests")
|
||||
@@ -59,7 +59,6 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
func (e *AutoEtcOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (e *AutoEtcOp) hostPath() *check.Absolute { return fhs.AbsEtc.Append(e.hostRel()) }
|
||||
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
|
||||
|
||||
@@ -69,7 +69,6 @@ func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (r *AutoRootOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (r *AutoRootOp) Is(op Op) bool {
|
||||
vr, ok := op.(*AutoRootOp)
|
||||
|
||||
@@ -202,7 +202,7 @@ func TestIsAutoRootBindable(t *testing.T) {
|
||||
t.Parallel()
|
||||
var msg message.Msg
|
||||
if tc.log {
|
||||
msg = &kstub{nil, nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
|
||||
msg = &kstub{nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),
|
||||
}})}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ const (
|
||||
|
||||
CAP_SYS_ADMIN = 0x15
|
||||
CAP_SETPCAP = 0x8
|
||||
CAP_NET_ADMIN = 0xc
|
||||
CAP_DAC_OVERRIDE = 0x1
|
||||
)
|
||||
|
||||
|
||||
@@ -9,60 +9,46 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unique"
|
||||
)
|
||||
|
||||
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
|
||||
type AbsoluteError string
|
||||
type AbsoluteError struct{ Pathname string }
|
||||
|
||||
func (e AbsoluteError) Error() string {
|
||||
return fmt.Sprintf("path %q is not absolute", string(e))
|
||||
}
|
||||
|
||||
func (e AbsoluteError) Is(target error) bool {
|
||||
var ce AbsoluteError
|
||||
func (e *AbsoluteError) Error() string { return fmt.Sprintf("path %q is not absolute", e.Pathname) }
|
||||
func (e *AbsoluteError) Is(target error) bool {
|
||||
var ce *AbsoluteError
|
||||
if !errors.As(target, &ce) {
|
||||
return errors.Is(target, syscall.EINVAL)
|
||||
}
|
||||
return e == ce
|
||||
return *e == *ce
|
||||
}
|
||||
|
||||
// Absolute holds a pathname checked to be absolute.
|
||||
type Absolute struct{ pathname unique.Handle[string] }
|
||||
|
||||
// ok returns whether [Absolute] is not the zero value.
|
||||
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
|
||||
type Absolute struct{ pathname string }
|
||||
|
||||
// unsafeAbs returns [check.Absolute] on any string value.
|
||||
func unsafeAbs(pathname string) *Absolute {
|
||||
return &Absolute{unique.Make(pathname)}
|
||||
}
|
||||
func unsafeAbs(pathname string) *Absolute { return &Absolute{pathname} }
|
||||
|
||||
// String returns the checked pathname.
|
||||
func (a *Absolute) String() string {
|
||||
if !a.ok() {
|
||||
if a.pathname == "" {
|
||||
panic("attempted use of zero Absolute")
|
||||
}
|
||||
return a.pathname.Value()
|
||||
}
|
||||
|
||||
// Handle returns the underlying [unique.Handle].
|
||||
func (a *Absolute) Handle() unique.Handle[string] {
|
||||
return a.pathname
|
||||
}
|
||||
|
||||
// Is efficiently compares the underlying pathname.
|
||||
func (a *Absolute) Is(v *Absolute) bool {
|
||||
if a == nil && v == nil {
|
||||
return true
|
||||
}
|
||||
return a.ok() && v.ok() && a.pathname == v.pathname
|
||||
return a != nil && v != nil &&
|
||||
a.pathname != "" && v.pathname != "" &&
|
||||
a.pathname == v.pathname
|
||||
}
|
||||
|
||||
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
|
||||
func NewAbs(pathname string) (*Absolute, error) {
|
||||
if !path.IsAbs(pathname) {
|
||||
return nil, AbsoluteError(pathname)
|
||||
return nil, &AbsoluteError{pathname}
|
||||
}
|
||||
return unsafeAbs(pathname), nil
|
||||
}
|
||||
@@ -84,49 +70,35 @@ func (a *Absolute) Append(elem ...string) *Absolute {
|
||||
// Dir calls [path.Dir] with [Absolute] as its argument.
|
||||
func (a *Absolute) Dir() *Absolute { return unsafeAbs(path.Dir(a.String())) }
|
||||
|
||||
// GobEncode returns the checked pathname.
|
||||
func (a *Absolute) GobEncode() ([]byte, error) {
|
||||
return []byte(a.String()), nil
|
||||
}
|
||||
|
||||
// GobDecode stores data if it represents an absolute pathname.
|
||||
func (a *Absolute) GobEncode() ([]byte, error) { return []byte(a.String()), nil }
|
||||
func (a *Absolute) GobDecode(data []byte) error {
|
||||
pathname := string(data)
|
||||
if !path.IsAbs(pathname) {
|
||||
return AbsoluteError(pathname)
|
||||
return &AbsoluteError{pathname}
|
||||
}
|
||||
a.pathname = unique.Make(pathname)
|
||||
a.pathname = pathname
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON returns a JSON representation of the checked pathname.
|
||||
func (a *Absolute) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(a.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON stores data if it represents an absolute pathname.
|
||||
func (a *Absolute) MarshalJSON() ([]byte, error) { return json.Marshal(a.String()) }
|
||||
func (a *Absolute) UnmarshalJSON(data []byte) error {
|
||||
var pathname string
|
||||
if err := json.Unmarshal(data, &pathname); err != nil {
|
||||
return err
|
||||
}
|
||||
if !path.IsAbs(pathname) {
|
||||
return AbsoluteError(pathname)
|
||||
return &AbsoluteError{pathname}
|
||||
}
|
||||
a.pathname = unique.Make(pathname)
|
||||
a.pathname = pathname
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
|
||||
func SortAbs(x []*Absolute) {
|
||||
slices.SortFunc(x, func(a, b *Absolute) int {
|
||||
return strings.Compare(a.String(), b.String())
|
||||
})
|
||||
slices.SortFunc(x, func(a, b *Absolute) int { return strings.Compare(a.String(), b.String()) })
|
||||
}
|
||||
|
||||
// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
|
||||
func CompactAbs(s []*Absolute) []*Absolute {
|
||||
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool {
|
||||
return a.Is(b)
|
||||
})
|
||||
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool { return a.String() == b.String() })
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ func TestAbsoluteError(t *testing.T) {
|
||||
}{
|
||||
{"EINVAL", new(AbsoluteError), syscall.EINVAL, true},
|
||||
{"not EINVAL", new(AbsoluteError), syscall.EBADE, false},
|
||||
{"ne val", new(AbsoluteError), AbsoluteError("etc"), false},
|
||||
{"equals", AbsoluteError("etc"), AbsoluteError("etc"), true},
|
||||
{"ne val", new(AbsoluteError), &AbsoluteError{Pathname: "etc"}, false},
|
||||
{"equals", &AbsoluteError{Pathname: "etc"}, &AbsoluteError{Pathname: "etc"}, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -45,7 +45,7 @@ func TestAbsoluteError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
want := `path "etc" is not absolute`
|
||||
if got := (AbsoluteError("etc")).Error(); got != want {
|
||||
if got := (&AbsoluteError{Pathname: "etc"}).Error(); got != want {
|
||||
t.Errorf("Error: %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
@@ -62,8 +62,8 @@ func TestNewAbs(t *testing.T) {
|
||||
wantErr error
|
||||
}{
|
||||
{"good", "/etc", MustAbs("/etc"), nil},
|
||||
{"not absolute", "etc", nil, AbsoluteError("etc")},
|
||||
{"zero", "", nil, AbsoluteError("")},
|
||||
{"not absolute", "etc", nil, &AbsoluteError{Pathname: "etc"}},
|
||||
{"zero", "", nil, &AbsoluteError{Pathname: ""}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -84,7 +84,7 @@ func TestNewAbs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
defer func() {
|
||||
wantPanic := AbsoluteError("etc")
|
||||
wantPanic := &AbsoluteError{Pathname: "etc"}
|
||||
|
||||
if r := recover(); !reflect.DeepEqual(r, wantPanic) {
|
||||
t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
|
||||
@@ -175,7 +175,7 @@ func TestCodecAbsolute(t *testing.T) {
|
||||
|
||||
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
|
||||
{"not absolute", nil,
|
||||
AbsoluteError("etc"),
|
||||
&AbsoluteError{Pathname: "etc"},
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
|
||||
|
||||
@@ -35,8 +35,6 @@ type (
|
||||
// Container represents a container environment being prepared or run.
|
||||
// None of [Container] methods are safe for concurrent use.
|
||||
Container struct {
|
||||
// Whether the container init should stay alive after its parent terminates.
|
||||
AllowOrphan bool
|
||||
// Cgroup fd, nil to disable.
|
||||
Cgroup *int
|
||||
// ExtraFiles passed through to initial process in the container,
|
||||
@@ -254,7 +252,8 @@ func (p *Container) Start() error {
|
||||
}
|
||||
p.cmd.Dir = fhs.Root
|
||||
p.cmd.SysProcAttr = &SysProcAttr{
|
||||
Setsid: !p.RetainSession,
|
||||
Setsid: !p.RetainSession,
|
||||
Pdeathsig: SIGKILL,
|
||||
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
|
||||
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
|
||||
|
||||
@@ -263,17 +262,12 @@ func (p *Container) Start() error {
|
||||
CAP_SYS_ADMIN,
|
||||
// drop capabilities
|
||||
CAP_SETPCAP,
|
||||
// bring up loopback interface
|
||||
CAP_NET_ADMIN,
|
||||
// overlay access to upperdir and workdir
|
||||
CAP_DAC_OVERRIDE,
|
||||
},
|
||||
|
||||
UseCgroupFD: p.Cgroup != nil,
|
||||
}
|
||||
if !p.AllowOrphan {
|
||||
p.cmd.SysProcAttr.Pdeathsig = SIGKILL
|
||||
}
|
||||
if p.cmd.SysProcAttr.UseCgroupFD {
|
||||
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
|
||||
}
|
||||
|
||||
@@ -274,13 +274,13 @@ var containerTestCases = []struct {
|
||||
Dev(check.MustAbs("/dev"), true),
|
||||
),
|
||||
earlyMnt(
|
||||
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", ignore, ignore),
|
||||
ent("/null", "/dev/null", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/zero", "/dev/zero", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/full", "/dev/full", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/random", "/dev/random", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/urandom", "/dev/urandom", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/tty", "/dev/tty", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
|
||||
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
|
||||
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
|
||||
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
||||
@@ -292,13 +292,13 @@ var containerTestCases = []struct {
|
||||
Dev(check.MustAbs("/dev"), false),
|
||||
),
|
||||
earlyMnt(
|
||||
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", ignore, ignore),
|
||||
ent("/null", "/dev/null", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/zero", "/dev/zero", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/full", "/dev/full", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/random", "/dev/random", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/urandom", "/dev/urandom", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/tty", "/dev/tty", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
|
||||
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
|
||||
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
||||
),
|
||||
@@ -690,22 +690,11 @@ func init() {
|
||||
return fmt.Errorf("got more than %d entries", len(mnt))
|
||||
}
|
||||
|
||||
// ugly hack but should be reliable and is less likely to
|
||||
//false negative than comparing by parsed flags
|
||||
for _, s := range []string{
|
||||
"relatime",
|
||||
"noatime",
|
||||
} {
|
||||
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ","+s)
|
||||
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ","+s)
|
||||
}
|
||||
for _, s := range []string{
|
||||
"seclabel",
|
||||
"inode64",
|
||||
} {
|
||||
cur.FsOptstr = strings.Replace(cur.FsOptstr, ","+s, "", 1)
|
||||
mnt[i].FsOptstr = strings.Replace(mnt[i].FsOptstr, ","+s, "", 1)
|
||||
}
|
||||
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
|
||||
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
|
||||
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
|
||||
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
|
||||
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
|
||||
|
||||
if !cur.EqualWithIgnore(mnt[i], "\x00") {
|
||||
fail = true
|
||||
|
||||
@@ -61,8 +61,6 @@ type syscallDispatcher interface {
|
||||
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
|
||||
// ensureFile provides ensureFile.
|
||||
ensureFile(name string, perm, pperm os.FileMode) error
|
||||
// mustLoopback provides mustLoopback.
|
||||
mustLoopback(msg message.Msg)
|
||||
|
||||
// seccompLoad provides [seccomp.Load].
|
||||
seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error
|
||||
@@ -166,7 +164,6 @@ func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm
|
||||
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||
return ensureFile(name, perm, pperm)
|
||||
}
|
||||
func (direct) mustLoopback(msg message.Msg) { mustLoopback(msg) }
|
||||
|
||||
func (direct) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
|
||||
return seccomp.Load(rules, flags)
|
||||
|
||||
@@ -162,8 +162,7 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
|
||||
t.Parallel()
|
||||
|
||||
wait4signal := make(chan struct{})
|
||||
lockNotify := make(chan struct{})
|
||||
k := &kstub{wait4signal, lockNotify, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, lockNotify, s} }, tc.want)}
|
||||
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
|
||||
defer stub.HandleExit(t)
|
||||
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
|
||||
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
|
||||
@@ -201,8 +200,8 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
|
||||
k := &kstub{nil, nil, stub.New(t,
|
||||
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, nil, s} },
|
||||
k := &kstub{nil, stub.New(t,
|
||||
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
|
||||
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
|
||||
)}
|
||||
state := &setupState{Params: tc.params, Msg: k}
|
||||
@@ -323,19 +322,12 @@ const (
|
||||
|
||||
type kstub struct {
|
||||
wait4signal chan struct{}
|
||||
lockNotify chan struct{}
|
||||
*stub.Stub[syscallDispatcher]
|
||||
}
|
||||
|
||||
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
|
||||
|
||||
func (k *kstub) lockOSThread() {
|
||||
k.Helper()
|
||||
expect := k.Expects("lockOSThread")
|
||||
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
|
||||
<-k.lockNotify
|
||||
}
|
||||
}
|
||||
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") }
|
||||
|
||||
func (k *kstub) setPtracer(pid uintptr) error {
|
||||
k.Helper()
|
||||
@@ -465,8 +457,6 @@ func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||
stub.CheckArg(k.Stub, "pperm", pperm, 2))
|
||||
}
|
||||
|
||||
func (*kstub) mustLoopback(message.Msg) { /* noop */ }
|
||||
|
||||
func (k *kstub) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
|
||||
k.Helper()
|
||||
return k.Expects("seccompLoad").Error(
|
||||
@@ -482,10 +472,6 @@ func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
|
||||
k.FailNow()
|
||||
}
|
||||
|
||||
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
|
||||
defer close(k.lockNotify)
|
||||
}
|
||||
|
||||
// export channel for external instrumentation
|
||||
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
|
||||
chanf(c)
|
||||
@@ -773,8 +759,7 @@ func (k *kstub) checkMsg(msg message.Msg) {
|
||||
}
|
||||
|
||||
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
|
||||
|
||||
func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
|
||||
func (k *kstub) IsVerbose() bool { panic("unreachable") }
|
||||
|
||||
func (k *kstub) SwapVerbose(verbose bool) bool {
|
||||
k.Helper()
|
||||
|
||||
@@ -7,36 +7,31 @@ import (
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/vfs"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// messageFromError returns a printable error message for a supported concrete type.
|
||||
func messageFromError(err error) (m string, ok bool) {
|
||||
if m, ok = messagePrefixP[MountError]("cannot ", err); ok {
|
||||
return
|
||||
func messageFromError(err error) (string, bool) {
|
||||
if m, ok := messagePrefixP[MountError]("cannot ", err); ok {
|
||||
return m, ok
|
||||
}
|
||||
if m, ok = messagePrefixP[os.PathError]("cannot ", err); ok {
|
||||
return
|
||||
if m, ok := messagePrefixP[os.PathError]("cannot ", err); ok {
|
||||
return m, ok
|
||||
}
|
||||
if m, ok = messagePrefix[check.AbsoluteError](zeroString, err); ok {
|
||||
return
|
||||
if m, ok := messagePrefixP[check.AbsoluteError]("", err); ok {
|
||||
return m, ok
|
||||
}
|
||||
if m, ok = messagePrefix[OpRepeatError](zeroString, err); ok {
|
||||
return
|
||||
if m, ok := messagePrefix[OpRepeatError]("", err); ok {
|
||||
return m, ok
|
||||
}
|
||||
if m, ok = messagePrefix[OpStateError](zeroString, err); ok {
|
||||
return
|
||||
if m, ok := messagePrefix[OpStateError]("", err); ok {
|
||||
return m, ok
|
||||
}
|
||||
|
||||
if m, ok = messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
|
||||
return
|
||||
if m, ok := messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
|
||||
return m, ok
|
||||
}
|
||||
if m, ok = messagePrefix[TmpfsSizeError](zeroString, err); ok {
|
||||
return
|
||||
}
|
||||
|
||||
if m, ok = message.GetMessage(err); ok {
|
||||
return
|
||||
if m, ok := messagePrefix[TmpfsSizeError]("", err); ok {
|
||||
return m, ok
|
||||
}
|
||||
|
||||
return zeroString, false
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestMessageFromError(t *testing.T) {
|
||||
Err: stub.UniqueError(0xdeadbeef),
|
||||
}, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true},
|
||||
|
||||
{"absolute", check.AbsoluteError("etc/mtab"),
|
||||
{"absolute", &check.AbsoluteError{Pathname: "etc/mtab"},
|
||||
`path "etc/mtab" is not absolute`, true},
|
||||
|
||||
{"repeat", OpRepeatError("autoetc"),
|
||||
|
||||
@@ -26,8 +26,6 @@ var (
|
||||
// AbsRunUser is [RunUser] as [check.Absolute].
|
||||
AbsRunUser = unsafeAbs(RunUser)
|
||||
|
||||
// AbsUsr is [Usr] as [check.Absolute].
|
||||
AbsUsr = unsafeAbs(Usr)
|
||||
// AbsUsrBin is [UsrBin] as [check.Absolute].
|
||||
AbsUsrBin = unsafeAbs(UsrBin)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -10,8 +9,6 @@ import (
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
. "syscall"
|
||||
"time"
|
||||
|
||||
@@ -21,28 +18,24 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
/* intermediateHostPath is the pathname of the intermediate tmpfs mount point.
|
||||
/* intermediate tmpfs mount point
|
||||
|
||||
This path might seem like a weird choice, however there are many good reasons to use it:
|
||||
- The contents of this path is never exposed to the container:
|
||||
The tmpfs root established here effectively becomes anonymous after pivot_root.
|
||||
- It is safe to assume this path exists and is a directory:
|
||||
This program will not work correctly without a proper /proc and neither will most others.
|
||||
- This path belongs to the container init:
|
||||
The container init is not any more privileged or trusted than the rest of the container.
|
||||
- This path is only accessible by init and root:
|
||||
The container init sets SUID_DUMP_DISABLE and terminates if that fails.
|
||||
this path might seem like a weird choice, however there are many good reasons to use it:
|
||||
- the contents of this path is never exposed to the container:
|
||||
the tmpfs root established here effectively becomes anonymous after pivot_root
|
||||
- it is safe to assume this path exists and is a directory:
|
||||
this program will not work correctly without a proper /proc and neither will most others
|
||||
- this path belongs to the container init:
|
||||
the container init is not any more privileged or trusted than the rest of the container
|
||||
- this path is only accessible by init and root:
|
||||
the container init sets SUID_DUMP_DISABLE and terminates if that fails;
|
||||
|
||||
It should be noted that none of this should become relevant at any point since the resulting
|
||||
intermediate root tmpfs should be effectively anonymous. */
|
||||
it should be noted that none of this should become relevant at any point since the resulting
|
||||
intermediate root tmpfs should be effectively anonymous */
|
||||
intermediateHostPath = fhs.Proc + "self/fd"
|
||||
|
||||
// setupEnv is the name of the environment variable holding the string representation of
|
||||
// the read end file descriptor of the setup params pipe.
|
||||
// setup params file descriptor
|
||||
setupEnv = "HAKUREI_SETUP"
|
||||
|
||||
// exitUnexpectedWait4 is the exit code if wait4 returns an unexpected errno.
|
||||
exitUnexpectedWait4 = 2
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -56,8 +49,6 @@ type (
|
||||
early(state *setupState, k syscallDispatcher) error
|
||||
// apply is called in intermediate root.
|
||||
apply(state *setupState, k syscallDispatcher) error
|
||||
// late is called right before starting the initial process.
|
||||
late(state *setupState, k syscallDispatcher) error
|
||||
|
||||
// prefix returns a log message prefix, and whether this Op prints no identifying message on its own.
|
||||
prefix() (string, bool)
|
||||
@@ -70,29 +61,11 @@ type (
|
||||
// setupState persists context between Ops.
|
||||
setupState struct {
|
||||
nonrepeatable uintptr
|
||||
|
||||
// Whether early reaping has concluded. Must only be accessed in the wait4 loop.
|
||||
processConcluded bool
|
||||
// Process to syscall.WaitStatus populated in the wait4 loop. Freed after early reaping concludes.
|
||||
process map[int]WaitStatus
|
||||
// Synchronises access to process.
|
||||
processMu sync.RWMutex
|
||||
|
||||
*Params
|
||||
context.Context
|
||||
message.Msg
|
||||
}
|
||||
)
|
||||
|
||||
// terminated returns whether the specified pid has been reaped, and its
|
||||
// syscall.WaitStatus if it had. This is only usable by [Op].
|
||||
func (state *setupState) terminated(pid int) (wstatus WaitStatus, ok bool) {
|
||||
state.processMu.RLock()
|
||||
wstatus, ok = state.process[pid]
|
||||
state.processMu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Grow grows the slice Ops points to using [slices.Grow].
|
||||
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
|
||||
|
||||
@@ -170,10 +143,6 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
offsetSetup = int(setupFd + 1)
|
||||
}
|
||||
|
||||
if !params.HostNet {
|
||||
k.mustLoopback(msg)
|
||||
}
|
||||
|
||||
// write uid/gid map here so parent does not need to set dumpable
|
||||
if err := k.setDumpable(SUID_DUMP_USER); err != nil {
|
||||
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
|
||||
@@ -211,9 +180,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
state := &setupState{process: make(map[int]WaitStatus), Params: ¶ms.Params, Msg: msg, Context: ctx}
|
||||
defer cancel()
|
||||
state := &setupState{Params: ¶ms.Params, Msg: msg}
|
||||
|
||||
/* early is called right before pivot_root into intermediate root;
|
||||
this step is mostly for gathering information that would otherwise be difficult to obtain
|
||||
@@ -363,97 +330,6 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
k.umask(oldmask)
|
||||
|
||||
// winfo represents an exited process from wait4.
|
||||
type winfo struct {
|
||||
wpid int
|
||||
wstatus WaitStatus
|
||||
}
|
||||
|
||||
// info is closed as the wait4 thread terminates
|
||||
// when there are no longer any processes left to reap
|
||||
info := make(chan winfo, 1)
|
||||
|
||||
// whether initial process has started
|
||||
var initialProcessStarted atomic.Bool
|
||||
|
||||
k.new(func(k syscallDispatcher) {
|
||||
k.lockOSThread()
|
||||
|
||||
wait4:
|
||||
var (
|
||||
err error
|
||||
wpid = -2
|
||||
wstatus WaitStatus
|
||||
|
||||
// whether initial process has started
|
||||
started bool
|
||||
)
|
||||
|
||||
// keep going until no child process is left
|
||||
for wpid != -1 {
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if wpid != -2 {
|
||||
if !state.processConcluded {
|
||||
state.processMu.Lock()
|
||||
if state.process == nil {
|
||||
// early reaping has already concluded at this point
|
||||
state.processConcluded = true
|
||||
info <- winfo{wpid, wstatus}
|
||||
} else {
|
||||
// initial process has not yet been created, and the
|
||||
// info channel is not yet being received from
|
||||
state.process[wpid] = wstatus
|
||||
}
|
||||
state.processMu.Unlock()
|
||||
} else {
|
||||
info <- winfo{wpid, wstatus}
|
||||
}
|
||||
}
|
||||
|
||||
if !started {
|
||||
started = initialProcessStarted.Load()
|
||||
}
|
||||
|
||||
err = EINTR
|
||||
for errors.Is(err, EINTR) {
|
||||
wpid, err = k.wait4(-1, &wstatus, 0, nil)
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.Is(err, ECHILD) {
|
||||
k.printf(msg, "unexpected wait4 response: %v", err)
|
||||
} else if !started {
|
||||
// initial process has not yet been reached and all daemons
|
||||
// terminated or none were started in the first place
|
||||
time.Sleep(500 * time.Microsecond)
|
||||
goto wait4
|
||||
}
|
||||
|
||||
close(info)
|
||||
})
|
||||
|
||||
// called right before startup of initial process, all state changes to the
|
||||
// current process is prohibited during late
|
||||
for i, op := range *params.Ops {
|
||||
// ops already checked during early setup
|
||||
if err := op.late(state, k); err != nil {
|
||||
if m, ok := messageFromError(err); ok {
|
||||
k.fatal(msg, m)
|
||||
} else if errors.Is(err, context.DeadlineExceeded) {
|
||||
k.fatalf(msg, "%s deadline exceeded", op.String())
|
||||
} else {
|
||||
k.fatalf(msg, "cannot complete op at index %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// early reaping has concluded, this must happen before initial process is created
|
||||
state.processMu.Lock()
|
||||
state.process = nil
|
||||
state.processMu.Unlock()
|
||||
|
||||
if err := closeSetup(); err != nil {
|
||||
k.fatalf(msg, "cannot close setup pipe: %v", err)
|
||||
}
|
||||
@@ -465,11 +341,50 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
cmd.ExtraFiles = extraFiles
|
||||
cmd.Dir = params.Dir.String()
|
||||
|
||||
msg.Verbosef("starting initial process %s", params.Path)
|
||||
msg.Verbosef("starting initial program %s", params.Path)
|
||||
if err := k.start(cmd); err != nil {
|
||||
k.fatalf(msg, "%v", err)
|
||||
}
|
||||
initialProcessStarted.Store(true)
|
||||
|
||||
type winfo struct {
|
||||
wpid int
|
||||
wstatus WaitStatus
|
||||
}
|
||||
|
||||
// info is closed as the wait4 thread terminates
|
||||
// when there are no longer any processes left to reap
|
||||
info := make(chan winfo, 1)
|
||||
|
||||
k.new(func(k syscallDispatcher) {
|
||||
k.lockOSThread()
|
||||
|
||||
var (
|
||||
err error
|
||||
wpid = -2
|
||||
wstatus WaitStatus
|
||||
)
|
||||
|
||||
// keep going until no child process is left
|
||||
for wpid != -1 {
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if wpid != -2 {
|
||||
info <- winfo{wpid, wstatus}
|
||||
}
|
||||
|
||||
err = EINTR
|
||||
for errors.Is(err, EINTR) {
|
||||
wpid, err = k.wait4(-1, &wstatus, 0, nil)
|
||||
}
|
||||
}
|
||||
if !errors.Is(err, ECHILD) {
|
||||
k.printf(msg, "unexpected wait4 response: %v", err)
|
||||
}
|
||||
|
||||
close(info)
|
||||
})
|
||||
|
||||
// handle signals to dump withheld messages
|
||||
sig := make(chan os.Signal, 2)
|
||||
@@ -479,7 +394,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
// closed after residualProcessTimeout has elapsed after initial process death
|
||||
timeout := make(chan struct{})
|
||||
|
||||
r := exitUnexpectedWait4
|
||||
r := 2
|
||||
for {
|
||||
select {
|
||||
case s := <-sig:
|
||||
@@ -511,9 +426,6 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
if w.wpid == cmd.Process.Pid {
|
||||
// cancel Op context early
|
||||
cancel()
|
||||
|
||||
// start timeout early
|
||||
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
|
||||
|
||||
|
||||
@@ -1983,20 +1983,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(13)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, nil, stub.UniqueError(12)),
|
||||
call("fatalf", stub.ExpectArgs{"%v", []any{stub.UniqueError(12)}}, nil, nil),
|
||||
},
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
|
||||
}}},
|
||||
}, nil},
|
||||
|
||||
{"lowlastcap signaled cancel forward error", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
|
||||
@@ -2071,11 +2062,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(10)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"forwarding context cancellation"}}, nil, nil),
|
||||
// magicWait4Signal as ret causes wait4 stub to unblock
|
||||
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", os.Interrupt}, magicWait4Signal, stub.UniqueError(9)),
|
||||
@@ -2090,7 +2081,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
|
||||
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
|
||||
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
|
||||
@@ -2171,11 +2162,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"got %s, forwarding to initial process", []any{"quit"}}, nil, nil),
|
||||
// magicWait4Signal as ret causes wait4 stub to unblock
|
||||
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", syscall.SIGQUIT}, magicWait4Signal, stub.UniqueError(0xfe)),
|
||||
@@ -2190,7 +2181,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
|
||||
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
|
||||
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
|
||||
@@ -2271,11 +2262,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil),
|
||||
call("beforeExit", stub.ExpectArgs{}, nil, nil),
|
||||
call("exit", stub.ExpectArgs{0}, nil, nil),
|
||||
@@ -2283,7 +2274,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
|
||||
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
|
||||
@@ -2362,11 +2353,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(5)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
|
||||
@@ -2377,7 +2368,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
|
||||
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil}, 0xbad, nil),
|
||||
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
||||
@@ -2457,11 +2448,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(3)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
|
||||
@@ -2471,7 +2462,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
@@ -2595,11 +2586,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"initial process exited with code %d", []any{1}}, nil, nil),
|
||||
@@ -2609,7 +2600,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
@@ -2737,11 +2728,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(11), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(12), "extra file 2"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(0)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/bin/zsh")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/bin/zsh", []string{"zsh", "-c", "exec vim"}, []string{"DISPLAY=:0"}, "/.hakurei"}, &os.Process{Pid: 0xcafe}, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
@@ -2752,7 +2743,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
|
||||
@@ -90,7 +90,6 @@ func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
return k.bindMount(state, source, target, flags)
|
||||
}
|
||||
func (b *BindMountOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (b *BindMountOp) Is(op Op) bool {
|
||||
vb, ok := op.(*BindMountOp)
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(DaemonOp)) }
|
||||
|
||||
const (
|
||||
// daemonTimeout is the duration a [DaemonOp] is allowed to block before the
|
||||
// [DaemonOp.Target] marker becomes available.
|
||||
daemonTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// 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 {
|
||||
*f = append(*f, &DaemonOp{target, path, args})
|
||||
return f
|
||||
}
|
||||
|
||||
// DaemonOp starts a daemon in the container and blocks until Target appears.
|
||||
type DaemonOp struct {
|
||||
// Pathname indicating readiness of daemon.
|
||||
Target *check.Absolute
|
||||
// Absolute pathname passed to [exec.Cmd].
|
||||
Path *check.Absolute
|
||||
// Arguments (excl. first) passed to [exec.Cmd].
|
||||
Args []string
|
||||
}
|
||||
|
||||
// earlyTerminationError is returned by [DaemonOp] when a daemon terminates
|
||||
// before [DaemonOp.Target] appears.
|
||||
type earlyTerminationError struct {
|
||||
// Returned by [DaemonOp.String].
|
||||
op string
|
||||
// Copied from wait4 loop.
|
||||
wstatus syscall.WaitStatus
|
||||
}
|
||||
|
||||
func (e *earlyTerminationError) Error() string {
|
||||
res := ""
|
||||
switch {
|
||||
case e.wstatus.Exited():
|
||||
res = "exit status " + strconv.Itoa(e.wstatus.ExitStatus())
|
||||
case e.wstatus.Signaled():
|
||||
res = "signal: " + e.wstatus.Signal().String()
|
||||
case e.wstatus.Stopped():
|
||||
res = "stop signal: " + e.wstatus.StopSignal().String()
|
||||
if e.wstatus.StopSignal() == syscall.SIGTRAP && e.wstatus.TrapCause() != 0 {
|
||||
res += " (trap " + strconv.Itoa(e.wstatus.TrapCause()) + ")"
|
||||
}
|
||||
case e.wstatus.Continued():
|
||||
res = "continued"
|
||||
}
|
||||
if e.wstatus.CoreDump() {
|
||||
res += " (core dumped)"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (e *earlyTerminationError) Message() string { return e.op + " " + e.Error() }
|
||||
|
||||
func (d *DaemonOp) Valid() bool { return d != nil && d.Target != nil && d.Path != nil }
|
||||
func (d *DaemonOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (d *DaemonOp) apply(*setupState, syscallDispatcher) error { return nil }
|
||||
func (d *DaemonOp) late(state *setupState, k syscallDispatcher) error {
|
||||
cmd := exec.CommandContext(state.Context, d.Path.String(), d.Args...)
|
||||
cmd.Env = state.Env
|
||||
cmd.Dir = fhs.Root
|
||||
if state.IsVerbose() {
|
||||
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||
}
|
||||
// WaitDelay: left unset because lifetime is bound by AdoptWaitDelay on cancellation
|
||||
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
|
||||
|
||||
state.Verbosef("starting %s", d.String())
|
||||
if err := k.start(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(daemonTimeout)
|
||||
var wstatusErr error
|
||||
|
||||
for {
|
||||
if _, err := k.stat(d.Target.String()); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
_ = k.signal(cmd, os.Kill)
|
||||
return err
|
||||
}
|
||||
|
||||
if time.Now().After(deadline) {
|
||||
_ = k.signal(cmd, os.Kill)
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
|
||||
if wstatusErr != nil {
|
||||
return wstatusErr
|
||||
}
|
||||
if wstatus, ok := state.terminated(cmd.Process.Pid); ok {
|
||||
// check once again: process could have satisfied Target between stat and the lookup
|
||||
wstatusErr = &earlyTerminationError{d.String(), wstatus}
|
||||
continue
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Microsecond)
|
||||
continue
|
||||
}
|
||||
|
||||
state.Verbosef("daemon process %d ready", cmd.Process.Pid)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DaemonOp) Is(op Op) bool {
|
||||
vd, ok := op.(*DaemonOp)
|
||||
return ok && d.Valid() && vd.Valid() &&
|
||||
d.Target.Is(vd.Target) && d.Path.Is(vd.Path) &&
|
||||
slices.Equal(d.Args, vd.Args)
|
||||
}
|
||||
func (*DaemonOp) prefix() (string, bool) { return zeroString, false }
|
||||
func (d *DaemonOp) String() string { return fmt.Sprintf("daemon providing %q", d.Target) }
|
||||
@@ -1,127 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestEarlyTerminationError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
msg string
|
||||
}{
|
||||
{"exited", &earlyTerminationError{
|
||||
`daemon providing "/run/user/1971/pulse/native"`, 127 << 8,
|
||||
}, "exit status 127", `daemon providing "/run/user/1971/pulse/native" exit status 127`},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := tc.err.Error(); got != tc.want {
|
||||
t.Errorf("Error: %q, want %q", got, tc.want)
|
||||
}
|
||||
if got := tc.err.(message.Error).Message(); got != tc.msg {
|
||||
t.Errorf("Message: %s, want %s", got, tc.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemonOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkSimple(t, "DaemonOp.late", []simpleTestCase{
|
||||
{"success", func(k *kstub) error {
|
||||
state := setupState{Params: &Params{Env: []string{"\x00"}}, Context: t.Context(), Msg: k}
|
||||
return (&DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
}).late(&state, k)
|
||||
}, stub.Expect{Calls: []stub.Call{
|
||||
call("isVerbose", stub.ExpectArgs{}, true, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting %s", []any{`daemon providing "/run/user/1971/pulse/native"`}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/pipewire-pulse", []string{"/run/current-system/sw/bin/pipewire-pulse", "-v"}, []string{"\x00"}, "/"}, &os.Process{Pid: 0xcafe}, nil),
|
||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), nil),
|
||||
call("verbosef", stub.ExpectArgs{"daemon process %d ready", []any{0xcafe}}, nil, nil),
|
||||
}}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*DaemonOp)(nil), false},
|
||||
{"zero", new(DaemonOp), false},
|
||||
{"valid", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"pipewire-pulse", new(Ops).Daemon(
|
||||
check.MustAbs("/run/user/1971/pulse/native"),
|
||||
check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"), "-v",
|
||||
), Ops{
|
||||
&DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(DaemonOp), new(DaemonOp), false},
|
||||
|
||||
{"args differs", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
}, &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, false},
|
||||
|
||||
{"path differs", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire"),
|
||||
}, &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, false},
|
||||
|
||||
{"target differs", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/65534/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, false},
|
||||
|
||||
{"equals", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"pipewire-pulse", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
}, zeroString, `daemon providing "/run/user/1971/pulse/native"`},
|
||||
})
|
||||
}
|
||||
@@ -126,7 +126,6 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
|
||||
}
|
||||
func (d *MountDevOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (d *MountDevOp) Is(op Op) bool {
|
||||
vd, ok := op.(*MountDevOp)
|
||||
|
||||
@@ -27,7 +27,6 @@ func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||
return k.mkdirAll(toSysroot(m.Path.String()), m.Perm)
|
||||
}
|
||||
func (m *MkdirOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (m *MkdirOp) Is(op Op) bool {
|
||||
vm, ok := op.(*MkdirOp)
|
||||
|
||||
@@ -205,8 +205,6 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption))
|
||||
}
|
||||
|
||||
func (o *MountOverlayOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (o *MountOverlayOp) Is(op Op) bool {
|
||||
vo, ok := op.(*MountOverlayOp)
|
||||
return ok && o.Valid() && vo.Valid() &&
|
||||
|
||||
@@ -312,10 +312,7 @@ func TestMountOverlayOp(t *testing.T) {
|
||||
},
|
||||
}},
|
||||
|
||||
{"ephemeral", new(Ops).OverlayEphemeral(
|
||||
check.MustAbs("/nix/store"),
|
||||
check.MustAbs("/mnt-root/nix/.ro-store"),
|
||||
), Ops{
|
||||
{"ephemeral", new(Ops).OverlayEphemeral(check.MustAbs("/nix/store"), check.MustAbs("/mnt-root/nix/.ro-store")), Ops{
|
||||
&MountOverlayOp{
|
||||
Target: check.MustAbs("/nix/store"),
|
||||
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
|
||||
@@ -323,10 +320,7 @@ func TestMountOverlayOp(t *testing.T) {
|
||||
},
|
||||
}},
|
||||
|
||||
{"readonly", new(Ops).OverlayReadonly(
|
||||
check.MustAbs("/nix/store"),
|
||||
check.MustAbs("/mnt-root/nix/.ro-store"),
|
||||
), Ops{
|
||||
{"readonly", new(Ops).OverlayReadonly(check.MustAbs("/nix/store"), check.MustAbs("/mnt-root/nix/.ro-store")), Ops{
|
||||
&MountOverlayOp{
|
||||
Target: check.MustAbs("/nix/store"),
|
||||
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
|
||||
|
||||
@@ -57,7 +57,6 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (t *TmpfileOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (t *TmpfileOp) Is(op Op) bool {
|
||||
vt, ok := op.(*TmpfileOp)
|
||||
|
||||
@@ -28,7 +28,6 @@ func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
return k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString)
|
||||
}
|
||||
func (p *MountProcOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (p *MountProcOp) Is(op Op) bool {
|
||||
vp, ok := op.(*MountProcOp)
|
||||
|
||||
@@ -26,7 +26,6 @@ func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (r *RemountOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
return k.remount(state, toSysroot(r.Target.String()), r.Flags)
|
||||
}
|
||||
func (r *RemountOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (r *RemountOp) Is(op Op) bool {
|
||||
vr, ok := op.(*RemountOp)
|
||||
|
||||
@@ -31,7 +31,7 @@ func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkN
|
||||
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
|
||||
if l.Dereference {
|
||||
if !path.IsAbs(l.LinkName) {
|
||||
return check.AbsoluteError(l.LinkName)
|
||||
return &check.AbsoluteError{Pathname: l.LinkName}
|
||||
}
|
||||
if name, err := k.readlink(l.LinkName); err != nil {
|
||||
return err
|
||||
@@ -50,8 +50,6 @@ func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
return k.symlink(l.LinkName, target)
|
||||
}
|
||||
|
||||
func (l *SymlinkOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (l *SymlinkOp) Is(op Op) bool {
|
||||
vl, ok := op.(*SymlinkOp)
|
||||
return ok && l.Valid() && vl.Valid() &&
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestSymlinkOp(t *testing.T) {
|
||||
Target: check.MustAbs("/etc/mtab"),
|
||||
LinkName: "etc/mtab",
|
||||
Dereference: true,
|
||||
}, nil, check.AbsoluteError("etc/mtab"), nil, nil},
|
||||
}, nil, &check.AbsoluteError{Pathname: "etc/mtab"}, nil, nil},
|
||||
|
||||
{"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
|
||||
Target: check.MustAbs("/etc/mtab"),
|
||||
|
||||
@@ -48,7 +48,6 @@ func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
|
||||
}
|
||||
func (t *MountTmpfsOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (t *MountTmpfsOp) Is(op Op) bool {
|
||||
vt, ok := op.(*MountTmpfsOp)
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
. "syscall"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// rtnetlink represents a NETLINK_ROUTE socket.
|
||||
type rtnetlink struct {
|
||||
// Sent as part of rtnetlink messages.
|
||||
pid uint32
|
||||
// AF_NETLINK socket.
|
||||
fd int
|
||||
// Whether the socket is open.
|
||||
ok bool
|
||||
// Message sequence number.
|
||||
seq uint32
|
||||
}
|
||||
|
||||
// open creates the underlying NETLINK_ROUTE socket.
|
||||
func (s *rtnetlink) open() (err error) {
|
||||
if s.ok || s.fd < 0 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
s.pid = uint32(Getpid())
|
||||
if s.fd, err = Socket(
|
||||
AF_NETLINK,
|
||||
SOCK_RAW|SOCK_CLOEXEC,
|
||||
NETLINK_ROUTE,
|
||||
); err != nil {
|
||||
return os.NewSyscallError("socket", err)
|
||||
} else if err = Bind(s.fd, &SockaddrNetlink{
|
||||
Family: AF_NETLINK,
|
||||
Pid: s.pid,
|
||||
}); err != nil {
|
||||
_ = s.close()
|
||||
return os.NewSyscallError("bind", err)
|
||||
} else {
|
||||
s.ok = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// close closes the underlying NETLINK_ROUTE socket.
|
||||
func (s *rtnetlink) close() error {
|
||||
if !s.ok {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
s.ok = false
|
||||
err := Close(s.fd)
|
||||
s.fd = -1
|
||||
return err
|
||||
}
|
||||
|
||||
// roundtrip sends a netlink message and handles the reply.
|
||||
func (s *rtnetlink) roundtrip(data []byte) error {
|
||||
if !s.ok {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
defer func() { s.seq++ }()
|
||||
|
||||
if err := Sendto(s.fd, data, 0, &SockaddrNetlink{
|
||||
Family: AF_NETLINK,
|
||||
}); err != nil {
|
||||
return os.NewSyscallError("sendto", err)
|
||||
}
|
||||
buf := make([]byte, Getpagesize())
|
||||
|
||||
done:
|
||||
for {
|
||||
p := buf
|
||||
if n, _, err := Recvfrom(s.fd, p, 0); err != nil {
|
||||
return os.NewSyscallError("recvfrom", err)
|
||||
} else if n < NLMSG_HDRLEN {
|
||||
return errors.ErrUnsupported
|
||||
} else {
|
||||
p = p[:n]
|
||||
}
|
||||
|
||||
if msgs, err := ParseNetlinkMessage(p); err != nil {
|
||||
return err
|
||||
} else {
|
||||
for _, m := range msgs {
|
||||
if m.Header.Seq != s.seq || m.Header.Pid != s.pid {
|
||||
return errors.ErrUnsupported
|
||||
}
|
||||
if m.Header.Type == NLMSG_DONE {
|
||||
break done
|
||||
}
|
||||
if m.Header.Type == NLMSG_ERROR {
|
||||
if len(m.Data) >= 4 {
|
||||
errno := Errno(-std.ScmpInt(binary.NativeEndian.Uint32(m.Data)))
|
||||
if errno == 0 {
|
||||
return nil
|
||||
}
|
||||
return errno
|
||||
}
|
||||
return errors.ErrUnsupported
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mustRoundtrip calls roundtrip and terminates via msg for a non-nil error.
|
||||
func (s *rtnetlink) mustRoundtrip(msg message.Msg, data []byte) {
|
||||
err := s.roundtrip(data)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if closeErr := Close(s.fd); closeErr != nil {
|
||||
msg.Verbosef("cannot close: %v", err)
|
||||
}
|
||||
|
||||
switch err.(type) {
|
||||
case *os.SyscallError:
|
||||
msg.GetLogger().Fatalf("cannot %v", err)
|
||||
|
||||
case Errno:
|
||||
msg.GetLogger().Fatalf("RTNETLINK answers: %v", err)
|
||||
|
||||
default:
|
||||
msg.GetLogger().Fatalln("RTNETLINK answers with unexpected message")
|
||||
}
|
||||
}
|
||||
|
||||
// newaddrLo represents a RTM_NEWADDR message with two addresses.
|
||||
type newaddrLo struct {
|
||||
header NlMsghdr
|
||||
data IfAddrmsg
|
||||
|
||||
r0 RtAttr
|
||||
a0 [4]byte // in_addr
|
||||
r1 RtAttr
|
||||
a1 [4]byte // in_addr
|
||||
}
|
||||
|
||||
// sizeofNewaddrLo is the expected size of newaddrLo.
|
||||
const sizeofNewaddrLo = NLMSG_HDRLEN + SizeofIfAddrmsg + (SizeofRtAttr+4)*2
|
||||
|
||||
// newaddrLo returns the address of a populated newaddrLo.
|
||||
func (s *rtnetlink) newaddrLo(lo int) *newaddrLo {
|
||||
return &newaddrLo{NlMsghdr{
|
||||
Len: sizeofNewaddrLo,
|
||||
Type: RTM_NEWADDR,
|
||||
Flags: NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL,
|
||||
Seq: s.seq,
|
||||
Pid: s.pid,
|
||||
}, IfAddrmsg{
|
||||
Family: AF_INET,
|
||||
Prefixlen: 8,
|
||||
Flags: IFA_F_PERMANENT,
|
||||
Scope: RT_SCOPE_HOST,
|
||||
Index: uint32(lo),
|
||||
}, RtAttr{
|
||||
Len: uint16(SizeofRtAttr + len(newaddrLo{}.a0)),
|
||||
Type: IFA_LOCAL,
|
||||
}, [4]byte{127, 0, 0, 1}, RtAttr{
|
||||
Len: uint16(SizeofRtAttr + len(newaddrLo{}.a1)),
|
||||
Type: IFA_ADDRESS,
|
||||
}, [4]byte{127, 0, 0, 1}}
|
||||
}
|
||||
|
||||
func (msg *newaddrLo) toWireFormat() []byte {
|
||||
var buf [sizeofNewaddrLo]byte
|
||||
|
||||
*(*uint32)(unsafe.Pointer(&buf[0:4][0])) = msg.header.Len
|
||||
*(*uint16)(unsafe.Pointer(&buf[4:6][0])) = msg.header.Type
|
||||
*(*uint16)(unsafe.Pointer(&buf[6:8][0])) = msg.header.Flags
|
||||
*(*uint32)(unsafe.Pointer(&buf[8:12][0])) = msg.header.Seq
|
||||
*(*uint32)(unsafe.Pointer(&buf[12:16][0])) = msg.header.Pid
|
||||
|
||||
buf[16] = msg.data.Family
|
||||
buf[17] = msg.data.Prefixlen
|
||||
buf[18] = msg.data.Flags
|
||||
buf[19] = msg.data.Scope
|
||||
*(*uint32)(unsafe.Pointer(&buf[20:24][0])) = msg.data.Index
|
||||
|
||||
*(*uint16)(unsafe.Pointer(&buf[24:26][0])) = msg.r0.Len
|
||||
*(*uint16)(unsafe.Pointer(&buf[26:28][0])) = msg.r0.Type
|
||||
copy(buf[28:32], msg.a0[:])
|
||||
*(*uint16)(unsafe.Pointer(&buf[32:34][0])) = msg.r1.Len
|
||||
*(*uint16)(unsafe.Pointer(&buf[34:36][0])) = msg.r1.Type
|
||||
copy(buf[36:40], msg.a1[:])
|
||||
|
||||
return buf[:]
|
||||
}
|
||||
|
||||
// newlinkLo represents a RTM_NEWLINK message.
|
||||
type newlinkLo struct {
|
||||
header NlMsghdr
|
||||
data IfInfomsg
|
||||
}
|
||||
|
||||
// sizeofNewlinkLo is the expected size of newlinkLo.
|
||||
const sizeofNewlinkLo = NLMSG_HDRLEN + SizeofIfInfomsg
|
||||
|
||||
// newlinkLo returns the address of a populated newlinkLo.
|
||||
func (s *rtnetlink) newlinkLo(lo int) *newlinkLo {
|
||||
return &newlinkLo{NlMsghdr{
|
||||
Len: sizeofNewlinkLo,
|
||||
Type: RTM_NEWLINK,
|
||||
Flags: NLM_F_REQUEST | NLM_F_ACK,
|
||||
Seq: s.seq,
|
||||
Pid: s.pid,
|
||||
}, IfInfomsg{
|
||||
Family: AF_UNSPEC,
|
||||
Index: int32(lo),
|
||||
Flags: IFF_UP,
|
||||
Change: IFF_UP,
|
||||
}}
|
||||
}
|
||||
|
||||
func (msg *newlinkLo) toWireFormat() []byte {
|
||||
var buf [sizeofNewlinkLo]byte
|
||||
|
||||
*(*uint32)(unsafe.Pointer(&buf[0:4][0])) = msg.header.Len
|
||||
*(*uint16)(unsafe.Pointer(&buf[4:6][0])) = msg.header.Type
|
||||
*(*uint16)(unsafe.Pointer(&buf[6:8][0])) = msg.header.Flags
|
||||
*(*uint32)(unsafe.Pointer(&buf[8:12][0])) = msg.header.Seq
|
||||
*(*uint32)(unsafe.Pointer(&buf[12:16][0])) = msg.header.Pid
|
||||
|
||||
buf[16] = msg.data.Family
|
||||
*(*uint16)(unsafe.Pointer(&buf[18:20][0])) = msg.data.Type
|
||||
*(*int32)(unsafe.Pointer(&buf[20:24][0])) = msg.data.Index
|
||||
*(*uint32)(unsafe.Pointer(&buf[24:28][0])) = msg.data.Flags
|
||||
*(*uint32)(unsafe.Pointer(&buf[28:32][0])) = msg.data.Change
|
||||
|
||||
return buf[:]
|
||||
}
|
||||
|
||||
// mustLoopback creates the loopback address and brings the lo interface up.
|
||||
// mustLoopback calls a fatal method of the underlying [log.Logger] of m with a
|
||||
// user-facing error message if RTNETLINK behaves unexpectedly.
|
||||
func mustLoopback(msg message.Msg) {
|
||||
log := msg.GetLogger()
|
||||
|
||||
var lo int
|
||||
if ifi, err := net.InterfaceByName("lo"); err != nil {
|
||||
log.Fatalln(err)
|
||||
} else {
|
||||
lo = ifi.Index
|
||||
}
|
||||
|
||||
var s rtnetlink
|
||||
if err := s.open(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := s.close(); err != nil {
|
||||
msg.Verbosef("cannot close netlink: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
s.mustRoundtrip(msg, s.newaddrLo(lo).toWireFormat())
|
||||
s.mustRoundtrip(msg, s.newlinkLo(lo).toWireFormat())
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func TestSizeof(t *testing.T) {
|
||||
if got := unsafe.Sizeof(newaddrLo{}); got != sizeofNewaddrLo {
|
||||
t.Fatalf("newaddrLo: sizeof = %#x, want %#x", got, sizeofNewaddrLo)
|
||||
}
|
||||
|
||||
if got := unsafe.Sizeof(newlinkLo{}); got != sizeofNewlinkLo {
|
||||
t.Fatalf("newlinkLo: sizeof = %#x, want %#x", got, sizeofNewlinkLo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRtnetlinkMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
msg interface{ toWireFormat() []byte }
|
||||
want []byte
|
||||
}{
|
||||
{"newaddrLo", (&rtnetlink{pid: 1, seq: 0}).newaddrLo(1), []byte{
|
||||
/* Len */ 0x28, 0, 0, 0,
|
||||
/* Type */ 0x14, 0,
|
||||
/* Flags */ 5, 6,
|
||||
/* Seq */ 0, 0, 0, 0,
|
||||
/* Pid */ 1, 0, 0, 0,
|
||||
|
||||
/* Family */ 2,
|
||||
/* Prefixlen */ 8,
|
||||
/* Flags */ 0x80,
|
||||
/* Scope */ 0xfe,
|
||||
/* Index */ 1, 0, 0, 0,
|
||||
|
||||
/* Len */ 8, 0,
|
||||
/* Type */ 2, 0,
|
||||
/* in_addr */ 127, 0, 0, 1,
|
||||
|
||||
/* Len */ 8, 0,
|
||||
/* Type */ 1, 0,
|
||||
/* in_addr */ 127, 0, 0, 1,
|
||||
}},
|
||||
|
||||
{"newlinkLo", (&rtnetlink{pid: 1, seq: 1}).newlinkLo(1), []byte{
|
||||
/* Len */ 0x20, 0, 0, 0,
|
||||
/* Type */ 0x10, 0,
|
||||
/* Flags */ 5, 0,
|
||||
/* Seq */ 1, 0, 0, 0,
|
||||
/* Pid */ 1, 0, 0, 0,
|
||||
|
||||
/* Family */ 0,
|
||||
/* pad */ 0,
|
||||
/* Type */ 0, 0,
|
||||
/* Index */ 1, 0, 0, 0,
|
||||
/* Flags */ 1, 0, 0, 0,
|
||||
/* Change */ 1, 0, 0, 0,
|
||||
}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := tc.msg.toWireFormat(); string(got) != string(tc.want) {
|
||||
t.Fatalf("toWireFormat: %#v, want %#v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package seccomp_test
|
||||
|
||||
import (
|
||||
. "hakurei.app/container/seccomp"
|
||||
. "hakurei.app/container/std"
|
||||
)
|
||||
|
||||
var bpfExpected = bpfLookup{
|
||||
{AllowMultiarch | AllowCAN |
|
||||
AllowBluetooth, PresetExt |
|
||||
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
|
||||
PresetLinux32}: toHash(
|
||||
"a1c4ffa35f4bfbf38061184760b9a09edfcb4964c3b534395e47327b83f3fb61f2f9573ddfcc4772424cc2f5dd12fd32471e6531dbe10e85eda3797dd4fa179f"),
|
||||
|
||||
{0, 0}: toHash(
|
||||
"f3910fd727d087def593e3876c2c6ab9ace71d82ec8cbc992a26223e7bba85e1d7a0b56c5fc6303703f24595825dad8561637edaedd5384b34a6cd080946633c"),
|
||||
{0, PresetExt}: toHash(
|
||||
"741438c5e3f11c36c92ae8c5934f13440675c6e719541c2dbffeda79a10081bcfd9ad8314a60c1d1f53db86c8080c13fffa3bbcf7fe753935679b4b902737286"),
|
||||
{0, PresetStrict}: toHash(
|
||||
"79e9e464d02405c6d74fd2c771bd72a1311e488221c73a9c32db9270219837c54fccec2f36fe2474895547e60c311514567e2e6cf4e7a7fcf909c1ecd1e254a7"),
|
||||
{0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel}: toHash(
|
||||
"3c443715a6c1e557a284862ea8efb70a5d4ecbe67d1226627323e861cd3646fb3e7768ec5b94b93760b7f652cf6916f66e317a4fbf8716d10c3673aa4fc3ae58"),
|
||||
{0, PresetExt | PresetDenyDevel}: toHash(
|
||||
"4448a74e8cc75a4ab63799c4f2cc2a5af63e5f4e8e9b8ac15a1873d647dfa67a4c67b39ed466d8dd32abc64136d401879fc6185c9ab00feeaf59ccf4305f8201"),
|
||||
{0, PresetExt | PresetDenyNS | PresetDenyDevel}: toHash(
|
||||
"c7c86e793cb7192f5f6c735f372cda27eb43ae1045e587f8eadb64c849520a3280b6570a3d7b601d32cddb38021585a2234db38e506cebfd10aa3d6c75440f17"),
|
||||
}
|
||||
@@ -12,7 +12,6 @@ my %syscall_cutoff_arch = (
|
||||
"x86" => 340,
|
||||
"x86_64" => 302,
|
||||
"aarch64" => 281,
|
||||
"riscv64" => 281,
|
||||
);
|
||||
|
||||
print <<EOF;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package std
|
||||
|
||||
import "syscall"
|
||||
|
||||
const (
|
||||
SYS_NEWFSTATAT = syscall.SYS_FSTATAT
|
||||
)
|
||||
|
||||
var syscallNumExtra = map[string]ScmpSyscall{
|
||||
"uselib": SNR_USELIB,
|
||||
"clock_adjtime64": SNR_CLOCK_ADJTIME64,
|
||||
"clock_settime64": SNR_CLOCK_SETTIME64,
|
||||
"umount": SNR_UMOUNT,
|
||||
"chown": SNR_CHOWN,
|
||||
"chown32": SNR_CHOWN32,
|
||||
"fchown32": SNR_FCHOWN32,
|
||||
"lchown": SNR_LCHOWN,
|
||||
"lchown32": SNR_LCHOWN32,
|
||||
"setgid32": SNR_SETGID32,
|
||||
"setgroups32": SNR_SETGROUPS32,
|
||||
"setregid32": SNR_SETREGID32,
|
||||
"setresgid32": SNR_SETRESGID32,
|
||||
"setresuid32": SNR_SETRESUID32,
|
||||
"setreuid32": SNR_SETREUID32,
|
||||
"setuid32": SNR_SETUID32,
|
||||
"modify_ldt": SNR_MODIFY_LDT,
|
||||
"subpage_prot": SNR_SUBPAGE_PROT,
|
||||
"switch_endian": SNR_SWITCH_ENDIAN,
|
||||
"vm86": SNR_VM86,
|
||||
"vm86old": SNR_VM86OLD,
|
||||
}
|
||||
|
||||
const (
|
||||
SNR_USELIB ScmpSyscall = __PNR_uselib
|
||||
SNR_CLOCK_ADJTIME64 ScmpSyscall = __PNR_clock_adjtime64
|
||||
SNR_CLOCK_SETTIME64 ScmpSyscall = __PNR_clock_settime64
|
||||
SNR_UMOUNT ScmpSyscall = __PNR_umount
|
||||
SNR_CHOWN ScmpSyscall = __PNR_chown
|
||||
SNR_CHOWN32 ScmpSyscall = __PNR_chown32
|
||||
SNR_FCHOWN32 ScmpSyscall = __PNR_fchown32
|
||||
SNR_LCHOWN ScmpSyscall = __PNR_lchown
|
||||
SNR_LCHOWN32 ScmpSyscall = __PNR_lchown32
|
||||
SNR_SETGID32 ScmpSyscall = __PNR_setgid32
|
||||
SNR_SETGROUPS32 ScmpSyscall = __PNR_setgroups32
|
||||
SNR_SETREGID32 ScmpSyscall = __PNR_setregid32
|
||||
SNR_SETRESGID32 ScmpSyscall = __PNR_setresgid32
|
||||
SNR_SETRESUID32 ScmpSyscall = __PNR_setresuid32
|
||||
SNR_SETREUID32 ScmpSyscall = __PNR_setreuid32
|
||||
SNR_SETUID32 ScmpSyscall = __PNR_setuid32
|
||||
SNR_MODIFY_LDT ScmpSyscall = __PNR_modify_ldt
|
||||
SNR_SUBPAGE_PROT ScmpSyscall = __PNR_subpage_prot
|
||||
SNR_SWITCH_ENDIAN ScmpSyscall = __PNR_switch_endian
|
||||
SNR_VM86 ScmpSyscall = __PNR_vm86
|
||||
SNR_VM86OLD ScmpSyscall = __PNR_vm86old
|
||||
)
|
||||
@@ -1,719 +0,0 @@
|
||||
// mksysnum_linux.pl /usr/include/riscv64-linux-gnu/asm/unistd.h
|
||||
// Code generated by the command above; DO NOT EDIT.
|
||||
|
||||
package std
|
||||
|
||||
import . "syscall"
|
||||
|
||||
var syscallNum = map[string]ScmpSyscall{
|
||||
"io_setup": SNR_IO_SETUP,
|
||||
"io_destroy": SNR_IO_DESTROY,
|
||||
"io_submit": SNR_IO_SUBMIT,
|
||||
"io_cancel": SNR_IO_CANCEL,
|
||||
"io_getevents": SNR_IO_GETEVENTS,
|
||||
"setxattr": SNR_SETXATTR,
|
||||
"lsetxattr": SNR_LSETXATTR,
|
||||
"fsetxattr": SNR_FSETXATTR,
|
||||
"getxattr": SNR_GETXATTR,
|
||||
"lgetxattr": SNR_LGETXATTR,
|
||||
"fgetxattr": SNR_FGETXATTR,
|
||||
"listxattr": SNR_LISTXATTR,
|
||||
"llistxattr": SNR_LLISTXATTR,
|
||||
"flistxattr": SNR_FLISTXATTR,
|
||||
"removexattr": SNR_REMOVEXATTR,
|
||||
"lremovexattr": SNR_LREMOVEXATTR,
|
||||
"fremovexattr": SNR_FREMOVEXATTR,
|
||||
"getcwd": SNR_GETCWD,
|
||||
"lookup_dcookie": SNR_LOOKUP_DCOOKIE,
|
||||
"eventfd2": SNR_EVENTFD2,
|
||||
"epoll_create1": SNR_EPOLL_CREATE1,
|
||||
"epoll_ctl": SNR_EPOLL_CTL,
|
||||
"epoll_pwait": SNR_EPOLL_PWAIT,
|
||||
"dup": SNR_DUP,
|
||||
"dup3": SNR_DUP3,
|
||||
"fcntl": SNR_FCNTL,
|
||||
"inotify_init1": SNR_INOTIFY_INIT1,
|
||||
"inotify_add_watch": SNR_INOTIFY_ADD_WATCH,
|
||||
"inotify_rm_watch": SNR_INOTIFY_RM_WATCH,
|
||||
"ioctl": SNR_IOCTL,
|
||||
"ioprio_set": SNR_IOPRIO_SET,
|
||||
"ioprio_get": SNR_IOPRIO_GET,
|
||||
"flock": SNR_FLOCK,
|
||||
"mknodat": SNR_MKNODAT,
|
||||
"mkdirat": SNR_MKDIRAT,
|
||||
"unlinkat": SNR_UNLINKAT,
|
||||
"symlinkat": SNR_SYMLINKAT,
|
||||
"linkat": SNR_LINKAT,
|
||||
"umount2": SNR_UMOUNT2,
|
||||
"mount": SNR_MOUNT,
|
||||
"pivot_root": SNR_PIVOT_ROOT,
|
||||
"nfsservctl": SNR_NFSSERVCTL,
|
||||
"statfs": SNR_STATFS,
|
||||
"fstatfs": SNR_FSTATFS,
|
||||
"truncate": SNR_TRUNCATE,
|
||||
"ftruncate": SNR_FTRUNCATE,
|
||||
"fallocate": SNR_FALLOCATE,
|
||||
"faccessat": SNR_FACCESSAT,
|
||||
"chdir": SNR_CHDIR,
|
||||
"fchdir": SNR_FCHDIR,
|
||||
"chroot": SNR_CHROOT,
|
||||
"fchmod": SNR_FCHMOD,
|
||||
"fchmodat": SNR_FCHMODAT,
|
||||
"fchownat": SNR_FCHOWNAT,
|
||||
"fchown": SNR_FCHOWN,
|
||||
"openat": SNR_OPENAT,
|
||||
"close": SNR_CLOSE,
|
||||
"vhangup": SNR_VHANGUP,
|
||||
"pipe2": SNR_PIPE2,
|
||||
"quotactl": SNR_QUOTACTL,
|
||||
"getdents64": SNR_GETDENTS64,
|
||||
"lseek": SNR_LSEEK,
|
||||
"read": SNR_READ,
|
||||
"write": SNR_WRITE,
|
||||
"readv": SNR_READV,
|
||||
"writev": SNR_WRITEV,
|
||||
"pread64": SNR_PREAD64,
|
||||
"pwrite64": SNR_PWRITE64,
|
||||
"preadv": SNR_PREADV,
|
||||
"pwritev": SNR_PWRITEV,
|
||||
"sendfile": SNR_SENDFILE,
|
||||
"pselect6": SNR_PSELECT6,
|
||||
"ppoll": SNR_PPOLL,
|
||||
"signalfd4": SNR_SIGNALFD4,
|
||||
"vmsplice": SNR_VMSPLICE,
|
||||
"splice": SNR_SPLICE,
|
||||
"tee": SNR_TEE,
|
||||
"readlinkat": SNR_READLINKAT,
|
||||
"newfstatat": SNR_NEWFSTATAT,
|
||||
"fstat": SNR_FSTAT,
|
||||
"sync": SNR_SYNC,
|
||||
"fsync": SNR_FSYNC,
|
||||
"fdatasync": SNR_FDATASYNC,
|
||||
"sync_file_range": SNR_SYNC_FILE_RANGE,
|
||||
"timerfd_create": SNR_TIMERFD_CREATE,
|
||||
"timerfd_settime": SNR_TIMERFD_SETTIME,
|
||||
"timerfd_gettime": SNR_TIMERFD_GETTIME,
|
||||
"utimensat": SNR_UTIMENSAT,
|
||||
"acct": SNR_ACCT,
|
||||
"capget": SNR_CAPGET,
|
||||
"capset": SNR_CAPSET,
|
||||
"personality": SNR_PERSONALITY,
|
||||
"exit": SNR_EXIT,
|
||||
"exit_group": SNR_EXIT_GROUP,
|
||||
"waitid": SNR_WAITID,
|
||||
"set_tid_address": SNR_SET_TID_ADDRESS,
|
||||
"unshare": SNR_UNSHARE,
|
||||
"futex": SNR_FUTEX,
|
||||
"set_robust_list": SNR_SET_ROBUST_LIST,
|
||||
"get_robust_list": SNR_GET_ROBUST_LIST,
|
||||
"nanosleep": SNR_NANOSLEEP,
|
||||
"getitimer": SNR_GETITIMER,
|
||||
"setitimer": SNR_SETITIMER,
|
||||
"kexec_load": SNR_KEXEC_LOAD,
|
||||
"init_module": SNR_INIT_MODULE,
|
||||
"delete_module": SNR_DELETE_MODULE,
|
||||
"timer_create": SNR_TIMER_CREATE,
|
||||
"timer_gettime": SNR_TIMER_GETTIME,
|
||||
"timer_getoverrun": SNR_TIMER_GETOVERRUN,
|
||||
"timer_settime": SNR_TIMER_SETTIME,
|
||||
"timer_delete": SNR_TIMER_DELETE,
|
||||
"clock_settime": SNR_CLOCK_SETTIME,
|
||||
"clock_gettime": SNR_CLOCK_GETTIME,
|
||||
"clock_getres": SNR_CLOCK_GETRES,
|
||||
"clock_nanosleep": SNR_CLOCK_NANOSLEEP,
|
||||
"syslog": SNR_SYSLOG,
|
||||
"ptrace": SNR_PTRACE,
|
||||
"sched_setparam": SNR_SCHED_SETPARAM,
|
||||
"sched_setscheduler": SNR_SCHED_SETSCHEDULER,
|
||||
"sched_getscheduler": SNR_SCHED_GETSCHEDULER,
|
||||
"sched_getparam": SNR_SCHED_GETPARAM,
|
||||
"sched_setaffinity": SNR_SCHED_SETAFFINITY,
|
||||
"sched_getaffinity": SNR_SCHED_GETAFFINITY,
|
||||
"sched_yield": SNR_SCHED_YIELD,
|
||||
"sched_get_priority_max": SNR_SCHED_GET_PRIORITY_MAX,
|
||||
"sched_get_priority_min": SNR_SCHED_GET_PRIORITY_MIN,
|
||||
"sched_rr_get_interval": SNR_SCHED_RR_GET_INTERVAL,
|
||||
"restart_syscall": SNR_RESTART_SYSCALL,
|
||||
"kill": SNR_KILL,
|
||||
"tkill": SNR_TKILL,
|
||||
"tgkill": SNR_TGKILL,
|
||||
"sigaltstack": SNR_SIGALTSTACK,
|
||||
"rt_sigsuspend": SNR_RT_SIGSUSPEND,
|
||||
"rt_sigaction": SNR_RT_SIGACTION,
|
||||
"rt_sigprocmask": SNR_RT_SIGPROCMASK,
|
||||
"rt_sigpending": SNR_RT_SIGPENDING,
|
||||
"rt_sigtimedwait": SNR_RT_SIGTIMEDWAIT,
|
||||
"rt_sigqueueinfo": SNR_RT_SIGQUEUEINFO,
|
||||
"rt_sigreturn": SNR_RT_SIGRETURN,
|
||||
"setpriority": SNR_SETPRIORITY,
|
||||
"getpriority": SNR_GETPRIORITY,
|
||||
"reboot": SNR_REBOOT,
|
||||
"setregid": SNR_SETREGID,
|
||||
"setgid": SNR_SETGID,
|
||||
"setreuid": SNR_SETREUID,
|
||||
"setuid": SNR_SETUID,
|
||||
"setresuid": SNR_SETRESUID,
|
||||
"getresuid": SNR_GETRESUID,
|
||||
"setresgid": SNR_SETRESGID,
|
||||
"getresgid": SNR_GETRESGID,
|
||||
"setfsuid": SNR_SETFSUID,
|
||||
"setfsgid": SNR_SETFSGID,
|
||||
"times": SNR_TIMES,
|
||||
"setpgid": SNR_SETPGID,
|
||||
"getpgid": SNR_GETPGID,
|
||||
"getsid": SNR_GETSID,
|
||||
"setsid": SNR_SETSID,
|
||||
"getgroups": SNR_GETGROUPS,
|
||||
"setgroups": SNR_SETGROUPS,
|
||||
"uname": SNR_UNAME,
|
||||
"sethostname": SNR_SETHOSTNAME,
|
||||
"setdomainname": SNR_SETDOMAINNAME,
|
||||
"getrlimit": SNR_GETRLIMIT,
|
||||
"setrlimit": SNR_SETRLIMIT,
|
||||
"getrusage": SNR_GETRUSAGE,
|
||||
"umask": SNR_UMASK,
|
||||
"prctl": SNR_PRCTL,
|
||||
"getcpu": SNR_GETCPU,
|
||||
"gettimeofday": SNR_GETTIMEOFDAY,
|
||||
"settimeofday": SNR_SETTIMEOFDAY,
|
||||
"adjtimex": SNR_ADJTIMEX,
|
||||
"getpid": SNR_GETPID,
|
||||
"getppid": SNR_GETPPID,
|
||||
"getuid": SNR_GETUID,
|
||||
"geteuid": SNR_GETEUID,
|
||||
"getgid": SNR_GETGID,
|
||||
"getegid": SNR_GETEGID,
|
||||
"gettid": SNR_GETTID,
|
||||
"sysinfo": SNR_SYSINFO,
|
||||
"mq_open": SNR_MQ_OPEN,
|
||||
"mq_unlink": SNR_MQ_UNLINK,
|
||||
"mq_timedsend": SNR_MQ_TIMEDSEND,
|
||||
"mq_timedreceive": SNR_MQ_TIMEDRECEIVE,
|
||||
"mq_notify": SNR_MQ_NOTIFY,
|
||||
"mq_getsetattr": SNR_MQ_GETSETATTR,
|
||||
"msgget": SNR_MSGGET,
|
||||
"msgctl": SNR_MSGCTL,
|
||||
"msgrcv": SNR_MSGRCV,
|
||||
"msgsnd": SNR_MSGSND,
|
||||
"semget": SNR_SEMGET,
|
||||
"semctl": SNR_SEMCTL,
|
||||
"semtimedop": SNR_SEMTIMEDOP,
|
||||
"semop": SNR_SEMOP,
|
||||
"shmget": SNR_SHMGET,
|
||||
"shmctl": SNR_SHMCTL,
|
||||
"shmat": SNR_SHMAT,
|
||||
"shmdt": SNR_SHMDT,
|
||||
"socket": SNR_SOCKET,
|
||||
"socketpair": SNR_SOCKETPAIR,
|
||||
"bind": SNR_BIND,
|
||||
"listen": SNR_LISTEN,
|
||||
"accept": SNR_ACCEPT,
|
||||
"connect": SNR_CONNECT,
|
||||
"getsockname": SNR_GETSOCKNAME,
|
||||
"getpeername": SNR_GETPEERNAME,
|
||||
"sendto": SNR_SENDTO,
|
||||
"recvfrom": SNR_RECVFROM,
|
||||
"setsockopt": SNR_SETSOCKOPT,
|
||||
"getsockopt": SNR_GETSOCKOPT,
|
||||
"shutdown": SNR_SHUTDOWN,
|
||||
"sendmsg": SNR_SENDMSG,
|
||||
"recvmsg": SNR_RECVMSG,
|
||||
"readahead": SNR_READAHEAD,
|
||||
"brk": SNR_BRK,
|
||||
"munmap": SNR_MUNMAP,
|
||||
"mremap": SNR_MREMAP,
|
||||
"add_key": SNR_ADD_KEY,
|
||||
"request_key": SNR_REQUEST_KEY,
|
||||
"keyctl": SNR_KEYCTL,
|
||||
"clone": SNR_CLONE,
|
||||
"execve": SNR_EXECVE,
|
||||
"mmap": SNR_MMAP,
|
||||
"fadvise64": SNR_FADVISE64,
|
||||
"swapon": SNR_SWAPON,
|
||||
"swapoff": SNR_SWAPOFF,
|
||||
"mprotect": SNR_MPROTECT,
|
||||
"msync": SNR_MSYNC,
|
||||
"mlock": SNR_MLOCK,
|
||||
"munlock": SNR_MUNLOCK,
|
||||
"mlockall": SNR_MLOCKALL,
|
||||
"munlockall": SNR_MUNLOCKALL,
|
||||
"mincore": SNR_MINCORE,
|
||||
"madvise": SNR_MADVISE,
|
||||
"remap_file_pages": SNR_REMAP_FILE_PAGES,
|
||||
"mbind": SNR_MBIND,
|
||||
"get_mempolicy": SNR_GET_MEMPOLICY,
|
||||
"set_mempolicy": SNR_SET_MEMPOLICY,
|
||||
"migrate_pages": SNR_MIGRATE_PAGES,
|
||||
"move_pages": SNR_MOVE_PAGES,
|
||||
"rt_tgsigqueueinfo": SNR_RT_TGSIGQUEUEINFO,
|
||||
"perf_event_open": SNR_PERF_EVENT_OPEN,
|
||||
"accept4": SNR_ACCEPT4,
|
||||
"recvmmsg": SNR_RECVMMSG,
|
||||
"wait4": SNR_WAIT4,
|
||||
"prlimit64": SNR_PRLIMIT64,
|
||||
"fanotify_init": SNR_FANOTIFY_INIT,
|
||||
"fanotify_mark": SNR_FANOTIFY_MARK,
|
||||
"name_to_handle_at": SNR_NAME_TO_HANDLE_AT,
|
||||
"open_by_handle_at": SNR_OPEN_BY_HANDLE_AT,
|
||||
"clock_adjtime": SNR_CLOCK_ADJTIME,
|
||||
"syncfs": SNR_SYNCFS,
|
||||
"setns": SNR_SETNS,
|
||||
"sendmmsg": SNR_SENDMMSG,
|
||||
"process_vm_readv": SNR_PROCESS_VM_READV,
|
||||
"process_vm_writev": SNR_PROCESS_VM_WRITEV,
|
||||
"kcmp": SNR_KCMP,
|
||||
"finit_module": SNR_FINIT_MODULE,
|
||||
"sched_setattr": SNR_SCHED_SETATTR,
|
||||
"sched_getattr": SNR_SCHED_GETATTR,
|
||||
"renameat2": SNR_RENAMEAT2,
|
||||
"seccomp": SNR_SECCOMP,
|
||||
"getrandom": SNR_GETRANDOM,
|
||||
"memfd_create": SNR_MEMFD_CREATE,
|
||||
"bpf": SNR_BPF,
|
||||
"execveat": SNR_EXECVEAT,
|
||||
"userfaultfd": SNR_USERFAULTFD,
|
||||
"membarrier": SNR_MEMBARRIER,
|
||||
"mlock2": SNR_MLOCK2,
|
||||
"copy_file_range": SNR_COPY_FILE_RANGE,
|
||||
"preadv2": SNR_PREADV2,
|
||||
"pwritev2": SNR_PWRITEV2,
|
||||
"pkey_mprotect": SNR_PKEY_MPROTECT,
|
||||
"pkey_alloc": SNR_PKEY_ALLOC,
|
||||
"pkey_free": SNR_PKEY_FREE,
|
||||
"statx": SNR_STATX,
|
||||
"io_pgetevents": SNR_IO_PGETEVENTS,
|
||||
"rseq": SNR_RSEQ,
|
||||
"kexec_file_load": SNR_KEXEC_FILE_LOAD,
|
||||
"pidfd_send_signal": SNR_PIDFD_SEND_SIGNAL,
|
||||
"io_uring_setup": SNR_IO_URING_SETUP,
|
||||
"io_uring_enter": SNR_IO_URING_ENTER,
|
||||
"io_uring_register": SNR_IO_URING_REGISTER,
|
||||
"open_tree": SNR_OPEN_TREE,
|
||||
"move_mount": SNR_MOVE_MOUNT,
|
||||
"fsopen": SNR_FSOPEN,
|
||||
"fsconfig": SNR_FSCONFIG,
|
||||
"fsmount": SNR_FSMOUNT,
|
||||
"fspick": SNR_FSPICK,
|
||||
"pidfd_open": SNR_PIDFD_OPEN,
|
||||
"clone3": SNR_CLONE3,
|
||||
"close_range": SNR_CLOSE_RANGE,
|
||||
"openat2": SNR_OPENAT2,
|
||||
"pidfd_getfd": SNR_PIDFD_GETFD,
|
||||
"faccessat2": SNR_FACCESSAT2,
|
||||
"process_madvise": SNR_PROCESS_MADVISE,
|
||||
"epoll_pwait2": SNR_EPOLL_PWAIT2,
|
||||
"mount_setattr": SNR_MOUNT_SETATTR,
|
||||
"quotactl_fd": SNR_QUOTACTL_FD,
|
||||
"landlock_create_ruleset": SNR_LANDLOCK_CREATE_RULESET,
|
||||
"landlock_add_rule": SNR_LANDLOCK_ADD_RULE,
|
||||
"landlock_restrict_self": SNR_LANDLOCK_RESTRICT_SELF,
|
||||
"memfd_secret": SNR_MEMFD_SECRET,
|
||||
"process_mrelease": SNR_PROCESS_MRELEASE,
|
||||
"futex_waitv": SNR_FUTEX_WAITV,
|
||||
"set_mempolicy_home_node": SNR_SET_MEMPOLICY_HOME_NODE,
|
||||
"cachestat": SNR_CACHESTAT,
|
||||
"fchmodat2": SNR_FCHMODAT2,
|
||||
"map_shadow_stack": SNR_MAP_SHADOW_STACK,
|
||||
"futex_wake": SNR_FUTEX_WAKE,
|
||||
"futex_wait": SNR_FUTEX_WAIT,
|
||||
"futex_requeue": SNR_FUTEX_REQUEUE,
|
||||
"statmount": SNR_STATMOUNT,
|
||||
"listmount": SNR_LISTMOUNT,
|
||||
"lsm_get_self_attr": SNR_LSM_GET_SELF_ATTR,
|
||||
"lsm_set_self_attr": SNR_LSM_SET_SELF_ATTR,
|
||||
"lsm_list_modules": SNR_LSM_LIST_MODULES,
|
||||
"mseal": SNR_MSEAL,
|
||||
"setxattrat": SNR_SETXATTRAT,
|
||||
"getxattrat": SNR_GETXATTRAT,
|
||||
"listxattrat": SNR_LISTXATTRAT,
|
||||
"removexattrat": SNR_REMOVEXATTRAT,
|
||||
}
|
||||
|
||||
const (
|
||||
SYS_USERFAULTFD = 282
|
||||
SYS_MEMBARRIER = 283
|
||||
SYS_MLOCK2 = 284
|
||||
SYS_COPY_FILE_RANGE = 285
|
||||
SYS_PREADV2 = 286
|
||||
SYS_PWRITEV2 = 287
|
||||
SYS_PKEY_MPROTECT = 288
|
||||
SYS_PKEY_ALLOC = 289
|
||||
SYS_PKEY_FREE = 290
|
||||
SYS_STATX = 291
|
||||
SYS_IO_PGETEVENTS = 292
|
||||
SYS_RSEQ = 293
|
||||
SYS_KEXEC_FILE_LOAD = 294
|
||||
SYS_PIDFD_SEND_SIGNAL = 424
|
||||
SYS_IO_URING_SETUP = 425
|
||||
SYS_IO_URING_ENTER = 426
|
||||
SYS_IO_URING_REGISTER = 427
|
||||
SYS_OPEN_TREE = 428
|
||||
SYS_MOVE_MOUNT = 429
|
||||
SYS_FSOPEN = 430
|
||||
SYS_FSCONFIG = 431
|
||||
SYS_FSMOUNT = 432
|
||||
SYS_FSPICK = 433
|
||||
SYS_PIDFD_OPEN = 434
|
||||
SYS_CLONE3 = 435
|
||||
SYS_CLOSE_RANGE = 436
|
||||
SYS_OPENAT2 = 437
|
||||
SYS_PIDFD_GETFD = 438
|
||||
SYS_FACCESSAT2 = 439
|
||||
SYS_PROCESS_MADVISE = 440
|
||||
SYS_EPOLL_PWAIT2 = 441
|
||||
SYS_MOUNT_SETATTR = 442
|
||||
SYS_QUOTACTL_FD = 443
|
||||
SYS_LANDLOCK_CREATE_RULESET = 444
|
||||
SYS_LANDLOCK_ADD_RULE = 445
|
||||
SYS_LANDLOCK_RESTRICT_SELF = 446
|
||||
SYS_MEMFD_SECRET = 447
|
||||
SYS_PROCESS_MRELEASE = 448
|
||||
SYS_FUTEX_WAITV = 449
|
||||
SYS_SET_MEMPOLICY_HOME_NODE = 450
|
||||
SYS_CACHESTAT = 451
|
||||
SYS_FCHMODAT2 = 452
|
||||
SYS_MAP_SHADOW_STACK = 453
|
||||
SYS_FUTEX_WAKE = 454
|
||||
SYS_FUTEX_WAIT = 455
|
||||
SYS_FUTEX_REQUEUE = 456
|
||||
SYS_STATMOUNT = 457
|
||||
SYS_LISTMOUNT = 458
|
||||
SYS_LSM_GET_SELF_ATTR = 459
|
||||
SYS_LSM_SET_SELF_ATTR = 460
|
||||
SYS_LSM_LIST_MODULES = 461
|
||||
SYS_MSEAL = 462
|
||||
SYS_SETXATTRAT = 463
|
||||
SYS_GETXATTRAT = 464
|
||||
SYS_LISTXATTRAT = 465
|
||||
SYS_REMOVEXATTRAT = 466
|
||||
SYS_OPEN_TREE_ATTR = 467
|
||||
SYS_FILE_GETATTR = 468
|
||||
SYS_FILE_SETATTR = 469
|
||||
)
|
||||
|
||||
const (
|
||||
SNR_IO_SETUP ScmpSyscall = SYS_IO_SETUP
|
||||
SNR_IO_DESTROY ScmpSyscall = SYS_IO_DESTROY
|
||||
SNR_IO_SUBMIT ScmpSyscall = SYS_IO_SUBMIT
|
||||
SNR_IO_CANCEL ScmpSyscall = SYS_IO_CANCEL
|
||||
SNR_IO_GETEVENTS ScmpSyscall = SYS_IO_GETEVENTS
|
||||
SNR_SETXATTR ScmpSyscall = SYS_SETXATTR
|
||||
SNR_LSETXATTR ScmpSyscall = SYS_LSETXATTR
|
||||
SNR_FSETXATTR ScmpSyscall = SYS_FSETXATTR
|
||||
SNR_GETXATTR ScmpSyscall = SYS_GETXATTR
|
||||
SNR_LGETXATTR ScmpSyscall = SYS_LGETXATTR
|
||||
SNR_FGETXATTR ScmpSyscall = SYS_FGETXATTR
|
||||
SNR_LISTXATTR ScmpSyscall = SYS_LISTXATTR
|
||||
SNR_LLISTXATTR ScmpSyscall = SYS_LLISTXATTR
|
||||
SNR_FLISTXATTR ScmpSyscall = SYS_FLISTXATTR
|
||||
SNR_REMOVEXATTR ScmpSyscall = SYS_REMOVEXATTR
|
||||
SNR_LREMOVEXATTR ScmpSyscall = SYS_LREMOVEXATTR
|
||||
SNR_FREMOVEXATTR ScmpSyscall = SYS_FREMOVEXATTR
|
||||
SNR_GETCWD ScmpSyscall = SYS_GETCWD
|
||||
SNR_LOOKUP_DCOOKIE ScmpSyscall = SYS_LOOKUP_DCOOKIE
|
||||
SNR_EVENTFD2 ScmpSyscall = SYS_EVENTFD2
|
||||
SNR_EPOLL_CREATE1 ScmpSyscall = SYS_EPOLL_CREATE1
|
||||
SNR_EPOLL_CTL ScmpSyscall = SYS_EPOLL_CTL
|
||||
SNR_EPOLL_PWAIT ScmpSyscall = SYS_EPOLL_PWAIT
|
||||
SNR_DUP ScmpSyscall = SYS_DUP
|
||||
SNR_DUP3 ScmpSyscall = SYS_DUP3
|
||||
SNR_FCNTL ScmpSyscall = SYS_FCNTL
|
||||
SNR_INOTIFY_INIT1 ScmpSyscall = SYS_INOTIFY_INIT1
|
||||
SNR_INOTIFY_ADD_WATCH ScmpSyscall = SYS_INOTIFY_ADD_WATCH
|
||||
SNR_INOTIFY_RM_WATCH ScmpSyscall = SYS_INOTIFY_RM_WATCH
|
||||
SNR_IOCTL ScmpSyscall = SYS_IOCTL
|
||||
SNR_IOPRIO_SET ScmpSyscall = SYS_IOPRIO_SET
|
||||
SNR_IOPRIO_GET ScmpSyscall = SYS_IOPRIO_GET
|
||||
SNR_FLOCK ScmpSyscall = SYS_FLOCK
|
||||
SNR_MKNODAT ScmpSyscall = SYS_MKNODAT
|
||||
SNR_MKDIRAT ScmpSyscall = SYS_MKDIRAT
|
||||
SNR_UNLINKAT ScmpSyscall = SYS_UNLINKAT
|
||||
SNR_SYMLINKAT ScmpSyscall = SYS_SYMLINKAT
|
||||
SNR_LINKAT ScmpSyscall = SYS_LINKAT
|
||||
SNR_UMOUNT2 ScmpSyscall = SYS_UMOUNT2
|
||||
SNR_MOUNT ScmpSyscall = SYS_MOUNT
|
||||
SNR_PIVOT_ROOT ScmpSyscall = SYS_PIVOT_ROOT
|
||||
SNR_NFSSERVCTL ScmpSyscall = SYS_NFSSERVCTL
|
||||
SNR_STATFS ScmpSyscall = SYS_STATFS
|
||||
SNR_FSTATFS ScmpSyscall = SYS_FSTATFS
|
||||
SNR_TRUNCATE ScmpSyscall = SYS_TRUNCATE
|
||||
SNR_FTRUNCATE ScmpSyscall = SYS_FTRUNCATE
|
||||
SNR_FALLOCATE ScmpSyscall = SYS_FALLOCATE
|
||||
SNR_FACCESSAT ScmpSyscall = SYS_FACCESSAT
|
||||
SNR_CHDIR ScmpSyscall = SYS_CHDIR
|
||||
SNR_FCHDIR ScmpSyscall = SYS_FCHDIR
|
||||
SNR_CHROOT ScmpSyscall = SYS_CHROOT
|
||||
SNR_FCHMOD ScmpSyscall = SYS_FCHMOD
|
||||
SNR_FCHMODAT ScmpSyscall = SYS_FCHMODAT
|
||||
SNR_FCHOWNAT ScmpSyscall = SYS_FCHOWNAT
|
||||
SNR_FCHOWN ScmpSyscall = SYS_FCHOWN
|
||||
SNR_OPENAT ScmpSyscall = SYS_OPENAT
|
||||
SNR_CLOSE ScmpSyscall = SYS_CLOSE
|
||||
SNR_VHANGUP ScmpSyscall = SYS_VHANGUP
|
||||
SNR_PIPE2 ScmpSyscall = SYS_PIPE2
|
||||
SNR_QUOTACTL ScmpSyscall = SYS_QUOTACTL
|
||||
SNR_GETDENTS64 ScmpSyscall = SYS_GETDENTS64
|
||||
SNR_LSEEK ScmpSyscall = SYS_LSEEK
|
||||
SNR_READ ScmpSyscall = SYS_READ
|
||||
SNR_WRITE ScmpSyscall = SYS_WRITE
|
||||
SNR_READV ScmpSyscall = SYS_READV
|
||||
SNR_WRITEV ScmpSyscall = SYS_WRITEV
|
||||
SNR_PREAD64 ScmpSyscall = SYS_PREAD64
|
||||
SNR_PWRITE64 ScmpSyscall = SYS_PWRITE64
|
||||
SNR_PREADV ScmpSyscall = SYS_PREADV
|
||||
SNR_PWRITEV ScmpSyscall = SYS_PWRITEV
|
||||
SNR_SENDFILE ScmpSyscall = SYS_SENDFILE
|
||||
SNR_PSELECT6 ScmpSyscall = SYS_PSELECT6
|
||||
SNR_PPOLL ScmpSyscall = SYS_PPOLL
|
||||
SNR_SIGNALFD4 ScmpSyscall = SYS_SIGNALFD4
|
||||
SNR_VMSPLICE ScmpSyscall = SYS_VMSPLICE
|
||||
SNR_SPLICE ScmpSyscall = SYS_SPLICE
|
||||
SNR_TEE ScmpSyscall = SYS_TEE
|
||||
SNR_READLINKAT ScmpSyscall = SYS_READLINKAT
|
||||
SNR_NEWFSTATAT ScmpSyscall = SYS_NEWFSTATAT
|
||||
SNR_FSTAT ScmpSyscall = SYS_FSTAT
|
||||
SNR_SYNC ScmpSyscall = SYS_SYNC
|
||||
SNR_FSYNC ScmpSyscall = SYS_FSYNC
|
||||
SNR_FDATASYNC ScmpSyscall = SYS_FDATASYNC
|
||||
SNR_SYNC_FILE_RANGE ScmpSyscall = SYS_SYNC_FILE_RANGE
|
||||
SNR_TIMERFD_CREATE ScmpSyscall = SYS_TIMERFD_CREATE
|
||||
SNR_TIMERFD_SETTIME ScmpSyscall = SYS_TIMERFD_SETTIME
|
||||
SNR_TIMERFD_GETTIME ScmpSyscall = SYS_TIMERFD_GETTIME
|
||||
SNR_UTIMENSAT ScmpSyscall = SYS_UTIMENSAT
|
||||
SNR_ACCT ScmpSyscall = SYS_ACCT
|
||||
SNR_CAPGET ScmpSyscall = SYS_CAPGET
|
||||
SNR_CAPSET ScmpSyscall = SYS_CAPSET
|
||||
SNR_PERSONALITY ScmpSyscall = SYS_PERSONALITY
|
||||
SNR_EXIT ScmpSyscall = SYS_EXIT
|
||||
SNR_EXIT_GROUP ScmpSyscall = SYS_EXIT_GROUP
|
||||
SNR_WAITID ScmpSyscall = SYS_WAITID
|
||||
SNR_SET_TID_ADDRESS ScmpSyscall = SYS_SET_TID_ADDRESS
|
||||
SNR_UNSHARE ScmpSyscall = SYS_UNSHARE
|
||||
SNR_FUTEX ScmpSyscall = SYS_FUTEX
|
||||
SNR_SET_ROBUST_LIST ScmpSyscall = SYS_SET_ROBUST_LIST
|
||||
SNR_GET_ROBUST_LIST ScmpSyscall = SYS_GET_ROBUST_LIST
|
||||
SNR_NANOSLEEP ScmpSyscall = SYS_NANOSLEEP
|
||||
SNR_GETITIMER ScmpSyscall = SYS_GETITIMER
|
||||
SNR_SETITIMER ScmpSyscall = SYS_SETITIMER
|
||||
SNR_KEXEC_LOAD ScmpSyscall = SYS_KEXEC_LOAD
|
||||
SNR_INIT_MODULE ScmpSyscall = SYS_INIT_MODULE
|
||||
SNR_DELETE_MODULE ScmpSyscall = SYS_DELETE_MODULE
|
||||
SNR_TIMER_CREATE ScmpSyscall = SYS_TIMER_CREATE
|
||||
SNR_TIMER_GETTIME ScmpSyscall = SYS_TIMER_GETTIME
|
||||
SNR_TIMER_GETOVERRUN ScmpSyscall = SYS_TIMER_GETOVERRUN
|
||||
SNR_TIMER_SETTIME ScmpSyscall = SYS_TIMER_SETTIME
|
||||
SNR_TIMER_DELETE ScmpSyscall = SYS_TIMER_DELETE
|
||||
SNR_CLOCK_SETTIME ScmpSyscall = SYS_CLOCK_SETTIME
|
||||
SNR_CLOCK_GETTIME ScmpSyscall = SYS_CLOCK_GETTIME
|
||||
SNR_CLOCK_GETRES ScmpSyscall = SYS_CLOCK_GETRES
|
||||
SNR_CLOCK_NANOSLEEP ScmpSyscall = SYS_CLOCK_NANOSLEEP
|
||||
SNR_SYSLOG ScmpSyscall = SYS_SYSLOG
|
||||
SNR_PTRACE ScmpSyscall = SYS_PTRACE
|
||||
SNR_SCHED_SETPARAM ScmpSyscall = SYS_SCHED_SETPARAM
|
||||
SNR_SCHED_SETSCHEDULER ScmpSyscall = SYS_SCHED_SETSCHEDULER
|
||||
SNR_SCHED_GETSCHEDULER ScmpSyscall = SYS_SCHED_GETSCHEDULER
|
||||
SNR_SCHED_GETPARAM ScmpSyscall = SYS_SCHED_GETPARAM
|
||||
SNR_SCHED_SETAFFINITY ScmpSyscall = SYS_SCHED_SETAFFINITY
|
||||
SNR_SCHED_GETAFFINITY ScmpSyscall = SYS_SCHED_GETAFFINITY
|
||||
SNR_SCHED_YIELD ScmpSyscall = SYS_SCHED_YIELD
|
||||
SNR_SCHED_GET_PRIORITY_MAX ScmpSyscall = SYS_SCHED_GET_PRIORITY_MAX
|
||||
SNR_SCHED_GET_PRIORITY_MIN ScmpSyscall = SYS_SCHED_GET_PRIORITY_MIN
|
||||
SNR_SCHED_RR_GET_INTERVAL ScmpSyscall = SYS_SCHED_RR_GET_INTERVAL
|
||||
SNR_RESTART_SYSCALL ScmpSyscall = SYS_RESTART_SYSCALL
|
||||
SNR_KILL ScmpSyscall = SYS_KILL
|
||||
SNR_TKILL ScmpSyscall = SYS_TKILL
|
||||
SNR_TGKILL ScmpSyscall = SYS_TGKILL
|
||||
SNR_SIGALTSTACK ScmpSyscall = SYS_SIGALTSTACK
|
||||
SNR_RT_SIGSUSPEND ScmpSyscall = SYS_RT_SIGSUSPEND
|
||||
SNR_RT_SIGACTION ScmpSyscall = SYS_RT_SIGACTION
|
||||
SNR_RT_SIGPROCMASK ScmpSyscall = SYS_RT_SIGPROCMASK
|
||||
SNR_RT_SIGPENDING ScmpSyscall = SYS_RT_SIGPENDING
|
||||
SNR_RT_SIGTIMEDWAIT ScmpSyscall = SYS_RT_SIGTIMEDWAIT
|
||||
SNR_RT_SIGQUEUEINFO ScmpSyscall = SYS_RT_SIGQUEUEINFO
|
||||
SNR_RT_SIGRETURN ScmpSyscall = SYS_RT_SIGRETURN
|
||||
SNR_SETPRIORITY ScmpSyscall = SYS_SETPRIORITY
|
||||
SNR_GETPRIORITY ScmpSyscall = SYS_GETPRIORITY
|
||||
SNR_REBOOT ScmpSyscall = SYS_REBOOT
|
||||
SNR_SETREGID ScmpSyscall = SYS_SETREGID
|
||||
SNR_SETGID ScmpSyscall = SYS_SETGID
|
||||
SNR_SETREUID ScmpSyscall = SYS_SETREUID
|
||||
SNR_SETUID ScmpSyscall = SYS_SETUID
|
||||
SNR_SETRESUID ScmpSyscall = SYS_SETRESUID
|
||||
SNR_GETRESUID ScmpSyscall = SYS_GETRESUID
|
||||
SNR_SETRESGID ScmpSyscall = SYS_SETRESGID
|
||||
SNR_GETRESGID ScmpSyscall = SYS_GETRESGID
|
||||
SNR_SETFSUID ScmpSyscall = SYS_SETFSUID
|
||||
SNR_SETFSGID ScmpSyscall = SYS_SETFSGID
|
||||
SNR_TIMES ScmpSyscall = SYS_TIMES
|
||||
SNR_SETPGID ScmpSyscall = SYS_SETPGID
|
||||
SNR_GETPGID ScmpSyscall = SYS_GETPGID
|
||||
SNR_GETSID ScmpSyscall = SYS_GETSID
|
||||
SNR_SETSID ScmpSyscall = SYS_SETSID
|
||||
SNR_GETGROUPS ScmpSyscall = SYS_GETGROUPS
|
||||
SNR_SETGROUPS ScmpSyscall = SYS_SETGROUPS
|
||||
SNR_UNAME ScmpSyscall = SYS_UNAME
|
||||
SNR_SETHOSTNAME ScmpSyscall = SYS_SETHOSTNAME
|
||||
SNR_SETDOMAINNAME ScmpSyscall = SYS_SETDOMAINNAME
|
||||
SNR_GETRLIMIT ScmpSyscall = SYS_GETRLIMIT
|
||||
SNR_SETRLIMIT ScmpSyscall = SYS_SETRLIMIT
|
||||
SNR_GETRUSAGE ScmpSyscall = SYS_GETRUSAGE
|
||||
SNR_UMASK ScmpSyscall = SYS_UMASK
|
||||
SNR_PRCTL ScmpSyscall = SYS_PRCTL
|
||||
SNR_GETCPU ScmpSyscall = SYS_GETCPU
|
||||
SNR_GETTIMEOFDAY ScmpSyscall = SYS_GETTIMEOFDAY
|
||||
SNR_SETTIMEOFDAY ScmpSyscall = SYS_SETTIMEOFDAY
|
||||
SNR_ADJTIMEX ScmpSyscall = SYS_ADJTIMEX
|
||||
SNR_GETPID ScmpSyscall = SYS_GETPID
|
||||
SNR_GETPPID ScmpSyscall = SYS_GETPPID
|
||||
SNR_GETUID ScmpSyscall = SYS_GETUID
|
||||
SNR_GETEUID ScmpSyscall = SYS_GETEUID
|
||||
SNR_GETGID ScmpSyscall = SYS_GETGID
|
||||
SNR_GETEGID ScmpSyscall = SYS_GETEGID
|
||||
SNR_GETTID ScmpSyscall = SYS_GETTID
|
||||
SNR_SYSINFO ScmpSyscall = SYS_SYSINFO
|
||||
SNR_MQ_OPEN ScmpSyscall = SYS_MQ_OPEN
|
||||
SNR_MQ_UNLINK ScmpSyscall = SYS_MQ_UNLINK
|
||||
SNR_MQ_TIMEDSEND ScmpSyscall = SYS_MQ_TIMEDSEND
|
||||
SNR_MQ_TIMEDRECEIVE ScmpSyscall = SYS_MQ_TIMEDRECEIVE
|
||||
SNR_MQ_NOTIFY ScmpSyscall = SYS_MQ_NOTIFY
|
||||
SNR_MQ_GETSETATTR ScmpSyscall = SYS_MQ_GETSETATTR
|
||||
SNR_MSGGET ScmpSyscall = SYS_MSGGET
|
||||
SNR_MSGCTL ScmpSyscall = SYS_MSGCTL
|
||||
SNR_MSGRCV ScmpSyscall = SYS_MSGRCV
|
||||
SNR_MSGSND ScmpSyscall = SYS_MSGSND
|
||||
SNR_SEMGET ScmpSyscall = SYS_SEMGET
|
||||
SNR_SEMCTL ScmpSyscall = SYS_SEMCTL
|
||||
SNR_SEMTIMEDOP ScmpSyscall = SYS_SEMTIMEDOP
|
||||
SNR_SEMOP ScmpSyscall = SYS_SEMOP
|
||||
SNR_SHMGET ScmpSyscall = SYS_SHMGET
|
||||
SNR_SHMCTL ScmpSyscall = SYS_SHMCTL
|
||||
SNR_SHMAT ScmpSyscall = SYS_SHMAT
|
||||
SNR_SHMDT ScmpSyscall = SYS_SHMDT
|
||||
SNR_SOCKET ScmpSyscall = SYS_SOCKET
|
||||
SNR_SOCKETPAIR ScmpSyscall = SYS_SOCKETPAIR
|
||||
SNR_BIND ScmpSyscall = SYS_BIND
|
||||
SNR_LISTEN ScmpSyscall = SYS_LISTEN
|
||||
SNR_ACCEPT ScmpSyscall = SYS_ACCEPT
|
||||
SNR_CONNECT ScmpSyscall = SYS_CONNECT
|
||||
SNR_GETSOCKNAME ScmpSyscall = SYS_GETSOCKNAME
|
||||
SNR_GETPEERNAME ScmpSyscall = SYS_GETPEERNAME
|
||||
SNR_SENDTO ScmpSyscall = SYS_SENDTO
|
||||
SNR_RECVFROM ScmpSyscall = SYS_RECVFROM
|
||||
SNR_SETSOCKOPT ScmpSyscall = SYS_SETSOCKOPT
|
||||
SNR_GETSOCKOPT ScmpSyscall = SYS_GETSOCKOPT
|
||||
SNR_SHUTDOWN ScmpSyscall = SYS_SHUTDOWN
|
||||
SNR_SENDMSG ScmpSyscall = SYS_SENDMSG
|
||||
SNR_RECVMSG ScmpSyscall = SYS_RECVMSG
|
||||
SNR_READAHEAD ScmpSyscall = SYS_READAHEAD
|
||||
SNR_BRK ScmpSyscall = SYS_BRK
|
||||
SNR_MUNMAP ScmpSyscall = SYS_MUNMAP
|
||||
SNR_MREMAP ScmpSyscall = SYS_MREMAP
|
||||
SNR_ADD_KEY ScmpSyscall = SYS_ADD_KEY
|
||||
SNR_REQUEST_KEY ScmpSyscall = SYS_REQUEST_KEY
|
||||
SNR_KEYCTL ScmpSyscall = SYS_KEYCTL
|
||||
SNR_CLONE ScmpSyscall = SYS_CLONE
|
||||
SNR_EXECVE ScmpSyscall = SYS_EXECVE
|
||||
SNR_MMAP ScmpSyscall = SYS_MMAP
|
||||
SNR_FADVISE64 ScmpSyscall = SYS_FADVISE64
|
||||
SNR_SWAPON ScmpSyscall = SYS_SWAPON
|
||||
SNR_SWAPOFF ScmpSyscall = SYS_SWAPOFF
|
||||
SNR_MPROTECT ScmpSyscall = SYS_MPROTECT
|
||||
SNR_MSYNC ScmpSyscall = SYS_MSYNC
|
||||
SNR_MLOCK ScmpSyscall = SYS_MLOCK
|
||||
SNR_MUNLOCK ScmpSyscall = SYS_MUNLOCK
|
||||
SNR_MLOCKALL ScmpSyscall = SYS_MLOCKALL
|
||||
SNR_MUNLOCKALL ScmpSyscall = SYS_MUNLOCKALL
|
||||
SNR_MINCORE ScmpSyscall = SYS_MINCORE
|
||||
SNR_MADVISE ScmpSyscall = SYS_MADVISE
|
||||
SNR_REMAP_FILE_PAGES ScmpSyscall = SYS_REMAP_FILE_PAGES
|
||||
SNR_MBIND ScmpSyscall = SYS_MBIND
|
||||
SNR_GET_MEMPOLICY ScmpSyscall = SYS_GET_MEMPOLICY
|
||||
SNR_SET_MEMPOLICY ScmpSyscall = SYS_SET_MEMPOLICY
|
||||
SNR_MIGRATE_PAGES ScmpSyscall = SYS_MIGRATE_PAGES
|
||||
SNR_MOVE_PAGES ScmpSyscall = SYS_MOVE_PAGES
|
||||
SNR_RT_TGSIGQUEUEINFO ScmpSyscall = SYS_RT_TGSIGQUEUEINFO
|
||||
SNR_PERF_EVENT_OPEN ScmpSyscall = SYS_PERF_EVENT_OPEN
|
||||
SNR_ACCEPT4 ScmpSyscall = SYS_ACCEPT4
|
||||
SNR_RECVMMSG ScmpSyscall = SYS_RECVMMSG
|
||||
SNR_WAIT4 ScmpSyscall = SYS_WAIT4
|
||||
SNR_PRLIMIT64 ScmpSyscall = SYS_PRLIMIT64
|
||||
SNR_FANOTIFY_INIT ScmpSyscall = SYS_FANOTIFY_INIT
|
||||
SNR_FANOTIFY_MARK ScmpSyscall = SYS_FANOTIFY_MARK
|
||||
SNR_NAME_TO_HANDLE_AT ScmpSyscall = SYS_NAME_TO_HANDLE_AT
|
||||
SNR_OPEN_BY_HANDLE_AT ScmpSyscall = SYS_OPEN_BY_HANDLE_AT
|
||||
SNR_CLOCK_ADJTIME ScmpSyscall = SYS_CLOCK_ADJTIME
|
||||
SNR_SYNCFS ScmpSyscall = SYS_SYNCFS
|
||||
SNR_SETNS ScmpSyscall = SYS_SETNS
|
||||
SNR_SENDMMSG ScmpSyscall = SYS_SENDMMSG
|
||||
SNR_PROCESS_VM_READV ScmpSyscall = SYS_PROCESS_VM_READV
|
||||
SNR_PROCESS_VM_WRITEV ScmpSyscall = SYS_PROCESS_VM_WRITEV
|
||||
SNR_KCMP ScmpSyscall = SYS_KCMP
|
||||
SNR_FINIT_MODULE ScmpSyscall = SYS_FINIT_MODULE
|
||||
SNR_SCHED_SETATTR ScmpSyscall = SYS_SCHED_SETATTR
|
||||
SNR_SCHED_GETATTR ScmpSyscall = SYS_SCHED_GETATTR
|
||||
SNR_RENAMEAT2 ScmpSyscall = SYS_RENAMEAT2
|
||||
SNR_SECCOMP ScmpSyscall = SYS_SECCOMP
|
||||
SNR_GETRANDOM ScmpSyscall = SYS_GETRANDOM
|
||||
SNR_MEMFD_CREATE ScmpSyscall = SYS_MEMFD_CREATE
|
||||
SNR_BPF ScmpSyscall = SYS_BPF
|
||||
SNR_EXECVEAT ScmpSyscall = SYS_EXECVEAT
|
||||
SNR_USERFAULTFD ScmpSyscall = SYS_USERFAULTFD
|
||||
SNR_MEMBARRIER ScmpSyscall = SYS_MEMBARRIER
|
||||
SNR_MLOCK2 ScmpSyscall = SYS_MLOCK2
|
||||
SNR_COPY_FILE_RANGE ScmpSyscall = SYS_COPY_FILE_RANGE
|
||||
SNR_PREADV2 ScmpSyscall = SYS_PREADV2
|
||||
SNR_PWRITEV2 ScmpSyscall = SYS_PWRITEV2
|
||||
SNR_PKEY_MPROTECT ScmpSyscall = SYS_PKEY_MPROTECT
|
||||
SNR_PKEY_ALLOC ScmpSyscall = SYS_PKEY_ALLOC
|
||||
SNR_PKEY_FREE ScmpSyscall = SYS_PKEY_FREE
|
||||
SNR_STATX ScmpSyscall = SYS_STATX
|
||||
SNR_IO_PGETEVENTS ScmpSyscall = SYS_IO_PGETEVENTS
|
||||
SNR_RSEQ ScmpSyscall = SYS_RSEQ
|
||||
SNR_KEXEC_FILE_LOAD ScmpSyscall = SYS_KEXEC_FILE_LOAD
|
||||
SNR_PIDFD_SEND_SIGNAL ScmpSyscall = SYS_PIDFD_SEND_SIGNAL
|
||||
SNR_IO_URING_SETUP ScmpSyscall = SYS_IO_URING_SETUP
|
||||
SNR_IO_URING_ENTER ScmpSyscall = SYS_IO_URING_ENTER
|
||||
SNR_IO_URING_REGISTER ScmpSyscall = SYS_IO_URING_REGISTER
|
||||
SNR_OPEN_TREE ScmpSyscall = SYS_OPEN_TREE
|
||||
SNR_MOVE_MOUNT ScmpSyscall = SYS_MOVE_MOUNT
|
||||
SNR_FSOPEN ScmpSyscall = SYS_FSOPEN
|
||||
SNR_FSCONFIG ScmpSyscall = SYS_FSCONFIG
|
||||
SNR_FSMOUNT ScmpSyscall = SYS_FSMOUNT
|
||||
SNR_FSPICK ScmpSyscall = SYS_FSPICK
|
||||
SNR_PIDFD_OPEN ScmpSyscall = SYS_PIDFD_OPEN
|
||||
SNR_CLONE3 ScmpSyscall = SYS_CLONE3
|
||||
SNR_CLOSE_RANGE ScmpSyscall = SYS_CLOSE_RANGE
|
||||
SNR_OPENAT2 ScmpSyscall = SYS_OPENAT2
|
||||
SNR_PIDFD_GETFD ScmpSyscall = SYS_PIDFD_GETFD
|
||||
SNR_FACCESSAT2 ScmpSyscall = SYS_FACCESSAT2
|
||||
SNR_PROCESS_MADVISE ScmpSyscall = SYS_PROCESS_MADVISE
|
||||
SNR_EPOLL_PWAIT2 ScmpSyscall = SYS_EPOLL_PWAIT2
|
||||
SNR_MOUNT_SETATTR ScmpSyscall = SYS_MOUNT_SETATTR
|
||||
SNR_QUOTACTL_FD ScmpSyscall = SYS_QUOTACTL_FD
|
||||
SNR_LANDLOCK_CREATE_RULESET ScmpSyscall = SYS_LANDLOCK_CREATE_RULESET
|
||||
SNR_LANDLOCK_ADD_RULE ScmpSyscall = SYS_LANDLOCK_ADD_RULE
|
||||
SNR_LANDLOCK_RESTRICT_SELF ScmpSyscall = SYS_LANDLOCK_RESTRICT_SELF
|
||||
SNR_MEMFD_SECRET ScmpSyscall = SYS_MEMFD_SECRET
|
||||
SNR_PROCESS_MRELEASE ScmpSyscall = SYS_PROCESS_MRELEASE
|
||||
SNR_FUTEX_WAITV ScmpSyscall = SYS_FUTEX_WAITV
|
||||
SNR_SET_MEMPOLICY_HOME_NODE ScmpSyscall = SYS_SET_MEMPOLICY_HOME_NODE
|
||||
SNR_CACHESTAT ScmpSyscall = SYS_CACHESTAT
|
||||
SNR_FCHMODAT2 ScmpSyscall = SYS_FCHMODAT2
|
||||
SNR_MAP_SHADOW_STACK ScmpSyscall = SYS_MAP_SHADOW_STACK
|
||||
SNR_FUTEX_WAKE ScmpSyscall = SYS_FUTEX_WAKE
|
||||
SNR_FUTEX_WAIT ScmpSyscall = SYS_FUTEX_WAIT
|
||||
SNR_FUTEX_REQUEUE ScmpSyscall = SYS_FUTEX_REQUEUE
|
||||
SNR_STATMOUNT ScmpSyscall = SYS_STATMOUNT
|
||||
SNR_LISTMOUNT ScmpSyscall = SYS_LISTMOUNT
|
||||
SNR_LSM_GET_SELF_ATTR ScmpSyscall = SYS_LSM_GET_SELF_ATTR
|
||||
SNR_LSM_SET_SELF_ATTR ScmpSyscall = SYS_LSM_SET_SELF_ATTR
|
||||
SNR_LSM_LIST_MODULES ScmpSyscall = SYS_LSM_LIST_MODULES
|
||||
SNR_MSEAL ScmpSyscall = SYS_MSEAL
|
||||
SNR_SETXATTRAT ScmpSyscall = SYS_SETXATTRAT
|
||||
SNR_GETXATTRAT ScmpSyscall = SYS_GETXATTRAT
|
||||
SNR_LISTXATTRAT ScmpSyscall = SYS_LISTXATTRAT
|
||||
SNR_REMOVEXATTRAT ScmpSyscall = SYS_REMOVEXATTRAT
|
||||
SNR_OPEN_TREE_ATTR ScmpSyscall = SYS_OPEN_TREE_ATTR
|
||||
SNR_FILE_GETATTR ScmpSyscall = SYS_FILE_GETATTR
|
||||
SNR_FILE_SETATTR ScmpSyscall = SYS_FILE_SETATTR
|
||||
)
|
||||
3
dist/comp/_hakurei
vendored
3
dist/comp/_hakurei
vendored
@@ -17,8 +17,7 @@ _hakurei_run() {
|
||||
'--wayland[Enable connection to Wayland via security-context-v1]' \
|
||||
'-X[Enable direct connection to X11]' \
|
||||
'--dbus[Enable proxied connection to D-Bus]' \
|
||||
'--pipewire[Enable connection to PipeWire via SecurityContext]' \
|
||||
'--pulse[Enable PulseAudio compatibility daemon]' \
|
||||
'--pulse[Enable direct connection to PulseAudio]' \
|
||||
'--dbus-config[Path to session bus proxy config file]: :_files -g "*.json"' \
|
||||
'--dbus-system[Path to system bus proxy config file]: :_files -g "*.json"' \
|
||||
'--mpris[Allow owning MPRIS D-Bus path]' \
|
||||
|
||||
12
dist/install.sh
vendored
12
dist/install.sh
vendored
@@ -1,12 +1,12 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname -- "$0")" || exit 1
|
||||
|
||||
install -vDm0755 "bin/hakurei" "${DESTDIR}/usr/bin/hakurei"
|
||||
install -vDm0755 "bin/sharefs" "${DESTDIR}/usr/bin/sharefs"
|
||||
install -vDm0755 "bin/hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hakurei"
|
||||
install -vDm0755 "bin/hpkg" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hpkg"
|
||||
|
||||
install -vDm4511 "bin/hsu" "${DESTDIR}/usr/bin/hsu"
|
||||
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
|
||||
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
|
||||
install -vDm4511 "bin/hsu" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hsu"
|
||||
if [ ! -f "${HAKUREI_INSTALL_PREFIX}/etc/hsurc" ]; then
|
||||
install -vDm0400 "hsurc.default" "${HAKUREI_INSTALL_PREFIX}/etc/hsurc"
|
||||
fi
|
||||
|
||||
install -vDm0644 "comp/_hakurei" "${DESTDIR}/usr/share/zsh/site-functions/_hakurei"
|
||||
install -vDm0644 "comp/_hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/share/zsh/site-functions/_hakurei"
|
||||
23
dist/release.sh
vendored
23
dist/release.sh
vendored
@@ -1,31 +1,20 @@
|
||||
#!/bin/sh -e
|
||||
cd "$(dirname -- "$0")/.."
|
||||
VERSION="${HAKUREI_VERSION:-untagged}"
|
||||
pname="hakurei-${VERSION}-$(go env GOARCH)"
|
||||
out="${DESTDIR:-dist}/${pname}"
|
||||
pname="hakurei-${VERSION}"
|
||||
out="dist/${pname}"
|
||||
|
||||
echo '# Preparing distribution files.'
|
||||
mkdir -p "${out}"
|
||||
cp -v "README.md" "dist/hsurc.default" "dist/install.sh" "${out}"
|
||||
cp -rv "dist/comp" "${out}"
|
||||
echo
|
||||
|
||||
echo '# Building hakurei.'
|
||||
go generate ./...
|
||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w
|
||||
-buildid= -extldflags '-static'
|
||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid=''
|
||||
-X hakurei.app/internal/info.buildVersion=${VERSION}
|
||||
-X hakurei.app/internal/info.hakureiPath=/usr/bin/hakurei
|
||||
-X hakurei.app/internal/info.hsuPath=/usr/bin/hsu
|
||||
-X main.hakureiPath=/usr/bin/hakurei" ./...
|
||||
echo
|
||||
|
||||
echo '# Testing hakurei.'
|
||||
go test -ldflags='-buildid= -extldflags=-static' ./...
|
||||
echo
|
||||
|
||||
echo '# Creating distribution.'
|
||||
rm -f "${out}.tar.gz" && tar -C "${out}/.." -vczf "${out}.tar.gz" "${pname}"
|
||||
rm -rf "${out}"
|
||||
(cd "${out}/.." && sha512sum "${pname}.tar.gz" > "${pname}.tar.gz.sha512")
|
||||
echo
|
||||
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
|
||||
rm -rf "./${out}"
|
||||
(cd dist && sha512sum "${pname}.tar.gz" > "${pname}.tar.gz.sha512")
|
||||
|
||||
16
flake.lock
generated
16
flake.lock
generated
@@ -7,32 +7,32 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1765384171,
|
||||
"narHash": "sha256-FuFtkJrW1Z7u+3lhzPRau69E0CNjADku1mLQQflUORo=",
|
||||
"lastModified": 1756679287,
|
||||
"narHash": "sha256-Xd1vOeY9ccDf5VtVK12yM0FS6qqvfUop8UQlxEB+gTQ=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "44777152652bc9eacf8876976fa72cc77ca8b9d8",
|
||||
"rev": "07fc025fe10487dd80f2ec694f1cd790e752d0e8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "release-25.11",
|
||||
"ref": "release-25.05",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1765311797,
|
||||
"narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
|
||||
"lastModified": 1757020766,
|
||||
"narHash": "sha256-PLoSjHRa2bUbi1x9HoXgTx2AiuzNXs54c8omhadyvp0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
|
||||
"rev": "fe83bbdde2ccdc2cb9573aa846abe8363f79a97a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.11",
|
||||
"ref": "nixos-25.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
53
flake.nix
53
flake.nix
@@ -2,10 +2,10 @@
|
||||
description = "hakurei container tool and nixos module";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager/release-25.11";
|
||||
url = "github:nix-community/home-manager/release-25.05";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
@@ -69,8 +69,6 @@
|
||||
withRace = true;
|
||||
};
|
||||
|
||||
sharefs = callPackage ./cmd/sharefs/test { inherit system self; };
|
||||
|
||||
hpkg = callPackage ./cmd/hpkg/test { inherit system self; };
|
||||
|
||||
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
|
||||
@@ -112,7 +110,7 @@
|
||||
in
|
||||
{
|
||||
default = hakurei;
|
||||
hakurei = pkgs.pkgsStatic.callPackage ./package.nix {
|
||||
hakurei = pkgs.callPackage ./package.nix {
|
||||
inherit (pkgs)
|
||||
# passthru.buildInputs
|
||||
go
|
||||
@@ -138,32 +136,20 @@
|
||||
;
|
||||
};
|
||||
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
|
||||
sharefs = pkgs.linkFarm "sharefs" {
|
||||
"bin/sharefs" = "${hakurei}/libexec/sharefs";
|
||||
"bin/mount.fuse.sharefs" = "${hakurei}/libexec/sharefs";
|
||||
};
|
||||
|
||||
dist =
|
||||
pkgs.runCommand "${hakurei.name}-dist"
|
||||
{
|
||||
buildInputs = hakurei.targetPkgs ++ [
|
||||
pkgs.pkgsStatic.musl
|
||||
];
|
||||
}
|
||||
''
|
||||
cd $(mktemp -d) \
|
||||
&& cp -r ${hakurei.src}/. . \
|
||||
&& chmod +w cmd && cp -r ${hsu.src}/. cmd/hsu/ \
|
||||
&& chmod -R +w .
|
||||
dist = pkgs.runCommand "${hakurei.name}-dist" { buildInputs = hakurei.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
|
||||
# go requires XDG_CACHE_HOME for the build cache
|
||||
export XDG_CACHE_HOME="$(mktemp -d)"
|
||||
|
||||
CC="musl-clang -O3 -Werror -Qunused-arguments" \
|
||||
GOCACHE="$(mktemp -d)" \
|
||||
HAKUREI_TEST_SKIP_ACL=1 \
|
||||
PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \
|
||||
DESTDIR="$out" \
|
||||
HAKUREI_VERSION="v${hakurei.version}" \
|
||||
./dist/release.sh
|
||||
'';
|
||||
# get a different workdir as go does not like /build
|
||||
cd $(mktemp -d) \
|
||||
&& cp -r ${hakurei.src}/. . \
|
||||
&& chmod +w cmd && cp -r ${hsu.src}/. cmd/hsu/ \
|
||||
&& chmod -R +w .
|
||||
|
||||
export HAKUREI_VERSION="v${hakurei.version}"
|
||||
CC="clang -O3 -Werror" ./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out
|
||||
'';
|
||||
}
|
||||
);
|
||||
|
||||
@@ -174,10 +160,7 @@
|
||||
pkgs = nixpkgsFor.${system};
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = hakurei.targetPkgs;
|
||||
hardeningDisable = [ "fortify" ];
|
||||
};
|
||||
default = pkgs.mkShell { buildInputs = hakurei.targetPkgs; };
|
||||
withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
|
||||
|
||||
vm =
|
||||
@@ -202,13 +185,13 @@
|
||||
hakurei =
|
||||
let
|
||||
# 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.system}.hakurei.override {
|
||||
buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; });
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit package;
|
||||
hsuPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.hsu.override { hakurei = package; };
|
||||
hsuPackage = self.packages.${pkgs.system}.hsu.override { hakurei = package; };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,54 +23,9 @@ type Config struct {
|
||||
// System D-Bus proxy configuration.
|
||||
// If set to nil, system bus proxy is disabled.
|
||||
SystemBus *BusConfig `json:"system_bus,omitempty"`
|
||||
|
||||
// Direct access to wayland socket, no attempt is made to attach security-context-v1
|
||||
// and the bare socket is made available to the container.
|
||||
//
|
||||
// This option is unsupported and most likely enables full control over the Wayland
|
||||
// session. Do not set this to true unless you are sure you know what you are doing.
|
||||
DirectWayland bool `json:"direct_wayland,omitempty"`
|
||||
// Direct access to the PipeWire socket established via SecurityContext::Create, no
|
||||
// attempt is made to start the pipewire-pulse server.
|
||||
//
|
||||
// The SecurityContext machinery is fatally flawed, it blindly sets read and execute
|
||||
// bits on all objects for clients with the lowest achievable privilege level (by
|
||||
// setting PW_KEY_ACCESS to "restricted"). This enables them to call any method
|
||||
// targeting any object, and since Registry::Destroy checks for the read and execute bit,
|
||||
// allows the destruction of any object other than PW_ID_CORE as well. This behaviour
|
||||
// is implemented separately in media-session and wireplumber, with the wireplumber
|
||||
// implementation in Lua via an embedded Lua vm. In all known setups, wireplumber is
|
||||
// in use, and there is no known way to change its behaviour and set permissions
|
||||
// differently without replacing the Lua script. Also, since PipeWire relies on these
|
||||
// permissions to work, reducing them is not possible.
|
||||
//
|
||||
// Currently, the only other sandboxed use case is flatpak, which is not aware of
|
||||
// PipeWire and blindly exposes the bare PulseAudio socket to the container (behaves
|
||||
// like DirectPulse). This socket is backed by the pipewire-pulse compatibility daemon,
|
||||
// which obtains client pid via the SO_PEERCRED option. The PipeWire daemon, pipewire-pulse
|
||||
// daemon and the session manager daemon then separately performs the /.flatpak-info hack
|
||||
// described in https://git.gensokyo.uk/security/hakurei/issues/21. Under such use case,
|
||||
// since the client has no direct access to PipeWire, insecure parts of the protocol are
|
||||
// obscured by pipewire-pulse simply not implementing them, and thus hiding the flaws
|
||||
// described above.
|
||||
//
|
||||
// Hakurei does not rely on the /.flatpak-info hack. Instead, a socket is sets up via
|
||||
// SecurityContext. A pipewire-pulse server connected through it achieves the same
|
||||
// permissions as flatpak does via the /.flatpak-info hack and is maintained for the
|
||||
// life of the container.
|
||||
//
|
||||
// This option is unsupported and enables a denial-of-service attack as the sandboxed
|
||||
// client is able to destroy any client object and thus disconnecting them from PipeWire,
|
||||
// or destroy the SecurityContext object preventing any further container creation.
|
||||
// Do not set this to true, it is insecure under any configuration.
|
||||
DirectPipeWire bool `json:"direct_pipewire,omitempty"`
|
||||
// Direct access to PulseAudio socket, no attempt is made to establish pipewire-pulse
|
||||
// server via a PipeWire socket with a SecurityContext attached and the bare socket
|
||||
// is made available to the container.
|
||||
//
|
||||
// This option is unsupported and enables arbitrary code execution as the PulseAudio
|
||||
// server. Do not set this to true, it is insecure under any configuration.
|
||||
DirectPulse bool `json:"direct_pulse,omitempty"`
|
||||
|
||||
// Extra acl updates to perform before setuid.
|
||||
ExtraPerms []ExtraPermConfig `json:"extra_perms,omitempty"`
|
||||
@@ -94,9 +49,6 @@ var (
|
||||
|
||||
// ErrEnviron is returned by [Config.Validate] if an environment variable name contains '=' or NUL.
|
||||
ErrEnviron = errors.New("invalid environment variable name")
|
||||
|
||||
// ErrInsecure is returned by [Config.Validate] if the configuration is considered insecure.
|
||||
ErrInsecure = errors.New("configuration is insecure")
|
||||
)
|
||||
|
||||
// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
|
||||
@@ -143,11 +95,6 @@ func (config *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
|
||||
return &AppError{Step: "validate configuration", Err: ErrInsecure,
|
||||
Msg: "enablement PulseAudio is insecure and no longer supported"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -53,12 +53,6 @@ func TestConfigValidate(t *testing.T) {
|
||||
Env: map[string]string{"TERM\x00": ""},
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
Msg: `invalid environment variable "TERM\x00"`}},
|
||||
{"insecure pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
Msg: "enablement PulseAudio is insecure and no longer supported"}},
|
||||
{"valid", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
|
||||
@@ -17,8 +17,6 @@ const (
|
||||
EX11
|
||||
// EDBus enables the per-container xdg-dbus-proxy daemon.
|
||||
EDBus
|
||||
// EPipeWire exposes a pipewire pathname socket via SecurityContext.
|
||||
EPipeWire
|
||||
// EPulse copies the PulseAudio cookie to [hst.PrivateTmp] and exposes the PulseAudio socket.
|
||||
EPulse
|
||||
|
||||
@@ -37,8 +35,6 @@ func (e Enablement) String() string {
|
||||
return "x11"
|
||||
case EDBus:
|
||||
return "dbus"
|
||||
case EPipeWire:
|
||||
return "pipewire"
|
||||
case EPulse:
|
||||
return "pulseaudio"
|
||||
default:
|
||||
@@ -66,11 +62,10 @@ type Enablements Enablement
|
||||
|
||||
// enablementsJSON is the [json] representation of [Enablements].
|
||||
type enablementsJSON = struct {
|
||||
Wayland bool `json:"wayland,omitempty"`
|
||||
X11 bool `json:"x11,omitempty"`
|
||||
DBus bool `json:"dbus,omitempty"`
|
||||
PipeWire bool `json:"pipewire,omitempty"`
|
||||
Pulse bool `json:"pulse,omitempty"`
|
||||
Wayland bool `json:"wayland,omitempty"`
|
||||
X11 bool `json:"x11,omitempty"`
|
||||
DBus bool `json:"dbus,omitempty"`
|
||||
Pulse bool `json:"pulse,omitempty"`
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying [Enablement].
|
||||
@@ -86,11 +81,10 @@ func (e *Enablements) MarshalJSON() ([]byte, error) {
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
return json.Marshal(&enablementsJSON{
|
||||
Wayland: Enablement(*e)&EWayland != 0,
|
||||
X11: Enablement(*e)&EX11 != 0,
|
||||
DBus: Enablement(*e)&EDBus != 0,
|
||||
PipeWire: Enablement(*e)&EPipeWire != 0,
|
||||
Pulse: Enablement(*e)&EPulse != 0,
|
||||
Wayland: Enablement(*e)&EWayland != 0,
|
||||
X11: Enablement(*e)&EX11 != 0,
|
||||
DBus: Enablement(*e)&EDBus != 0,
|
||||
Pulse: Enablement(*e)&EPulse != 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -114,9 +108,6 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
|
||||
if v.DBus {
|
||||
ve |= EDBus
|
||||
}
|
||||
if v.PipeWire {
|
||||
ve |= EPipeWire
|
||||
}
|
||||
if v.Pulse {
|
||||
ve |= EPulse
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ func TestEnablementString(t *testing.T) {
|
||||
{hst.EWayland | hst.EDBus | hst.EPulse, "wayland, dbus, pulseaudio"},
|
||||
{hst.EX11 | hst.EDBus | hst.EPulse, "x11, dbus, pulseaudio"},
|
||||
{hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse, "wayland, x11, dbus, pulseaudio"},
|
||||
{hst.EM - 1, "wayland, x11, dbus, pipewire, pulseaudio"},
|
||||
|
||||
{1 << 5, "e20"},
|
||||
{1 << 6, "e40"},
|
||||
@@ -63,9 +62,8 @@ func TestEnablements(t *testing.T) {
|
||||
{"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
|
||||
{"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
|
||||
{"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
|
||||
{"pipewire", hst.NewEnablements(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
|
||||
{"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
|
||||
{"all", hst.NewEnablements(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`},
|
||||
{"all", hst.NewEnablements(hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse), `{"wayland":true,"x11":true,"dbus":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pulse":true},"magic":3236757504}`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
12
hst/fs.go
12
hst/fs.go
@@ -45,9 +45,6 @@ type Ops interface {
|
||||
Root(host *check.Absolute, flags int) Ops
|
||||
// Etc appends an op that expands host /etc into a toplevel symlink mirror with /etc semantics.
|
||||
Etc(host *check.Absolute, prefix string) Ops
|
||||
|
||||
// Daemon appends an op that starts a daemon in the container and blocks until target appears.
|
||||
Daemon(target, path *check.Absolute, args ...string) Ops
|
||||
}
|
||||
|
||||
// ApplyState holds the address of [Ops] and any relevant application state.
|
||||
@@ -127,12 +124,6 @@ func (f *FilesystemConfigJSON) MarshalJSON() ([]byte, error) {
|
||||
*FSLink
|
||||
}{fsType{FilesystemLink}, cv}
|
||||
|
||||
case *FSDaemon:
|
||||
v = &struct {
|
||||
fsType
|
||||
*FSDaemon
|
||||
}{fsType{FilesystemDaemon}, cv}
|
||||
|
||||
default:
|
||||
return nil, FSImplError{f.FilesystemConfig}
|
||||
}
|
||||
@@ -161,9 +152,6 @@ func (f *FilesystemConfigJSON) UnmarshalJSON(data []byte) error {
|
||||
case FilesystemLink:
|
||||
*f = FilesystemConfigJSON{new(FSLink)}
|
||||
|
||||
case FilesystemDaemon:
|
||||
*f = FilesystemConfigJSON{new(FSDaemon)}
|
||||
|
||||
default:
|
||||
return FSTypeError(t.Type)
|
||||
}
|
||||
|
||||
@@ -84,16 +84,6 @@ func TestFilesystemConfigJSON(t *testing.T) {
|
||||
}, nil,
|
||||
`{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true}`,
|
||||
`{"fs":{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true},"magic":3236757504}`},
|
||||
|
||||
{"daemon", hst.FilesystemConfigJSON{
|
||||
FilesystemConfig: &hst.FSDaemon{
|
||||
Target: m("/run/user/1971/pulse/native"),
|
||||
Exec: m("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
},
|
||||
}, nil,
|
||||
`{"type":"daemon","dst":"/run/user/1971/pulse/native","path":"/run/current-system/sw/bin/pipewire-pulse","args":["-v"]}`,
|
||||
`{"fs":{"type":"daemon","dst":"/run/user/1971/pulse/native","path":"/run/current-system/sw/bin/pipewire-pulse","args":["-v"]},"magic":3236757504}`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -355,10 +345,6 @@ func (p opsAdapter) Etc(host *check.Absolute, prefix string) hst.Ops {
|
||||
return opsAdapter{p.Ops.Etc(host, prefix)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) Daemon(target, path *check.Absolute, args ...string) hst.Ops {
|
||||
return opsAdapter{p.Ops.Daemon(target, path, args...)}
|
||||
}
|
||||
|
||||
func m(pathname string) *check.Absolute { return check.MustAbs(pathname) }
|
||||
func ms(pathnames ...string) []*check.Absolute {
|
||||
as := make([]*check.Absolute, len(pathnames))
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package hst
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(FSDaemon)) }
|
||||
|
||||
// FilesystemDaemon is the type string of a daemon.
|
||||
const FilesystemDaemon = "daemon"
|
||||
|
||||
// FSDaemon represents a daemon to be started in the container.
|
||||
type FSDaemon struct {
|
||||
// Pathname indicating readiness of daemon.
|
||||
Target *check.Absolute `json:"dst"`
|
||||
// Absolute pathname to daemon executable file.
|
||||
Exec *check.Absolute `json:"path"`
|
||||
// Arguments (excl. first) passed to daemon.
|
||||
Args []string `json:"args"`
|
||||
}
|
||||
|
||||
func (d *FSDaemon) Valid() bool { return d != nil && d.Target != nil && d.Exec != nil }
|
||||
|
||||
func (d *FSDaemon) Path() *check.Absolute {
|
||||
if !d.Valid() {
|
||||
return nil
|
||||
}
|
||||
return d.Target
|
||||
}
|
||||
|
||||
func (d *FSDaemon) Host() []*check.Absolute { return nil }
|
||||
|
||||
func (d *FSDaemon) Apply(z *ApplyState) {
|
||||
if !d.Valid() {
|
||||
return
|
||||
}
|
||||
z.Daemon(d.Target, d.Exec, d.Args...)
|
||||
}
|
||||
|
||||
func (d *FSDaemon) String() string {
|
||||
if !d.Valid() {
|
||||
return "<invalid>"
|
||||
}
|
||||
|
||||
return "daemon:" + d.Target.String()
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package hst_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
func TestFSDaemon(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkFs(t, []fsTestCase{
|
||||
{"nil", (*hst.FSDaemon)(nil), false, nil, nil, nil, "<invalid>"},
|
||||
{"zero", new(hst.FSDaemon), false, nil, nil, nil, "<invalid>"},
|
||||
|
||||
{"pipewire-pulse", &hst.FSDaemon{
|
||||
Target: m("/run/user/1971/pulse/native"),
|
||||
Exec: m("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
}, true, container.Ops{
|
||||
&container.DaemonOp{
|
||||
Target: m("/run/user/1971/pulse/native"),
|
||||
Path: m("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
},
|
||||
}, m("/run/user/1971/pulse/native"), nil, `daemon:/run/user/1971/pulse/native`},
|
||||
})
|
||||
}
|
||||
@@ -56,6 +56,8 @@ type Paths struct {
|
||||
type Info struct {
|
||||
// WaylandVersion is the libwayland value of WAYLAND_VERSION.
|
||||
WaylandVersion string `json:"WAYLAND_VERSION"`
|
||||
// PipeWireVersion is the pipewire value of pw_get_headers_version().
|
||||
PipeWireVersion string `json:"pw_get_headers_version"`
|
||||
|
||||
// Version is a hardcoded version string.
|
||||
Version string `json:"version"`
|
||||
@@ -70,7 +72,7 @@ func Template() *Config {
|
||||
return &Config{
|
||||
ID: "org.chromium.Chromium",
|
||||
|
||||
Enablements: NewEnablements(EWayland | EDBus | EPipeWire),
|
||||
Enablements: NewEnablements(EWayland | EDBus | EPulse),
|
||||
|
||||
SessionBus: &BusConfig{
|
||||
See: nil,
|
||||
@@ -92,6 +94,7 @@ func Template() *Config {
|
||||
Log: false,
|
||||
Filter: true,
|
||||
},
|
||||
DirectWayland: false,
|
||||
|
||||
ExtraPerms: []ExtraPermConfig{
|
||||
{Path: fhs.AbsVarLib.Append("hakurei/u0"), Ensure: true, Execute: true},
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestTemplate(t *testing.T) {
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"dbus": true,
|
||||
"pipewire": true
|
||||
"pulse": true
|
||||
},
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
|
||||
@@ -24,8 +24,9 @@ var (
|
||||
)
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
if os.Getenv("HAKUREI_TEST_SKIP_ACL") == "1" {
|
||||
t.Skip("acl test skipped")
|
||||
if os.Getenv("GO_TEST_SKIP_ACL") == "1" {
|
||||
t.Log("acl test skipped")
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
testFilePath := path.Join(t.TempDir(), testFileName)
|
||||
@@ -142,7 +143,6 @@ func (c *getFAclInvocation) run(name string) error {
|
||||
}
|
||||
|
||||
c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name)
|
||||
c.cmd.Stderr = os.Stderr
|
||||
|
||||
scanErr := make(chan error, 1)
|
||||
if p, err := c.cmd.StdoutPipe(); err != nil {
|
||||
@@ -254,7 +254,7 @@ func getfacl(t *testing.T, name string) []*getFAclResp {
|
||||
t.Fatalf("getfacl: error = %v", err)
|
||||
}
|
||||
if len(c.pe) != 0 {
|
||||
t.Errorf("errors encountered parsing getfacl output\n%s", errors.Join(c.pe...))
|
||||
t.Errorf("errors encountered parsing getfacl output\n%s", errors.Join(c.pe...).Error())
|
||||
}
|
||||
return c.val
|
||||
}
|
||||
|
||||
@@ -53,10 +53,6 @@ type syscallDispatcher interface {
|
||||
readdir(name string) ([]os.DirEntry, error)
|
||||
// tempdir provides [os.TempDir].
|
||||
tempdir() string
|
||||
// mkdir provides [os.Mkdir].
|
||||
mkdir(name string, perm os.FileMode) error
|
||||
// removeAll provides [os.RemoveAll].
|
||||
removeAll(path string) error
|
||||
// exit provides [os.Exit].
|
||||
exit(code int)
|
||||
|
||||
@@ -66,8 +62,6 @@ type syscallDispatcher interface {
|
||||
// lookupGroupId calls [user.LookupGroup] and returns the Gid field of the resulting [user.Group] struct.
|
||||
lookupGroupId(name string) (string, error)
|
||||
|
||||
// lookPath provides exec.LookPath.
|
||||
lookPath(file string) (string, error)
|
||||
// cmdOutput provides the Output method of [exec.Cmd].
|
||||
cmdOutput(cmd *exec.Cmd) ([]byte, error)
|
||||
|
||||
@@ -127,8 +121,6 @@ func (direct) stat(name string) (os.FileInfo, error) { return os.Stat(name)
|
||||
func (direct) open(name string) (osFile, error) { return os.Open(name) }
|
||||
func (direct) readdir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
|
||||
func (direct) tempdir() string { return os.TempDir() }
|
||||
func (direct) mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) }
|
||||
func (direct) removeAll(path string) error { return os.RemoveAll(path) }
|
||||
func (direct) exit(code int) { os.Exit(code) }
|
||||
|
||||
func (direct) evalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) }
|
||||
@@ -142,7 +134,6 @@ func (direct) lookupGroupId(name string) (gid string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (direct) lookPath(file string) (string, error) { return exec.LookPath(file) }
|
||||
func (direct) cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() }
|
||||
|
||||
func (direct) notifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {
|
||||
|
||||
@@ -701,13 +701,10 @@ func (panicDispatcher) stat(string) (os.FileInfo, error) { pa
|
||||
func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") }
|
||||
func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
|
||||
func (panicDispatcher) tempdir() string { panic("unreachable") }
|
||||
func (panicDispatcher) mkdir(string, os.FileMode) error { panic("unreachable") }
|
||||
func (panicDispatcher) removeAll(string) error { panic("unreachable") }
|
||||
func (panicDispatcher) exit(int) { panic("unreachable") }
|
||||
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) prctl(uintptr, uintptr, uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) lookPath(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
|
||||
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/env"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/pipewire"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/wayland"
|
||||
"hakurei.app/message"
|
||||
@@ -21,7 +22,7 @@ import (
|
||||
//
|
||||
// This must not be called from within package outcome.
|
||||
func Info() *hst.Info {
|
||||
hi := hst.Info{WaylandVersion: wayland.Version,
|
||||
hi := hst.Info{WaylandVersion: wayland.Version, PipeWireVersion: pipewire.Version,
|
||||
Version: info.Version(), User: new(Hsu).MustID(nil)}
|
||||
env.CopyPaths().Copy(&hi.Paths, hi.User)
|
||||
return &hi
|
||||
@@ -70,7 +71,7 @@ type outcomeState struct {
|
||||
// Copied from their respective exported values.
|
||||
mapuid, mapgid *stringPair[int]
|
||||
|
||||
// Copied from [env.Paths] per-process.
|
||||
// Copied from [EnvPaths] per-process.
|
||||
sc hst.Paths
|
||||
*env.Paths
|
||||
|
||||
@@ -172,10 +173,6 @@ type outcomeStateSys struct {
|
||||
|
||||
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
|
||||
directWayland bool
|
||||
// Copied from [hst.Config]. Safe for read by spPipeWireOp.toSystem only.
|
||||
directPipeWire bool
|
||||
// Copied from [hst.Config]. Safe for read by spPulseOp.toSystem only.
|
||||
directPulse bool
|
||||
// Copied header from [hst.Config]. Safe for read by spFilesystemOp.toSystem only.
|
||||
extraPerms []hst.ExtraPermConfig
|
||||
// Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only.
|
||||
@@ -189,8 +186,8 @@ type outcomeStateSys struct {
|
||||
func (s *outcomeState) newSys(config *hst.Config, sys *system.I) *outcomeStateSys {
|
||||
return &outcomeStateSys{
|
||||
appId: config.ID, et: config.Enablements.Unwrap(),
|
||||
directWayland: config.DirectWayland, directPipeWire: config.DirectPipeWire, directPulse: config.DirectPulse,
|
||||
extraPerms: config.ExtraPerms, sessionBus: config.SessionBus, systemBus: config.SystemBus,
|
||||
directWayland: config.DirectWayland, extraPerms: config.ExtraPerms,
|
||||
sessionBus: config.SessionBus, systemBus: config.SystemBus,
|
||||
sys: sys, outcomeState: s,
|
||||
}
|
||||
}
|
||||
@@ -257,10 +254,6 @@ type outcomeStateParams struct {
|
||||
// Populated by spRuntimeOp.
|
||||
runtimeDir *check.Absolute
|
||||
|
||||
// Path to pipewire-pulse server.
|
||||
// Populated by spPipeWireOp if DirectPipeWire is false.
|
||||
pipewirePulsePath *check.Absolute
|
||||
|
||||
as hst.ApplyState
|
||||
*outcomeState
|
||||
}
|
||||
@@ -300,7 +293,6 @@ func (state *outcomeStateSys) toSystem() error {
|
||||
// optional via enablements
|
||||
&spWaylandOp{},
|
||||
&spX11Op{},
|
||||
&spPipeWireOp{},
|
||||
&spPulseOp{},
|
||||
&spDBusOp{},
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestOutcomeRun(t *testing.T) {
|
||||
func TestOutcomeMain(t *testing.T) {
|
||||
t.Parallel()
|
||||
msg := message.New(nil)
|
||||
msg.SwapVerbose(testing.Verbose())
|
||||
@@ -67,12 +67,18 @@ func TestOutcomeRun(t *testing.T) {
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
).
|
||||
|
||||
// spPipeWireOp
|
||||
PipeWire(
|
||||
m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
).
|
||||
// ensureRuntimeDir
|
||||
Ensure(m("/run/user/1971"), 0700).
|
||||
UpdatePermType(system.User, m("/run/user/1971"), acl.Execute).
|
||||
Ensure(m("/run/user/1971/hakurei"), 0700).
|
||||
UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
|
||||
|
||||
// runtime
|
||||
Ephemeral(system.Process, m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0700).
|
||||
UpdatePerm(m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), acl.Execute).
|
||||
|
||||
// spPulseOp
|
||||
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse")).
|
||||
|
||||
// spDBusOp
|
||||
MustProxyDBus(
|
||||
@@ -100,6 +106,8 @@ func TestOutcomeRun(t *testing.T) {
|
||||
"GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
|
||||
"HOME=/data/data/org.chromium.Chromium",
|
||||
"PULSE_COOKIE=/.hakurei/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
@@ -136,7 +144,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
Tmpfs(fhs.AbsDevShm, 0, 01777).
|
||||
|
||||
// spRuntimeOp
|
||||
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/1971"), std.BindWritable).
|
||||
|
||||
// spTmpdirOp
|
||||
@@ -149,6 +157,10 @@ func TestOutcomeRun(t *testing.T) {
|
||||
// spWaylandOp
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1971/wayland-0"), 0).
|
||||
|
||||
// spPulseOp
|
||||
Bind(m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse"), m("/run/user/1971/pulse/native"), 0).
|
||||
Place(m("/.hakurei/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
|
||||
|
||||
// spDBusOp
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1971/bus"), 0).
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
|
||||
@@ -170,7 +182,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
Remount(fhs.AbsRoot, syscall.MS_RDONLY),
|
||||
}},
|
||||
|
||||
{"nixos permissive defaults no enablements", new(stubNixOS), &hst.Config{DirectPipeWire: true, Container: &hst.ContainerConfig{
|
||||
{"nixos permissive defaults no enablements", new(stubNixOS), &hst.Config{Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Target: fhs.AbsRoot,
|
||||
@@ -232,7 +244,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
|
||||
DevWritable(m("/dev/"), true).
|
||||
Tmpfs(m("/dev/shm/"), 0, 01777).
|
||||
Tmpfs(m("/run/user/"), xdgRuntimeDirSize, 0755).
|
||||
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/0"), m("/run/user/65534"), std.BindWritable).
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), std.BindWritable).
|
||||
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
||||
@@ -252,8 +264,6 @@ func TestOutcomeRun(t *testing.T) {
|
||||
}},
|
||||
|
||||
{"nixos permissive defaults chromium", new(stubNixOS), &hst.Config{
|
||||
DirectPipeWire: true,
|
||||
|
||||
ID: "org.chromium.Chromium",
|
||||
Identity: 9,
|
||||
Groups: []string{"video"},
|
||||
@@ -288,7 +298,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
@@ -337,7 +347,10 @@ func TestOutcomeRun(t *testing.T) {
|
||||
Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
|
||||
Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711).
|
||||
Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
|
||||
PipeWire(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
|
||||
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
||||
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
|
||||
Ephemeral(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), acl.Execute).
|
||||
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse")).
|
||||
MustProxyDBus(&hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.freedesktop.Notifications",
|
||||
@@ -384,7 +397,8 @@ func TestOutcomeRun(t *testing.T) {
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
|
||||
"HOME=/home/chronos",
|
||||
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0",
|
||||
"PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
@@ -399,13 +413,14 @@ func TestOutcomeRun(t *testing.T) {
|
||||
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
|
||||
DevWritable(m("/dev/"), true).
|
||||
Tmpfs(m("/dev/shm/"), 0, 01777).
|
||||
Tmpfs(m("/run/user/"), xdgRuntimeDirSize, 0755).
|
||||
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/65534"), std.BindWritable).
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/9"), m("/tmp/"), std.BindWritable).
|
||||
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
||||
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
|
||||
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0).
|
||||
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire"), m("/run/user/65534/pipewire-0"), 0).
|
||||
Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0).
|
||||
Place(m(hst.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
|
||||
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0).
|
||||
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
|
||||
Bind(m("/dev/dri"), m("/dev/dri"), std.BindWritable|std.BindDevice|std.BindOptional).
|
||||
@@ -424,10 +439,8 @@ func TestOutcomeRun(t *testing.T) {
|
||||
}},
|
||||
|
||||
{"nixos chromium direct wayland", new(stubNixOS), &hst.Config{
|
||||
DirectPipeWire: true,
|
||||
|
||||
ID: "org.chromium.Chromium",
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
|
||||
Container: &hst.ContainerConfig{
|
||||
Env: nil,
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
@@ -489,8 +502,9 @@ func TestOutcomeRun(t *testing.T) {
|
||||
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
||||
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
|
||||
UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute).
|
||||
Ephemeral(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), acl.Execute).
|
||||
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse")).
|
||||
Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711).
|
||||
PipeWire(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire"), "org.chromium.Chromium", "8e2c76b066dabe574cf073bdb46eb5c1").
|
||||
MustProxyDBus(&hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
|
||||
@@ -530,7 +544,8 @@ func TestOutcomeRun(t *testing.T) {
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
|
||||
"HOME=/var/lib/persist/module/hakurei/0/1",
|
||||
"PIPEWIRE_REMOTE=/run/user/1971/pipewire-0",
|
||||
"PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=u0_a1",
|
||||
@@ -544,13 +559,14 @@ func TestOutcomeRun(t *testing.T) {
|
||||
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
|
||||
DevWritable(m("/dev/"), true).
|
||||
Tmpfs(m("/dev/shm/"), 0, 01777).
|
||||
Tmpfs(m("/run/user/"), xdgRuntimeDirSize, 0755).
|
||||
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), std.BindWritable).
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), std.BindWritable).
|
||||
Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")).
|
||||
Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
|
||||
Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0).
|
||||
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire"), m("/run/user/1971/pipewire-0"), 0).
|
||||
Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0).
|
||||
Place(m(hst.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
|
||||
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
|
||||
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
|
||||
Bind(m("/bin"), m("/bin"), 0).
|
||||
@@ -883,16 +899,6 @@ func (k *stubNixOS) lookupGroupId(name string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) lookPath(file string) (string, error) {
|
||||
switch file {
|
||||
case "pipewire-pulse":
|
||||
return "/run/current-system/sw/bin/pipewire-pulse", nil
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected file %q", file))
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
|
||||
switch cmd.Path {
|
||||
case "/proc/nonexistent/hsu":
|
||||
|
||||
@@ -14,12 +14,9 @@ import (
|
||||
"time"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/pipewire"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -86,55 +83,6 @@ func Shim(msg message.Msg) {
|
||||
shimEntrypoint(direct{msg})
|
||||
}
|
||||
|
||||
// A shimPrivate holds state of the private work directory owned by shim.
|
||||
type shimPrivate struct {
|
||||
// Path to directory if created.
|
||||
pathname *check.Absolute
|
||||
|
||||
k syscallDispatcher
|
||||
id *stringPair[hst.ID]
|
||||
}
|
||||
|
||||
// unwrap returns the underlying pathname.
|
||||
func (sp *shimPrivate) unwrap() *check.Absolute {
|
||||
if sp.pathname == nil {
|
||||
if a, err := check.NewAbs(sp.k.tempdir()); err != nil {
|
||||
sp.k.fatal(err)
|
||||
panic("unreachable")
|
||||
} else {
|
||||
pathname := a.Append(".hakurei-shim-" + sp.id.String())
|
||||
sp.k.getMsg().Verbosef("creating private work directory %q", pathname)
|
||||
if err = sp.k.mkdir(pathname.String(), 0700); err != nil {
|
||||
sp.k.fatal(err)
|
||||
panic("unreachable")
|
||||
}
|
||||
sp.pathname = pathname
|
||||
return sp.unwrap()
|
||||
}
|
||||
} else {
|
||||
return sp.pathname
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the absolute pathname to the directory held by shimPrivate.
|
||||
func (sp *shimPrivate) String() string { return sp.unwrap().String() }
|
||||
|
||||
// destroy removes the directory held by shimPrivate.
|
||||
func (sp *shimPrivate) destroy() {
|
||||
defer func() { sp.pathname = nil }()
|
||||
if sp.pathname != nil {
|
||||
sp.k.getMsg().Verbosef("destroying private work directory %q", sp.pathname)
|
||||
if err := sp.k.removeAll(sp.pathname.String()); err != nil {
|
||||
sp.k.getMsg().GetLogger().Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// shimPipeWireTimeout is the duration pipewire-pulse is allowed to run before its socket becomes available.
|
||||
shimPipeWireTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
func shimEntrypoint(k syscallDispatcher) {
|
||||
msg := k.getMsg()
|
||||
if msg == nil {
|
||||
@@ -260,7 +208,6 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
|
||||
ctx, stop := k.notifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
cancelContainer.Store(&stop)
|
||||
sp := shimPrivate{k: k, id: state.id}
|
||||
z := container.New(ctx, msg)
|
||||
z.Params = *stateParams.params
|
||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
@@ -268,79 +215,6 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
// bounds and default enforced in finalise.go
|
||||
z.WaitDelay = state.Shim.WaitDelay
|
||||
|
||||
if stateParams.pipewirePulsePath != nil {
|
||||
zpw := container.NewCommand(ctx, msg, stateParams.pipewirePulsePath, pipewirePulseName)
|
||||
zpw.Hostname = "hakurei-" + pipewirePulseName
|
||||
zpw.SeccompFlags |= seccomp.AllowMultiarch
|
||||
zpw.SeccompPresets |= std.PresetStrict
|
||||
zpw.Env = []string{
|
||||
// pipewire SecurityContext socket path
|
||||
pipewire.Remote + "=" + stateParams.instancePath().Append("pipewire").String(),
|
||||
// pipewire-pulse socket directory path
|
||||
envXDGRuntimeDir + "=" + sp.String(),
|
||||
}
|
||||
if msg.IsVerbose() {
|
||||
zpw.Stdin, zpw.Stdout, zpw.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
}
|
||||
zpw.
|
||||
Bind(fhs.AbsRoot, fhs.AbsRoot, 0).
|
||||
Bind(sp.unwrap(), sp.unwrap(), std.BindWritable).
|
||||
Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
|
||||
socketPath := sp.unwrap().Append("pulse", "native")
|
||||
innerSocketPath := stateParams.runtimeDir.Append("pulse", "native")
|
||||
|
||||
if err := k.containerStart(zpw); err != nil {
|
||||
sp.destroy()
|
||||
printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) },
|
||||
"cannot start "+pipewirePulseName+" container:", err)
|
||||
}
|
||||
if err := k.containerServe(zpw); err != nil {
|
||||
sp.destroy()
|
||||
printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) },
|
||||
"cannot configure "+pipewirePulseName+" container:", err)
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
k.new(func(k syscallDispatcher, msg message.Msg) { done <- k.containerWait(zpw) })
|
||||
|
||||
socketTimer := time.NewTimer(shimPipeWireTimeout)
|
||||
for {
|
||||
select {
|
||||
case <-socketTimer.C:
|
||||
sp.destroy()
|
||||
k.fatal(pipewirePulseName + " exceeded deadline before socket appeared")
|
||||
break
|
||||
|
||||
case err := <-done:
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
msg.Verbosef("cannot wait: %v", err)
|
||||
k.exit(127)
|
||||
}
|
||||
sp.destroy()
|
||||
k.fatal(pipewirePulseName + " " + exitError.ProcessState.String())
|
||||
break
|
||||
|
||||
default:
|
||||
if _, err := k.stat(socketPath.String()); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
sp.destroy()
|
||||
k.fatal(err)
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Microsecond)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
z.Bind(socketPath, innerSocketPath, 0)
|
||||
z.Env = append(z.Env, "PULSE_SERVER=unix:"+innerSocketPath.String())
|
||||
}
|
||||
|
||||
if err := k.containerStart(z); err != nil {
|
||||
var f func(v ...any)
|
||||
if logger := msg.GetLogger(); logger != nil {
|
||||
@@ -351,11 +225,9 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
}
|
||||
}
|
||||
printMessageError(f, "cannot start container:", err)
|
||||
sp.destroy()
|
||||
k.exit(hst.ExitFailure)
|
||||
}
|
||||
if err := k.containerServe(z); err != nil {
|
||||
sp.destroy()
|
||||
printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) },
|
||||
"cannot configure container:", err)
|
||||
}
|
||||
@@ -364,13 +236,10 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
seccomp.Preset(std.PresetStrict, seccomp.AllowMultiarch),
|
||||
seccomp.AllowMultiarch,
|
||||
); err != nil {
|
||||
sp.destroy()
|
||||
k.fatalf("cannot load syscall filter: %v", err)
|
||||
}
|
||||
|
||||
if err := k.containerWait(z); err != nil {
|
||||
sp.destroy()
|
||||
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
@@ -381,5 +250,4 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
}
|
||||
k.exit(exitError.ExitCode())
|
||||
}
|
||||
sp.destroy()
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestShimEntrypoint(t *testing.T) {
|
||||
Tmpfs(fhs.AbsDevShm, 0, 01777).
|
||||
|
||||
// spRuntimeOp
|
||||
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Bind(m("/tmp/hakurei.10/runtime/9999"), m("/run/user/1000"), std.BindWritable).
|
||||
|
||||
// spTmpdirOp
|
||||
|
||||
@@ -382,10 +382,6 @@ func (p opsAdapter) Link(target *check.Absolute, linkName string, dereference bo
|
||||
return opsAdapter{p.Ops.Link(target, linkName, dereference)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) Daemon(target, path *check.Absolute, args ...string) hst.Ops {
|
||||
return opsAdapter{p.Ops.Daemon(target, path, args...)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) Root(host *check.Absolute, flags int) hst.Ops {
|
||||
return opsAdapter{p.Ops.Root(host, flags)}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
const pipewirePulseName = "pipewire-pulse"
|
||||
|
||||
func init() { gob.Register(new(spPipeWireOp)) }
|
||||
|
||||
// spPipeWireOp exports the PipeWire server to the container via SecurityContext.
|
||||
// Runs after spRuntimeOp.
|
||||
type spPipeWireOp struct {
|
||||
// Path to pipewire-pulse server. Populated during toSystem if DirectPipeWire is false.
|
||||
CompatServerPath *check.Absolute
|
||||
}
|
||||
|
||||
func (s *spPipeWireOp) toSystem(state *outcomeStateSys) error {
|
||||
if state.et&hst.EPipeWire == 0 {
|
||||
return errNotEnabled
|
||||
}
|
||||
if !state.directPipeWire {
|
||||
if n, err := state.k.lookPath(pipewirePulseName); err != nil {
|
||||
return &hst.AppError{Step: "look up " + pipewirePulseName, Err: err}
|
||||
} else if s.CompatServerPath, err = check.NewAbs(n); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
appId := state.appId
|
||||
if appId == "" {
|
||||
// use instance ID in case app id is not set
|
||||
appId = "app.hakurei." + state.id.String()
|
||||
}
|
||||
state.sys.PipeWire(state.instance().Append("pipewire"), appId, state.id.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spPipeWireOp) toContainer(state *outcomeStateParams) error {
|
||||
if s.CompatServerPath == nil {
|
||||
innerPath := state.runtimeDir.Append(pipewire.PW_DEFAULT_REMOTE)
|
||||
state.env[pipewire.Remote] = innerPath.String()
|
||||
state.params.Bind(state.instancePath().Append("pipewire"), innerPath, 0)
|
||||
}
|
||||
|
||||
// pipewire-pulse behaviour implemented in shim.go
|
||||
state.pipewirePulsePath = s.CompatServerPath
|
||||
return nil
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/pipewire"
|
||||
"hakurei.app/internal/system"
|
||||
)
|
||||
|
||||
func TestSpPipeWireOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
config := hst.Template()
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"not enabled", func(bool, bool) outcomeOp {
|
||||
return new(spPipeWireOp)
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
*c.Enablements = 0
|
||||
return c
|
||||
}, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
|
||||
|
||||
{"success", func(bool, bool) outcomeOp {
|
||||
return new(spPipeWireOp)
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
c.DirectPipeWire = true
|
||||
return c
|
||||
}, nil, []stub.Call{}, newI().
|
||||
// state.instance
|
||||
Ephemeral(system.Process, m(wantInstancePrefix), 0711).
|
||||
// toSystem
|
||||
PipeWire(
|
||||
m(wantInstancePrefix+"/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Bind(m(wantInstancePrefix+"/pipewire"), m("/run/user/1000/pipewire-0"), 0),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
pipewire.Remote: "/run/user/1000/pipewire-0",
|
||||
}, nil), nil},
|
||||
})
|
||||
}
|
||||
@@ -29,7 +29,7 @@ type spPulseOp struct {
|
||||
}
|
||||
|
||||
func (s *spPulseOp) toSystem(state *outcomeStateSys) error {
|
||||
if !state.directPulse || state.et&hst.EPulse == 0 {
|
||||
if state.et&hst.EPulse == 0 {
|
||||
return errNotEnabled
|
||||
}
|
||||
|
||||
|
||||
@@ -18,40 +18,24 @@ import (
|
||||
func TestSpPulseOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
newConfig := func() *hst.Config {
|
||||
config := hst.Template()
|
||||
config.DirectPulse = true
|
||||
config.Enablements = hst.NewEnablements(hst.EPulse)
|
||||
return config
|
||||
}
|
||||
|
||||
config := newConfig()
|
||||
config := hst.Template()
|
||||
sampleCookie := bytes.Repeat([]byte{0xfc}, pulseCookieSizeMax)
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"not enabled", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, func() *hst.Config {
|
||||
c := newConfig()
|
||||
c.DirectPulse = true
|
||||
c := hst.Template()
|
||||
*c.Enablements = 0
|
||||
return c
|
||||
}, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
|
||||
|
||||
{"not enabled direct", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, func() *hst.Config {
|
||||
c := newConfig()
|
||||
c.DirectPulse = false
|
||||
return c
|
||||
}, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
|
||||
|
||||
{"socketDir stat", func(isShim, _ bool) outcomeOp {
|
||||
if !isShim {
|
||||
return new(spPulseOp)
|
||||
}
|
||||
return &spPulseOp{Cookie: (*[256]byte)(sampleCookie)}
|
||||
}, newConfig, nil, []stub.Call{
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), stub.UniqueError(2)),
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: `access PulseAudio directory "/proc/nonexistent/xdg_runtime_dir/pulse"`,
|
||||
@@ -60,7 +44,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"socketDir nonexistent", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, newConfig, nil, []stub.Call{
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), os.ErrNotExist),
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "finalise",
|
||||
@@ -70,7 +54,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"socket stat", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, newConfig, nil, []stub.Call{
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, (*stubFi)(nil), stub.UniqueError(1)),
|
||||
}, nil, nil, &hst.AppError{
|
||||
@@ -80,7 +64,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"socket nonexistent", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, newConfig, nil, []stub.Call{
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, (*stubFi)(nil), os.ErrNotExist),
|
||||
}, nil, nil, &hst.AppError{
|
||||
@@ -91,7 +75,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"socket mode", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, newConfig, nil, []stub.Call{
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0660}, nil),
|
||||
}, nil, nil, &hst.AppError{
|
||||
@@ -102,18 +86,18 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"cookie notAbs", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, newConfig, nil, []stub.Call{
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "proc/nonexistent/cookie", nil),
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "locate PulseAudio cookie",
|
||||
Err: check.AbsoluteError("proc/nonexistent/cookie"),
|
||||
Err: &check.AbsoluteError{Pathname: "proc/nonexistent/cookie"},
|
||||
}, nil, nil, nil, nil, nil},
|
||||
|
||||
{"cookie loadFile", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, newConfig, nil, []stub.Call{
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
|
||||
@@ -134,7 +118,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
op.CookieSize += +0xfd
|
||||
}
|
||||
return op
|
||||
}, newConfig, nil, []stub.Call{
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
|
||||
@@ -166,7 +150,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
sampleCookieTrunc := make([]byte, pulseCookieSizeMax)
|
||||
copy(sampleCookieTrunc, sampleCookie[:len(sampleCookie)-0xe])
|
||||
return &spPulseOp{Cookie: (*[pulseCookieSizeMax]byte)(sampleCookieTrunc), CookieSize: pulseCookieSizeMax - 0xe}
|
||||
}, newConfig, nil, []stub.Call{
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
|
||||
@@ -199,7 +183,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
return new(spPulseOp)
|
||||
}
|
||||
return &spPulseOp{Cookie: (*[pulseCookieSizeMax]byte)(sampleCookie), CookieSize: pulseCookieSizeMax}
|
||||
}, newConfig, nil, []stub.Call{
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
|
||||
@@ -229,7 +213,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"success", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, newConfig, nil, []stub.Call{
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
|
||||
@@ -272,7 +256,7 @@ func TestDiscoverPulseCookie(t *testing.T) {
|
||||
call("verbose", stub.ExpectArgs{[]any{(*check.Absolute)(nil)}}, nil, nil),
|
||||
}}, &hst.AppError{
|
||||
Step: "locate PulseAudio cookie",
|
||||
Err: check.AbsoluteError("proc/nonexistent/pulse-cookie"),
|
||||
Err: &check.AbsoluteError{Pathname: "proc/nonexistent/pulse-cookie"},
|
||||
}},
|
||||
|
||||
{"success override", fCheckPathname, stub.Expect{Calls: []stub.Call{
|
||||
@@ -286,7 +270,7 @@ func TestDiscoverPulseCookie(t *testing.T) {
|
||||
call("verbose", stub.ExpectArgs{[]any{(*check.Absolute)(nil)}}, nil, nil),
|
||||
}}, &hst.AppError{
|
||||
Step: "locate PulseAudio cookie",
|
||||
Err: check.AbsoluteError("proc/nonexistent/home"),
|
||||
Err: &check.AbsoluteError{Pathname: "proc/nonexistent/home"},
|
||||
}},
|
||||
|
||||
{"home stat", fCheckPathname, stub.Expect{Calls: []stub.Call{
|
||||
@@ -321,7 +305,7 @@ func TestDiscoverPulseCookie(t *testing.T) {
|
||||
call("verbose", stub.ExpectArgs{[]any{(*check.Absolute)(nil)}}, nil, nil),
|
||||
}}, &hst.AppError{
|
||||
Step: "locate PulseAudio cookie",
|
||||
Err: check.AbsoluteError("proc/nonexistent/xdg"),
|
||||
Err: &check.AbsoluteError{Pathname: "proc/nonexistent/xdg"},
|
||||
}},
|
||||
|
||||
{"xdg stat", fCheckPathname, stub.Expect{Calls: []stub.Call{
|
||||
|
||||
@@ -91,9 +91,6 @@ func (s *spRuntimeOp) toSystem(state *outcomeStateSys) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// xdgRuntimeDirSize is the size of the filesystem mounted on inner XDG_RUNTIME_DIR.
|
||||
const xdgRuntimeDirSize = 1 << 24
|
||||
|
||||
func (s *spRuntimeOp) toContainer(state *outcomeStateParams) error {
|
||||
state.runtimeDir = fhs.AbsRunUser.Append(state.mapuid.String())
|
||||
state.env[envXDGRuntimeDir] = state.runtimeDir.String()
|
||||
@@ -111,7 +108,7 @@ func (s *spRuntimeOp) toContainer(state *outcomeStateParams) error {
|
||||
|
||||
}
|
||||
|
||||
state.params.Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755)
|
||||
state.params.Tmpfs(fhs.AbsRunUser, 1<<12, 0755)
|
||||
if state.Container.Flags&hst.FShareRuntime != 0 {
|
||||
_, runtimeDirInst := s.commonPaths(state.outcomeState)
|
||||
state.params.Bind(runtimeDirInst, state.runtimeDir, std.BindWritable)
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestSpRuntimeOp(t *testing.T) {
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
@@ -67,7 +67,7 @@ func TestSpRuntimeOp(t *testing.T) {
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
@@ -94,7 +94,7 @@ func TestSpRuntimeOp(t *testing.T) {
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
@@ -117,7 +117,7 @@ func TestSpRuntimeOp(t *testing.T) {
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
package pipewire
|
||||
|
||||
/* pipewire/client.h */
|
||||
|
||||
const (
|
||||
PW_TYPE_INTERFACE_Client = PW_TYPE_INFO_INTERFACE_BASE + "Client"
|
||||
PW_CLIENT_PERM_MASK = PW_PERM_RWXM
|
||||
PW_VERSION_CLIENT = 3
|
||||
|
||||
PW_ID_CLIENT = 1
|
||||
)
|
||||
|
||||
const (
|
||||
PW_CLIENT_CHANGE_MASK_PROPS = 1 << iota
|
||||
|
||||
PW_CLIENT_CHANGE_MASK_ALL = 1<<iota - 1
|
||||
)
|
||||
|
||||
const (
|
||||
PW_CLIENT_EVENT_INFO = iota
|
||||
PW_CLIENT_EVENT_PERMISSIONS
|
||||
PW_CLIENT_EVENT_NUM
|
||||
|
||||
PW_VERSION_CLIENT_EVENTS = 0
|
||||
)
|
||||
|
||||
const (
|
||||
PW_CLIENT_METHOD_ADD_LISTENER = iota
|
||||
PW_CLIENT_METHOD_ERROR
|
||||
PW_CLIENT_METHOD_UPDATE_PROPERTIES
|
||||
PW_CLIENT_METHOD_GET_PERMISSIONS
|
||||
PW_CLIENT_METHOD_UPDATE_PERMISSIONS
|
||||
PW_CLIENT_METHOD_NUM
|
||||
|
||||
PW_VERSION_CLIENT_METHODS = 0
|
||||
)
|
||||
|
||||
// The ClientInfo event provides client information updates.
|
||||
// This is emitted when binding to a client or when the client info is updated later.
|
||||
type ClientInfo struct {
|
||||
// The global id of the client.
|
||||
ID Int `json:"id"`
|
||||
// The changes emitted by this event.
|
||||
ChangeMask Long `json:"change_mask"`
|
||||
// Properties of this object, valid when change_mask has PW_CLIENT_CHANGE_MASK_PROPS.
|
||||
Properties *SPADict `json:"props"`
|
||||
}
|
||||
|
||||
// Opcode satisfies [Message] with a constant value.
|
||||
func (c *ClientInfo) Opcode() byte { return PW_CLIENT_EVENT_INFO }
|
||||
|
||||
// FileCount satisfies [Message] with a constant value.
|
||||
func (c *ClientInfo) FileCount() Int { return 0 }
|
||||
|
||||
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||
func (c *ClientInfo) Size() Word {
|
||||
return SizePrefix +
|
||||
Size(SizeInt) +
|
||||
Size(SizeLong) +
|
||||
c.Properties.Size()
|
||||
}
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (c *ClientInfo) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *ClientInfo) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// ClientUpdateProperties is used to update the properties of a client.
|
||||
type ClientUpdateProperties struct {
|
||||
// Properties to update on the client.
|
||||
Properties *SPADict `json:"props"`
|
||||
}
|
||||
|
||||
// Opcode satisfies [Message] with a constant value.
|
||||
func (c *ClientUpdateProperties) Opcode() byte { return PW_CLIENT_METHOD_UPDATE_PROPERTIES }
|
||||
|
||||
// FileCount satisfies [Message] with a constant value.
|
||||
func (c *ClientUpdateProperties) FileCount() Int { return 0 }
|
||||
|
||||
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||
func (c *ClientUpdateProperties) Size() Word { return SizePrefix + c.Properties.Size() }
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (c *ClientUpdateProperties) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *ClientUpdateProperties) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// clientUpdateProperties queues a [ClientUpdateProperties] message for the PipeWire server.
|
||||
// This method should not be called directly, the New function queues this message.
|
||||
func (ctx *Context) clientUpdateProperties(props SPADict) error {
|
||||
return ctx.writeMessage(
|
||||
PW_ID_CLIENT,
|
||||
&ClientUpdateProperties{&props},
|
||||
)
|
||||
}
|
||||
|
||||
// Client holds state of [PW_TYPE_INTERFACE_Client].
|
||||
type Client struct {
|
||||
// Additional information from the server, populated or updated during [Context.Roundtrip].
|
||||
Info *ClientInfo `json:"info"`
|
||||
|
||||
// Populated by [CoreBoundProps] events targeting [Client].
|
||||
Properties SPADict `json:"props"`
|
||||
|
||||
noRemove
|
||||
}
|
||||
|
||||
func (client *Client) consume(opcode byte, files []int, unmarshal func(v any)) error {
|
||||
closeReceivedFiles(files...)
|
||||
switch opcode {
|
||||
case PW_CLIENT_EVENT_INFO:
|
||||
unmarshal(&client.Info)
|
||||
return nil
|
||||
|
||||
default:
|
||||
panic(&UnsupportedOpcodeError{opcode, client.String()})
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) setBoundProps(event *CoreBoundProps) error {
|
||||
if event.Properties != nil {
|
||||
client.Properties = *event.Properties
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) String() string { return PW_TYPE_INTERFACE_Registry }
|
||||
@@ -1,157 +0,0 @@
|
||||
package pipewire_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
func TestClientInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.ClientInfo, *pipewire.ClientInfo]{
|
||||
{"sample", samplePWContainer[1][2][1], pipewire.ClientInfo{
|
||||
ID: 34,
|
||||
ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
}}, nil},
|
||||
|
||||
{"sample*", samplePWContainer[1][3][1], pipewire.ClientInfo{
|
||||
ID: 34,
|
||||
ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-alice-1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_SESSION_ID, Value: "1"},
|
||||
{Key: pipewire.PW_KEY_WINDOW_X11_DISPLAY, Value: ":0"},
|
||||
{Key: "cpu.vm.name", Value: "qemu"},
|
||||
{Key: "log.level", Value: "0"},
|
||||
{Key: pipewire.PW_KEY_CPU_MAX_ALIGN, Value: "32"},
|
||||
{Key: "default.clock.rate", Value: "48000"},
|
||||
{Key: "default.clock.quantum", Value: "1024"},
|
||||
{Key: "default.clock.min-quantum", Value: "32"},
|
||||
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||
{Key: "default.video.width", Value: "640"},
|
||||
{Key: "default.video.height", Value: "480"},
|
||||
{Key: "default.video.rate.num", Value: "25"},
|
||||
{Key: "default.video.rate.denom", Value: "1"},
|
||||
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||
{Key: "link.max-buffers", Value: "64"},
|
||||
{Key: "mem.warn-mlock", Value: "false"},
|
||||
{Key: "mem.allow-mlock", Value: "true"},
|
||||
{Key: "settings.check-quantum", Value: "false"},
|
||||
{Key: "settings.check-rate", Value: "false"},
|
||||
{Key: pipewire.PW_KEY_CORE_VERSION, Value: "1.4.7"},
|
||||
}}, nil},
|
||||
|
||||
{"sample**", samplePWContainer[1][4][1], pipewire.ClientInfo{
|
||||
ID: 34,
|
||||
ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-alice-1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_SESSION_ID, Value: "1"},
|
||||
{Key: pipewire.PW_KEY_WINDOW_X11_DISPLAY, Value: ":0"},
|
||||
{Key: "cpu.vm.name", Value: "qemu"},
|
||||
{Key: "log.level", Value: "0"},
|
||||
{Key: pipewire.PW_KEY_CPU_MAX_ALIGN, Value: "32"},
|
||||
{Key: "default.clock.rate", Value: "48000"},
|
||||
{Key: "default.clock.quantum", Value: "1024"},
|
||||
{Key: "default.clock.min-quantum", Value: "32"},
|
||||
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||
{Key: "default.video.width", Value: "640"},
|
||||
{Key: "default.video.height", Value: "480"},
|
||||
{Key: "default.video.rate.num", Value: "25"},
|
||||
{Key: "default.video.rate.denom", Value: "1"},
|
||||
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||
{Key: "link.max-buffers", Value: "64"},
|
||||
{Key: "mem.warn-mlock", Value: "false"},
|
||||
{Key: "mem.allow-mlock", Value: "true"},
|
||||
{Key: "settings.check-quantum", Value: "false"},
|
||||
{Key: "settings.check-rate", Value: "false"},
|
||||
{Key: pipewire.PW_KEY_CORE_VERSION, Value: "1.4.7"},
|
||||
{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
|
||||
},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestClientUpdateProperties(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.ClientUpdateProperties, *pipewire.ClientUpdateProperties]{
|
||||
{"sample", samplePWContainer[0][1][1], pipewire.ClientUpdateProperties{Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_SESSION_ID, Value: "1"},
|
||||
{Key: pipewire.PW_KEY_WINDOW_X11_DISPLAY, Value: ":0"},
|
||||
{Key: "cpu.vm.name", Value: "qemu"},
|
||||
{Key: "log.level", Value: "0"},
|
||||
{Key: pipewire.PW_KEY_CPU_MAX_ALIGN, Value: "32"},
|
||||
{Key: "default.clock.rate", Value: "48000"},
|
||||
{Key: "default.clock.quantum", Value: "1024"},
|
||||
{Key: "default.clock.min-quantum", Value: "32"},
|
||||
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||
{Key: "default.video.width", Value: "640"},
|
||||
{Key: "default.video.height", Value: "480"},
|
||||
{Key: "default.video.rate.num", Value: "25"},
|
||||
{Key: "default.video.rate.denom", Value: "1"},
|
||||
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||
{Key: "link.max-buffers", Value: "64"},
|
||||
{Key: "mem.warn-mlock", Value: "false"},
|
||||
{Key: "mem.allow-mlock", Value: "true"},
|
||||
{Key: "settings.check-quantum", Value: "false"},
|
||||
{Key: "settings.check-rate", Value: "false"},
|
||||
{Key: pipewire.PW_KEY_CORE_VERSION, Value: "1.4.7"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-alice-1443"},
|
||||
}}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
85
internal/pipewire/conn.go
Normal file
85
internal/pipewire/conn.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package pipewire
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
)
|
||||
|
||||
// SecurityContext holds resources associated with a PipeWire security context.
|
||||
type SecurityContext struct {
|
||||
// Pipe with its write end passed to the PipeWire security context.
|
||||
closeFds [2]int
|
||||
// Absolute pathname the socket was bound to.
|
||||
bindPath *check.Absolute
|
||||
}
|
||||
|
||||
// Close releases any resources held by [SecurityContext], and prevents further
|
||||
// connections to its associated socket.
|
||||
//
|
||||
// A non-nil error has the concrete type [Error].
|
||||
func (sc *SecurityContext) Close() error {
|
||||
if sc == nil || sc.bindPath == nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
e := Error{RCleanup, sc.bindPath.String(), errors.Join(
|
||||
syscall.Close(sc.closeFds[1]),
|
||||
syscall.Close(sc.closeFds[0]),
|
||||
// there is still technically a TOCTOU here but this is internal
|
||||
// and has access to the privileged pipewire socket, so it only
|
||||
// receives trusted input (e.g. from cmd/hakurei) anyway
|
||||
os.Remove(sc.bindPath.String()),
|
||||
)}
|
||||
if e.Errno != nil {
|
||||
return &e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// New creates a new security context on the PipeWire remote at remotePath
|
||||
// or auto-detected, and associates it with a new socket bound to bindPath.
|
||||
//
|
||||
// New does not attach a finalizer to the resulting [SecurityContext] struct.
|
||||
// The caller is responsible for calling [SecurityContext.Close].
|
||||
//
|
||||
// A non-nil error unwraps to concrete type [Error].
|
||||
func New(remotePath, bindPath *check.Absolute) (*SecurityContext, error) {
|
||||
// ensure bindPath is available
|
||||
if f, err := os.Create(bindPath.String()); err != nil {
|
||||
return nil, &Error{RCreate, bindPath.String(), err}
|
||||
} else if err = f.Close(); err != nil {
|
||||
return nil, &Error{RCreate, bindPath.String(), err}
|
||||
} else if err = os.Remove(bindPath.String()); err != nil {
|
||||
return nil, &Error{RCreate, bindPath.String(), err}
|
||||
}
|
||||
|
||||
// write end passed to PipeWire security context close_fd
|
||||
var closeFds [2]int
|
||||
if err := syscall.Pipe2(closeFds[0:], syscall.O_CLOEXEC); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// zero value causes auto-detect
|
||||
var remotePathVal string
|
||||
if remotePath != nil {
|
||||
remotePathVal = remotePath.String()
|
||||
}
|
||||
|
||||
// returned error is already wrapped
|
||||
if err := securityContextBind(
|
||||
bindPath.String(),
|
||||
remotePathVal,
|
||||
closeFds[1],
|
||||
); err != nil {
|
||||
return nil, errors.Join(err, // already wrapped
|
||||
syscall.Close(closeFds[1]),
|
||||
syscall.Close(closeFds[0]),
|
||||
)
|
||||
} else {
|
||||
return &SecurityContext{closeFds, bindPath}, nil
|
||||
}
|
||||
}
|
||||
66
internal/pipewire/conn_test.go
Normal file
66
internal/pipewire/conn_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package pipewire
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
)
|
||||
|
||||
func TestSecurityContextClose(t *testing.T) {
|
||||
// do not parallel: fd test not thread safe
|
||||
|
||||
if err := (*SecurityContext)(nil).Close(); !reflect.DeepEqual(err, os.ErrInvalid) {
|
||||
t.Fatalf("Close: error = %v", err)
|
||||
}
|
||||
|
||||
var ctx SecurityContext
|
||||
if err := syscall.Pipe2(ctx.closeFds[0:], syscall.O_CLOEXEC); err != nil {
|
||||
t.Fatalf("Pipe: error = %v", err)
|
||||
}
|
||||
if f, err := os.Create(path.Join(t.TempDir(), "remove")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
ctx.bindPath = check.MustAbs(f.Name())
|
||||
}
|
||||
t.Cleanup(func() { _ = syscall.Close(ctx.closeFds[0]); _ = syscall.Close(ctx.closeFds[1]) })
|
||||
|
||||
if err := ctx.Close(); err != nil {
|
||||
t.Fatalf("Close: error = %v", err)
|
||||
} else if _, err = os.Stat(ctx.bindPath.String()); err == nil || !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("Did not remove %q", ctx.bindPath)
|
||||
}
|
||||
|
||||
wantErr := &Error{Cause: RCleanup, Path: ctx.bindPath.String(), Errno: errors.Join(syscall.EBADF, syscall.EBADF, &os.PathError{
|
||||
Op: "remove",
|
||||
Path: ctx.bindPath.String(),
|
||||
Err: syscall.ENOENT,
|
||||
})}
|
||||
if err := ctx.Close(); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Fatalf("Close: error = %#v, want %#v", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEnsure(t *testing.T) {
|
||||
existingDirPath := check.MustAbs(t.TempDir()).Append("dir")
|
||||
if err := os.MkdirAll(existingDirPath.String(), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nonexistent := check.MustAbs("/proc/nonexistent")
|
||||
|
||||
wantErr := &Error{RCreate, existingDirPath.String(), &os.PathError{
|
||||
Op: "open",
|
||||
Path: existingDirPath.String(),
|
||||
Err: syscall.EISDIR,
|
||||
}}
|
||||
if _, err := New(
|
||||
nonexistent,
|
||||
existingDirPath,
|
||||
); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Fatalf("New: error = %#v, want %#v", err, wantErr)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,888 +0,0 @@
|
||||
package pipewire_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
func TestFooterCoreGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.Footer[pipewire.FooterCoreGeneration], *pipewire.Footer[pipewire.FooterCoreGeneration]]{
|
||||
|
||||
/* recvmsg 0 */
|
||||
|
||||
{"sample0", samplePWContainer[1][0][2], pipewire.Footer[pipewire.FooterCoreGeneration]{
|
||||
Opcode: pipewire.FOOTER_CORE_OPCODE_GENERATION,
|
||||
Payload: pipewire.FooterCoreGeneration{RegistryGeneration: 0x22},
|
||||
}, nil},
|
||||
|
||||
{"sample1", samplePWContainer[1][5][2], pipewire.Footer[pipewire.FooterCoreGeneration]{
|
||||
Opcode: pipewire.FOOTER_CORE_OPCODE_GENERATION,
|
||||
Payload: pipewire.FooterCoreGeneration{RegistryGeneration: 0x23},
|
||||
}, nil},
|
||||
|
||||
// happens on the last message, client footer sent in the next roundtrip
|
||||
{"sample2", samplePWContainer[1][42][2], pipewire.Footer[pipewire.FooterCoreGeneration]{
|
||||
Opcode: pipewire.FOOTER_CORE_OPCODE_GENERATION,
|
||||
Payload: pipewire.FooterCoreGeneration{RegistryGeneration: 0x24},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
|
||||
encodingTestCases[pipewire.Footer[pipewire.FooterClientGeneration], *pipewire.Footer[pipewire.FooterClientGeneration]]{
|
||||
|
||||
/* sendmsg 1 */
|
||||
|
||||
{"sample0", samplePWContainer[3][0][2], pipewire.Footer[pipewire.FooterClientGeneration]{
|
||||
Opcode: pipewire.FOOTER_CORE_OPCODE_GENERATION,
|
||||
// triggered by difference in sample1, sample0 is overwritten in the same roundtrip
|
||||
Payload: pipewire.FooterClientGeneration{ClientGeneration: 0x23},
|
||||
}, nil},
|
||||
|
||||
/* sendmsg 2 */
|
||||
|
||||
{"sample1", samplePWContainer[6][0][2], pipewire.Footer[pipewire.FooterClientGeneration]{
|
||||
// triggered by difference in sample2, last footer in the previous roundtrip
|
||||
Opcode: pipewire.FOOTER_CORE_OPCODE_GENERATION,
|
||||
Payload: pipewire.FooterClientGeneration{ClientGeneration: 0x24},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreInfo, *pipewire.CoreInfo]{
|
||||
{"sample", samplePWContainer[1][0][1], pipewire.CoreInfo{
|
||||
ID: 0,
|
||||
Cookie: -2069267610,
|
||||
UserName: "alice",
|
||||
HostName: "nixos",
|
||||
Version: "1.4.7",
|
||||
Name: "pipewire-0",
|
||||
ChangeMask: pipewire.PW_CORE_CHANGE_MASK_PROPS,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_CONFIG_NAME, Value: "pipewire.conf"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pipewire"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pipewire"},
|
||||
{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1446"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
|
||||
{Key: pipewire.PW_KEY_WINDOW_X11_DISPLAY, Value: ":0"},
|
||||
{Key: "cpu.vm.name", Value: "qemu"},
|
||||
{Key: "link.max-buffers", Value: "16"},
|
||||
{Key: pipewire.PW_KEY_CORE_DAEMON, Value: "true"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
|
||||
{Key: "default.clock.min-quantum", Value: "1024"},
|
||||
{Key: pipewire.PW_KEY_CPU_MAX_ALIGN, Value: "32"},
|
||||
{Key: "default.clock.rate", Value: "48000"},
|
||||
{Key: "default.clock.quantum", Value: "1024"},
|
||||
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||
{Key: "default.video.width", Value: "640"},
|
||||
{Key: "default.video.height", Value: "480"},
|
||||
{Key: "default.video.rate.num", Value: "25"},
|
||||
{Key: "default.video.rate.denom", Value: "1"},
|
||||
{Key: "log.level", Value: "2"},
|
||||
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||
{Key: "mem.warn-mlock", Value: "false"},
|
||||
{Key: "mem.allow-mlock", Value: "true"},
|
||||
{Key: "settings.check-quantum", Value: "false"},
|
||||
{Key: "settings.check-rate", Value: "false"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "0"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"}},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreDone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreDone, *pipewire.CoreDone]{
|
||||
{"sample0", samplePWContainer[1][5][1], pipewire.CoreDone{
|
||||
ID: -1,
|
||||
Sequence: 0,
|
||||
}, nil},
|
||||
|
||||
// matches the Core::Sync sample
|
||||
{"sample1", samplePWContainer[1][41][1], pipewire.CoreDone{
|
||||
ID: 0,
|
||||
Sequence: pipewire.CoreSyncSequenceOffset + 3,
|
||||
}, nil},
|
||||
|
||||
// matches the second Core::Sync sample
|
||||
{"sample2", samplePWContainer[7][0][1], pipewire.CoreDone{
|
||||
ID: 0,
|
||||
Sequence: pipewire.CoreSyncSequenceOffset + 6,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCorePing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CorePing, *pipewire.CorePing]{
|
||||
// handmade sample
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x20, 0, 0, 0,
|
||||
/* type: Struct */ byte(pipewire.SPA_TYPE_Struct), 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ byte(pipewire.SPA_TYPE_Int), 0, 0, 0,
|
||||
/* value: -1 */ 0xff, 0xff, 0xff, 0xff,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ byte(pipewire.SPA_TYPE_Int), 0, 0, 0,
|
||||
/* value: 0 */ 0, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CorePing{
|
||||
ID: -1,
|
||||
Sequence: 0,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreError, *pipewire.CoreError]{
|
||||
// captured from pw-cli
|
||||
{"pw-cli", []byte{
|
||||
/* size: rest of data */ 0x58, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 2 */ 2, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0x67 */ 0x67, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: -1 */ 0xff, 0xff, 0xff, 0xff,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 0x1b bytes */ 0x1b, 0, 0, 0,
|
||||
/* type: String */ 8, 0, 0, 0,
|
||||
|
||||
// value: "no permission to destroy 0\x00"
|
||||
0x6e, 0x6f, 0x20, 0x70,
|
||||
0x65, 0x72, 0x6d, 0x69,
|
||||
0x73, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x20, 0x74, 0x6f,
|
||||
0x20, 0x64, 0x65, 0x73,
|
||||
0x74, 0x72, 0x6f, 0x79,
|
||||
0x20, 0x30, 0,
|
||||
|
||||
/* padding */ 0, 0, 0, 0, 0,
|
||||
}, pipewire.CoreError{
|
||||
ID: 2,
|
||||
Sequence: 0x67,
|
||||
Result: -1,
|
||||
Message: "no permission to destroy 0",
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreRemoveId(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreRemoveId, *pipewire.CoreRemoveId]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 3 */ 3, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CoreRemoveId{
|
||||
ID: 3,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreBoundProps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreBoundProps, *pipewire.CoreBoundProps]{
|
||||
|
||||
/* recvmsg 0 */
|
||||
|
||||
{"sample0", samplePWContainer[1][1][1], pipewire.CoreBoundProps{
|
||||
ID: pipewire.PW_ID_CLIENT,
|
||||
GlobalID: 34,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
/* recvmsg 1 */
|
||||
|
||||
{"sample1", samplePWContainer[4][0][1], pipewire.CoreBoundProps{
|
||||
ID: 3,
|
||||
GlobalID: 3,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreHello(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreHello, *pipewire.CoreHello]{
|
||||
{"sample", samplePWContainer[0][0][1], pipewire.CoreHello{
|
||||
Version: pipewire.PW_VERSION_CORE,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreSync(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreSync, *pipewire.CoreSync]{
|
||||
{"sample0", samplePWContainer[0][3][1], pipewire.CoreSync{
|
||||
ID: 0,
|
||||
Sequence: pipewire.CoreSyncSequenceOffset + 3,
|
||||
}, nil},
|
||||
|
||||
{"sample1", samplePWContainer[6][1][1], pipewire.CoreSync{
|
||||
ID: 0,
|
||||
Sequence: pipewire.CoreSyncSequenceOffset + 6,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCorePong(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CorePong, *pipewire.CorePong]{
|
||||
// handmade sample
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x20, 0, 0, 0,
|
||||
/* type: Struct */ byte(pipewire.SPA_TYPE_Struct), 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ byte(pipewire.SPA_TYPE_Int), 0, 0, 0,
|
||||
/* value: -1 */ 0xff, 0xff, 0xff, 0xff,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ byte(pipewire.SPA_TYPE_Int), 0, 0, 0,
|
||||
/* value: 0 */ 0, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CorePong{
|
||||
ID: -1,
|
||||
Sequence: 0,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreGetRegistry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreGetRegistry, *pipewire.CoreGetRegistry]{
|
||||
{"sample", samplePWContainer[0][2][1], pipewire.CoreGetRegistry{
|
||||
Version: pipewire.PW_VERSION_REGISTRY,
|
||||
// this ends up as the Id of PW_TYPE_INTERFACE_Registry
|
||||
NewID: 2,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreCreateObject(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreCreateObject, *pipewire.CoreCreateObject]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x80, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 0x13 bytes */ 0x13, 0, 0, 0,
|
||||
/* type: String */ 8, 0, 0, 0,
|
||||
|
||||
// value: "spa-device-factory\x00"
|
||||
0x73, 0x70, 0x61, 0x2d,
|
||||
0x64, 0x65, 0x76, 0x69,
|
||||
0x63, 0x65, 0x2d, 0x66,
|
||||
0x61, 0x63, 0x74, 0x6f,
|
||||
0x72, 0x79, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
|
||||
/* size: 0x1a bytes */ 0x1a, 0, 0, 0,
|
||||
/* type: String */ 8, 0, 0, 0,
|
||||
|
||||
// value: "PipeWire:Interface:Device\x00"
|
||||
0x50, 0x69, 0x70, 0x65,
|
||||
0x57, 0x69, 0x72, 0x65,
|
||||
0x3a, 0x49, 0x6e, 0x74,
|
||||
0x65, 0x72, 0x66, 0x61,
|
||||
0x63, 0x65, 0x3a, 0x44,
|
||||
0x65, 0x76, 0x69, 0x63,
|
||||
0x65, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 3 */ 3, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size */ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0 */ 0, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0xbad */ 0xad, 0xb, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CoreCreateObject{
|
||||
FactoryName: "spa-device-factory",
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Device,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{},
|
||||
NewID: 0xbad,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreDestroy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreDestroy, *pipewire.CoreDestroy]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 3 */ 3, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CoreDestroy{
|
||||
ID: 3,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestRegistryGlobal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.RegistryGlobal, *pipewire.RegistryGlobal]{
|
||||
{"sample0", samplePWContainer[1][6][1], pipewire.RegistryGlobal{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Permissions: pipewire.PW_CORE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Core,
|
||||
Version: pipewire.PW_VERSION_CORE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample1", samplePWContainer[1][7][1], pipewire.RegistryGlobal{
|
||||
ID: 1,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "1"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-rt"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample2", samplePWContainer[1][8][1], pipewire.RegistryGlobal{
|
||||
ID: 3,
|
||||
Permissions: pipewire.PW_SECURITY_CONTEXT_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_SecurityContext,
|
||||
Version: pipewire.PW_VERSION_SECURITY_CONTEXT,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample3", samplePWContainer[1][9][1], pipewire.RegistryGlobal{
|
||||
ID: 2,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-protocol-native"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample4", samplePWContainer[1][10][1], pipewire.RegistryGlobal{
|
||||
ID: 5,
|
||||
Permissions: pipewire.PW_PROFILER_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Profiler,
|
||||
Version: pipewire.PW_VERSION_PROFILER,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "5"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample5", samplePWContainer[1][11][1], pipewire.RegistryGlobal{
|
||||
ID: 4,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "4"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-profiler"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample6", samplePWContainer[1][12][1], pipewire.RegistryGlobal{
|
||||
ID: 6,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "6"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-metadata"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample7", samplePWContainer[1][13][1], pipewire.RegistryGlobal{
|
||||
ID: 7,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "7"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "6"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "metadata"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Metadata},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample8", samplePWContainer[1][14][1], pipewire.RegistryGlobal{
|
||||
ID: 8,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "8"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-spa-device-factory"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample9", samplePWContainer[1][15][1], pipewire.RegistryGlobal{
|
||||
ID: 9,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "9"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "8"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "spa-device-factory"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Device},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample10", samplePWContainer[1][16][1], pipewire.RegistryGlobal{
|
||||
ID: 10,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "10"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-spa-node-factory"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample11", samplePWContainer[1][17][1], pipewire.RegistryGlobal{
|
||||
ID: 11,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "11"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "10"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "spa-node-factory"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Node},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample12", samplePWContainer[1][18][1], pipewire.RegistryGlobal{
|
||||
ID: 12,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "12"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-client-node"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample13", samplePWContainer[1][19][1], pipewire.RegistryGlobal{
|
||||
ID: 13,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "13"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "12"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "client-node"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_ClientNode},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "6"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample14", samplePWContainer[1][20][1], pipewire.RegistryGlobal{
|
||||
ID: 14,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "14"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-client-device"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample15", samplePWContainer[1][21][1], pipewire.RegistryGlobal{
|
||||
ID: 15,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "15"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "14"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "client-device"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "Spa:Pointer:Interface:Device"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample16", samplePWContainer[1][22][1], pipewire.RegistryGlobal{
|
||||
ID: 16,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "16"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-portal"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample17", samplePWContainer[1][23][1], pipewire.RegistryGlobal{
|
||||
ID: 17,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "17"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-access"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample18", samplePWContainer[1][24][1], pipewire.RegistryGlobal{
|
||||
ID: 18,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "18"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-adapter"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample19", samplePWContainer[1][25][1], pipewire.RegistryGlobal{
|
||||
ID: 19,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "19"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "18"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "adapter"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Node},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample20", samplePWContainer[1][26][1], pipewire.RegistryGlobal{
|
||||
ID: 20,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "20"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-link-factory"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample21", samplePWContainer[1][27][1], pipewire.RegistryGlobal{
|
||||
ID: 21,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "21"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "20"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "link-factory"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Link},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample22", samplePWContainer[1][28][1], pipewire.RegistryGlobal{
|
||||
ID: 22,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-session-manager"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample23", samplePWContainer[1][29][1], pipewire.RegistryGlobal{
|
||||
ID: 23,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "23"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "client-endpoint"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:ClientEndpoint"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample24", samplePWContainer[1][30][1], pipewire.RegistryGlobal{
|
||||
ID: 24,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "24"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "client-session"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:ClientSession"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample25", samplePWContainer[1][31][1], pipewire.RegistryGlobal{
|
||||
ID: 25,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "25"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "session"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:Session"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample26", samplePWContainer[1][32][1], pipewire.RegistryGlobal{
|
||||
ID: 26,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "26"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "endpoint"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:Endpoint"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample27", samplePWContainer[1][33][1], pipewire.RegistryGlobal{
|
||||
ID: 27,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "27"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "endpoint-stream"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:EndpointStream"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample28", samplePWContainer[1][34][1], pipewire.RegistryGlobal{
|
||||
ID: 28,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "28"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "endpoint-link"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:EndpointLink"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample29", samplePWContainer[1][35][1], pipewire.RegistryGlobal{
|
||||
ID: 29,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "29"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-x11-bell"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample30", samplePWContainer[1][36][1], pipewire.RegistryGlobal{
|
||||
ID: 30,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "30"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-jackdbus-detect"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample31", samplePWContainer[1][37][1], pipewire.RegistryGlobal{
|
||||
ID: 31,
|
||||
Permissions: pipewire.PW_PERM_RWXM, // why is this not PW_NODE_PERM_MASK?
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Node,
|
||||
Version: pipewire.PW_VERSION_NODE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "31"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_ID, Value: "11"},
|
||||
{Key: pipewire.PW_KEY_PRIORITY_DRIVER, Value: "200000"},
|
||||
{Key: pipewire.PW_KEY_NODE_NAME, Value: "Dummy-Driver"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample32", samplePWContainer[1][38][1], pipewire.RegistryGlobal{
|
||||
ID: 32,
|
||||
Permissions: pipewire.PW_PERM_RWXM, // why is this not PW_NODE_PERM_MASK?
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Node,
|
||||
Version: pipewire.PW_VERSION_NODE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "32"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_ID, Value: "11"},
|
||||
{Key: pipewire.PW_KEY_PRIORITY_DRIVER, Value: "190000"},
|
||||
{Key: pipewire.PW_KEY_NODE_NAME, Value: "Freewheel-Driver"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample33", samplePWContainer[1][39][1], pipewire.RegistryGlobal{
|
||||
ID: 33,
|
||||
Permissions: pipewire.PW_METADATA_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Metadata,
|
||||
Version: pipewire.PW_VERSION_METADATA,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "33"},
|
||||
{Key: "metadata.name", Value: "settings"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample34", samplePWContainer[1][40][1], pipewire.RegistryGlobal{
|
||||
ID: 34,
|
||||
Permissions: pipewire.PW_CLIENT_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Client,
|
||||
Version: pipewire.PW_VERSION_CLIENT,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pw-container"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample35", samplePWContainer[1][42][1], pipewire.RegistryGlobal{
|
||||
ID: 35,
|
||||
Permissions: pipewire.PW_CLIENT_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Client,
|
||||
Version: pipewire.PW_VERSION_CLIENT,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "35"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1447"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "WirePlumber"},
|
||||
},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestRegistryGlobalRemove(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.RegistryGlobalRemove, *pipewire.RegistryGlobalRemove]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data*/ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0xbad */ 0xad, 0xb, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.RegistryGlobalRemove{
|
||||
ID: 0xbad,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestRegistryBind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.RegistryBind, *pipewire.RegistryBind]{
|
||||
{"sample", samplePWContainer[3][0][1], pipewire.RegistryBind{
|
||||
ID: 3,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_SecurityContext,
|
||||
Version: pipewire.PW_VERSION_SECURITY_CONTEXT,
|
||||
NewID: 3, // registry takes up 2
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestRegistryDestroy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.RegistryDestroy, *pipewire.RegistryDestroy]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data*/ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0xbad */ 0xad, 0xb, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.RegistryDestroy{
|
||||
ID: 0xbad,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package pipewire
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// SizeHeader is the fixed size of [Header].
|
||||
SizeHeader = 16
|
||||
// SizeMax is the largest value of [Header.Size] that can be represented in its 3-byte segment.
|
||||
SizeMax = 0x00ffffff
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrSizeRange indicates that the value of [Header.Size] cannot be represented in its 3-byte segment.
|
||||
ErrSizeRange = errors.New("size out of range")
|
||||
// ErrBadHeader indicates that the header slice does not have length [HeaderSize].
|
||||
ErrBadHeader = errors.New("incorrect header size")
|
||||
)
|
||||
|
||||
// A Header is the fixed-size message header described in protocol native.
|
||||
type Header struct {
|
||||
// The message id this is the destination resource/proxy id.
|
||||
ID Int `json:"Id"`
|
||||
// The opcode on the resource/proxy interface.
|
||||
Opcode byte `json:"opcode"`
|
||||
// The size of the payload and optional footer of the message.
|
||||
// Note: this value is only 24 bits long in the format.
|
||||
Size uint32 `json:"size"`
|
||||
// An increasing sequence number for each message.
|
||||
Sequence Int `json:"seq"`
|
||||
// Number of file descriptors in this message.
|
||||
FileCount Int `json:"n_fds"`
|
||||
}
|
||||
|
||||
// append appends the protocol native message header to data.
|
||||
//
|
||||
// Callers must perform bounds check on [Header.Size].
|
||||
func (h *Header) append(data []byte) []byte {
|
||||
data = binary.NativeEndian.AppendUint32(data, Word(h.ID))
|
||||
data = binary.NativeEndian.AppendUint32(data, Word(h.Opcode)<<24|h.Size)
|
||||
data = binary.NativeEndian.AppendUint32(data, Word(h.Sequence))
|
||||
data = binary.NativeEndian.AppendUint32(data, Word(h.FileCount))
|
||||
return data
|
||||
}
|
||||
|
||||
// MarshalBinary encodes the protocol native message header.
|
||||
func (h *Header) MarshalBinary() (data []byte, err error) {
|
||||
if h.Size&^SizeMax != 0 {
|
||||
return nil, ErrSizeRange
|
||||
}
|
||||
return h.append(make([]byte, 0, SizeHeader)), nil
|
||||
}
|
||||
|
||||
// unmarshalBinary decodes the protocol native message header.
|
||||
func (h *Header) unmarshalBinary(data [SizeHeader]byte) {
|
||||
h.ID = Int(binary.NativeEndian.Uint32(data[0:4]))
|
||||
h.Size = binary.NativeEndian.Uint32(data[4:8])
|
||||
h.Opcode = byte(h.Size >> 24)
|
||||
h.Size &= SizeMax
|
||||
h.Sequence = Int(binary.NativeEndian.Uint32(data[8:]))
|
||||
h.FileCount = Int(binary.NativeEndian.Uint32(data[12:]))
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes the protocol native message header.
|
||||
func (h *Header) UnmarshalBinary(data []byte) error {
|
||||
if len(data) != SizeHeader {
|
||||
return ErrBadHeader
|
||||
}
|
||||
h.unmarshalBinary(([SizeHeader]byte)(data))
|
||||
return nil
|
||||
}
|
||||
@@ -1,407 +0,0 @@
|
||||
package pipewire_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
func TestHeader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.Header, *pipewire.Header]{
|
||||
|
||||
/* sendmsg 0 */
|
||||
|
||||
{"PW_CORE_METHOD_HELLO", samplePWContainer[0][0][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_METHOD_HELLO,
|
||||
Size: 0x18, Sequence: 0, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CLIENT_METHOD_UPDATE_PROPERTIES", samplePWContainer[0][1][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CLIENT,
|
||||
Opcode: pipewire.PW_CLIENT_METHOD_UPDATE_PROPERTIES,
|
||||
Size: 0x600, Sequence: 1, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_METHOD_GET_REGISTRY", samplePWContainer[0][2][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_METHOD_GET_REGISTRY,
|
||||
Size: 0x28, Sequence: 2, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_METHOD_SYNC 0", samplePWContainer[0][3][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_METHOD_SYNC,
|
||||
Size: 0x28, Sequence: 3, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* recvmsg 0 */
|
||||
|
||||
{"PW_CORE_EVENT_INFO", samplePWContainer[1][0][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_INFO,
|
||||
Size: 0x6b8, Sequence: 0, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_EVENT_BOUND_PROPS 0", samplePWContainer[1][1][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_BOUND_PROPS,
|
||||
Size: 0x198, Sequence: 1, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CLIENT_EVENT_INFO 0", samplePWContainer[1][2][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CLIENT,
|
||||
Opcode: pipewire.PW_CLIENT_EVENT_INFO,
|
||||
Size: 0x1f0, Sequence: 2, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CLIENT_EVENT_INFO 1", samplePWContainer[1][3][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CLIENT,
|
||||
Opcode: pipewire.PW_CLIENT_EVENT_INFO,
|
||||
Size: 0x7a0, Sequence: 3, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CLIENT_EVENT_INFO 2", samplePWContainer[1][4][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CLIENT,
|
||||
Opcode: pipewire.PW_CLIENT_EVENT_INFO,
|
||||
Size: 0x7d0, Sequence: 4, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_EVENT_DONE 0", samplePWContainer[1][5][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_DONE,
|
||||
Size: 0x58, Sequence: 5, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 0", samplePWContainer[1][6][0], pipewire.Header{
|
||||
ID: 2, // this is specified by Core::GetRegistry in samplePWContainer[0][2][1]
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xc8, Sequence: 6, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 1", samplePWContainer[1][7][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xd8, Sequence: 7, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 2", samplePWContainer[1][8][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xa8, Sequence: 8, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 3", samplePWContainer[1][9][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe8, Sequence: 9, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 4", samplePWContainer[1][10][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xa0, Sequence: 10, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 5", samplePWContainer[1][11][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 11, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 6", samplePWContainer[1][12][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 12, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 7", samplePWContainer[1][13][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x170, Sequence: 13, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 8", samplePWContainer[1][14][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe8, Sequence: 14, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 9", samplePWContainer[1][15][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x178, Sequence: 15, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 10", samplePWContainer[1][16][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe8, Sequence: 16, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 11", samplePWContainer[1][17][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x170, Sequence: 17, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 12", samplePWContainer[1][18][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 18, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 13", samplePWContainer[1][19][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x170, Sequence: 19, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 14", samplePWContainer[1][20][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe8, Sequence: 20, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 15", samplePWContainer[1][21][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x170, Sequence: 21, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 16", samplePWContainer[1][22][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 22, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 17", samplePWContainer[1][23][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 23, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 18", samplePWContainer[1][24][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 24, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 19", samplePWContainer[1][25][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x160, Sequence: 25, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 20", samplePWContainer[1][26][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 26, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 21", samplePWContainer[1][27][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x168, Sequence: 27, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 22", samplePWContainer[1][28][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe8, Sequence: 28, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 23", samplePWContainer[1][29][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x178, Sequence: 29, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 24", samplePWContainer[1][30][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x178, Sequence: 30, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 25", samplePWContainer[1][31][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x168, Sequence: 31, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 26", samplePWContainer[1][32][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x170, Sequence: 32, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 27", samplePWContainer[1][33][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x178, Sequence: 33, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 28", samplePWContainer[1][34][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x170, Sequence: 34, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 29", samplePWContainer[1][35][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 35, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 30", samplePWContainer[1][36][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe8, Sequence: 36, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 31", samplePWContainer[1][37][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x118, Sequence: 37, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 32", samplePWContainer[1][38][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x120, Sequence: 38, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 33", samplePWContainer[1][39][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xd0, Sequence: 39, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 34", samplePWContainer[1][40][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x238, Sequence: 40, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_EVENT_DONE 1", samplePWContainer[1][41][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_DONE,
|
||||
Size: 0x28, Sequence: 41, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 35", samplePWContainer[1][42][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x268, Sequence: 42, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* sendmsg 1 */
|
||||
|
||||
{"PW_REGISTRY_METHOD_BIND", samplePWContainer[3][0][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_METHOD_BIND,
|
||||
Size: 0x98, Sequence: 4, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* recvmsg 1 */
|
||||
|
||||
{"PW_CORE_EVENT_BOUND_PROPS 1", samplePWContainer[4][0][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_BOUND_PROPS,
|
||||
Size: 0x68, Sequence: 43, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* sendmsg 2 */
|
||||
|
||||
{"PW_SECURITY_CONTEXT_METHOD_CREATE", samplePWContainer[6][0][0], pipewire.Header{
|
||||
ID: 3,
|
||||
Opcode: pipewire.PW_SECURITY_CONTEXT_METHOD_CREATE,
|
||||
Size: 0xd8, Sequence: 5, FileCount: 2,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_METHOD_SYNC 1", samplePWContainer[6][1][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_METHOD_SYNC,
|
||||
Size: 0x28, Sequence: 6, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* recvmsg 2 */
|
||||
|
||||
{"PW_CORE_EVENT_DONE 2", samplePWContainer[7][0][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_DONE,
|
||||
Size: 0x28, Sequence: 44, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* excerpts */
|
||||
|
||||
{"PW_CORE_EVENT_ERROR", []byte{
|
||||
/* Id: */ 0, 0, 0, 0,
|
||||
/* size: */ 0x60, 0, 0,
|
||||
/* opcode: */ 3,
|
||||
/* seq: */ 0xf5, 0, 0, 0,
|
||||
/* n_fds: */ 0, 0, 0, 0,
|
||||
}, pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_ERROR,
|
||||
Size: 0x60, Sequence: 0xf5, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* handmade samples */
|
||||
|
||||
{"PW_CORE_EVENT_PING", []byte{
|
||||
/* Id: */ 0, 0, 0, 0,
|
||||
/* size: */ 0xed, 0xb, 0,
|
||||
/* opcode: */ 2,
|
||||
/* seq: */ 0xff, 0xff, 0, 0,
|
||||
/* n_fds: */ 0xfe, 0xca, 0, 0,
|
||||
}, pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_PING,
|
||||
Size: 0xbed, Sequence: 0xffff, FileCount: 0xcafe,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_METHOD_PONG", []byte{
|
||||
/* Id: */ 0, 0, 0, 0,
|
||||
/* size: */ 0xed, 0xb, 0,
|
||||
/* opcode: */ 3,
|
||||
/* seq: */ 0xff, 0xff, 0, 0,
|
||||
/* n_fds: */ 0xfe, 0xca, 0, 0,
|
||||
}, pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_METHOD_PONG,
|
||||
Size: 0xbed, Sequence: 0xffff, FileCount: 0xcafe,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_METHOD_ERROR", []byte{
|
||||
/* Id: */ 0, 0, 0, 0,
|
||||
/* size: */ 0xad, 0xb, 0,
|
||||
/* opcode: */ 4,
|
||||
/* seq: */ 0xfe, 0xfe, 0, 0,
|
||||
/* n_fds: */ 0xfe, 0xca, 0, 0,
|
||||
}, pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_METHOD_ERROR,
|
||||
Size: 0xbad, Sequence: 0xfefe, FileCount: 0xcafe,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
|
||||
t.Run("size range", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := (&pipewire.Header{Size: 0xff000000}).MarshalBinary(); !reflect.DeepEqual(err, pipewire.ErrSizeRange) {
|
||||
t.Errorf("UnmarshalBinary: error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("header size", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if err := (*pipewire.Header)(nil).UnmarshalBinary(nil); !reflect.DeepEqual(err, pipewire.ErrBadHeader) {
|
||||
t.Errorf("UnmarshalBinary: error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
252
internal/pipewire/pipewire-helper.c
Normal file
252
internal/pipewire/pipewire-helper.c
Normal file
@@ -0,0 +1,252 @@
|
||||
#include "pipewire-helper.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include <spa/utils/result.h>
|
||||
#include <spa/utils/string.h>
|
||||
#include <spa/utils/ansi.h>
|
||||
#include <spa/debug/pod.h>
|
||||
#include <spa/debug/format.h>
|
||||
#include <spa/debug/types.h>
|
||||
#include <spa/debug/file.h>
|
||||
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <pipewire/extensions/security-context.h>
|
||||
|
||||
/* contains most of the state used by hakurei_pw_security_context_bind,
|
||||
* not ideal, but it is too painful to separate state with the abysmal
|
||||
* API of pipewire */
|
||||
struct hakurei_pw_security_context_state {
|
||||
struct pw_main_loop *loop;
|
||||
struct pw_context *context;
|
||||
|
||||
struct pw_core *core;
|
||||
struct spa_hook core_listener;
|
||||
|
||||
struct pw_registry *registry;
|
||||
struct spa_hook registry_listener;
|
||||
|
||||
struct pw_properties *props;
|
||||
|
||||
struct pw_security_context *sec;
|
||||
|
||||
int pending_create;
|
||||
int create_result;
|
||||
int pending;
|
||||
int done;
|
||||
};
|
||||
|
||||
/* for field global of registry_events */
|
||||
static void registry_event_global(
|
||||
void *data, uint32_t id,
|
||||
uint32_t permissions, const char *type, uint32_t version,
|
||||
const struct spa_dict *props) {
|
||||
struct hakurei_pw_security_context_state *state = data;
|
||||
|
||||
if (spa_streq(type, PW_TYPE_INTERFACE_SecurityContext))
|
||||
state->sec = pw_registry_bind(state->registry, id, type, version, 0);
|
||||
}
|
||||
|
||||
/* for field global_remove of registry_events */
|
||||
static void registry_event_global_remove(void *data, uint32_t id) {} /* no-op */
|
||||
|
||||
static const struct pw_registry_events registry_events = {
|
||||
PW_VERSION_REGISTRY_EVENTS,
|
||||
.global = registry_event_global,
|
||||
.global_remove = registry_event_global_remove,
|
||||
};
|
||||
|
||||
/* for field error of core_events */
|
||||
static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message) {
|
||||
struct hakurei_pw_security_context_state *state = data;
|
||||
|
||||
pw_log_error("error id:%u seq:%d res:%d (%s): %s",
|
||||
id, seq, res, spa_strerror(res), message);
|
||||
|
||||
if (seq == SPA_RESULT_ASYNC_SEQ(state->pending_create))
|
||||
state->create_result = res;
|
||||
|
||||
if (id == PW_ID_CORE && res == -EPIPE) {
|
||||
state->done = true;
|
||||
pw_main_loop_quit(state->loop);
|
||||
}
|
||||
}
|
||||
|
||||
static const struct pw_core_events core_events = {
|
||||
PW_VERSION_CORE_EVENTS,
|
||||
.error = on_core_error,
|
||||
};
|
||||
|
||||
/* for field done of stack allocated core_events in roundtrip */
|
||||
static void core_event_done(void *data, uint32_t id, int seq) {
|
||||
struct hakurei_pw_security_context_state *state = data;
|
||||
if (id == PW_ID_CORE && seq == state->pending) {
|
||||
state->done = true;
|
||||
pw_main_loop_quit(state->loop);
|
||||
}
|
||||
}
|
||||
|
||||
static void roundtrip(struct hakurei_pw_security_context_state *state) {
|
||||
struct spa_hook core_listener;
|
||||
static const struct pw_core_events core_events = {
|
||||
PW_VERSION_CORE_EVENTS,
|
||||
.done = core_event_done,
|
||||
};
|
||||
spa_zero(core_listener);
|
||||
pw_core_add_listener(state->core, &core_listener, &core_events, state);
|
||||
|
||||
state->done = false;
|
||||
state->pending = pw_core_sync(state->core, PW_ID_CORE, 0);
|
||||
|
||||
while (!state->done)
|
||||
pw_main_loop_run(state->loop);
|
||||
|
||||
spa_hook_remove(&core_listener);
|
||||
}
|
||||
|
||||
hakurei_pipewire_res hakurei_pw_security_context_bind(
|
||||
char *socket_path,
|
||||
char *remote_path,
|
||||
int close_fd) {
|
||||
hakurei_pipewire_res res = HAKUREI_PIPEWIRE_SUCCESS; /* see pipewire.go for handling */
|
||||
|
||||
struct hakurei_pw_security_context_state state = {0};
|
||||
struct pw_loop *l;
|
||||
struct spa_error_location loc;
|
||||
int listen_fd;
|
||||
struct sockaddr_un sockaddr = {0};
|
||||
|
||||
/* stack allocated because pw_deinit is always called before returning,
|
||||
* in the implementation it actually does nothing with these addresses
|
||||
* and I have no idea why it would even need these, still it is safe to
|
||||
* do this to not risk a future version of pipewire clobbering strings */
|
||||
int fake_argc = 1;
|
||||
char *fake_argv[] = {"hakurei", NULL};
|
||||
/* this makes multiple getenv calls, caller must ensure to NOT setenv
|
||||
* before this function returns */
|
||||
pw_init(&fake_argc, (char ***)&fake_argv);
|
||||
|
||||
/* as far as I can tell, setting engine to "org.flatpak" gets special
|
||||
* treatment, and should never be used here because the .flatpak-info
|
||||
* hack is vulnerable to a confused deputy attack */
|
||||
state.props = pw_properties_new(
|
||||
PW_KEY_SEC_ENGINE, "app.hakurei",
|
||||
PW_KEY_ACCESS, "restricted",
|
||||
NULL);
|
||||
|
||||
/* this is unfortunately required to do ANYTHING with pipewire */
|
||||
state.loop = pw_main_loop_new(NULL);
|
||||
if (state.loop == NULL) {
|
||||
res = HAKUREI_PIPEWIRE_MAINLOOP;
|
||||
goto out;
|
||||
}
|
||||
l = pw_main_loop_get_loop(state.loop);
|
||||
|
||||
/* boilerplate from src/tools/pw-container.c */
|
||||
state.context = pw_context_new(l, NULL, 0);
|
||||
if (state.context == NULL) {
|
||||
res = HAKUREI_PIPEWIRE_CTX;
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* boilerplate from src/tools/pw-container.c;
|
||||
* this does not unsetenv, so special handling is not required
|
||||
* unlike for libwayland-client */
|
||||
state.core = pw_context_connect(
|
||||
state.context,
|
||||
pw_properties_new(
|
||||
PW_KEY_REMOTE_INTENTION, "manager",
|
||||
PW_KEY_REMOTE_NAME, remote_path,
|
||||
NULL),
|
||||
0);
|
||||
if (state.core == NULL) {
|
||||
res = HAKUREI_PIPEWIRE_CONNECT;
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* obtains the security context */
|
||||
pw_core_add_listener(state.core, &state.core_listener, &core_events, &state);
|
||||
state.registry = pw_core_get_registry(state.core, PW_VERSION_REGISTRY, 0);
|
||||
if (state.registry == NULL) {
|
||||
res = HAKUREI_PIPEWIRE_REGISTRY;
|
||||
goto out;
|
||||
}
|
||||
/* undocumented, this ends up calling registry_method_marshal_add_listener,
|
||||
* which is hard-coded to return 0, note that the function pointer this calls
|
||||
* is uninitialised for some pw_registry objects so if you are using this code
|
||||
* as an example you must keep that in mind */
|
||||
pw_registry_add_listener(state.registry, &state.registry_listener, ®istry_events, &state);
|
||||
roundtrip(&state);
|
||||
if (state.sec == NULL) {
|
||||
res = HAKUREI_PIPEWIRE_NOT_AVAIL;
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* socket to attach security context */
|
||||
listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (listen_fd < 0) {
|
||||
res = HAKUREI_PIPEWIRE_SOCKET;
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* similar to libwayland, pipewire requires bind and listen to be called
|
||||
* on the socket before being passed to pw_security_context_create */
|
||||
sockaddr.sun_family = AF_UNIX;
|
||||
snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", socket_path);
|
||||
if (bind(listen_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != 0) {
|
||||
res = HAKUREI_PIPEWIRE_BIND;
|
||||
goto out;
|
||||
}
|
||||
if (listen(listen_fd, 0) != 0) {
|
||||
res = HAKUREI_PIPEWIRE_LISTEN;
|
||||
goto out;
|
||||
}
|
||||
|
||||
|
||||
/* attach security context to socket */
|
||||
state.create_result = 0;
|
||||
state.pending_create = pw_security_context_create(state.sec, listen_fd, close_fd, &state.props->dict);
|
||||
if (SPA_RESULT_IS_ASYNC(state.pending_create)) {
|
||||
pw_log_debug("create: %d", state.pending_create);
|
||||
roundtrip(&state);
|
||||
}
|
||||
pw_log_debug("create result: %d", state.create_result);
|
||||
if (state.create_result < 0) {
|
||||
/* spa_strerror */
|
||||
if (SPA_RESULT_IS_ASYNC(-state.create_result))
|
||||
errno = EINPROGRESS;
|
||||
else
|
||||
errno = -state.create_result;
|
||||
|
||||
res = HAKUREI_PIPEWIRE_ATTACH;
|
||||
goto out;
|
||||
}
|
||||
|
||||
out:
|
||||
if (listen_fd >= 0)
|
||||
close(listen_fd);
|
||||
if (state.sec != NULL)
|
||||
pw_proxy_destroy((struct pw_proxy *)state.sec);
|
||||
if (state.registry != NULL)
|
||||
pw_proxy_destroy((struct pw_proxy *)state.registry);
|
||||
if (state.core != NULL) {
|
||||
/* these happen after core is checked non-NULL and always succeeds */
|
||||
spa_hook_remove(&state.registry_listener);
|
||||
spa_hook_remove(&state.core_listener);
|
||||
|
||||
pw_core_disconnect(state.core);
|
||||
}
|
||||
if (state.context != NULL)
|
||||
pw_context_destroy(state.context);
|
||||
if (state.loop != NULL)
|
||||
pw_main_loop_destroy(state.loop);
|
||||
pw_properties_free(state.props);
|
||||
pw_deinit();
|
||||
|
||||
free((void *)socket_path);
|
||||
if (remote_path != NULL)
|
||||
free((void *)remote_path);
|
||||
return res;
|
||||
}
|
||||
40
internal/pipewire/pipewire-helper.h
Normal file
40
internal/pipewire/pipewire-helper.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#include <stdbool.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
typedef enum {
|
||||
HAKUREI_PIPEWIRE_SUCCESS,
|
||||
/* pw_main_loop_new failed, errno */
|
||||
HAKUREI_PIPEWIRE_MAINLOOP,
|
||||
/* pw_context_new failed, errno */
|
||||
HAKUREI_PIPEWIRE_CTX,
|
||||
/* pw_context_connect failed, errno */
|
||||
HAKUREI_PIPEWIRE_CONNECT,
|
||||
/* pw_core_get_registry failed */
|
||||
HAKUREI_PIPEWIRE_REGISTRY,
|
||||
/* no security context object found */
|
||||
HAKUREI_PIPEWIRE_NOT_AVAIL,
|
||||
/* socket failed, errno */
|
||||
HAKUREI_PIPEWIRE_SOCKET,
|
||||
/* bind failed, errno */
|
||||
HAKUREI_PIPEWIRE_BIND,
|
||||
/* listen failed, errno */
|
||||
HAKUREI_PIPEWIRE_LISTEN,
|
||||
/* pw_security_context_create failed, translated errno */
|
||||
HAKUREI_PIPEWIRE_ATTACH,
|
||||
|
||||
/* ensure pathname failed, implemented in conn.go */
|
||||
HAKUREI_PIPEWIRE_CREAT,
|
||||
/* cleanup failed, implemented in conn.go */
|
||||
HAKUREI_PIPEWIRE_CLEANUP,
|
||||
} hakurei_pipewire_res;
|
||||
|
||||
hakurei_pipewire_res hakurei_pw_security_context_bind(
|
||||
char *socket_path,
|
||||
char *remote_path,
|
||||
int close_fd);
|
||||
|
||||
/* returns whether the specified size fits in the sun_path field of sockaddr_un */
|
||||
static inline bool hakurei_pw_is_valid_size_sun_path(size_t sz) {
|
||||
struct sockaddr_un sockaddr;
|
||||
return sz <= sizeof(sockaddr.sun_path);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,641 +0,0 @@
|
||||
package pipewire
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
// A Word is a 32-bit unsigned integer.
|
||||
//
|
||||
// Values internal to a message appear to always be aligned to 32-bit boundary.
|
||||
Word = uint32
|
||||
|
||||
// A Bool is a boolean value representing SPA_TYPE_Bool.
|
||||
Bool = bool
|
||||
// An Id is an enumerated value representing SPA_TYPE_Id.
|
||||
Id = Word
|
||||
// An Int is a signed integer value representing SPA_TYPE_Int.
|
||||
Int = int32
|
||||
// A Long is a signed integer value representing SPA_TYPE_Long.
|
||||
Long = int64
|
||||
// A Float is a floating point value representing SPA_TYPE_Float.
|
||||
Float = float32
|
||||
// A Double is a floating point value representing SPA_TYPE_Double.
|
||||
Double = float64
|
||||
// A String is a string value representing SPA_TYPE_String.
|
||||
String = string
|
||||
// Bytes is a byte slice representing SPA_TYPE_Bytes.
|
||||
Bytes = []byte
|
||||
|
||||
// A Fd is a signed integer value representing SPA_TYPE_Fd.
|
||||
Fd Long
|
||||
)
|
||||
|
||||
const (
|
||||
// SizeAlign is the boundary which POD starts are always aligned to.
|
||||
SizeAlign = 8
|
||||
|
||||
// SizeSPrefix is the fixed, unpadded size of the fixed-size prefix encoding POD wire size.
|
||||
SizeSPrefix = 4
|
||||
// SizeTPrefix is the fixed, unpadded size of the fixed-size prefix encoding POD value type.
|
||||
SizeTPrefix = 4
|
||||
// SizePrefix is the fixed, unpadded size of the fixed-size POD prefix.
|
||||
SizePrefix = SizeSPrefix + SizeTPrefix
|
||||
|
||||
// SizeId is the fixed, unpadded size of a [SPA_TYPE_Id] value.
|
||||
SizeId Word = 4
|
||||
// SizeInt is the fixed, unpadded size of a [SPA_TYPE_Int] value.
|
||||
SizeInt Word = 4
|
||||
// SizeLong is the fixed, unpadded size of a [SPA_TYPE_Long] value.
|
||||
SizeLong Word = 8
|
||||
|
||||
// SizeFd is the fixed, unpadded size of a [SPA_TYPE_Fd] value.
|
||||
SizeFd = SizeLong
|
||||
)
|
||||
|
||||
// A KnownSize value has known POD encoded size before serialisation.
|
||||
type KnownSize interface {
|
||||
// Size returns the POD encoded size of the receiver.
|
||||
Size() Word
|
||||
}
|
||||
|
||||
// PaddingSize returns the padding size corresponding to a wire size.
|
||||
func PaddingSize[W Word | int](wireSize W) W { return (SizeAlign - (wireSize)%SizeAlign) % SizeAlign }
|
||||
|
||||
// PaddedSize returns the padded size corresponding to a wire size.
|
||||
func PaddedSize[W Word | int](wireSize W) W { return wireSize + PaddingSize(wireSize) }
|
||||
|
||||
// Size returns prefixed and padded size corresponding to a wire size.
|
||||
func Size[W Word | int](wireSize W) W { return SizePrefix + PaddedSize(wireSize) }
|
||||
|
||||
// SizeString returns prefixed and padded size corresponding to a string.
|
||||
func SizeString[W Word | int](s string) W { return Size(W(len(s)) + 1) }
|
||||
|
||||
// PODMarshaler is the interface implemented by an object that can
|
||||
// marshal itself into PipeWire POD encoding.
|
||||
type PODMarshaler interface {
|
||||
// MarshalPOD encodes the receiver into PipeWire POD encoding,
|
||||
// appends it to data, and returns the result.
|
||||
MarshalPOD(data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// An UnsupportedTypeError is returned by [Marshal] when attempting
|
||||
// to encode an unsupported value type.
|
||||
type UnsupportedTypeError struct{ Type reflect.Type }
|
||||
|
||||
func (e *UnsupportedTypeError) Error() string { return "unsupported type " + e.Type.String() }
|
||||
|
||||
// An UnsupportedSizeError is returned by [Marshal] when attempting
|
||||
// to encode a value with its encoded size exceeding what could be
|
||||
// represented by the format.
|
||||
type UnsupportedSizeError int
|
||||
|
||||
func (e UnsupportedSizeError) Error() string { return "size " + strconv.Itoa(int(e)) + " out of range" }
|
||||
|
||||
// Marshal returns the PipeWire POD encoding of v.
|
||||
func Marshal(v any) ([]byte, error) {
|
||||
var data []byte
|
||||
if s, ok := v.(KnownSize); ok {
|
||||
data = make([]byte, 0, s.Size())
|
||||
}
|
||||
return MarshalAppend(data, v)
|
||||
}
|
||||
|
||||
// MarshalAppend appends the PipeWire POD encoding of v to data.
|
||||
func MarshalAppend(data []byte, v any) ([]byte, error) {
|
||||
return marshalValueAppend(data, reflect.ValueOf(v))
|
||||
}
|
||||
|
||||
// appendInner calls f and handles size prefix and padding around the appended data.
|
||||
// f must only append to data.
|
||||
func appendInner(data []byte, f func(data []byte) ([]byte, error)) ([]byte, error) {
|
||||
data = append(data, make([]byte, SizeSPrefix)...)
|
||||
|
||||
rData, err := f(data)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
size := len(rData) - len(data) + SizeSPrefix
|
||||
// compensated for size and type prefix
|
||||
wireSize := size - SizePrefix
|
||||
if wireSize > math.MaxUint32 {
|
||||
return data, UnsupportedSizeError(wireSize)
|
||||
}
|
||||
binary.NativeEndian.PutUint32(rData[len(data)-SizeSPrefix:len(data)], Word(wireSize))
|
||||
rData = append(rData, make([]byte, PaddingSize(size))...)
|
||||
|
||||
return rData, nil
|
||||
}
|
||||
|
||||
// marshalValueAppendRaw implements [MarshalAppend] on [reflect.Value].
|
||||
func marshalValueAppend(data []byte, v reflect.Value) ([]byte, error) {
|
||||
if v.CanInterface() && (v.Kind() != reflect.Pointer || !v.IsNil()) {
|
||||
if m, ok := v.Interface().(PODMarshaler); ok {
|
||||
var err error
|
||||
data, err = m.MarshalPOD(data)
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
return appendInner(data, func(data []byte) ([]byte, error) { return marshalValueAppendRaw(data, v) })
|
||||
}
|
||||
|
||||
// marshalValueAppendRaw implements [MarshalAppend] on [reflect.Value] without the size prefix.
|
||||
func marshalValueAppendRaw(data []byte, v reflect.Value) ([]byte, error) {
|
||||
if v.CanInterface() {
|
||||
switch c := v.Interface().(type) {
|
||||
case Fd:
|
||||
data = SPA_TYPE_Fd.append(data)
|
||||
data = binary.NativeEndian.AppendUint64(data, uint64(c))
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Uint32:
|
||||
data = SPA_TYPE_Id.append(data)
|
||||
data = binary.NativeEndian.AppendUint32(data, Word(v.Uint()))
|
||||
return data, nil
|
||||
|
||||
case reflect.Int32:
|
||||
data = SPA_TYPE_Int.append(data)
|
||||
data = binary.NativeEndian.AppendUint32(data, Word(v.Int()))
|
||||
return data, nil
|
||||
|
||||
case reflect.Int64:
|
||||
data = SPA_TYPE_Long.append(data)
|
||||
data = binary.NativeEndian.AppendUint64(data, uint64(v.Int()))
|
||||
return data, nil
|
||||
|
||||
case reflect.Struct:
|
||||
data = SPA_TYPE_Struct.append(data)
|
||||
var err error
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
data, err = marshalValueAppend(data, v.Field(i))
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
|
||||
case reflect.Pointer:
|
||||
if v.IsNil() {
|
||||
data = SPA_TYPE_None.append(data)
|
||||
return data, nil
|
||||
}
|
||||
return marshalValueAppendRaw(data, v.Elem())
|
||||
|
||||
case reflect.String:
|
||||
data = SPA_TYPE_String.append(data)
|
||||
data = append(data, []byte(v.String())...)
|
||||
data = append(data, 0)
|
||||
return data, nil
|
||||
|
||||
default:
|
||||
return data, &UnsupportedTypeError{v.Type()}
|
||||
}
|
||||
}
|
||||
|
||||
// PODUnmarshaler is the interface implemented by an object that can
|
||||
// unmarshal a PipeWire POD encoding representation of itself.
|
||||
type PODUnmarshaler interface {
|
||||
// UnmarshalPOD must be able to decode the form generated by MarshalPOD.
|
||||
// UnmarshalPOD must copy the data if it wishes to retain the data
|
||||
// after returning.
|
||||
UnmarshalPOD(data []byte) (Word, error)
|
||||
}
|
||||
|
||||
// An InvalidUnmarshalError describes an invalid argument passed to [Unmarshal].
|
||||
// (The argument to [Unmarshal] must be a non-nil pointer.)
|
||||
type InvalidUnmarshalError struct{ Type reflect.Type }
|
||||
|
||||
func (e *InvalidUnmarshalError) Error() string {
|
||||
if e.Type == nil {
|
||||
return "attempting to unmarshal to nil"
|
||||
}
|
||||
|
||||
if e.Type.Kind() != reflect.Pointer {
|
||||
return "attempting to unmarshal to non-pointer type " + e.Type.String()
|
||||
}
|
||||
return "attempting to unmarshal to nil " + e.Type.String()
|
||||
}
|
||||
|
||||
// UnexpectedEOFError describes an unexpected EOF encountered in the middle of decoding POD data.
|
||||
type UnexpectedEOFError uintptr
|
||||
|
||||
const (
|
||||
// ErrEOFPrefix is returned when unexpectedly encountering EOF
|
||||
// decoding the fixed-size POD prefix.
|
||||
ErrEOFPrefix UnexpectedEOFError = iota
|
||||
// ErrEOFData is returned when unexpectedly encountering EOF
|
||||
// establishing POD data bounds.
|
||||
ErrEOFData
|
||||
// ErrEOFDataString is returned when unexpectedly encountering EOF
|
||||
// establishing POD [String] bounds.
|
||||
ErrEOFDataString
|
||||
)
|
||||
|
||||
func (UnexpectedEOFError) Unwrap() error { return io.ErrUnexpectedEOF }
|
||||
func (e UnexpectedEOFError) Error() string {
|
||||
var suffix string
|
||||
switch e {
|
||||
case ErrEOFPrefix:
|
||||
suffix = "decoding fixed-size POD prefix"
|
||||
case ErrEOFData:
|
||||
suffix = "establishing POD data bounds"
|
||||
case ErrEOFDataString:
|
||||
suffix = "establishing POD String bounds"
|
||||
|
||||
default:
|
||||
return "unexpected EOF"
|
||||
}
|
||||
|
||||
return "unexpected EOF " + suffix
|
||||
}
|
||||
|
||||
// Unmarshal parses the PipeWire POD encoded data and stores the result
|
||||
// in the value pointed to by v. If v is nil or not a pointer,
|
||||
// Unmarshal returns an [InvalidUnmarshalError].
|
||||
func Unmarshal(data []byte, v any) error {
|
||||
if n, err := UnmarshalNext(data, v); err != nil {
|
||||
return err
|
||||
} else if len(data) > int(n) {
|
||||
return TrailingGarbageError(data[int(n):])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalNext implements [Unmarshal] but returns the size of message decoded
|
||||
// and skips the final trailing garbage check.
|
||||
func UnmarshalNext(data []byte, v any) (size Word, err error) {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
||||
return 0, &InvalidUnmarshalError{reflect.TypeOf(v)}
|
||||
}
|
||||
err = unmarshalValue(data, rv.Elem(), &size)
|
||||
// prefix and padding size
|
||||
size = Size(size)
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalSetError describes a value that cannot be set during [Unmarshal].
|
||||
// This is likely an unexported struct field.
|
||||
type UnmarshalSetError struct{ Type reflect.Type }
|
||||
|
||||
func (u *UnmarshalSetError) Error() string { return "cannot set " + u.Type.String() }
|
||||
|
||||
// A TrailingGarbageError describes extra bytes after decoding
|
||||
// has completed during [Unmarshal].
|
||||
type TrailingGarbageError []byte
|
||||
|
||||
func (e TrailingGarbageError) Error() string {
|
||||
if len(e) < SizePrefix {
|
||||
return "got " + strconv.Itoa(len(e)) + " bytes of trailing garbage"
|
||||
}
|
||||
return "data has extra values starting with " + SPAKind(binary.NativeEndian.Uint32(e[SizeSPrefix:])).String()
|
||||
}
|
||||
|
||||
// A StringTerminationError describes an incorrectly terminated string
|
||||
// encountered during [Unmarshal].
|
||||
type StringTerminationError byte
|
||||
|
||||
func (e StringTerminationError) Error() string {
|
||||
return "got byte " + strconv.Itoa(int(e)) + " instead of NUL"
|
||||
}
|
||||
|
||||
// unmarshalValue implements [Unmarshal] on [reflect.Value] without compensating for prefix and padding size.
|
||||
func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error {
|
||||
if !v.CanSet() {
|
||||
return &UnmarshalSetError{v.Type()}
|
||||
}
|
||||
|
||||
if v.CanInterface() {
|
||||
if v.Kind() == reflect.Pointer {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
|
||||
if u, ok := v.Interface().(PODUnmarshaler); ok {
|
||||
var err error
|
||||
*wireSizeP, err = u.UnmarshalPOD(data)
|
||||
return err
|
||||
}
|
||||
|
||||
switch v.Interface().(type) {
|
||||
case Fd:
|
||||
*wireSizeP = SizeFd
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Fd, wireSizeP); err != nil {
|
||||
return err
|
||||
}
|
||||
v.SetInt(int64(binary.NativeEndian.Uint64(data)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Uint32:
|
||||
*wireSizeP = SizeId
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Id, wireSizeP); err != nil {
|
||||
return err
|
||||
}
|
||||
v.SetUint(uint64(binary.NativeEndian.Uint32(data)))
|
||||
return nil
|
||||
|
||||
case reflect.Int32:
|
||||
*wireSizeP = SizeInt
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Int, wireSizeP); err != nil {
|
||||
return err
|
||||
}
|
||||
v.SetInt(int64(binary.NativeEndian.Uint32(data)))
|
||||
return nil
|
||||
|
||||
case reflect.Int64:
|
||||
*wireSizeP = SizeLong
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Long, wireSizeP); err != nil {
|
||||
return err
|
||||
}
|
||||
v.SetInt(int64(binary.NativeEndian.Uint64(data)))
|
||||
return nil
|
||||
|
||||
case reflect.Struct:
|
||||
*wireSizeP = 0
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Struct, wireSizeP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fieldWireSize Word
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if err := unmarshalValue(data, v.Field(i), &fieldWireSize); err != nil {
|
||||
return err
|
||||
}
|
||||
// bounds check completed in successful call to unmarshalValue
|
||||
data = data[Size(fieldWireSize):]
|
||||
}
|
||||
|
||||
if len(data) != 0 {
|
||||
return TrailingGarbageError(data)
|
||||
}
|
||||
return nil
|
||||
|
||||
case reflect.Pointer:
|
||||
if ok, err := unmarshalHandleNone(&data, wireSizeP); err != nil {
|
||||
return err
|
||||
} else if ok {
|
||||
v.SetZero()
|
||||
return nil
|
||||
}
|
||||
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
return unmarshalValue(data, v.Elem(), wireSizeP)
|
||||
|
||||
case reflect.String:
|
||||
*wireSizeP = 0
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_String, wireSizeP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// string size, one extra NUL byte
|
||||
size := int(*wireSizeP)
|
||||
if len(data) < size {
|
||||
return ErrEOFDataString
|
||||
}
|
||||
|
||||
// the serialised strings still include NUL termination
|
||||
if data[size-1] != 0 {
|
||||
return StringTerminationError(data[size-1])
|
||||
}
|
||||
|
||||
v.SetString(string(data[:size-1]))
|
||||
return nil
|
||||
|
||||
default:
|
||||
return &UnsupportedTypeError{v.Type()}
|
||||
}
|
||||
}
|
||||
|
||||
// unmarshalHandleNone establishes prefix bounds and, for a value of type [SPA_TYPE_None],
|
||||
// validates its size and skips the header. This is for unmarshalling values that can be nil.
|
||||
func unmarshalHandleNone(data *[]byte, wireSizeP *Word) (bool, error) {
|
||||
if len(*data) < SizePrefix {
|
||||
return false, ErrEOFPrefix
|
||||
}
|
||||
|
||||
if SPAKind(binary.NativeEndian.Uint32((*data)[SizeSPrefix:])) != SPA_TYPE_None {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
*wireSizeP = 0
|
||||
if err := unmarshalCheckTypeBounds(data, SPA_TYPE_None, wireSizeP); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if len(*data) != 0 {
|
||||
return true, TrailingGarbageError(*data)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// An InconsistentSizeError describes an inconsistent size prefix encountered
|
||||
// in data passed to [Unmarshal].
|
||||
type InconsistentSizeError struct{ Prefix, Expect Word }
|
||||
|
||||
func (e InconsistentSizeError) Error() string {
|
||||
return "prefix claims size " + strconv.Itoa(int(e.Prefix)) +
|
||||
" for a " + strconv.Itoa(int(e.Expect)) + "-byte long segment"
|
||||
}
|
||||
|
||||
// An UnexpectedTypeError describes an unexpected type encountered
|
||||
// in data passed to [Unmarshal].
|
||||
type UnexpectedTypeError struct{ Type, Expect SPAKind }
|
||||
|
||||
func (e UnexpectedTypeError) Error() string {
|
||||
return "received " + e.Type.String() + " for a value of type " + e.Expect.String()
|
||||
}
|
||||
|
||||
// unmarshalCheckTypeBounds performs bounds checks on data and validates the type and size prefixes.
|
||||
// An expected size of zero skips further bounds checks.
|
||||
func unmarshalCheckTypeBounds(data *[]byte, t SPAKind, sizeP *Word) error {
|
||||
if len(*data) < SizePrefix {
|
||||
return ErrEOFPrefix
|
||||
}
|
||||
|
||||
wantSize := *sizeP
|
||||
gotSize := binary.NativeEndian.Uint32(*data)
|
||||
*sizeP = gotSize
|
||||
|
||||
if wantSize != 0 && gotSize != wantSize {
|
||||
return InconsistentSizeError{gotSize, wantSize}
|
||||
}
|
||||
if len(*data)-SizePrefix < int(gotSize) {
|
||||
return ErrEOFData
|
||||
}
|
||||
|
||||
gotType := SPAKind(binary.NativeEndian.Uint32((*data)[SizeSPrefix:]))
|
||||
if gotType != t {
|
||||
return UnexpectedTypeError{gotType, t}
|
||||
}
|
||||
|
||||
*data = (*data)[SizePrefix : gotSize+SizePrefix]
|
||||
return nil
|
||||
}
|
||||
|
||||
// The Footer contains additional messages, not directed to
|
||||
// the destination object defined by the Id field.
|
||||
type Footer[P KnownSize] struct {
|
||||
// The footer opcode.
|
||||
Opcode Id `json:"opcode"`
|
||||
// The footer payload struct.
|
||||
Payload P `json:"payload"`
|
||||
}
|
||||
|
||||
// Size satisfies [KnownSize] with a usually compile-time known value.
|
||||
func (f *Footer[P]) Size() Word {
|
||||
return SizePrefix +
|
||||
Size(SizeId) +
|
||||
f.Payload.Size()
|
||||
}
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (f *Footer[T]) MarshalBinary() ([]byte, error) { return Marshal(f) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (f *Footer[T]) UnmarshalBinary(data []byte) error { return Unmarshal(data, f) }
|
||||
|
||||
// SPADictItem represents spa_dict_item.
|
||||
type SPADictItem struct {
|
||||
// Dot-separated string.
|
||||
Key string `json:"key"`
|
||||
// Arbitrary string.
|
||||
//
|
||||
// Integer values are represented in base 10,
|
||||
// boolean values are represented as "true" or "false".
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// SPADict represents spa_dict.
|
||||
type SPADict []SPADictItem
|
||||
|
||||
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||
func (d *SPADict) Size() Word {
|
||||
if d == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// struct prefix, NItems value
|
||||
size := SizePrefix + int(Size(SizeInt))
|
||||
for i := range *d {
|
||||
size += SizeString[int]((*d)[i].Key)
|
||||
size += SizeString[int]((*d)[i].Value)
|
||||
}
|
||||
return Word(size)
|
||||
}
|
||||
|
||||
// MarshalPOD satisfies [PODMarshaler] as [SPADict] violates the POD type system.
|
||||
func (d *SPADict) MarshalPOD(data []byte) ([]byte, error) {
|
||||
return appendInner(data, func(dataPrefix []byte) (data []byte, err error) {
|
||||
data = SPA_TYPE_Struct.append(dataPrefix)
|
||||
if data, err = MarshalAppend(data, Int(len(*d))); err != nil {
|
||||
return
|
||||
}
|
||||
for i := range *d {
|
||||
if data, err = MarshalAppend(data, (*d)[i].Key); err != nil {
|
||||
return
|
||||
}
|
||||
if data, err = MarshalAppend(data, (*d)[i].Value); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalPOD satisfies [PODUnmarshaler] as [SPADict] violates the POD type system.
|
||||
func (d *SPADict) UnmarshalPOD(data []byte) (Word, error) {
|
||||
var wireSize Word
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Struct, &wireSize); err != nil {
|
||||
return wireSize, err
|
||||
}
|
||||
// bounds check completed in successful call to unmarshalCheckTypeBounds
|
||||
data = data[:wireSize]
|
||||
|
||||
var count Int
|
||||
if size, err := UnmarshalNext(data, &count); err != nil {
|
||||
return wireSize, err
|
||||
} else {
|
||||
// bounds check completed in successful call to Unmarshal
|
||||
data = data[size:]
|
||||
}
|
||||
|
||||
*d = make([]SPADictItem, count)
|
||||
for i := range *d {
|
||||
if size, err := UnmarshalNext(data, &(*d)[i].Key); err != nil {
|
||||
return wireSize, err
|
||||
} else {
|
||||
// bounds check completed in successful call to Unmarshal
|
||||
data = data[size:]
|
||||
}
|
||||
if size, err := UnmarshalNext(data, &(*d)[i].Value); err != nil {
|
||||
return wireSize, err
|
||||
} else {
|
||||
// bounds check completed in successful call to Unmarshal
|
||||
data = data[size:]
|
||||
}
|
||||
}
|
||||
|
||||
if len(data) != 0 {
|
||||
return wireSize, TrailingGarbageError(data)
|
||||
}
|
||||
return wireSize, nil
|
||||
}
|
||||
|
||||
// A Message is a value that can be transmitted as a message over PipeWire protocol native.
|
||||
type Message interface {
|
||||
// Opcode returns the opcode of this message.
|
||||
Opcode() byte
|
||||
// FileCount returns the number of files associated with this message.
|
||||
FileCount() Int
|
||||
|
||||
KnownSize
|
||||
}
|
||||
|
||||
// A MessageEncoder provides methods for encoding a [Message].
|
||||
type MessageEncoder struct{ Message }
|
||||
|
||||
// SizeMessage returns the size of Message transmitted over protocol native.
|
||||
func (m MessageEncoder) SizeMessage(footer KnownSize) (size Word) {
|
||||
size = SizeHeader + m.Message.Size()
|
||||
if footer != nil {
|
||||
size += footer.Size()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AppendMessage appends the protocol native encoding of Message to dst and returns the appended slice.
|
||||
func (m MessageEncoder) AppendMessage(dst []byte, Id, sequence Int, footer KnownSize) (data []byte, err error) {
|
||||
size := m.SizeMessage(footer)
|
||||
if size&^SizeMax != 0 {
|
||||
return dst, ErrSizeRange
|
||||
}
|
||||
|
||||
data = slices.Grow(dst, int(size))
|
||||
data = (&Header{
|
||||
ID: Id,
|
||||
Opcode: m.Message.Opcode(),
|
||||
Size: size - SizeHeader,
|
||||
Sequence: sequence,
|
||||
FileCount: m.Message.FileCount(),
|
||||
}).append(data)
|
||||
data, err = MarshalAppend(data, m.Message)
|
||||
if err == nil && footer != nil {
|
||||
data, err = MarshalAppend(data, footer)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
package pipewire_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
type encodingTestCases[V any, S interface {
|
||||
encoding.BinaryMarshaler
|
||||
encoding.BinaryUnmarshaler
|
||||
|
||||
*V
|
||||
}] []struct {
|
||||
// Uninterpreted name of subtest.
|
||||
name string
|
||||
// Encoded data.
|
||||
wantData []byte
|
||||
// Value corresponding to wantData.
|
||||
value V
|
||||
// Expected decoding error. Skips encoding check if non-nil.
|
||||
wantErr error
|
||||
}
|
||||
|
||||
// run runs all test cases as subtests of [testing.T].
|
||||
func (testCases encodingTestCases[V, S]) run(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("decode", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var value V
|
||||
if err := S(&value).UnmarshalBinary(tc.wantData); err != nil {
|
||||
t.Fatalf("UnmarshalBinary: error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(&value, &tc.value) {
|
||||
t.Fatalf("UnmarshalBinary:\n%s\nwant\n%s", mustMarshalJSON(value), mustMarshalJSON(tc.value))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("encode", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if gotData, err := S(&tc.value).MarshalBinary(); err != nil {
|
||||
t.Fatalf("MarshalBinary: error = %v", err)
|
||||
} else if string(gotData) != string(tc.wantData) {
|
||||
t.Fatalf("MarshalBinary: %#v, want %#v", gotData, tc.wantData)
|
||||
}
|
||||
})
|
||||
|
||||
if s, ok := any(&tc.value).(pipewire.KnownSize); ok {
|
||||
t.Run("size", func(t *testing.T) {
|
||||
if got := int(s.Size()); got != len(tc.wantData) {
|
||||
t.Errorf("Size: %d, want %d", got, len(tc.wantData))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPODErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{"UnsupportedTypeError", &pipewire.UnsupportedTypeError{
|
||||
Type: reflect.TypeFor[any](),
|
||||
}, "unsupported type interface {}"},
|
||||
|
||||
{"UnsupportedSizeError", pipewire.UnsupportedSizeError(pipewire.SizeMax + 1), "size 16777216 out of range"},
|
||||
|
||||
{"InvalidUnmarshalError untyped nil", new(pipewire.InvalidUnmarshalError), "attempting to unmarshal to nil"},
|
||||
{"InvalidUnmarshalError non-pointer", &pipewire.InvalidUnmarshalError{
|
||||
Type: reflect.TypeFor[uintptr](),
|
||||
}, "attempting to unmarshal to non-pointer type uintptr"},
|
||||
{"InvalidUnmarshalError nil", &pipewire.InvalidUnmarshalError{
|
||||
Type: reflect.TypeFor[*uintptr](),
|
||||
}, "attempting to unmarshal to nil *uintptr"},
|
||||
|
||||
{"UnexpectedEOFError ErrEOFPrefix", pipewire.ErrEOFPrefix, "unexpected EOF decoding fixed-size POD prefix"},
|
||||
{"UnexpectedEOFError ErrEOFData", pipewire.ErrEOFData, "unexpected EOF establishing POD data bounds"},
|
||||
{"UnexpectedEOFError ErrEOFDataString", pipewire.ErrEOFDataString, "unexpected EOF establishing POD String bounds"},
|
||||
{"UnexpectedEOFError invalid", pipewire.UnexpectedEOFError(0xbad), "unexpected EOF"},
|
||||
|
||||
{"UnmarshalSetError", &pipewire.UnmarshalSetError{
|
||||
Type: reflect.TypeFor[*uintptr](),
|
||||
}, "cannot set *uintptr"},
|
||||
|
||||
{"TrailingGarbageError short", make(pipewire.TrailingGarbageError, 1<<3-1), "got 7 bytes of trailing garbage"},
|
||||
{"TrailingGarbageError String", pipewire.TrailingGarbageError{
|
||||
/* size: */ 0, 0, 0, 0,
|
||||
/* type: */ byte(pipewire.SPA_TYPE_String), 0, 0, 0,
|
||||
}, "data has extra values starting with String"},
|
||||
{"TrailingGarbageError invalid", pipewire.TrailingGarbageError{
|
||||
/* size: */ 0, 0, 0, 0,
|
||||
/* type: */ 0xff, 0xff, 0xff, 0xff,
|
||||
/* garbage: */ 0,
|
||||
}, "data has extra values starting with invalid type field 0xffffffff"},
|
||||
|
||||
{"StringTerminationError", pipewire.StringTerminationError(0xff), "got byte 255 instead of NUL"},
|
||||
|
||||
{"InconsistentSizeError", pipewire.InconsistentSizeError{
|
||||
Prefix: 0xbad,
|
||||
Expect: 0xff,
|
||||
}, "prefix claims size 2989 for a 255-byte long segment"},
|
||||
|
||||
{"UnexpectedTypeError zero", pipewire.UnexpectedTypeError{}, "received invalid type field 0x0 for a value of type invalid type field 0x0"},
|
||||
{"UnexpectedTypeError", pipewire.UnexpectedTypeError{
|
||||
Type: pipewire.SPA_TYPE_String,
|
||||
Expect: pipewire.SPA_TYPE_Array,
|
||||
}, "received String for a value of type Array"},
|
||||
{"UnexpectedTypeError invalid", pipewire.UnexpectedTypeError{
|
||||
Type: 0xdeadbeef,
|
||||
Expect: pipewire.SPA_TYPE_Long,
|
||||
}, "received invalid type field 0xdeadbeef for a value of type Long"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := tc.err.Error(); got != tc.want {
|
||||
t.Errorf("Error: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var benchmarkSample = func() (sample pipewire.CoreInfo) {
|
||||
if err := sample.UnmarshalBinary(samplePWContainer[1][0][1]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
func BenchmarkMarshal(b *testing.B) {
|
||||
for b.Loop() {
|
||||
if _, err := benchmarkSample.MarshalBinary(); err != nil {
|
||||
b.Fatalf("MarshalBinary: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalJSON(b *testing.B) {
|
||||
for b.Loop() {
|
||||
if _, err := json.Marshal(benchmarkSample); err != nil {
|
||||
b.Fatalf("json.Marshal: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGobEncode(b *testing.B) {
|
||||
e := gob.NewEncoder(io.Discard)
|
||||
type sampleRaw pipewire.CoreInfo
|
||||
|
||||
for b.Loop() {
|
||||
if err := e.Encode((*sampleRaw)(&benchmarkSample)); err != nil {
|
||||
b.Fatalf("(*gob.Encoder).Encode: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshal(b *testing.B) {
|
||||
var got pipewire.CoreInfo
|
||||
|
||||
for b.Loop() {
|
||||
if err := got.UnmarshalBinary(samplePWContainer[1][0][1]); err != nil {
|
||||
b.Fatalf("UnmarshalBinary: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalJSON(b *testing.B) {
|
||||
var got pipewire.CoreInfo
|
||||
data, err := json.Marshal(benchmarkSample)
|
||||
if err != nil {
|
||||
b.Fatalf("json.Marshal: error = %v", err)
|
||||
}
|
||||
|
||||
for b.Loop() {
|
||||
if err = json.Unmarshal(data, &got); err != nil {
|
||||
b.Fatalf("json.Unmarshal: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGobDecode(b *testing.B) {
|
||||
type sampleRaw pipewire.CoreInfo
|
||||
var buf bytes.Buffer
|
||||
e := gob.NewEncoder(&buf)
|
||||
d := gob.NewDecoder(&buf)
|
||||
|
||||
for b.Loop() {
|
||||
b.StopTimer()
|
||||
if err := e.Encode((*sampleRaw)(&benchmarkSample)); err != nil {
|
||||
b.Fatalf("(*gob.Encoder).Encode: error = %v", err)
|
||||
}
|
||||
b.StartTimer()
|
||||
|
||||
if err := d.Decode(new(sampleRaw)); err != nil {
|
||||
b.Fatalf("(*gob.Encoder).Decode: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mustMarshalJSON calls [json.Marshal] and returns the result.
|
||||
func mustMarshalJSON(v any) string {
|
||||
if data, err := json.Marshal(v); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
return string(data)
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
package pipewire
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
/* pipewire/extensions/security-context.h */
|
||||
|
||||
const (
|
||||
PW_TYPE_INTERFACE_SecurityContext = PW_TYPE_INFO_INTERFACE_BASE + "SecurityContext"
|
||||
PW_SECURITY_CONTEXT_PERM_MASK = PW_PERM_RWX
|
||||
PW_VERSION_SECURITY_CONTEXT = 3
|
||||
|
||||
PW_EXTENSION_MODULE_SECURITY_CONTEXT = PIPEWIRE_MODULE_PREFIX + "module-security-context"
|
||||
)
|
||||
|
||||
const (
|
||||
PW_SECURITY_CONTEXT_EVENT_NUM = iota
|
||||
|
||||
PW_VERSION_SECURITY_CONTEXT_EVENTS = 0
|
||||
)
|
||||
|
||||
const (
|
||||
PW_SECURITY_CONTEXT_METHOD_ADD_LISTENER = iota
|
||||
|
||||
PW_SECURITY_CONTEXT_METHOD_CREATE
|
||||
|
||||
PW_SECURITY_CONTEXT_METHOD_NUM
|
||||
PW_VERSION_SECURITY_CONTEXT_METHODS = 0
|
||||
)
|
||||
|
||||
// SecurityContextCreate is sent to create a new security context.
|
||||
//
|
||||
// Creates a new security context with a socket listening FD.
|
||||
// PipeWire will accept new client connections on listen_fd.
|
||||
//
|
||||
// listen_fd must be ready to accept new connections when this
|
||||
// request is sent by the client. In other words, the client must
|
||||
// call bind(2) and listen(2) before sending the FD.
|
||||
//
|
||||
// close_fd is a FD closed by the client when PipeWire should stop
|
||||
// accepting new connections on listen_fd.
|
||||
//
|
||||
// PipeWire must continue to accept connections on listen_fd when
|
||||
// the client which created the security context disconnects.
|
||||
//
|
||||
// After sending this request, closing listen_fd and close_fd
|
||||
// remains the only valid operation on them.
|
||||
type SecurityContextCreate struct {
|
||||
// The offset in the SCM_RIGHTS msg_control message to
|
||||
// the fd to listen on for new connections.
|
||||
ListenFd Fd
|
||||
// The offset in the SCM_RIGHTS msg_control message to
|
||||
// the fd used to stop listening.
|
||||
CloseFd Fd
|
||||
|
||||
// Extra properties. These will be copied on the client
|
||||
// that connects through this context.
|
||||
Properties *SPADict `json:"props"`
|
||||
}
|
||||
|
||||
// Opcode satisfies [Message] with a constant value.
|
||||
func (c *SecurityContextCreate) Opcode() byte { return PW_SECURITY_CONTEXT_METHOD_CREATE }
|
||||
|
||||
// FileCount satisfies [Message] with a constant value.
|
||||
func (c *SecurityContextCreate) FileCount() Int { return 2 }
|
||||
|
||||
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||
func (c *SecurityContextCreate) Size() Word {
|
||||
return SizePrefix +
|
||||
Size(SizeFd) +
|
||||
Size(SizeFd) +
|
||||
c.Properties.Size()
|
||||
}
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (c *SecurityContextCreate) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *SecurityContextCreate) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// SecurityContext holds state of [PW_TYPE_INTERFACE_SecurityContext].
|
||||
type SecurityContext struct {
|
||||
// Proxy id as tracked by [Context].
|
||||
ID Int `json:"proxy_id"`
|
||||
// Global id as tracked by [Registry].
|
||||
GlobalID Int `json:"id"`
|
||||
|
||||
ctx *Context
|
||||
|
||||
destructible
|
||||
}
|
||||
|
||||
// GetSecurityContext queues a [RegistryBind] message for the PipeWire server
|
||||
// and returns the address of the newly allocated [SecurityContext].
|
||||
func (registry *Registry) GetSecurityContext() (securityContext *SecurityContext, err error) {
|
||||
securityContext = &SecurityContext{ctx: registry.ctx}
|
||||
for globalId, object := range registry.Objects {
|
||||
if object.Type == securityContext.String() {
|
||||
securityContext.GlobalID = globalId
|
||||
securityContext.ID, err = registry.bind(securityContext, securityContext.GlobalID, PW_VERSION_SECURITY_CONTEXT)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, UnsupportedObjectTypeError(securityContext.String())
|
||||
}
|
||||
|
||||
// Create queues a [SecurityContextCreate] message for the PipeWire server.
|
||||
func (securityContext *SecurityContext) Create(listenFd, closeFd int, props SPADict) (err error) {
|
||||
if err = securityContext.checkDestroy(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
asCoreError := securityContext.ctx.expectsCoreError(securityContext.ID, &err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// queued in reverse based on upstream behaviour, unsure why
|
||||
offset := securityContext.ctx.queueFiles(closeFd, listenFd)
|
||||
if err = securityContext.ctx.writeMessage(
|
||||
securityContext.ID,
|
||||
&SecurityContextCreate{ListenFd: offset + 1, CloseFd: offset + 0, Properties: &props},
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
if err = securityContext.ctx.GetCore().Sync(); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if coreError := asCoreError(); coreError == nil {
|
||||
return
|
||||
} else {
|
||||
switch syscall.Errno(-coreError.Result) {
|
||||
case syscall.EPERM:
|
||||
return &PermissionError{securityContext.ID, coreError.Message}
|
||||
|
||||
default:
|
||||
return coreError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// securityContextCloser holds onto resources associated to the security context.
|
||||
type securityContextCloser struct {
|
||||
// Pipe with its write end passed to [SecurityContextCreate.CloseFd].
|
||||
closeFds [2]int
|
||||
// Pathname the socket was bound to.
|
||||
pathname string
|
||||
}
|
||||
|
||||
// Close closes both ends of the pipe.
|
||||
func (scc *securityContextCloser) Close() (err error) {
|
||||
err = errors.Join(
|
||||
syscall.Close(scc.closeFds[1]),
|
||||
syscall.Close(scc.closeFds[0]),
|
||||
// there is still technically a TOCTOU here but this is internal
|
||||
// and has access to the privileged pipewire socket, so it only
|
||||
// receives trusted input (e.g. from cmd/hakurei) anyway
|
||||
os.Remove(scc.pathname),
|
||||
)
|
||||
|
||||
// no need for a finalizer anymore
|
||||
runtime.SetFinalizer(scc, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// BindAndCreate binds a new socket to the specified pathname and pass it to Create.
|
||||
// It returns an [io.Closer] corresponding to [SecurityContextCreate.CloseFd].
|
||||
func (securityContext *SecurityContext) BindAndCreate(pathname string, props SPADict) (io.Closer, error) {
|
||||
if err := securityContext.checkDestroy(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var scc securityContextCloser
|
||||
|
||||
// ensure pathname is available
|
||||
if f, err := os.Create(pathname); err != nil {
|
||||
return nil, err
|
||||
} else if err = f.Close(); err != nil {
|
||||
_ = os.Remove(pathname)
|
||||
return nil, err
|
||||
} else if err = os.Remove(pathname); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scc.pathname = pathname
|
||||
|
||||
var listenFd int
|
||||
if fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0); err != nil {
|
||||
return nil, os.NewSyscallError("socket", err)
|
||||
} else {
|
||||
securityContext.ctx.cleanup(func() error { return syscall.Close(fd) })
|
||||
listenFd = fd
|
||||
}
|
||||
if err := syscall.Bind(listenFd, &syscall.SockaddrUnix{Name: pathname}); err != nil {
|
||||
return nil, os.NewSyscallError("bind", err)
|
||||
} else if err = syscall.Listen(listenFd, 0); err != nil {
|
||||
_ = os.Remove(pathname)
|
||||
return nil, os.NewSyscallError("listen", err)
|
||||
}
|
||||
|
||||
if err := syscall.Pipe2(scc.closeFds[0:], syscall.O_CLOEXEC); err != nil {
|
||||
_ = os.Remove(pathname)
|
||||
return nil, err
|
||||
}
|
||||
runtime.SetFinalizer(&scc, (*securityContextCloser).Close)
|
||||
|
||||
if err := securityContext.Create(listenFd, scc.closeFds[1], props); err != nil {
|
||||
_ = scc.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &scc, nil
|
||||
}
|
||||
|
||||
func (securityContext *SecurityContext) consume(opcode byte, files []int, _ func(v any)) error {
|
||||
securityContext.mustCheckDestroy()
|
||||
closeReceivedFiles(files...)
|
||||
switch opcode {
|
||||
// SecurityContext does not receive any events
|
||||
|
||||
default:
|
||||
panic(&UnsupportedOpcodeError{opcode, securityContext.String()})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (securityContext *SecurityContext) setBoundProps(event *CoreBoundProps) error {
|
||||
securityContext.mustCheckDestroy()
|
||||
if securityContext.ID != event.ID {
|
||||
return &InconsistentIdError{Proxy: securityContext, ID: securityContext.ID, ServerID: event.ID}
|
||||
}
|
||||
if securityContext.GlobalID != event.GlobalID {
|
||||
return &InconsistentIdError{Global: true, Proxy: securityContext, ID: securityContext.GlobalID, ServerID: event.GlobalID}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy destroys this [SecurityContext] proxy.
|
||||
func (securityContext *SecurityContext) Destroy() error {
|
||||
return securityContext.destroy(securityContext.ctx, securityContext.ID)
|
||||
}
|
||||
|
||||
func (securityContext *SecurityContext) String() string { return PW_TYPE_INTERFACE_SecurityContext }
|
||||
@@ -1,21 +0,0 @@
|
||||
package pipewire_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
func TestSecurityContextCreate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.SecurityContextCreate, *pipewire.SecurityContextCreate]{
|
||||
{"sample", samplePWContainer[6][0][1], pipewire.SecurityContextCreate{
|
||||
ListenFd: 1 /* 21: duplicated from listen_fd */, CloseFd: 0, /* 20: duplicated from close_fd */
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_SEC_ENGINE, Value: "org.flatpak"},
|
||||
{Key: pipewire.PW_KEY_ACCESS, Value: "restricted"},
|
||||
},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user