Yesterday I decided to finally try add seccomp sandbox to the SDL reference frontend.
The code for the seccomp filter setup itself is pretty simple:
fn installSeccompSandbox(mem: []const u8) !void {
const seccomp = @import("loader/seccomp.zig");
const cbpf = @import("loader/cbpf.zig");
const filter: []const cbpf.Insn = switch (comptime builtin.target.ptrBitWidth()) {
64 => D: {
const hi_start: u32 = @truncate(@intFromPtr(mem.ptr) >> 32);
const lo_start: u32 = @truncate(@intFromPtr(mem.ptr) & 0xFFFFFFFF);
const hi_end: u32 = @truncate((@intFromPtr(mem.ptr) + mem.len) >> 32);
const lo_end: u32 = @truncate((@intFromPtr(mem.ptr) + mem.len) & 0xFFFFFFFF);
break :D &.{
.ld_abs(seccomp.OFF.IP_HI),
.jge(hi_start, 1, 0), // if hi(IP) < hi(start) → allow
.ret(.allow),
.jgt(hi_end, 0, 1), // if hi(IP) > hi(end) → allow
.ret(.allow),
.ld_abs(seccomp.OFF.IP_LO),
.jge(lo_start, 1, 0), // if lo(IP) < lo(start) → allow
.ret(.allow),
.jgt(lo_end -| 1, 0, 1), // if lo(IP) >= lo(end) → allow
.ret(.allow),
.ret(.kill_process),
};
},
32 => D: {
const lo_start: u32 = @intFromPtr(mem.ptr);
const lo_end: u32 = @intFromPtr(mem.ptr) + mem.len;
break :D &.{
.ld_abs(seccomp.OFF.IP_LO),
.jge(lo_start, 1, 0), // if lo(IP) < lo(start) → allow
.ret(.allow),
.jgt(lo_end, 0, 1), // if lo(IP) > lo(end) → allow
.ret(.allow),
.ret(.kill_process),
};
},
else => return error.SandboxUnavailable,
};
log.info("installing seccomp-bpf sandbox", .{});
_ = try std.posix.prctl(std.os.linux.PR.SET_NO_NEW_PRIVS, .{ 1, 0, 0, 0 });
const prog: seccomp.Filter = .init(filter);
_ = try std.posix.prctl(std.os.linux.PR.SET_SECCOMP, .{ std.os.linux.SECCOMP.MODE.FILTER, @intFromPtr(&prog), 0, 0 });
}
We can test this by creating the following sorvi core:
const std = @import("std");
const sorvi = @import("sorvi");
const log = std.log.scoped(.minimal_example);
pub const std_options: std.Options = .{
.logFn = sorvi.defaultLog,
.queryPageSize = sorvi.queryPageSize,
.page_size_max = sorvi.page_size_max,
.log_level = .debug,
};
pub const os = sorvi.os;
pub const panic = std.debug.FullPanic(sorvi.defaultPanic);
comptime {
sorvi.init(@This(), .{
.id = "org.sorvi.example.minimal",
.name = "minimal-example",
.version = "0.0.0",
.core_extensions = &.{.core_v1},
.frontend_extensions = &.{.core_v1},
});
}
pub fn core_v1_init(_: *@This()) !void {
log.info("Hello world!", .{});
const msg = "SNEAKY SYSCALL\n";
_ = std.os.linux.write(1, msg.ptr, msg.len);
}
pub fn core_v1_deinit(self: *@This()) void {
self.* = undefined;
}
Running it indeed causes SIGSYS:
~/d/p/s/sorvi master• 5.7s | 1 ❱ /home/nix/.cache/zig/o/5eca55ed1681473313a2239ad46c1311/sorvi-frontend /home/nix/.cache/zig/o/f7a9873210e3dab748239ed02239af80/minimal.sorvi
info(libsorvi_loader): /home/nix/.cache/zig/o/f7a9873210e3dab748239ed02239af80/minimal.sorvi: loaded at address u8@22f1f000
info(libsorvi_loader): installing seccomp-bpf sandbox
info(sorvi_sdl): platform: Linux
info(sorvi_sdl): build time version: 3.4.4
info(sorvi_sdl): runtime version: 3.4.4
info(sorvi_sdl): runtime revision: SDL-3.4.4 (https://github.com/castholm/SDL 0.4.2)
info(sorvi_sdl): core id: org.sorvi.example.minimal
info(sorvi_sdl): core name: minimal-example
info(sorvi_sdl): core version: 0.0.0
info(sorvi_sdl): video drivers: wayland (current), x11, kmsdrm, offscreen, dummy, evdev
info(sorvi_sdl): audio drivers: pipewire, pulseaudio, alsa (current), sndio, jack, disk, dummy
info(minimal_example): Hello world!
fish: Job 1, '/home/nix/.cache/zig/o/5eca55ed…' terminated by signal SIGSYS (Bad system call)
Trusting the core instead allows it to go through:
/home/nix/.cache/zig/o/5eca55ed1681473313a2239ad46c1311/sorvi-frontend --trust /home/nix/.cache/zig/o/f7a9873210e3dab748239ed02239af80/minimal.sorvi
info(libsorvi_loader): /home/nix/.cache/zig/o/f7a9873210e3dab748239ed02239af80/minimal.sorvi: loaded at address u8@3f3c5000
info(sorvi_sdl): platform: Linux
info(sorvi_sdl): build time version: 3.4.4
info(sorvi_sdl): runtime version: 3.4.4
info(sorvi_sdl): runtime revision: SDL-3.4.4 (https://github.com/castholm/SDL 0.4.2)
info(sorvi_sdl): core id: org.sorvi.example.minimal
info(sorvi_sdl): core name: minimal-example
info(sorvi_sdl): core version: 0.0.0
info(sorvi_sdl): video drivers: wayland (current), x11, kmsdrm, offscreen, dummy, evdev
info(sorvi_sdl): audio drivers: pipewire, pulseaudio, alsa (current), sndio, jack, disk, dummy
info(minimal_example): Hello world!
SNEAKY SYSCALL
info(sorvi_sdl): core did not initialize any video context, exiting the core
My plan for other operating systems is to launch the frontend inside a debugger and communicate the loaded memory range to the debugger process which can then watch syscalls and abort if they come from the loaded memory range. This has bit of cost, but i think the default mode should be untrusted. On openbsd I don’t have to do anything because openbsd doesn’t allow syscalls from unknown call sites by default (what a great idea). Of course this is only one of the sandboxing layers, I also want the frontend to lock itself down so it only has access to places it needs. I’d imagine you could still do nasty things if you were able to write to the frontend’s memory space and let frontend call the syscall for you.
Goal is to avoid things like this from happening: