aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-07-29 02:21:12 +0900
committerOphestra <cat@gensokyo.uk>2025-07-29 02:23:06 +0900
commitb43d10468039a07ada412dcab83e8e074ab9ef46 (patch)
tree3eca94296dea7e9c364be9e3b29e18c6d33971a2
parentddf48a6c22d4387ff2ca2e45afbcc86fa7d81968 (diff)
app: integrate interrupt forwarding
This significantly increases usability of command line tools running through hakurei. Signed-off-by: Ophestra <cat@gensokyo.uk>
-rw-r--r--cmd/hakurei/print_test.go6
-rw-r--r--hst/container.go2
-rw-r--r--hst/template.go22
-rw-r--r--hst/template_test.go2
-rw-r--r--internal/app/app_nixos_linux_test.go1
-rw-r--r--internal/app/app_pd_linux_test.go2
-rw-r--r--internal/app/container_linux.go4
-rw-r--r--internal/app/shim_linux.go28
-rw-r--r--nixos.nix1
-rw-r--r--options.nix1
-rw-r--r--test/configuration.nix15
-rw-r--r--test/test.py10
12 files changed, 76 insertions, 18 deletions
diff --git a/cmd/hakurei/print_test.go b/cmd/hakurei/print_test.go
index 98486e02..b9d4bb27 100644
--- a/cmd/hakurei/print_test.go
+++ b/cmd/hakurei/print_test.go
@@ -256,8 +256,10 @@ App
],
"container": {
"hostname": "localhost",
+ "immediate_termination": true,
"seccomp_flags": 1,
"seccomp_presets": 1,
+ "seccomp_compat": true,
"devel": true,
"userns": true,
"net": true,
@@ -382,8 +384,10 @@ App
],
"container": {
"hostname": "localhost",
+ "immediate_termination": true,
"seccomp_flags": 1,
"seccomp_presets": 1,
+ "seccomp_compat": true,
"devel": true,
"userns": true,
"net": true,
@@ -562,8 +566,10 @@ func Test_printPs(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/hst/container.go b/hst/container.go
index 00f74795..1ee9e839 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 3f0101ea..60af2ebc 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 2baf991b..a9b92262 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 ba56bfaa..c1bda924 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 a2f9f246..5712a42a 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 98cddc7a..baea57d9 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 066f51da..c88c8672 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 511cfbdd..919b2c96 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 684485e5..17c55372 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 18aaeda9..58bc19a0 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 9e150399..d0252cfd 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}")