The Pedigree Project 0.1
sandbox-state.cc
1/* Copyright (c) 2026, Pedigree Developers. */
2#include "pedigree/kernel/process/Process.h"
3#include "pedigree/kernel/process/TerminationDeferral.h"
4#include "pedigree/kernel/process/Thread.h"
5#include "pedigree/kernel/processor/Processor.h"
6#include "pedigree/kernel/processor/ProcessorInformation.h"
7#include "pedigree/kernel/processor/SyscallManager.h"
8#include "pedigree/kernel/syscallError.h"
9#include "pedigree/kernel/utilities/Pointers.h"
10
11#include <signal.h>
12
13#include "PosixSubsystem.h"
14#include "ipc-namespace.h"
15#include "landlock.h"
16#include "network-namespace.h"
17#include "sandbox-state.h"
18#include "seccomp-filter.h"
19#include "syscalls/translate.h"
20#include "user-namespace.h"
21
22namespace {
23constexpr uint32_t Allow = 0x7fff0000;
24constexpr uint32_t Errno = 0x00050000;
25constexpr uint32_t KillThread = 0;
26constexpr uint32_t ActionMask = 0xffff0000;
27constexpr size_t MaximumFilterPath = 32768;
28constexpr size_t MaximumFilters = 64;
29
30struct SeccompProgram {
32 size_t length = 0;
33 size_t totalLength = 0;
34 size_t depth = 0;
36};
37
38class SandboxState final : public Thread::SecurityState {
39 public:
40 bool noNewPrivileges = false;
44 SharedPointer<IpcNamespace> ipcNamespace;
45 NetworkNamespaceRef networkNamespace;
46 bool interceptSyscall(SyscallState& state, uintptr_t& result) const override;
47};
48
49Thread& current() {
50 return *Processor::information().getCurrentThread();
51}
52
53Thread::SecurityStateRef snapshot() {
54 return current().securityState();
55}
56
57Thread::SecurityStateRef copyState(const Thread::SecurityStateRef& previous) {
58 auto* replacement = new SandboxState;
59 if (!replacement) {
60 SYSCALL_ERROR(OutOfMemory);
61 return {};
62 }
63 if (previous) {
64 *replacement = *static_cast<SandboxState*>(previous.get());
65 }
66 auto owner = Thread::SecurityStateRef::tryAdopt(replacement);
67 if (!owner) {
68 SYSCALL_ERROR(OutOfMemory);
69 }
70 return owner;
71}
72
73uint32_t architecture() {
74#if ARM64
75 return 0xc00000b7;
76#elif ARMV7
77 return 0x40000028;
78#else
79 // Personality affects uname, not the x86-64 syscall entry convention.
80 return 0xc000003e;
81#endif
82}
83
84bool restrictedOperation(uint64_t number) {
85 switch (posix_translate_syscall(number)) {
86 case POSIX_MOUNT:
87 case POSIX_UMOUNT2:
88 case POSIX_PIVOT_ROOT:
89 case POSIX_PTRACE:
90 case POSIX_PROCESS_VM_READV:
91 case POSIX_PROCESS_VM_WRITEV:
92 case POSIX_OPEN_BY_HANDLE_AT:
93 case POSIX_INIT_MODULE:
94 case POSIX_DELETE_MODULE:
95 case POSIX_SWAPON:
96 case POSIX_SWAPOFF:
97 case POSIX_REBOOT:
98 case POSIX_ACCT:
99 case POSIX_QUOTACTL:
100 return true;
101 default:
102 return false;
103 }
104}
105
106bool SandboxState::interceptSyscall(SyscallState& state, uintptr_t& result) const {
107 if (!filters && !domain) {
108 return false;
109 }
110 // The Linux filter is a policy over Linux numbers and argument layouts.
111 // Other services must not provide a second, unfiltered way to perform I/O.
112 if (state.getSyscallService() != linuxCompat) {
113 result = uintptr_t(-1);
114 SYSCALL_ERROR(NotEnoughPermissions);
115 return true;
116 }
117 uint32_t decision = Allow;
118 if (filters) {
119 PosixSeccomp::Data data = {};
120 data.nr = static_cast<int32_t>(state.getSyscallNumber());
121 data.arch = architecture();
122 data.instructionPointer = state.getInstructionPointer();
123 for (size_t i = 0; i < 6; ++i) {
124 data.args[i] = state.getSyscallParameter(6 + i);
125 }
126 for (auto filter = filters; filter; filter = filter->previous) {
127 const uint32_t value =
128 PosixSeccomp::evaluate(filter->instructions.get(), filter->length, data);
129 // Linux action precedence is the signed ordering of the action word.
130 if (static_cast<int32_t>(value & ActionMask) < static_cast<int32_t>(decision & ActionMask)) {
131 decision = value;
132 }
133 }
134 }
135 if ((decision & ActionMask) == Allow) {
136 if (!domain || !restrictedOperation(state.getSyscallNumber())) {
137 return false;
138 }
139 SYSCALL_ERROR(NotEnoughPermissions);
140 result = uintptr_t(-1);
141 return true;
142 }
143 if ((decision & ActionMask) == Errno) {
144 const unsigned int error = decision & 0xffff;
145 current().setErrno(error > 4095 ? 4095 : error);
146 result = error ? uintptr_t(-1) : 0;
147 return true;
148 }
149 // Unsupported actions fail closed. No listener/tracer action is advertised.
150 if ((decision & ActionMask) == KillThread && !current().getParent()->prepareThreadExit()) {
151 if (!SyscallManager::instance().requestThreadExit()) {
152 FATAL("seccomp thread exit could not be dispatched");
153 }
154 } else {
155 current().deferSignalExit(SIGSYS);
156 }
157 current().setErrno(0);
158 result = 0;
159 return true;
160}
161} // namespace
162
163bool posix_no_new_privs() {
164 auto owner = snapshot();
165 return owner && static_cast<SandboxState*>(owner.get())->noNewPrivileges;
166}
167
168int posix_set_no_new_privs() {
169 TerminationDeferral lifetime;
170 auto old = snapshot();
171 if (old && static_cast<SandboxState*>(old.get())->noNewPrivileges) {
172 current().setErrno(0);
173 return 0;
174 }
175 auto next = copyState(old);
176 if (!next) {
177 return -1;
178 }
179 static_cast<SandboxState*>(next.get())->noNewPrivileges = true;
180 current().setSecurityState(next);
181 current().setErrno(0);
182 return 0;
183}
184
185int posix_seccomp_mode() {
186 auto owner = snapshot();
187 current().setErrno(0);
188 return owner && static_cast<SandboxState*>(owner.get())->filters ? 2 : 0;
189}
190
191void posix_sandbox_inherit(Thread& child, Thread& parent) {
192 child.setSecurityState(parent.securityState());
193}
194
195bool posix_sandbox_prepare_namespaces(Thread& source,
196 const SharedPointer<PosixTaskCredentials>& credentials,
198 const NetworkNamespaceRef& network,
199 Thread::SecurityStateRef& prepared) {
200 auto previous = source.securityState();
201 if (!credentials && !ipc && !network) {
202 prepared = previous;
203 return true;
204 }
205 auto state = copyState(previous);
206 if (!state) {
207 return false;
208 }
209 auto* replacement = static_cast<SandboxState*>(state.get());
210 if (credentials) {
211 replacement->credentials = credentials;
212 }
213 if (ipc) {
214 replacement->ipcNamespace = ipc;
215 }
216 if (network) {
217 replacement->networkNamespace = network;
218 }
219 prepared = state;
220 return true;
221}
222
223SharedPointer<PosixTaskCredentials> posix_sandbox_credentials(Thread& thread) {
224 auto state = thread.securityState();
225 return state ? static_cast<SandboxState*>(state.get())->credentials
227}
228
229bool posix_sandbox_set_credentials(Thread& thread,
230 const SharedPointer<PosixTaskCredentials>& credentials) {
231 TerminationDeferral lifetime;
232 auto state = copyState(thread.securityState());
233 if (!state) {
234 return false;
235 }
236 static_cast<SandboxState*>(state.get())->credentials = credentials;
237 thread.setSecurityState(state);
238 return true;
239}
240
241NetworkNamespaceRef posix_sandbox_network(Thread& thread) {
242 auto state = thread.securityState();
243 return state ? static_cast<SandboxState*>(state.get())->networkNamespace : NetworkNamespaceRef();
244}
245
246bool posix_sandbox_set_network(Thread& thread, const NetworkNamespaceRef& space) {
247 TerminationDeferral lifetime;
248 auto state = copyState(thread.securityState());
249 if (!state) {
250 return false;
251 }
252 static_cast<SandboxState*>(state.get())->networkNamespace = space;
253 thread.setSecurityState(state);
254 return true;
255}
256
257SharedPointer<IpcNamespace> posix_ipc_namespace(Thread& thread) {
258 auto state = thread.securityState();
259 return state ? static_cast<SandboxState*>(state.get())->ipcNamespace
261}
262
263bool posix_set_ipc_namespace(Thread& thread, const SharedPointer<IpcNamespace>& space) {
264 TerminationDeferral lifetime;
265 auto state = copyState(thread.securityState());
266 if (!state) {
267 return false;
268 }
269 static_cast<SandboxState*>(state.get())->ipcNamespace = space;
270 thread.setSecurityState(state);
271 return true;
272}
273
274SharedPointer<LandlockDomain> posix_sandbox_domain() {
275 auto owner = snapshot();
276 return owner ? static_cast<SandboxState*>(owner.get())->domain : SharedPointer<LandlockDomain>();
277}
278
279bool posix_sandbox_restrict(const SharedPointer<LandlockDomain>& domain) {
280 TerminationDeferral lifetime;
281 if (!domain || !posix_no_new_privs()) {
282 SYSCALL_ERROR(NotEnoughPermissions);
283 return false;
284 }
285 auto next = copyState(snapshot());
286 if (!next) {
287 return false;
288 }
289 static_cast<SandboxState*>(next.get())->domain = domain;
290 current().setSecurityState(next);
291 return true;
292}
293
294int posix_seccomp(unsigned int operation, unsigned int flags, const void* args) {
295 TerminationDeferral lifetime;
296 if (flags) {
297 // TSYNC and notification listeners need separate lifecycle support.
298 SYSCALL_ERROR(InvalidArgument);
299 return -1;
300 }
301 if (operation == 2) { // SECCOMP_GET_ACTION_AVAIL
302 uint32_t action = 0;
303 if (!PosixSubsystem::copyFromUser(&action, args, sizeof(action))) {
304 SYSCALL_ERROR(BadAddress);
305 return -1;
306 }
307 if (action != Allow && action != Errno && action != KillThread &&
308 action != PosixSeccomp::KillProcess) {
309 SYSCALL_ERROR(OperationNotSupported);
310 return -1;
311 }
312 current().setErrno(0);
313 return 0;
314 }
315 if (operation != 1) { // SECCOMP_SET_MODE_FILTER
316 SYSCALL_ERROR(InvalidArgument);
317 return -1;
318 }
319 if (!posix_no_new_privs()) {
320 SYSCALL_ERROR(PermissionDenied);
321 return -1;
322 }
323 struct Program {
324 uint16_t length;
325 const PosixSeccomp::Instruction* instructions;
326 } program = {};
327 if (!PosixSubsystem::copyFromUser(&program, args, sizeof(program))) {
328 SYSCALL_ERROR(BadAddress);
329 return -1;
330 }
331 if (!program.length || program.length > PosixSeccomp::MaximumInstructions) {
332 SYSCALL_ERROR(InvalidArgument);
333 return -1;
334 }
335 auto old = snapshot();
336 auto previous = static_cast<SandboxState*>(old.get())->filters;
337 const size_t total = program.length + (previous ? previous->totalLength + 4 : 0);
338 const size_t depth = previous ? previous->depth + 1 : 1;
339 if (total > MaximumFilterPath || depth > MaximumFilters) {
340 SYSCALL_ERROR(OutOfMemory);
341 return -1;
342 }
344 if (!filter) {
345 SYSCALL_ERROR(OutOfMemory);
346 return -1;
347 }
348 filter->instructions = UniqueArray<PosixSeccomp::Instruction>::allocate(program.length);
349 if (!filter->instructions) {
350 SYSCALL_ERROR(OutOfMemory);
351 return -1;
352 }
353 if (!PosixSubsystem::copyFromUser(filter->instructions.get(), program.instructions,
354 program.length * sizeof(PosixSeccomp::Instruction))) {
355 SYSCALL_ERROR(BadAddress);
356 return -1;
357 }
358 if (!PosixSeccomp::validate(filter->instructions.get(), program.length)) {
359 SYSCALL_ERROR(InvalidArgument);
360 return -1;
361 }
362 filter->length = program.length;
363 filter->totalLength = total;
364 filter->depth = depth;
365 filter->previous = previous;
366 auto next = copyState(old);
367 if (!next) {
368 return -1;
369 }
370 static_cast<SandboxState*>(next.get())->filters = filter;
371 current().setSecurityState(next);
372 current().setErrno(0);
373 return 0;
374}
static bool copyFromUser(void *destination, const void *source, size_t count, size_t elementSize=1)
static ProcessorInformation & information()
static SharedPointer< SecurityState > tryAdopt(SecurityState *ptr)
static SharedPointer< T > tryAllocate(Args...)
T * get() const
static EXPORTED_PUBLIC SyscallManager & instance()