From e72f96ad0aeff7cdf4ba99e18520618d0efc7c47 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Tue, 29 Jul 2025 02:21:12 +0900 Subject: [PATCH] app: integrate interrupt forwarding This significantly increases usability of command line tools running through hakurei. Signed-off-by: Ophestra --- hst/container.go | 2 ++ hst/template.go | 22 ++++++++++++---------- hst/template_test.go | 2 ++ internal/app/app_nixos_linux_test.go | 1 + internal/app/app_pd_linux_test.go | 2 ++ internal/app/container_linux.go | 4 ++++ internal/app/shim_linux.go | 28 ++++++++++++++++++++-------- nixos.nix | 1 + options.nix | 1 + test/configuration.nix | 15 +++++++++++++++ test/test.py | 10 ++++++++++ 11 files changed, 70 insertions(+), 18 deletions(-) diff --git a/hst/container.go b/hst/container.go index 00f7479..1ee9e83 100644 --- a/hst/container.go +++ b/hst/container.go @@ -10,6 +10,8 @@ type ( // container hostname Hostname string `json:"hostname,omitempty"` + // do not interrupt and wait for initial process during termination + ImmediateTermination bool `json:"immediate_termination,omitempty"` // extra seccomp flags SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"` // extra seccomp presets diff --git a/hst/template.go b/hst/template.go index 3f0101e..60af2eb 100644 --- a/hst/template.go +++ b/hst/template.go @@ -57,16 +57,18 @@ func Template() *Config { Groups: []string{"video", "dialout", "plugdev"}, Container: &ContainerConfig{ - Hostname: "localhost", - Devel: true, - Userns: true, - Net: true, - Device: true, - SeccompFlags: seccomp.AllowMultiarch, - SeccompPresets: seccomp.PresetExt, - Tty: true, - Multiarch: true, - MapRealUID: true, + Hostname: "localhost", + Devel: true, + Userns: true, + Net: true, + Device: true, + ImmediateTermination: true, + SeccompFlags: seccomp.AllowMultiarch, + SeccompPresets: seccomp.PresetExt, + SeccompCompat: true, + Tty: true, + Multiarch: true, + MapRealUID: true, // example API credentials pulled from Google Chrome // DO NOT USE THESE IN A REAL BROWSER Env: map[string]string{ diff --git a/hst/template_test.go b/hst/template_test.go index 2baf991..a9b9226 100644 --- a/hst/template_test.go +++ b/hst/template_test.go @@ -80,8 +80,10 @@ func TestTemplate(t *testing.T) { ], "container": { "hostname": "localhost", + "immediate_termination": true, "seccomp_flags": 1, "seccomp_presets": 1, + "seccomp_compat": true, "devel": true, "userns": true, "net": true, diff --git a/internal/app/app_nixos_linux_test.go b/internal/app/app_nixos_linux_test.go index ba56bfa..c1bda92 100644 --- a/internal/app/app_nixos_linux_test.go +++ b/internal/app/app_nixos_linux_test.go @@ -144,6 +144,7 @@ var testCasesNixos = []sealTestCase{ Tmpfs("/var/run/nscd", 8192, 0755), SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel, HostNet: true, + ForwardCancel: true, }, }, } diff --git a/internal/app/app_pd_linux_test.go b/internal/app/app_pd_linux_test.go index a2f9f24..5712a42 100644 --- a/internal/app/app_pd_linux_test.go +++ b/internal/app/app_pd_linux_test.go @@ -71,6 +71,7 @@ var testCasesPd = []sealTestCase{ SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, HostNet: true, RetainSession: true, + ForwardCancel: true, }, }, { @@ -220,6 +221,7 @@ var testCasesPd = []sealTestCase{ SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, HostNet: true, RetainSession: true, + ForwardCancel: true, }, }, } diff --git a/internal/app/container_linux.go b/internal/app/container_linux.go index 98cddc7..baea57d 100644 --- a/internal/app/container_linux.go +++ b/internal/app/container_linux.go @@ -32,6 +32,10 @@ func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*contain SeccompPresets: s.SeccompPresets, RetainSession: s.Tty, 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, } { diff --git a/internal/app/shim_linux.go b/internal/app/shim_linux.go index 066f51d..c88c867 100644 --- a/internal/app/shim_linux.go +++ b/internal/app/shim_linux.go @@ -9,6 +9,7 @@ import ( "os/exec" "os/signal" "runtime" + "sync/atomic" "syscall" "time" @@ -41,6 +42,10 @@ const ( ShimExitRequest = 254 // ShimExitOrphan is returned when the shim is orphaned before monitor delivers a signal. 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. @@ -86,6 +91,7 @@ func ShimMain() { } // signal handler outcome + var cancelContainer atomic.Pointer[context.CancelFunc] go func() { buf := make([]byte, 1) for { @@ -94,23 +100,30 @@ func ShimMain() { } 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() os.Exit(ShimExitRequest) return - case 1: + case 1: // got SIGCONT after adoption: monitor died before delivering signal hlog.BeforeExit() os.Exit(ShimExitOrphan) return - case 2: + case 2: // unreachable 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") - default: + default: // unreachable log.Fatalf("got invalid message %d from signal handler", buf[0]) } } @@ -146,12 +159,11 @@ func ShimMain() { name = params.Container.Args[0] } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer stop() // unreachable + cancelContainer.Store(&stop) z := container.New(ctx, name) z.Params = *params.Container 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 = 2 * time.Second + z.WaitDelay = ShimWaitDelay if err := z.Start(); err != nil { hlog.PrintBaseError(err, "cannot start container:") diff --git a/nixos.nix b/nixos.nix index 511cfbd..919b2c9 100644 --- a/nixos.nix +++ b/nixos.nix @@ -128,6 +128,7 @@ in container = { inherit (app) + immediate_termination devel userns net diff --git a/options.nix b/options.nix index 684485e..17c5537 100644 --- a/options.nix +++ b/options.nix @@ -195,6 +195,7 @@ in ''; }; + immediate_termination = mkEnableOption "immediate termination of the container on interrupt"; devel = mkEnableOption "debugging-related kernel interfaces"; userns = mkEnableOption "user namespace creation"; tty = mkEnableOption "access to the controlling terminal"; diff --git a/test/configuration.nix b/test/configuration.nix index 18aaeda..58bc19a 100644 --- a/test/configuration.nix +++ b/test/configuration.nix @@ -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" = { name = "pa-foot"; identity = 2; diff --git a/test/test.py b/test/test.py index 9e15039..d0252cf 100644 --- a/test/test.py +++ b/test/test.py @@ -178,6 +178,16 @@ 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 != 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: raise Exception(f"unexpected exit code {interrupt_exit_code}")