| ... | ... | @@ -0,0 +1,212 @@ |
| 1 | //! API bits for the Secure Computing facility in the Linux kernel, which allows |
| 2 | //! processes to restrict access to the system call API. |
| 3 | //! |
| 4 | //! Seccomp started life with a single "strict" mode, which only allowed calls |
| 5 | //! to read(2), write(2), _exit(2) and sigreturn(2). It turns out that this |
| 6 | //! isn't that useful for general-purpose applications, and so a mode that |
| 7 | //! utilizes user-supplied filters mode was added. |
| 8 | //! |
| 9 | //! Seccomp filters are classic BPF programs, which means that all the |
| 10 | //! information under `std.x.net.bpf` applies here as well. Conceptually, a |
| 11 | //! seccomp program is attached to the kernel and is executed on each syscall. |
| 12 | //! The "packet" being validated is the `data` structure, and the verdict is an |
| 13 | //! action that the kernel performs on the calling process. The actions are |
| 14 | //! variations on a "pass" or "fail" result, where a pass allows the syscall to |
| 15 | //! continue and a fail blocks the syscall and returns some sort of error value. |
| 16 | //! See the full list of actions under ::RET for more information. Finally, only |
| 17 | //! word-sized, absolute loads (`ld [k]`) are supported to read from the `data` |
| 18 | //! structure. |
| 19 | //! |
| 20 | //! There are some issues with the filter API that have traditionally made |
| 21 | //! writing them a pain: |
| 22 | //! |
| 23 | //! 1. Each CPU architecture supported by Linux has its own unique ABI and |
| 24 | //! syscall API. It is not guaranteed that the syscall numbers and arguments |
| 25 | //! are the same across architectures, or that they're even implemted. Thus, |
| 26 | //! filters cannot be assumed to be portable without consulting documentation |
| 27 | //! like syscalls(2) and testing on target hardware. This also requires |
| 28 | //! checking the value of `data.arch` to make sure that a filter was compiled |
| 29 | //! for the correct architecture. |
| 30 | //! 2. Many syscalls take an `unsigned long` or `size_t` argument, the size of |
| 31 | //! which is dependant on the ABI. Since BPF programs execute in a 32-bit |
| 32 | //! machine, validation of 64-bit arguments necessitates two load-and-compare |
| 33 | //! instructions for the upper and lower words. |
| 34 | //! 3. A further wrinkle to the above is endianess. Unlike network packets, |
| 35 | //! syscall data shares the endianess of the target machine. A filter |
| 36 | //! compiled on a little-endian machine will not work on a big-endian one, |
| 37 | //! and vice-versa. For example: Checking the upper 32-bits of `data.arg1` |
| 38 | //! requires a load at `@offsetOf(data, "arg1") + 4` on big-endian systems |
| 39 | //! and `@offsetOf(data, "arg1")` on little-endian systems. Endian-portable |
| 40 | //! filters require adjusting these offsets at compile time, similar to how |
| 41 | //! e.g. OpenSSH does[1]. |
| 42 | //! 4. Syscalls with userspace implementations via the vDSO cannot be traced or |
| 43 | //! filtered. The vDSO can be disabled or just ignored, which must be taken |
| 44 | //! into account when writing filters. |
| 45 | //! 5. Software libraries - especially dynamically loaded ones - tend to use |
| 46 | //! more of the syscall API over time, thus filters must evolve with them. |
| 47 | //! Static filters can result in reduced or even broken functionality when |
| 48 | //! calling newer code from these libraries. This is known to happen with |
| 49 | //! critical libraries like glibc[2]. |
| 50 | //! |
| 51 | //! Some of these issues can be mitigated with help from Zig and the standard |
| 52 | //! library. Since the target CPU is known at compile time, the proper syscall |
| 53 | //! numbers are mixed into the `os` namespace under `std.os.SYS (see the code |
| 54 | //! for `arch_bits` in `os/linux.zig`). Referencing an unimplemented syscall |
| 55 | //! would be a compile error. Endian offsets can also be defined in a similar |
| 56 | //! manner to the OpenSSH example: |
| 57 | //! |
| 58 | //! ```zig |
| 59 | //! const offset = if (native_endian == .Little) struct { |
| 60 | //! pub const low = 0; |
| 61 | //! pub const high = @sizeOf(u32); |
| 62 | //! } else struct { |
| 63 | //! pub const low = @sizeOf(u32); |
| 64 | //! pub const high = 0; |
| 65 | //! }; |
| 66 | //! ``` |
| 67 | //! |
| 68 | //! Unfortunately, there is no easy solution for issue 5. The most reliable |
| 69 | //! strategy is to keep testing; test newer Zig versions, different libcs, |
| 70 | //! different distros, and design your filter to accomidate all of them. |
| 71 | //! Alternatively, you could inject a filter at runtime. Since filters are |
| 72 | //! preserved across execve(2), a filter could be setup before executing your |
| 73 | //! program, without your program having any knowledge of this happening. This |
| 74 | //! is the method used by systemd[3] and Cloudflare's sandbox library[4]. |
| 75 | //! |
| 76 | //! [1]: https://github.com/openssh/openssh-portable/blob/master/sandbox-seccomp-filter.c#L81 |
| 77 | //! [2]: https://sourceware.org/legacy-ml/libc-alpha/2017-11/msg00246.html |
| 78 | //! [3]: https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter= |
| 79 | //! [4]: https://github.com/cloudflare/sandbox |
| 80 | //! |
| 81 | //! See Also |
| 82 | //! - seccomp(2), seccomp_unotify(2) |
| 83 | //! - https://www.kernel.org/doc/html/latest/userspace-api/seccomp_filter.html |
| 84 | const IOCTL = @import("ioctl.zig"); |
| 85 | |
| 86 | // Modes for the prctl(2) form `prctl(PR_SET_SECCOMP, mode)` |
| 87 | pub const MODE = struct { |
| 88 | /// Seccomp not in use. |
| 89 | pub const DISABLED = 0; |
| 90 | /// Uses a hard-coded filter. |
| 91 | pub const STRICT = 1; |
| 92 | /// Uses a user-supplied filter. |
| 93 | pub const FILTER = 2; |
| 94 | }; |
| 95 | |
| 96 | // Operations for the seccomp(2) form `seccomp(operation, flags, args)` |
| 97 | pub const SET_MODE_STRICT = 0; |
| 98 | pub const SET_MODE_FILTER = 1; |
| 99 | pub const GET_ACTION_AVAIL = 2; |
| 100 | pub const GET_NOTIF_SIZES = 3; |
| 101 | |
| 102 | /// Bitflags for the SET_MODE_FILTER operation. |
| 103 | pub const FILTER_FLAG = struct { |
| 104 | pub const TSYNC = 1 << 0; |
| 105 | pub const LOG = 1 << 1; |
| 106 | pub const SPEC_ALLOW = 1 << 2; |
| 107 | pub const NEW_LISTENER = 1 << 3; |
| 108 | pub const TSYNC_ESRCH = 1 << 4; |
| 109 | }; |
| 110 | |
| 111 | /// Action values for seccomp BPF programs. |
| 112 | /// The lower 16-bits are for optional return data. |
| 113 | /// The upper 16-bits are ordered from least permissive values to most. |
| 114 | pub const RET = struct { |
| 115 | /// Kill the process. |
| 116 | pub const KILL_PROCESS = 0x80000000; |
| 117 | /// Kill the thread. |
| 118 | pub const KILL_THREAD = 0x00000000; |
| 119 | pub const KILL = KILL_THREAD; |
| 120 | /// Disallow and force a SIGSYS. |
| 121 | pub const TRAP = 0x00030000; |
| 122 | /// Return an errno. |
| 123 | pub const ERRNO = 0x00050000; |
| 124 | /// Forward the syscall to a userspace supervisor to make a decision. |
| 125 | pub const USER_NOTIF = 0x7fc00000; |
| 126 | /// Pass to a tracer or disallow. |
| 127 | pub const TRACE = 0x7ff00000; |
| 128 | /// Allow after logging. |
| 129 | pub const LOG = 0x7ffc0000; |
| 130 | /// Allow. |
| 131 | pub const ALLOW = 0x7fff0000; |
| 132 | |
| 133 | // Masks for the return value sections. |
| 134 | pub const ACTION_FULL = 0xffff0000; |
| 135 | pub const ACTION = 0x7fff0000; |
| 136 | pub const DATA = 0x0000ffff; |
| 137 | }; |
| 138 | |
| 139 | pub const IOCTL_NOTIF = struct { |
| 140 | pub const RECV = IOCTL.IOWR('!', 0, notif); |
| 141 | pub const SEND = IOCTL.IOWR('!', 1, notif_resp); |
| 142 | pub const ID_VALID = IOCTL.IOW('!', 2, u64); |
| 143 | pub const ADDFD = IOCTL.IOW('!', 3, notif_addfd); |
| 144 | }; |
| 145 | |
| 146 | /// Tells the kernel that the supervisor allows the syscall to continue. |
| 147 | pub const USER_NOTIF_FLAG_CONTINUE = 1 << 0; |
| 148 | |
| 149 | /// See seccomp_unotify(2). |
| 150 | pub const ADDFD_FLAG = struct { |
| 151 | pub const SETFD = 1 << 0; |
| 152 | pub const SEND = 1 << 1; |
| 153 | }; |
| 154 | |
| 155 | pub const data = extern struct { |
| 156 | /// The system call number. |
| 157 | nr: c_int, |
| 158 | /// The CPU architecture/system call convention. |
| 159 | /// One of the values defined in `std.os.linux.AUDIT`. |
| 160 | arch: u32, |
| 161 | instruction_pointer: u64, |
| 162 | arg0: u64, |
| 163 | arg1: u64, |
| 164 | arg2: u64, |
| 165 | arg3: u64, |
| 166 | arg4: u64, |
| 167 | arg5: u64, |
| 168 | }; |
| 169 | |
| 170 | /// Used with the ::GET_NOTIF_SIZES command to check if the kernel structures |
| 171 | /// have changed. |
| 172 | pub const notif_sizes = extern struct { |
| 173 | /// Size of ::notif. |
| 174 | notif: u16, |
| 175 | /// Size of ::resp. |
| 176 | notif_resp: u16, |
| 177 | /// Size of ::data. |
| 178 | data: u16, |
| 179 | }; |
| 180 | |
| 181 | pub const notif = extern struct { |
| 182 | /// Unique notification cookie for each filter. |
| 183 | id: u64, |
| 184 | /// ID of the thread that triggered the notification. |
| 185 | pid: u32, |
| 186 | /// Bitmask for event information. Currently set to zero. |
| 187 | flags: u32, |
| 188 | /// The current system call data. |
| 189 | data: data, |
| 190 | }; |
| 191 | |
| 192 | /// The decision payload the supervisor process sends to the kernel. |
| 193 | pub const notif_resp = extern struct { |
| 194 | /// The filter cookie. |
| 195 | id: u64, |
| 196 | /// The return value for a spoofed syscall. |
| 197 | val: i64, |
| 198 | /// Set to zero for a spoofed success or a negative error number for a |
| 199 | /// failure. |
| 200 | @"error": i32, |
| 201 | /// Bitmask containing the decision. Either USER_NOTIF_FLAG_CONTINUE to |
| 202 | /// allow the syscall or zero to spoof the return values. |
| 203 | flags: u32, |
| 204 | }; |
| 205 | |
| 206 | pub const notif_addfd = extern struct { |
| 207 | id: u64, |
| 208 | flags: u32, |
| 209 | srcfd: u32, |
| 210 | newfd: u32, |
| 211 | newfd_flags: u32, |
| 212 | }; |