diff --git a/images/benchmarks/syscallbench/syscallbench.c b/images/benchmarks/syscallbench/syscallbench.c index fed56987a..795439fb5 100644 --- a/images/benchmarks/syscallbench/syscallbench.c +++ b/images/benchmarks/syscallbench/syscallbench.c @@ -13,14 +13,19 @@ // limitations under the License. #include +#include +#include +#include #include #include +#include #include #include static int loops = 10000000; enum syscall_type { get_pid, get_pid_opt }; +enum seccomp_policy { seccomp_none, seccomp_cacheable, seccomp_uncacheable }; #ifdef __x86_64__ @@ -39,22 +44,92 @@ static void show_usage(const char *cmd) { "Usage: %s [options]\n" "-l, --loops \t\t Number of syscall loops, default 10000000\n" "-s, --syscall \t\tSyscall to run (default getpid)\n" + "--seccomp_cacheable\t\tAdd a cacheable ALLOW " + "seccomp filter for this syscall\n" + "--seccomp_notcacheable\t\tAdd a non-cacheable ALLOW " + "seccomp filter for this syscall\n" "\tOptions:\n" "\t%d) getpid\n" "\t%d) getpidopt\n", cmd, get_pid, get_pid_opt); } +static void set_cacheable_filter() { + // "Prior to [PR_SET_SECCOMP], the task must call prctl(PR_SET_NO_NEW_PRIVS, + // 1) or run with CAP_SYS_ADMIN privileges in its namespace." - + // Documentation/prctl/seccomp_filter.txt + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) { + fprintf(stderr, "prctl(PR_SET_NO_NEW_PRIVS) failed\n"); + exit(1); + } + + struct sock_filter filter[] = { + // A = seccomp_data.arch + BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 4), + // if (A != AUDIT_ARCH_X86_64) goto kill + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 2), + // A = seccomp_data.nr + BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 0), + // return SECCOMP_RET_ALLOW + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), + // kill: return SECCOMP_RET_KILL + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL), + }; + struct sock_fprog prog; + prog.len = 5; + prog.filter = filter; + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog, 0, 0) != 0) { + fprintf(stderr, "prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER) failed\n"); + exit(1); + } +} + +static void set_uncacheable_filter() { + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) { + fprintf(stderr, "prctl(PR_SET_NO_NEW_PRIVS) failed\n"); + exit(1); + } + + struct sock_filter filter[] = { + // A = seccomp_data.arch + BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 4), + // if (A != AUDIT_ARCH_X86_64) goto kill + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 3), + // A = seccomp_data.nr + BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 0), + // A = seccomp_data.args[0] + BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 16), + // return SECCOMP_RET_ALLOW + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), + // kill: return SECCOMP_RET_KILL + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL), + }; + struct sock_fprog prog; + prog.len = 6; + prog.filter = filter; + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog, 0, 0) != 0) { + fprintf(stderr, "prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER) failed\n"); + exit(1); + } +} + int main(int argc, char *argv[]) { int i, c, sys_val = get_pid; + int seccomp_policy_flag = seccomp_none; struct option long_options[] = {{"loops", required_argument, 0, 'l'}, {"syscall", required_argument, 0, 's'}, + {"seccomp_cacheable", no_argument, + &seccomp_policy_flag, seccomp_cacheable}, + {"seccomp_notcacheable", no_argument, + &seccomp_policy_flag, seccomp_uncacheable}, {0, 0, 0, 0}}; int option_index = 0; - while ((c = getopt_long(argc, argv, "l:s:", long_options, &option_index)) != + while ((c = getopt_long(argc, argv, "l:s:c:", long_options, &option_index)) != -1) { switch (c) { + case 0: + break; case 'l': loops = atoi(optarg); if (loops <= 0) { @@ -69,6 +144,13 @@ int main(int argc, char *argv[]) { exit(1); } break; + case 'c': + sys_val = atoi(optarg); + if (sys_val < 0) { + show_usage(argv[0]); + exit(1); + } + break; default: fprintf(stderr, "unknown option: '%c'\n", c); show_usage(argv[0]); @@ -76,6 +158,21 @@ int main(int argc, char *argv[]) { } } + switch (seccomp_policy_flag) { + case seccomp_none: + break; + case seccomp_cacheable: + set_cacheable_filter(); + break; + case seccomp_uncacheable: + set_uncacheable_filter(); + break; + default: + fprintf(stderr, "unknown seccomp option: %d\n", seccomp_policy_flag); + show_usage(argv[0]); + exit(1); + } + switch (sys_val) { case (int)get_pid: for (i = 0; i < loops; i++) syscall(SYS_getpid); diff --git a/pkg/sentry/kernel/seccomp.go b/pkg/sentry/kernel/seccomp.go index da45b3455..515678e1c 100644 --- a/pkg/sentry/kernel/seccomp.go +++ b/pkg/sentry/kernel/seccomp.go @@ -15,15 +15,56 @@ package kernel import ( + "fmt" + "reflect" + "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/abi/sentry" "gvisor.dev/gvisor/pkg/bpf" "gvisor.dev/gvisor/pkg/errors/linuxerr" "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/sentry/arch" ) -const maxSyscallFilterInstructions = 1 << 15 +const ( + maxSyscallFilterInstructions = 1 << 15 + + // uncacheableBPFAction is an invalid seccomp action code. + // It is used as a sentinel value in `taskSeccompFilters.cache` to indicate + // that a specific syscall number is uncachable. + uncacheableBPFAction = linux.SECCOMP_RET_ACTION_FULL +) + +// taskSeccomp holds seccomp-related data for a `Task`. +// +// +stateify savable +type taskSeccomp struct { + // filters is the list of seccomp programs that are applied to the task, + // in the order in which they were installed. + filters []bpf.Program + + // cache maps syscall numbers to the action to take for that syscall number. + // It is only populated for syscalls where determining this action does not + // involve any input data other than the architecture and the syscall + // number in any of `filters`. + // If any other input is necessary, the cache stores `uncacheableBPFAction` + // to indicate that this syscall number's rules are not cacheable. + cache [sentry.MaxSyscallNum + 1]linux.BPFAction + + // cacheAuditNumber is the AUDIT_ARCH_* constant of the task image used + // at the time of computing `cache`. + cacheAuditNumber uint32 +} + +// copy returns a copy of this `taskSeccomp`. +func (ts *taskSeccomp) copy() *taskSeccomp { + return &taskSeccomp{ + filters: append(([]bpf.Program)(nil), ts.filters...), + cacheAuditNumber: ts.cacheAuditNumber, + cache: ts.cache, + } +} // dataAsBPFInput returns a serialized BPF program, only valid on the current task // goroutine. @@ -99,9 +140,21 @@ func (t *Task) checkSeccompSyscall(sysno int32, args arch.SyscallArguments, ip h } func (t *Task) evaluateSyscallFilters(sysno int32, args arch.SyscallArguments, ip hostarch.Addr) uint32 { + ret := uint32(linux.SECCOMP_RET_ALLOW) + ts := t.seccomp.Load().(*taskSeccomp) + if ts == nil { + return ret + } + arch := t.image.st.AuditNumber + if arch == ts.cacheAuditNumber && sysno >= 0 && sysno <= sentry.MaxSyscallNum { + if cached := ts.cache[sysno]; cached != uncacheableBPFAction { + return uint32(cached) + } + } + data := linux.SeccompData{ Nr: sysno, - Arch: t.image.st.AuditNumber, + Arch: arch, InstructionPointer: uint64(ip), } // data.args is []uint64 and args is []arch.SyscallArgument (uintptr), so @@ -114,16 +167,10 @@ func (t *Task) evaluateSyscallFilters(sysno int32, args arch.SyscallArguments, i } input := dataAsBPFInput(t, &data) - ret := uint32(linux.SECCOMP_RET_ALLOW) - f := t.syscallFilters.Load() - if f == nil { - return ret - } - // "Every filter successfully installed will be evaluated (in reverse // order) for each system call the task makes." - kernel/seccomp.c - for i := len(f.([]bpf.Program)) - 1; i >= 0; i-- { - thisRet, err := bpf.Exec[bpf.NativeEndian](f.([]bpf.Program)[i], input) + for i := len(ts.filters) - 1; i >= 0; i-- { + thisRet, err := bpf.Exec[bpf.NativeEndian](ts.filters[i], input) if err != nil { t.Debugf("seccomp-bpf filter %d returned error: %v", i, err) thisRet = uint32(linux.SECCOMP_RET_KILL_THREAD) @@ -147,6 +194,74 @@ func (t *Task) evaluateSyscallFilters(sysno int32, args arch.SyscallArguments, i return ret } +// checkFilterCacheability executes `program` on the given `input`, and +// checks if its result is cacheable. If it is, it returns that result. +func checkFilterCacheability(program bpf.Program, input bpf.Input) (uint32, error) { + // Look up Nr and Arch fields, we'll use their offsets later + // to verify whether they were accessed. + sdType := reflect.TypeOf(linux.SeccompData{}) + nrField, ok := sdType.FieldByName("Nr") + if !ok { + panic("linux.SeccompData.Nr field not found") + } + archField, ok := sdType.FieldByName("Arch") + if !ok { + panic("linux.SeccompData.Arch field not found") + } + + exec, err := bpf.InstrumentedExec[bpf.NativeEndian](program, input) + if err != nil { + return 0, err + } + for offset, accessed := range exec.InputAccessed { + if !accessed { + continue // Input byte not accessed by the program. + } + if uintptr(offset) >= nrField.Offset && uintptr(offset) < nrField.Offset+nrField.Type.Size() { + continue // The program accessed the "Nr" field, this is OK. + } + if uintptr(offset) >= archField.Offset && uintptr(offset) < archField.Offset+archField.Type.Size() { + continue // The program accessed the "Arch" field, this is OK. + } + return 0, fmt.Errorf("program accessed byte at offset %d which is not the sysno or arch field", offset) + } + return exec.ReturnValue, nil +} + +// populateCache recomputes `ts.cache` from `ts.filters`. +func (ts *taskSeccomp) populateCache(t *Task) { + sd := linux.SeccompData{} + ts.cacheAuditNumber = t.image.st.AuditNumber + + for sysno := int32(0); sysno <= sentry.MaxSyscallNum; sysno++ { + sd.Nr = sysno + sd.Arch = ts.cacheAuditNumber + input := dataAsBPFInput(t, &sd) + sysnoIsCacheable := true + ret := linux.BPFAction(linux.SECCOMP_RET_ALLOW) + // See notes in `evaluateSyscallFilters` for how to properly interpret + // seccomp filter and results. We use the same approach here: iterate + // through filters backwards, and take the smallest result. + // If any filter is not cacheable, then we cannot cache the result for + // this sysno. + for i := len(ts.filters) - 1; i >= 0; i-- { + result, cacheErr := checkFilterCacheability(ts.filters[i], input) + if cacheErr != nil { + sysnoIsCacheable = false + break + } + if (linux.BPFAction(result) & linux.SECCOMP_RET_ACTION) < (ret & linux.SECCOMP_RET_ACTION) { + ret = linux.BPFAction(result) + } + } + if sysnoIsCacheable { + ts.cache[sysno] = ret + } else { + ts.cache[sysno] = uncacheableBPFAction + } + } +} + // AppendSyscallFilter adds BPF program p as a system call filter. // // Preconditions: The caller must be running on the task goroutine. @@ -161,30 +276,28 @@ func (t *Task) AppendSyscallFilter(p bpf.Program, syncAll bool) error { // instructions per filter beyond the first) to maxSyscallFilterInstructions. // This restriction is inherited from Linux. totalLength := p.Length() - var newFilters []bpf.Program + newSeccomp := &taskSeccomp{} - if sf := t.syscallFilters.Load(); sf != nil { - oldFilters := sf.([]bpf.Program) - for _, f := range oldFilters { + if ts := t.seccomp.Load().(*taskSeccomp); ts != nil { + for _, f := range ts.filters { totalLength += f.Length() + 4 } - newFilters = append(newFilters, oldFilters...) + newSeccomp.filters = append(newSeccomp.filters, ts.filters...) } if totalLength > maxSyscallFilterInstructions { return linuxerr.ENOMEM } - newFilters = append(newFilters, p) - t.syscallFilters.Store(newFilters) + newSeccomp.filters = append(newSeccomp.filters, p) + newSeccomp.populateCache(t) + t.seccomp.Store(newSeccomp) if syncAll { // Note: No new privs is always assumed to be set. for ot := t.tg.tasks.Front(); ot != nil; ot = ot.Next() { if ot != t { - var copiedFilters []bpf.Program - copiedFilters = append(copiedFilters, newFilters...) - ot.syscallFilters.Store(copiedFilters) + ot.seccomp.Store(newSeccomp.copy()) } } } @@ -196,8 +309,7 @@ func (t *Task) AppendSyscallFilter(p bpf.Program, syncAll bool) error { // seccomp syscall filtering mode, appropriate for both prctl(PR_GET_SECCOMP) // and /proc/[pid]/status. func (t *Task) SeccompMode() int { - f := t.syscallFilters.Load() - if f != nil && len(f.([]bpf.Program)) > 0 { + if ts := t.seccomp.Load().(*taskSeccomp); ts != nil && len(ts.filters) > 0 { return linux.SECCOMP_MODE_FILTER } return linux.SECCOMP_MODE_NONE diff --git a/pkg/sentry/kernel/task.go b/pkg/sentry/kernel/task.go index 261601304..c0af03092 100644 --- a/pkg/sentry/kernel/task.go +++ b/pkg/sentry/kernel/task.go @@ -21,7 +21,6 @@ import ( "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/atomicbitops" - "gvisor.dev/gvisor/pkg/bpf" "gvisor.dev/gvisor/pkg/errors/linuxerr" "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/metric" @@ -451,12 +450,14 @@ type Task struct { // parentDeathSignal is protected by mu. parentDeathSignal linux.Signal - // syscallFilters is all seccomp-bpf syscall filters applicable to the - // task, in the order in which they were installed. The type of the atomic - // is []bpf.Program. Writing needs to be protected by the signal mutex. + // seccomp contains all seccomp-bpf syscall filters applicable to the task. + // The type of the atomic is *taskSeccomp. + // Writing needs to be protected by the signal mutex. Note that due to + // atomic.Value limitations (atomic.Value.Store(nil) panics), a nil + // seccomp is always represented as a typed nil (i.e. (*taskSeccomp)(nil)). // - // syscallFilters is owned by the task goroutine. - syscallFilters atomic.Value `state:".([]bpf.Program)"` + // seccomp is owned by the task goroutine. + seccomp atomic.Value `state:".(*taskSeccomp)"` // If cleartid is non-zero, treat it as a pointer to a ThreadID in the // task's virtual address space; when the task exits, set the pointed-to @@ -622,20 +623,20 @@ func (t *Task) loadPtraceTracer(tracer *Task) { t.ptraceTracer.Store(tracer) } -func (t *Task) saveSyscallFilters() []bpf.Program { - if f := t.syscallFilters.Load(); f != nil { - return f.([]bpf.Program) - } - return nil +func (t *Task) saveSeccomp() *taskSeccomp { + return t.seccomp.Load().(*taskSeccomp) } -func (t *Task) loadSyscallFilters(filters []bpf.Program) { - t.syscallFilters.Store(filters) +func (t *Task) loadSeccomp(seccompData *taskSeccomp) { + t.seccomp.Store(seccompData) } // afterLoad is invoked by stateify. func (t *Task) afterLoad() { t.updateInfoLocked() + if ts := t.seccomp.Load().(*taskSeccomp); ts != nil { + ts.populateCache(t) + } t.interruptChan = make(chan struct{}, 1) t.gosched.State = TaskGoroutineNonexistent if t.stop != nil { diff --git a/pkg/sentry/kernel/task_clone.go b/pkg/sentry/kernel/task_clone.go index 7131e083e..5ebf0b3e8 100644 --- a/pkg/sentry/kernel/task_clone.go +++ b/pkg/sentry/kernel/task_clone.go @@ -17,7 +17,6 @@ package kernel import ( "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/atomicbitops" - "gvisor.dev/gvisor/pkg/bpf" "gvisor.dev/gvisor/pkg/cleanup" "gvisor.dev/gvisor/pkg/errors/linuxerr" "gvisor.dev/gvisor/pkg/hostarch" @@ -324,9 +323,12 @@ func (t *Task) Clone(args *linux.CloneArgs) (ThreadID, *SyscallControl, error) { // "If fork/clone and execve are allowed by @prog, any child processes will // be constrained to the same filters and system call ABI as the parent." - // Documentation/prctl/seccomp_filter.txt - if f := t.syscallFilters.Load(); f != nil { - copiedFilters := append([]bpf.Program(nil), f.([]bpf.Program)...) - nt.syscallFilters.Store(copiedFilters) + if ts := t.seccomp.Load().(*taskSeccomp); ts != nil { + seccompCopy := ts.copy() + seccompCopy.populateCache(nt) + nt.seccomp.Store(seccompCopy) + } else { + nt.seccomp.Store((*taskSeccomp)(nil)) } if args.Flags&linux.CLONE_VFORK != 0 { nt.vforkParent = t diff --git a/pkg/sentry/kernel/task_start.go b/pkg/sentry/kernel/task_start.go index 3aedd1a07..bb6a0606c 100644 --- a/pkg/sentry/kernel/task_start.go +++ b/pkg/sentry/kernel/task_start.go @@ -177,6 +177,7 @@ func (ts *TaskSet) newTask(ctx context.Context, cfg *TaskConfig) (*Task, error) t.creds.Store(cfg.Credentials) t.endStopCond.L = &t.tg.signalHandlers.mu t.ptraceTracer.Store((*Task)(nil)) + t.seccomp.Store((*taskSeccomp)(nil)) // We don't construct t.blockingTimer until Task.run(); see that function // for justification. diff --git a/pkg/sentry/kernel/task_syscall.go b/pkg/sentry/kernel/task_syscall.go index d2f26dd9d..87cf67bb5 100644 --- a/pkg/sentry/kernel/task_syscall.go +++ b/pkg/sentry/kernel/task_syscall.go @@ -234,7 +234,7 @@ func (t *Task) doSyscall() taskRunState { // Check seccomp filters. The nil check is for performance (as seccomp use // is rare), not needed for correctness. - if t.syscallFilters.Load() != nil { + if t.seccomp.Load() != nil { switch r := t.checkSeccompSyscall(int32(sysno), args, hostarch.Addr(t.Arch().IP())); r { case linux.SECCOMP_RET_ERRNO, linux.SECCOMP_RET_TRAP: t.Debugf("Syscall %d: denied by seccomp", sysno) @@ -382,7 +382,7 @@ func (t *Task) doVsyscall(addr hostarch.Addr, sysno uintptr) taskRunState { // to syscall ABI because they both use RDI, RSI, and RDX for the first three // arguments and none of the vsyscalls uses more than two arguments. args := t.Arch().SyscallArgs() - if t.syscallFilters.Load() != nil { + if t.seccomp.Load() != nil { switch r := t.checkSeccompSyscall(int32(sysno), args, addr); r { case linux.SECCOMP_RET_ERRNO, linux.SECCOMP_RET_TRAP: t.Debugf("vsyscall %d, caller %x: denied by seccomp", sysno, t.Arch().Value(caller)) diff --git a/test/benchmarks/base/syscallbench_test.go b/test/benchmarks/base/syscallbench_test.go index c5738c8e3..5c99946d3 100644 --- a/test/benchmarks/base/syscallbench_test.go +++ b/test/benchmarks/base/syscallbench_test.go @@ -77,5 +77,56 @@ func BenchmarkSyscallbench(b *testing.B) { }) }() } +} +// BenchmarkSyscallUnderSeccomp runs a syscall b.N times with a seccomp filter +// enabled for it. +func BenchmarkSyscallUnderSeccomp(b *testing.B) { + ctx := context.Background() + machine, err := harness.GetMachine() + if err != nil { + b.Fatalf("failed to get machine: %v", err) + } + defer machine.CleanUp() + + for _, tc := range []tools.Parameter{ + { + Name: "cacheable", + Value: "false", + }, + { + Name: "cacheable", + Value: "true", + }, + } { + name, err := tools.ParametersToName(tc) + if err != nil { + b.Fatalf("Failed to parse params: %v", err) + } + func() { + container := machine.GetContainer(ctx, b) + defer container.CleanUp(ctx) + if err := container.Spawn( + ctx, dockerutil.RunOpts{ + Image: "benchmarks/syscallbench", + }, + "sleep", "24h", + ); err != nil { + b.Fatalf("run failed with: %v", err) + } + b.Run(name, func(b *testing.B) { + cmd := []string{"syscallbench", "--syscall=1", fmt.Sprintf("--loops=%d", b.N)} + if tc.Value == "true" { + cmd = append(cmd, "--seccomp_cacheable") + } else { + cmd = append(cmd, "--seccomp_notcacheable") + } + b.ResetTimer() + out, err := container.Exec(ctx, dockerutil.ExecOpts{}, cmd...) + if err != nil { + b.Fatalf("failed to run syscallbench: %v, logs:%s", err, out) + } + }) + }() + } } diff --git a/test/syscalls/linux/seccomp.cc b/test/syscalls/linux/seccomp.cc index 35b2e6a19..5ba16a4a6 100644 --- a/test/syscalls/linux/seccomp.cc +++ b/test/syscalls/linux/seccomp.cc @@ -105,6 +105,49 @@ void ApplySeccompFilter(uint32_t sysno, uint32_t filtered_result, MaybeSave(); } +// ApplyUncacheableFilter adds a no-op filter which reads one of the +// syscall arguments when queried about `sysno`, and returns ALLOW. +// This purposefully breaks the Linux seccomp cache. +void ApplyUncacheableFilter(uint32_t sysno) { + // "Prior to [PR_SET_SECCOMP], the task must call prctl(PR_SET_NO_NEW_PRIVS, + // 1) or run with CAP_SYS_ADMIN privileges in its namespace." - + // Documentation/prctl/seccomp_filter.txt + // + // prctl(PR_SET_NO_NEW_PRIVS, 1) may be called repeatedly; calls after the + // first are no-ops. + TEST_PCHECK(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0); + MaybeSave(); + + struct sock_filter filter[] = { + // A = seccomp_data.arch + BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 4), +#if defined(__x86_64__) + // if (A != AUDIT_ARCH_X86_64) goto kill + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 4), +#elif defined(__aarch64__) + // if (A != AUDIT_ARCH_AARCH64) goto kill + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_AARCH64, 0, 4), +#else +#error "Unknown architecture" +#endif + // A = seccomp_data.nr + BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 0), + // if (A != sysno) goto end + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, sysno, 0, 1), + // A = seccomp_data.args[0] + BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 16), + // end: return SECCOMP_RET_ALLOW + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), + // kill: return SECCOMP_RET_KILL + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL), + }; + struct sock_fprog prog; + prog.len = ABSL_ARRAYSIZE(filter); + prog.filter = filter; + TEST_PCHECK(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog, 0, 0) == 0); + MaybeSave(); +} + // Wrapper for sigaction. Async-signal-safe. void RegisterSignalHandler(int signum, void (*handler)(int, siginfo_t*, void*)) { @@ -311,6 +354,21 @@ TEST(SeccompTest, RetAllowAllowsSyscall) { << "status " << status; } +TEST(SeccompTest, RetAllowAllowsNonCachableSyscall) { + pid_t const pid = fork(); + if (pid == 0) { + ApplySeccompFilter(kFilteredSyscall, SECCOMP_RET_ALLOW); + ApplyUncacheableFilter(kFilteredSyscall); + TEST_CHECK(syscall(kFilteredSyscall) == -1 && errno == ENOSYS); + _exit(0); + } + ASSERT_THAT(pid, SyscallSucceeds()); + int status; + ASSERT_THAT(waitpid(pid, &status, 0), SyscallSucceedsWithValue(pid)); + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0) + << "status " << status; +} + // This test will validate that TSYNC will apply to all threads. TEST(SeccompTest, TsyncAppliesToAllThreads) { Mapping stack = ASSERT_NO_ERRNO_AND_VALUE(