From a04b816e28299fcdf1a7fa34e00d2303711bc781 Mon Sep 17 00:00:00 2001 From: Sascha Grunert Date: Thu, 1 Oct 2026 14:17:39 +0200 Subject: [PATCH] bpf: load the syscall number in the first syscall that is emitted _gen_bpf_syscalls() emits the instruction that loads the syscall number only in the block of the head of the sorted syscall list, but the loop omits the head when _skip_syscall() says so: an entry that only carries a priority, which seccomp_syscall_priority() leaves for a syscall without a rule, and since ad873bd ("bpf: do not add pseudo-syscalls to the BPF filter", v2.5.0) a pseudo-syscall. No block loads the number then, every rule of that architecture compares against whatever the accumulator holds after the architecture check, and every call gets the default action. The head-only load dates from 90882e7 ("bpf: fix a problem when creating filters that include syscall arguments", v2.0.0). This takes a filter whose first syscall by priority is such an entry, on an architecture whose syscall blocks load the number, which is every one but a lone x86_64 or x32. A pseudo-syscall heads the list when every unconditional rule names a syscall the architecture lacks, which the architectures with socketcall multiplexing avoid by dropping such a rule in seccomp_rule_add; a priority-only entry heads it on any architecture. With a permissive default action every denying rule of the architecture is disabled. The PFC export shows the rules as intended, so it cannot be used to find the problem. Reproducer on x86_64 with 2.5.5, 2.6.1 and main: ctx = seccomp_init(SCMP_ACT_ALLOW); seccomp_arch_add(ctx, SCMP_ARCH_X32); seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, AF_VSOCK)); seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(socketcall), 0); seccomp_load(ctx); socket(AF_VSOCK, SOCK_STREAM, 0); /* succeeds, expected EPERM */ The exported BPF has no "ld [0]" after the x86_64 architecture check; without SCMP_ARCH_X32 the call fails with EPERM. The same happens with seccomp_syscall_priority(ctx, SCMP_SYS(getpid), 255) in place of the socketcall rule, on every architecture. Load the number in the first syscall that is not skipped instead of in the head. The new tests 65-sim-pseudo_syscall_first and 66-sim-priority_only_first fail without the change and pass with it; the rest of the suite still passes. Signed-off-by: Sascha Grunert --- src/gen_bpf.c | 16 ++++- tests/65-sim-pseudo_syscall_first.c | 85 +++++++++++++++++++++++++ tests/65-sim-pseudo_syscall_first.py | 54 ++++++++++++++++ tests/65-sim-pseudo_syscall_first.tests | 39 ++++++++++++ tests/66-sim-priority_only_first.c | 77 ++++++++++++++++++++++ tests/66-sim-priority_only_first.py | 50 +++++++++++++++ tests/66-sim-priority_only_first.tests | 29 +++++++++ tests/Makefile.am | 12 +++- 8 files changed, 357 insertions(+), 5 deletions(-) create mode 100644 tests/65-sim-pseudo_syscall_first.c create mode 100644 tests/65-sim-pseudo_syscall_first.py create mode 100644 tests/65-sim-pseudo_syscall_first.tests create mode 100644 tests/66-sim-priority_only_first.c create mode 100644 tests/66-sim-priority_only_first.py create mode 100644 tests/66-sim-priority_only_first.tests diff --git a/src/gen_bpf.c b/src/gen_bpf.c index 2cb79a5f..9294a5df 100644 --- a/src/gen_bpf.c +++ b/src/gen_bpf.c @@ -1583,7 +1583,7 @@ static int _gen_bpf_syscalls(struct bpf_state *state, unsigned int *blks_added, uint32_t optimize, unsigned int *bintree_levels) { - struct db_sys_list *s_head = NULL, *s_tail = NULL, *s_iter; + struct db_sys_list *s_head = NULL, *s_tail = NULL, *s_iter, *s_first = NULL; unsigned int syscall_cnt, empty_cnt = 0; uint64_t *bintree_hashes = NULL, nxt_hsh; unsigned int *bintree_syscalls = NULL; @@ -1623,6 +1623,18 @@ static int _gen_bpf_syscalls(struct bpf_state *state, */ acc_reset = false; + /* the first syscall in the filter loads the syscall number for the + * ones that follow, so it must be the first syscall that is not + * omitted below; the head of the sorted list may be a pseudo-syscall + * or an entry that only carries a priority, and if we used it, no + * syscall would load the number */ + for (s_iter = s_head; s_iter != NULL; s_iter = s_iter->pri_nxt) { + if (!_skip_syscall(state, s_iter)) { + s_first = s_iter; + break; + } + } + syscall_cnt = 0; /* create the syscall filters and add them to block list group */ @@ -1641,7 +1653,7 @@ static int _gen_bpf_syscalls(struct bpf_state *state, /* build the syscall filter */ state->b_new = _gen_bpf_syscall(state, s_iter, nxt_hsh, - (s_iter == s_head ? + (s_iter == s_first ? acc_reset : false)); if (state->b_new == NULL) goto out; diff --git a/tests/65-sim-pseudo_syscall_first.c b/tests/65-sim-pseudo_syscall_first.c new file mode 100644 index 00000000..5b21243d --- /dev/null +++ b/tests/65-sim-pseudo_syscall_first.c @@ -0,0 +1,85 @@ +/** + * Seccomp Library test program + * + * Copyright (c) 2026 Sascha Grunert + * Author: Sascha Grunert + */ + +/* + * This library is free software; you can redistribute it and/or modify it + * under the terms of version 2.1 of the GNU Lesser General Public License as + * published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +#include +#include + +#include + +#include "util.h" + +int main(int argc, char *argv[]) +{ + int rc; + struct util_options opts; + scmp_filter_ctx ctx = NULL; + + rc = util_getopt(argc, argv, &opts); + if (rc < 0) + goto out; + + ctx = seccomp_init(SCMP_ACT_ALLOW); + if (ctx == NULL) + return ENOMEM; + + rc = seccomp_arch_remove(ctx, SCMP_ARCH_NATIVE); + if (rc < 0) + goto out; + rc = seccomp_arch_add(ctx, SCMP_ARCH_X86); + if (rc < 0) + goto out; + rc = seccomp_arch_add(ctx, SCMP_ARCH_X86_64); + if (rc < 0) + goto out; + rc = seccomp_arch_add(ctx, SCMP_ARCH_X32); + if (rc < 0) + goto out; + rc = seccomp_arch_add(ctx, SCMP_ARCH_AARCH64); + if (rc < 0) + goto out; + + /* a pseudo-syscall on every arch above, without conditions so that it + * sorts before the conditional rules and becomes the head of the + * syscall list, where it is omitted from the filter; x86 drops the + * rule when it is added, the others keep it */ + rc = seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(arm_fadvise64_64), 0); + if (rc < 0) + goto out; + + /* the real rules must still be applied, which they are only if the + * filter loads the syscall number before testing them */ + rc = seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(read), 1, + SCMP_A0(SCMP_CMP_EQ, 1)); + if (rc < 0) + goto out; + rc = seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(write), 1, + SCMP_A0(SCMP_CMP_EQ, 1)); + if (rc < 0) + goto out; + + rc = util_filter_output(&opts, ctx); + if (rc) + goto out; + +out: + seccomp_release(ctx); + return (rc < 0 ? -rc : rc); +} diff --git a/tests/65-sim-pseudo_syscall_first.py b/tests/65-sim-pseudo_syscall_first.py new file mode 100644 index 00000000..0260d8ed --- /dev/null +++ b/tests/65-sim-pseudo_syscall_first.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +# +# Seccomp Library test program +# +# Copyright (c) 2026 Sascha Grunert +# Author: Sascha Grunert +# + +# +# This library is free software; you can redistribute it and/or modify it +# under the terms of version 2.1 of the GNU Lesser General Public License as +# published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, see . +# + +import argparse +import sys + +import util + +from seccomp import * + +def test(args): + f = SyscallFilter(ALLOW) + f.remove_arch(Arch()) + f.add_arch(Arch("x86")) + f.add_arch(Arch("x86_64")) + f.add_arch(Arch("x32")) + f.add_arch(Arch("aarch64")) + # a pseudo-syscall on every arch above, without conditions so that it + # sorts before the conditional rules and becomes the head of the syscall + # list, where it is omitted from the filter; x86 drops the rule when it + # is added, the others keep it + f.add_rule(KILL, "arm_fadvise64_64") + # the real rules must still be applied, which they are only if the + # filter loads the syscall number before testing them + f.add_rule(KILL, "read", Arg(0, EQ, 1)) + f.add_rule(KILL, "write", Arg(0, EQ, 1)) + return f + +args = util.get_opt() +ctx = test(args) +util.filter_output(args, ctx) + +# kate: syntax python; +# kate: indent-mode python; space-indent on; indent-width 4; mixedindent off; diff --git a/tests/65-sim-pseudo_syscall_first.tests b/tests/65-sim-pseudo_syscall_first.tests new file mode 100644 index 00000000..092ebaf9 --- /dev/null +++ b/tests/65-sim-pseudo_syscall_first.tests @@ -0,0 +1,39 @@ +# +# libseccomp regression test automation data +# +# Copyright (c) 2026 Sascha Grunert +# Author: Sascha Grunert +# + +test type: bpf-sim + +# Testname Arch Syscall Arg0 Arg1 Arg2 Arg3 Arg4 Arg5 Result +65-sim-pseudo_syscall_first +x86 read 1 N N N N N KILL +65-sim-pseudo_syscall_first +x86 read 0 N N N N N ALLOW +65-sim-pseudo_syscall_first +x86 read 2-10 N N N N N ALLOW +65-sim-pseudo_syscall_first +x86 write 1 N N N N N KILL +65-sim-pseudo_syscall_first +x86 write 0 N N N N N ALLOW +65-sim-pseudo_syscall_first +x86 open 1 N N N N N ALLOW +65-sim-pseudo_syscall_first +x86_64 read 1 N N N N N KILL +65-sim-pseudo_syscall_first +x86_64 read 0 N N N N N ALLOW +65-sim-pseudo_syscall_first +x86_64 read 2-10 N N N N N ALLOW +65-sim-pseudo_syscall_first +x86_64 write 1 N N N N N KILL +65-sim-pseudo_syscall_first +x86_64 write 0 N N N N N ALLOW +65-sim-pseudo_syscall_first +x86_64 open 1 N N N N N ALLOW +65-sim-pseudo_syscall_first +x32 read 1 N N N N N KILL +65-sim-pseudo_syscall_first +x32 read 0 N N N N N ALLOW +65-sim-pseudo_syscall_first +x32 read 2-10 N N N N N ALLOW +65-sim-pseudo_syscall_first +x32 write 1 N N N N N KILL +65-sim-pseudo_syscall_first +x32 write 0 N N N N N ALLOW +65-sim-pseudo_syscall_first +x32 open 1 N N N N N ALLOW +65-sim-pseudo_syscall_first +aarch64 read 1 N N N N N KILL +65-sim-pseudo_syscall_first +aarch64 read 0 N N N N N ALLOW +65-sim-pseudo_syscall_first +aarch64 read 2-10 N N N N N ALLOW +65-sim-pseudo_syscall_first +aarch64 write 1 N N N N N KILL +65-sim-pseudo_syscall_first +aarch64 write 0 N N N N N ALLOW +65-sim-pseudo_syscall_first +aarch64 openat 1 N N N N N ALLOW + +test type: bpf-valgrind + +# Testname +65-sim-pseudo_syscall_first diff --git a/tests/66-sim-priority_only_first.c b/tests/66-sim-priority_only_first.c new file mode 100644 index 00000000..ab83929d --- /dev/null +++ b/tests/66-sim-priority_only_first.c @@ -0,0 +1,77 @@ +/** + * Seccomp Library test program + * + * Copyright (c) 2026 Sascha Grunert + * Author: Sascha Grunert + */ + +/* + * This library is free software; you can redistribute it and/or modify it + * under the terms of version 2.1 of the GNU Lesser General Public License as + * published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +#include +#include + +#include + +#include "util.h" + +int main(int argc, char *argv[]) +{ + int rc; + struct util_options opts; + scmp_filter_ctx ctx = NULL; + + rc = util_getopt(argc, argv, &opts); + if (rc < 0) + goto out; + + ctx = seccomp_init(SCMP_ACT_ALLOW); + if (ctx == NULL) + return ENOMEM; + + rc = seccomp_arch_remove(ctx, SCMP_ARCH_NATIVE); + if (rc < 0) + goto out; + rc = seccomp_arch_add(ctx, SCMP_ARCH_X86); + if (rc < 0) + goto out; + rc = seccomp_arch_add(ctx, SCMP_ARCH_AARCH64); + if (rc < 0) + goto out; + + /* a priority without a rule leaves an entry that sorts first and is + * omitted from the filter, on every arch */ + rc = seccomp_syscall_priority(ctx, SCMP_SYS(getpid), 255); + if (rc < 0) + goto out; + + /* the real rules must still be applied, which they are only if the + * filter loads the syscall number before testing them */ + rc = seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(read), 1, + SCMP_A0(SCMP_CMP_EQ, 1)); + if (rc < 0) + goto out; + rc = seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(write), 1, + SCMP_A0(SCMP_CMP_EQ, 1)); + if (rc < 0) + goto out; + + rc = util_filter_output(&opts, ctx); + if (rc) + goto out; + +out: + seccomp_release(ctx); + return (rc < 0 ? -rc : rc); +} diff --git a/tests/66-sim-priority_only_first.py b/tests/66-sim-priority_only_first.py new file mode 100644 index 00000000..5ba13b31 --- /dev/null +++ b/tests/66-sim-priority_only_first.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +# +# Seccomp Library test program +# +# Copyright (c) 2026 Sascha Grunert +# Author: Sascha Grunert +# + +# +# This library is free software; you can redistribute it and/or modify it +# under the terms of version 2.1 of the GNU Lesser General Public License as +# published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, see . +# + +import argparse +import sys + +import util + +from seccomp import * + +def test(args): + f = SyscallFilter(ALLOW) + f.remove_arch(Arch()) + f.add_arch(Arch("x86")) + f.add_arch(Arch("aarch64")) + # a priority without a rule leaves an entry that sorts first and is + # omitted from the filter, on every arch + f.syscall_priority("getpid", 255) + # the real rules must still be applied, which they are only if the + # filter loads the syscall number before testing them + f.add_rule(KILL, "read", Arg(0, EQ, 1)) + f.add_rule(KILL, "write", Arg(0, EQ, 1)) + return f + +args = util.get_opt() +ctx = test(args) +util.filter_output(args, ctx) + +# kate: syntax python; +# kate: indent-mode python; space-indent on; indent-width 4; mixedindent off; diff --git a/tests/66-sim-priority_only_first.tests b/tests/66-sim-priority_only_first.tests new file mode 100644 index 00000000..748be05d --- /dev/null +++ b/tests/66-sim-priority_only_first.tests @@ -0,0 +1,29 @@ +# +# libseccomp regression test automation data +# +# Copyright (c) 2026 Sascha Grunert +# Author: Sascha Grunert +# + +test type: bpf-sim + +# Testname Arch Syscall Arg0 Arg1 Arg2 Arg3 Arg4 Arg5 Result +66-sim-priority_only_first +x86 read 1 N N N N N KILL +66-sim-priority_only_first +x86 read 0 N N N N N ALLOW +66-sim-priority_only_first +x86 read 2-10 N N N N N ALLOW +66-sim-priority_only_first +x86 write 1 N N N N N KILL +66-sim-priority_only_first +x86 write 0 N N N N N ALLOW +66-sim-priority_only_first +x86 getpid N N N N N N ALLOW +66-sim-priority_only_first +x86 open 1 N N N N N ALLOW +66-sim-priority_only_first +aarch64 read 1 N N N N N KILL +66-sim-priority_only_first +aarch64 read 0 N N N N N ALLOW +66-sim-priority_only_first +aarch64 read 2-10 N N N N N ALLOW +66-sim-priority_only_first +aarch64 write 1 N N N N N KILL +66-sim-priority_only_first +aarch64 write 0 N N N N N ALLOW +66-sim-priority_only_first +aarch64 getpid N N N N N N ALLOW +66-sim-priority_only_first +aarch64 openat 1 N N N N N ALLOW + +test type: bpf-valgrind + +# Testname +66-sim-priority_only_first diff --git a/tests/Makefile.am b/tests/Makefile.am index 280cfa83..2cb0e6a8 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -99,7 +99,9 @@ check_PROGRAMS = \ 61-sim-transactions \ 62-sim-arch_transactions \ 63-live-notify_addfd \ - 64-live-trapx + 64-live-trapx \ + 65-sim-pseudo_syscall_first \ + 66-sim-priority_only_first EXTRA_DIST_TESTPYTHON = \ util.py \ @@ -164,7 +166,9 @@ EXTRA_DIST_TESTPYTHON = \ 61-sim-transactions.py \ 62-sim-arch_transactions.py \ 63-live-notify_addfd.py \ - 64-live-trapx + 64-live-trapx \ + 65-sim-pseudo_syscall_first.py \ + 66-sim-priority_only_first.py EXTRA_DIST_TESTCFGS = \ 01-sim-allow.tests \ @@ -230,7 +234,9 @@ EXTRA_DIST_TESTCFGS = \ 61-sim-transactions.tests \ 62-sim-arch_transactions.tests \ 63-live-notify_addfd.tests \ - 64-live-trapx.tests + 64-live-trapx.tests \ + 65-sim-pseudo_syscall_first.tests \ + 66-sim-priority_only_first.tests EXTRA_DIST_TESTSCRIPTS = \ 38-basic-pfc_coverage.sh 38-basic-pfc_coverage.pfc \