Skip to content

Commit 9615dfa

Browse files
authored
fix: forward signals and drop --new-session for TUI support (#15)
* fix: forward all relevant signals to sandboxed child process Interactive/TUI apps (Claude Code, opencode) running inside greywall could not respond to terminal resizes, and copy/paste line breaks were broken. Two issues were at play: 1. Only SIGINT and SIGTERM were forwarded to the child. Added SIGWINCH, SIGQUIT, SIGHUP, SIGUSR1, SIGUSR2. 2. bwrap --new-session called setsid(), detaching the child from the controlling terminal entirely. This prevented SIGWINCH delivery regardless of forwarding. Removed --new-session and instead block the TIOCSTI ioctl (terminal input injection) via the seccomp BPF filter, which was the security concern --new-session addressed. Closes #13 * fix: gofumpt formatting in seccomp TIOCSTI filter
1 parent 2d17bfc commit 9615dfa

File tree

3 files changed

+67
-15
lines changed

3 files changed

+67
-15
lines changed

cmd/greywall/main.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ func runCommand(cmd *cobra.Command, args []string) error {
370370
execCmd.Stderr = os.Stderr
371371

372372
sigChan := make(chan os.Signal, 1)
373-
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
373+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP, syscall.SIGWINCH, syscall.SIGUSR1, syscall.SIGUSR2)
374374

375375
// Start the command (non-blocking) so we can get the PID
376376
if err := execCmd.Start(); err != nil {
@@ -396,18 +396,20 @@ func runCommand(cmd *cobra.Command, args []string) error {
396396
}
397397

398398
go func() {
399-
sigCount := 0
399+
termCount := 0
400400
for sig := range sigChan {
401-
sigCount++
402401
if execCmd.Process == nil {
403402
continue
404403
}
405-
// First signal: graceful termination; second signal: force kill
406-
if sigCount >= 2 {
407-
_ = execCmd.Process.Kill()
408-
} else {
409-
_ = execCmd.Process.Signal(sig)
404+
// For termination signals, force kill on the second attempt
405+
if sig == syscall.SIGINT || sig == syscall.SIGTERM {
406+
termCount++
407+
if termCount >= 2 {
408+
_ = execCmd.Process.Kill()
409+
continue
410+
}
410411
}
412+
_ = execCmd.Process.Signal(sig)
411413
}
412414
}()
413415

internal/sandbox/linux.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -626,13 +626,12 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
626626
bwrapArgs := []string{
627627
"bwrap",
628628
}
629-
// --new-session calls setsid() which detaches from the controlling terminal.
630-
// Skip it in learning mode so interactive programs (TUIs, prompts) can
631-
// read from /dev/tty. Learning mode already relaxes security constraints
632-
// (no seccomp, no landlock), so skipping new-session is acceptable.
633-
if !opts.Learning {
634-
bwrapArgs = append(bwrapArgs, "--new-session")
635-
}
629+
// NOTE: We intentionally do NOT use --new-session here.
630+
// --new-session calls setsid() which detaches from the controlling terminal,
631+
// breaking SIGWINCH delivery and making all interactive/TUI apps (Claude Code,
632+
// opencode, etc.) unable to respond to terminal resizes.
633+
// The TIOCSTI attack vector (terminal input injection) that --new-session
634+
// mitigates is instead blocked by our seccomp filter (see linux_seccomp.go).
636635
bwrapArgs = append(bwrapArgs, "--die-with-parent")
637636

638637
// Always use --unshare-net when available (network namespace isolation)

internal/sandbox/linux_seccomp.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ func NewSeccompFilter(debug bool) *SeccompFilter {
2020
return &SeccompFilter{debug: debug}
2121
}
2222

23+
// TIOCSTI is the ioctl command for terminal input injection.
24+
// Blocking this via seccomp replaces the need for bwrap --new-session,
25+
// which broke SIGWINCH delivery to interactive/TUI applications.
26+
const TIOCSTI = 0x5412
27+
2328
// DangerousSyscalls lists syscalls that should be blocked for security.
2429
var DangerousSyscalls = []string{
2530
"ptrace", // Process debugging/injection
@@ -144,6 +149,50 @@ func (s *SeccompFilter) writeBPFProgram(path string) error {
144149
})
145150
}
146151

152+
// Block ioctl(fd, TIOCSTI, ...) to prevent terminal input injection.
153+
// This replaces bwrap --new-session which broke SIGWINCH for TUI apps.
154+
//
155+
// seccomp_data layout (all fields are u32 on both x86_64 and aarch64):
156+
// offset 0: nr (syscall number)
157+
// offset 4: arch
158+
// offset 8: instruction_pointer (low 32 bits)
159+
// offset 12: instruction_pointer (high 32 bits)
160+
// offset 16: args[0] (low 32 bits) - fd
161+
// offset 20: args[0] (high 32 bits)
162+
// offset 24: args[1] (low 32 bits) - ioctl command
163+
// offset 28: args[1] (high 32 bits)
164+
if ioctlNum, ok := getSyscallNumber("ioctl"); ok {
165+
// Reload syscall number (previous jumps may have changed accumulator)
166+
program = append(program, bpfInstruction{
167+
code: BPF_LD | BPF_W | BPF_ABS,
168+
k: 0, // offsetof(seccomp_data, nr)
169+
})
170+
// if syscall != ioctl, skip 3 instructions (to default allow)
171+
program = append(program, bpfInstruction{
172+
code: BPF_JMP | BPF_JEQ | BPF_K,
173+
jt: 0, // match: continue to arg check
174+
jf: 3, // no match: skip to default allow
175+
k: uint32(ioctlNum), //nolint:gosec // syscall number fits in uint32
176+
})
177+
// Load ioctl command argument (args[1], low 32 bits at offset 24)
178+
program = append(program, bpfInstruction{
179+
code: BPF_LD | BPF_W | BPF_ABS,
180+
k: 24, // offsetof(seccomp_data, args[1])
181+
})
182+
// if ioctl command == TIOCSTI, block it
183+
program = append(program, bpfInstruction{
184+
code: BPF_JMP | BPF_JEQ | BPF_K,
185+
jt: 0, // match: block
186+
jf: 1, // no match: allow
187+
k: TIOCSTI,
188+
})
189+
// Block with EPERM
190+
program = append(program, bpfInstruction{
191+
code: BPF_RET | BPF_K,
192+
k: uint32(action),
193+
})
194+
}
195+
147196
// Default: allow
148197
program = append(program, bpfInstruction{
149198
code: BPF_RET | BPF_K,
@@ -263,6 +312,7 @@ func getSyscallNumber(name string) (int, bool) {
263312
"finit_module": 273,
264313
"delete_module": 106,
265314
// ioperm and iopl don't exist on ARM64
315+
"ioctl": 29,
266316
}
267317
} else {
268318
// x86_64 syscall numbers
@@ -294,6 +344,7 @@ func getSyscallNumber(name string) (int, bool) {
294344
"delete_module": 176,
295345
"ioperm": 173,
296346
"iopl": 172,
347+
"ioctl": 16,
297348
}
298349
}
299350

0 commit comments

Comments
 (0)