app: integrate interrupt forwarding
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m58s
Test / Hakurei (push) Successful in 2m53s
Test / Sandbox (race detector) (push) Successful in 3m53s
Test / Planterette (push) Successful in 3m53s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m19s

This significantly increases usability of command line tools running through hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-07-29 02:21:12 +09:00
parent ddf48a6c22
commit b43d104680
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
12 changed files with 76 additions and 18 deletions

View File

@ -256,8 +256,10 @@ App
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"immediate_termination": true,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
@ -382,8 +384,10 @@ App
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"immediate_termination": true,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
@ -562,8 +566,10 @@ func Test_printPs(t *testing.T) {
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"immediate_termination": true,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,

View File

@ -10,6 +10,8 @@ type (
// container hostname // container hostname
Hostname string `json:"hostname,omitempty"` Hostname string `json:"hostname,omitempty"`
// do not interrupt and wait for initial process during termination
ImmediateTermination bool `json:"immediate_termination,omitempty"`
// extra seccomp flags // extra seccomp flags
SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"` SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"`
// extra seccomp presets // extra seccomp presets

View File

@ -57,16 +57,18 @@ func Template() *Config {
Groups: []string{"video", "dialout", "plugdev"}, Groups: []string{"video", "dialout", "plugdev"},
Container: &ContainerConfig{ Container: &ContainerConfig{
Hostname: "localhost", Hostname: "localhost",
Devel: true, Devel: true,
Userns: true, Userns: true,
Net: true, Net: true,
Device: true, Device: true,
SeccompFlags: seccomp.AllowMultiarch, ImmediateTermination: true,
SeccompPresets: seccomp.PresetExt, SeccompFlags: seccomp.AllowMultiarch,
Tty: true, SeccompPresets: seccomp.PresetExt,
Multiarch: true, SeccompCompat: true,
MapRealUID: true, Tty: true,
Multiarch: true,
MapRealUID: true,
// example API credentials pulled from Google Chrome // example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER // DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{ Env: map[string]string{

View File

@ -80,8 +80,10 @@ func TestTemplate(t *testing.T) {
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"immediate_termination": true,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,

View File

@ -144,6 +144,7 @@ var testCasesNixos = []sealTestCase{
Tmpfs("/var/run/nscd", 8192, 0755), Tmpfs("/var/run/nscd", 8192, 0755),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
ForwardCancel: true,
}, },
}, },
} }

View File

@ -71,6 +71,7 @@ var testCasesPd = []sealTestCase{
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
RetainSession: true, RetainSession: true,
ForwardCancel: true,
}, },
}, },
{ {
@ -220,6 +221,7 @@ var testCasesPd = []sealTestCase{
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
RetainSession: true, RetainSession: true,
ForwardCancel: true,
}, },
}, },
} }

View File

@ -32,6 +32,10 @@ func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*contain
SeccompPresets: s.SeccompPresets, SeccompPresets: s.SeccompPresets,
RetainSession: s.Tty, RetainSession: s.Tty,
HostNet: s.Net, HostNet: s.Net,
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
// this behaviour is implemented in the shim
ForwardCancel: !s.ImmediateTermination,
} }
{ {

View File

@ -9,6 +9,7 @@ import (
"os/exec" "os/exec"
"os/signal" "os/signal"
"runtime" "runtime"
"sync/atomic"
"syscall" "syscall"
"time" "time"
@ -41,6 +42,10 @@ const (
ShimExitRequest = 254 ShimExitRequest = 254
// ShimExitOrphan is returned when the shim is orphaned before monitor delivers a signal. // ShimExitOrphan is returned when the shim is orphaned before monitor delivers a signal.
ShimExitOrphan = 3 ShimExitOrphan = 3
// ShimWaitDelay is the duration to wait after interrupting a container's initial process
// before the container is fully killed off.
ShimWaitDelay = 5 * time.Second
) )
// ShimMain is the main function of the shim process and runs as the unconstrained target user. // ShimMain is the main function of the shim process and runs as the unconstrained target user.
@ -86,6 +91,7 @@ func ShimMain() {
} }
// signal handler outcome // signal handler outcome
var cancelContainer atomic.Pointer[context.CancelFunc]
go func() { go func() {
buf := make([]byte, 1) buf := make([]byte, 1)
for { for {
@ -94,23 +100,30 @@ func ShimMain() {
} }
switch buf[0] { switch buf[0] {
case 0: case 0: // got SIGCONT from monitor: shim exit requested
if fp := cancelContainer.Load(); params.Container.ForwardCancel && fp != nil && *fp != nil {
(*fp)()
// shim now bound by ShimWaitDelay, implemented below
continue
}
// setup has not completed, terminate immediately
hlog.Resume() hlog.Resume()
os.Exit(ShimExitRequest) os.Exit(ShimExitRequest)
return return
case 1: case 1: // got SIGCONT after adoption: monitor died before delivering signal
hlog.BeforeExit() hlog.BeforeExit()
os.Exit(ShimExitOrphan) os.Exit(ShimExitOrphan)
return return
case 2: case 2: // unreachable
log.Println("sa_sigaction got invalid siginfo") log.Println("sa_sigaction got invalid siginfo")
case 3: case 3: // got SIGCONT from unexpected process: hopefully the terminal driver
log.Println("got SIGCONT from unexpected process") log.Println("got SIGCONT from unexpected process")
default: default: // unreachable
log.Fatalf("got invalid message %d from signal handler", buf[0]) log.Fatalf("got invalid message %d from signal handler", buf[0])
} }
} }
@ -146,12 +159,11 @@ func ShimMain() {
name = params.Container.Args[0] name = params.Container.Args[0]
} }
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() // unreachable cancelContainer.Store(&stop)
z := container.New(ctx, name) z := container.New(ctx, name)
z.Params = *params.Container z.Params = *params.Container
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
z.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) } z.WaitDelay = ShimWaitDelay
z.WaitDelay = 2 * time.Second
if err := z.Start(); err != nil { if err := z.Start(); err != nil {
hlog.PrintBaseError(err, "cannot start container:") hlog.PrintBaseError(err, "cannot start container:")

View File

@ -128,6 +128,7 @@ in
container = { container = {
inherit (app) inherit (app)
immediate_termination
devel devel
userns userns
net net

View File

@ -195,6 +195,7 @@ in
''; '';
}; };
immediate_termination = mkEnableOption "immediate termination of the container on interrupt";
devel = mkEnableOption "debugging-related kernel interfaces"; devel = mkEnableOption "debugging-related kernel interfaces";
userns = mkEnableOption "user namespace creation"; userns = mkEnableOption "user namespace creation";
tty = mkEnableOption "access to the controlling terminal"; tty = mkEnableOption "access to the controlling terminal";

View File

@ -127,6 +127,21 @@
}; };
}; };
"cat.gensokyo.extern.foot.noEnablements.immediate" = {
name = "ne-foot-immediate";
identity = 1;
shareUid = true;
verbose = true;
immediate_termination = true;
share = pkgs.foot;
packages = [ ];
command = "foot";
capability = {
dbus = false;
pulse = false;
};
};
"cat.gensokyo.extern.foot.pulseaudio" = { "cat.gensokyo.extern.foot.pulseaudio" = {
name = "pa-foot"; name = "pa-foot";
identity = 2; identity = 2;

View File

@ -178,6 +178,16 @@ machine.succeed("pkill -INT -f 'hakurei -v app '")
machine.wait_until_fails("pgrep foot", timeout=5) machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/monitor-exit-code") machine.wait_for_file("/tmp/monitor-exit-code")
interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code")) interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code"))
if interrupt_exit_code != 230:
raise Exception(f"unexpected exit code {interrupt_exit_code}")
# Check interrupt shim behaviour immediate termination:
swaymsg("exec sh -c 'ne-foot-immediate; echo -n $? > /tmp/monitor-exit-code'")
wait_for_window(f"u0_a{aid(0)}@machine")
machine.succeed("pkill -INT -f 'hakurei -v app '")
machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/monitor-exit-code")
interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code"))
if interrupt_exit_code != 254: if interrupt_exit_code != 254:
raise Exception(f"unexpected exit code {interrupt_exit_code}") raise Exception(f"unexpected exit code {interrupt_exit_code}")