Skip to content

Commit 427d7c6

Browse files
committed
Block TIOCLINUX ioctl in seccomp arg-level filter
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent be295c2 commit 427d7c6

File tree

3 files changed

+42
-10
lines changed

3 files changed

+42
-10
lines changed

src/sandlock/_seccomp.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,13 @@
7979
| CLONE_NEWNET | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWCGROUP
8080
)
8181

82-
# Dangerous ioctl commands — TIOCSTI allows injecting input into
83-
# another terminal session (terminal escape attack).
82+
# Dangerous ioctl commands:
83+
# - TIOCSTI: inject input into another terminal session (terminal escape)
84+
# - TIOCLINUX: access kernel console (keystroke injection on VTs,
85+
# selection buffer read, keyboard reprogramming)
8486
TIOCSTI = 0x5412
87+
TIOCLINUX = 0x541C
88+
_DANGEROUS_IOCTLS = (TIOCSTI, TIOCLINUX)
8589

8690
# Dangerous prctl(2) options — these allow a sandboxed process to
8791
# weaken its own confinement.
@@ -304,7 +308,7 @@ def _build_arg_filters() -> bytes:
304308
- clone(2): Block namespace flags (CLONE_NEW*) with ERRNO.
305309
Plain forks fall through to the main filter (USER_NOTIF if
306310
clone is in the notif list, or ALLOW if not).
307-
- ioctl(2): Block TIOCSTI (terminal input injection).
311+
- ioctl(2): Block TIOCSTI and TIOCLINUX (terminal attacks).
308312
- prctl(2): Block dangerous options (PR_SET_DUMPABLE,
309313
PR_SET_SECCOMP, PR_SET_SECUREBITS, PR_SET_PTRACER).
310314
"""
@@ -323,16 +327,17 @@ def _build_arg_filters() -> bytes:
323327
insns += _bpf_jump(BPF_JMP | BPF_JSET | BPF_K, _CLONE_NS_FLAGS, 0, 1)
324328
insns += _bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | ERRNO_EPERM)
325329

326-
# --- ioctl: block TIOCSTI (terminal input injection) ---
327-
# Load syscall number
330+
# --- ioctl: block dangerous commands (TIOCSTI, TIOCLINUX) ---
328331
insns += _bpf_stmt(BPF_LD | BPF_W | BPF_ABS, OFFSET_NR)
329-
# if nr != ioctl, skip ahead
330-
insns += _bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, _SYSCALL_NR["ioctl"], 0, 3)
332+
n_ioctls = len(_DANGEROUS_IOCTLS)
333+
# if nr != ioctl, skip: 1 (load arg1) + n_ioctls*2 (check+deny each)
334+
skip_count = 1 + n_ioctls * 2
335+
insns += _bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, _SYSCALL_NR["ioctl"], 0, skip_count)
331336
# Load ioctl request (arg1, low 32 bits)
332337
insns += _bpf_stmt(BPF_LD | BPF_W | BPF_ABS, OFFSET_ARGS1_LO)
333-
# if request == TIOCSTI → deny
334-
insns += _bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, TIOCSTI, 0, 1)
335-
insns += _bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | ERRNO_EPERM)
338+
for cmd in _DANGEROUS_IOCTLS:
339+
insns += _bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, cmd, 0, 1)
340+
insns += _bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | ERRNO_EPERM)
336341

337342
# --- prctl: block dangerous options that weaken the sandbox ---
338343
insns += _bpf_stmt(BPF_LD | BPF_W | BPF_ABS, OFFSET_NR)

tests/test_integration.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,24 @@ def test_default_deny_blocks_ptrace(self):
301301
ret = libc.ptrace(0, 0, 0, 0) # PTRACE_TRACEME
302302
err = ctypes.get_errno()
303303
print("BLOCKED" if err == errno.EPERM else f"UNEXPECTED {err}")
304+
"""]
305+
)
306+
assert result.success
307+
assert b"BLOCKED" in result.stdout
308+
309+
def test_ioctl_tioclinux_blocked(self):
310+
"""TIOCLINUX ioctl is blocked by arg-level seccomp filter."""
311+
policy = Policy(fs_readable=_PYTHON_READABLE)
312+
result = Sandbox(policy).run(
313+
["python3", "-c", """
314+
import ctypes, ctypes.util, errno, fcntl
315+
TIOCLINUX = 0x541C
316+
buf = ctypes.create_string_buffer(1)
317+
try:
318+
fcntl.ioctl(0, TIOCLINUX, buf)
319+
print("ALLOWED")
320+
except OSError as e:
321+
print("BLOCKED" if e.errno == errno.EPERM else f"UNEXPECTED {e.errno}")
304322
"""]
305323
)
306324
assert result.success

tests/test_seccomp.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
from sandlock._seccomp import (
99
AUDIT_ARCH,
1010
DEFAULT_DENY_SYSCALLS,
11+
TIOCLINUX,
1112
TIOCSTI,
1213
_ARCH_AARCH64,
1314
_ARCH_X86_64,
1415
_CLONE_NS_FLAGS,
16+
_DANGEROUS_IOCTLS,
1517
_DANGEROUS_PRCTL_OPS,
1618
_MACHINE_TO_ARCH,
1719
_SYSCALL_NR,
@@ -101,6 +103,13 @@ def test_clone_ns_flags_defined(self):
101103
def test_tiocsti_defined(self):
102104
assert TIOCSTI == 0x5412
103105

106+
def test_tioclinux_defined(self):
107+
assert TIOCLINUX == 0x541C
108+
109+
def test_dangerous_ioctls_defined(self):
110+
assert TIOCSTI in _DANGEROUS_IOCTLS
111+
assert TIOCLINUX in _DANGEROUS_IOCTLS
112+
104113
def test_prctl_in_syscall_map(self):
105114
assert "prctl" in _SYSCALL_NR
106115

0 commit comments

Comments
 (0)