The Pedigree Project 0.1
landlock.cc
1/* Copyright (c) 2026, Pedigree Developers. */
2#include "landlock.h"
3#include "pedigree/kernel/LockGuard.h"
4#include "pedigree/kernel/process/TerminationDeferral.h"
5#include "pedigree/kernel/process/Thread.h"
6#include "pedigree/kernel/processor/Processor.h"
7#include "pedigree/kernel/processor/ProcessorInformation.h"
8#include "pedigree/kernel/syscallError.h"
9
10#include <fcntl.h>
11
12#include "FileDescriptor.h"
13#include "PosixSubsystem.h"
14#include "memfd-file.h"
16#include "modules/system/vfs/MountView.h"
17#include "namespace-file.h"
18#include "sandbox-state.h"
19
20namespace {
21constexpr size_t MaximumRules = 4096;
22constexpr size_t MaximumLayers = 16;
23constexpr int BadDescriptorState = 77;
24constexpr uint64_t FileRights = LandlockAccess::Execute | LandlockAccess::ReadFile |
25 LandlockAccess::WriteFile | LandlockAccess::Truncate;
26
27class LandlockResult {
28 public:
29 ~LandlockResult() {
30 syscallError(error);
31 }
32 int finish(int value) {
33 error = value < 0 ? Processor::information().getCurrentThread()->getErrno() : 0;
34 return value;
35 }
36
37 private:
38 TerminationDeferral lifetime;
39 size_t error = 0;
40};
41
42PosixSubsystem* subsystem() {
43 auto* process = Processor::information().getCurrentThread()->getParent();
44 return process->getType() == Process::Posix
45 ? static_cast<PosixSubsystem*>(process->getSubsystem())
46 : nullptr;
47}
48
49class RulesetFilesystem final : public RamFs {
50 public:
51 RulesetFilesystem() {
52 setProcessOwnership(false);
53 }
54};
55RulesetFilesystem rulesetFilesystem;
56
57class RulesetFile final : public File {
58 public:
59 explicit RulesetFile(uint64_t requested)
60 : File(String("landlock-ruleset"), 0, 0, 0, 0, &rulesetFilesystem, 0, nullptr),
61 handled(requested) {}
62 Mutex lock;
64 const uint64_t handled;
65
66 bool isSeekable() const override {
67 return false;
68 }
69 bool allowMapping(bool, bool, bool&) override {
70 SYSCALL_ERROR(NoSuchDevice);
71 return false;
72 }
73
74 protected:
75 bool allowResize(size_t, size_t) override {
76 SYSCALL_ERROR(InvalidArgument);
77 return false;
78 }
79 bool isBytewise() const override {
80 return true;
81 }
82 uint64_t readBytewise(uint64_t, uint64_t, uintptr_t, bool) override {
83 SYSCALL_ERROR(InvalidArgument);
84 return 0;
85 }
86 uint64_t writeBytewise(uint64_t, uint64_t, uintptr_t, bool) override {
87 SYSCALL_ERROR(InvalidArgument);
88 return 0;
89 }
90};
91
92RulesetFile* acquireRuleset(int fd, DescriptorLease& descriptor) {
93 auto* current = subsystem();
94 if (!current || fd < 0 || !current->acquireFileDescriptor(fd, descriptor)) {
95 SYSCALL_ERROR(BadFileDescriptor);
96 return nullptr;
97 }
98 auto* file = descriptor->getFile();
99 if (!file || file->getFilesystem() != &rulesetFilesystem) {
100 syscallError(BadDescriptorState);
101 return nullptr;
102 }
103 return static_cast<RulesetFile*>(file);
104}
105
106uint64_t inodeIdentity(File* file) {
107 const auto attributes = file->getAttributes();
108 return attributes.inode ? attributes.inode : file->getInode();
109}
110
111bool sameObject(const LandlockRule& rule, const FilesystemPathRef& path) {
112 if (!rule.anchor || !path) {
113 return false;
114 }
115 File* candidate = path->node();
116 File* anchor = rule.anchor->node();
117 return candidate == anchor || (candidate->getFilesystem() == anchor->getFilesystem() &&
118 rule.inode && inodeIdentity(candidate) == rule.inode);
119}
120} // namespace
121
122uint64_t LandlockDomain::layerAccess(const FilesystemPathRef* ancestry, size_t count) const {
123 uint64_t allowed = ~handled;
124 for (size_t i = 0; i < count; ++i) {
125 for (const auto& rule : rules) {
126 if (sameObject(rule, ancestry[i])) {
127 allowed |= rule.access;
128 }
129 }
130 }
131 return allowed;
132}
133
134bool PosixSubsystem::filesystemConstrained() const {
135 return static_cast<bool>(posix_sandbox_domain());
136}
137
138uint64_t PosixSubsystem::filesystemAccess(const FilesystemPathRef* ancestry, size_t count) const {
139 auto* view = count ? VfsMountView::fromPath(ancestry[0]) : nullptr;
140 if (count == 1 && ancestry[0] && view && !view->attachmentId(ancestry[0])) {
141 File* file = ancestry[0]->node();
142 UtsRef space;
143 if (MemFdFile::fromFile(file) || (file->isPipe() && !file->isFifo()) || file->isSocket() ||
144 file->getFilesystem() == &rulesetFilesystem || posix_uts_file_namespace(file, space)) {
145 return ~uint64_t(0);
146 }
147 return 0;
148 }
149 uint64_t allowed = ~uint64_t(0);
150 for (auto domain = posix_sandbox_domain(); domain; domain = domain->previous) {
151 allowed &= domain->layerAccess(ancestry, count);
152 }
153 return allowed;
154}
155
156bool PosixSubsystem::filesystemReparent(const FilesystemPathRef* source, size_t sourceCount,
157 const FilesystemPathRef* destination,
158 size_t destinationCount) const {
159 for (auto domain = posix_sandbox_domain(); domain; domain = domain->previous) {
160 const uint64_t before = domain->layerAccess(source, sourceCount);
161 const uint64_t after = domain->layerAccess(destination, destinationCount);
162 if (after & ~before & LandlockAccess::All) {
163 return false;
164 }
165 }
166 return true;
167}
168
169bool posix_landlock_check(const FilesystemPathRef& path, uint64_t requested) {
170 if (!posix_sandbox_domain()) {
171 return true;
172 }
173 auto* view = VfsMountView::fromPath(path);
174 if (!view || !path) {
175 SYSCALL_ERROR(PermissionDenied);
176 return false;
177 }
178 return view->checkFilesystemAccess(path, requested);
179}
180
181bool posix_landlock_open(const FilesystemPathRef& path, int flags, bool& allowTruncate) {
182 allowTruncate = true;
183 if ((flags & O_PATH) || !posix_sandbox_domain()) {
184 return true;
185 }
186 auto* view = VfsMountView::fromPath(path);
187 if (!path || !view) {
188 SYSCALL_ERROR(PermissionDenied);
189 return false;
190 }
191 const uint64_t allowed = view->filesystemAccess(path);
192 allowTruncate = allowed & LandlockAccess::Truncate;
193 File* file = path->node();
194 uint64_t required = 0;
195 if (file->isDirectory()) {
196 required = LandlockAccess::ReadDir;
197 } else {
198 if ((flags & O_ACCMODE) != O_WRONLY) {
199 required |= LandlockAccess::ReadFile;
200 }
201 if ((flags & O_ACCMODE) != O_RDONLY) {
202 required |= LandlockAccess::WriteFile;
203 }
204 if ((flags & O_TRUNC) && file->supportsRegularFileOperations()) {
205 required |= LandlockAccess::Truncate;
206 }
207 }
208 if ((allowed & required) != required) {
209 SYSCALL_ERROR(PermissionDenied);
210 return false;
211 }
212 return true;
213}
214
215int posix_landlock_create_ruleset(const void* attributes, size_t size, unsigned flags) {
216 LandlockResult result;
217 if (flags) {
218 if (flags != 1 || attributes || size) {
219 SYSCALL_ERROR(InvalidArgument);
220 return result.finish(-1);
221 }
222 return result.finish(3);
223 }
224 if (!attributes) {
225 SYSCALL_ERROR(BadAddress);
226 return result.finish(-1);
227 }
228 if (size < sizeof(uint64_t) || size > 4096) {
229 syscallError(size > 4096 ? Error::TooBig : Error::InvalidArgument);
230 return result.finish(-1);
231 }
232 uint64_t handled;
233 if (!PosixSubsystem::copyFromUser(&handled, attributes, sizeof(handled))) {
234 SYSCALL_ERROR(BadAddress);
235 return result.finish(-1);
236 }
237 uint8_t bytes[64];
238 for (size_t offset = sizeof(handled); offset < size;) {
239 const size_t amount = size - offset < sizeof(bytes) ? size - offset : sizeof(bytes);
240 const uintptr_t base = reinterpret_cast<uintptr_t>(attributes);
241 if (base > ~uintptr_t(0) - offset ||
242 !PosixSubsystem::copyFromUser(bytes, reinterpret_cast<const void*>(base + offset),
243 amount)) {
244 SYSCALL_ERROR(BadAddress);
245 return result.finish(-1);
246 }
247 for (size_t i = 0; i < amount; ++i) {
248 if (bytes[i]) {
249 SYSCALL_ERROR(TooBig);
250 return result.finish(-1);
251 }
252 }
253 offset += amount;
254 }
255 if (!handled || (handled & ~LandlockAccess::All)) {
256 syscallError(handled ? Error::InvalidArgument : Error::NoMessage);
257 return result.finish(-1);
258 }
259 auto* current = subsystem();
260 auto* file = current ? new RulesetFile(handled) : nullptr;
261 if (!file || !VFS::instance().tryTrackFile(file)) {
262 delete file;
263 SYSCALL_ERROR(OutOfMemory);
264 return result.finish(-1);
265 }
266 RetainedFile retained;
267 retained.adopt(file);
268 auto* descriptor = new FileDescriptor(file, 0, 0xffffffff, FD_CLOEXEC, O_RDWR);
269 if (!descriptor || !descriptor->acquireOpenFileDescription()) {
270 delete descriptor;
271 SYSCALL_ERROR(OutOfMemory);
272 return result.finish(-1);
273 }
274 DescriptorLease published;
275 return result.finish(static_cast<int>(current->installFileDescriptor(descriptor, published)));
276}
277
278int posix_landlock_add_rule(int ruleset, int type, const void* attributes, unsigned flags) {
279 LandlockResult result;
280 if (flags || type != 1) {
281 SYSCALL_ERROR(InvalidArgument);
282 return result.finish(-1);
283 }
284 DescriptorLease rulesetDescriptor;
285 auto* file = acquireRuleset(ruleset, rulesetDescriptor);
286 if (!file) {
287 return result.finish(-1);
288 }
289 struct __attribute__((packed)) PathRule {
290 uint64_t allowed;
291 int32_t parentFd;
292 } input;
293 static_assert(sizeof(input) == 12, "Linux Landlock path rule ABI");
294 if (!PosixSubsystem::copyFromUser(&input, attributes, sizeof(input))) {
295 SYSCALL_ERROR(BadAddress);
296 return result.finish(-1);
297 }
298 if (!input.allowed || (input.allowed & ~file->handled)) {
299 syscallError(input.allowed ? Error::InvalidArgument : Error::NoMessage);
300 return result.finish(-1);
301 }
302 DescriptorLease parent;
303 if (input.parentFd < 0 || !subsystem()->acquireFileDescriptor(input.parentFd, parent)) {
304 SYSCALL_ERROR(BadFileDescriptor);
305 return result.finish(-1);
306 }
307 auto anchor = parent->openingPath();
308 auto* view = VfsMountView::fromPath(anchor);
309 if (!anchor || !view || !view->attachmentId(anchor) || anchor->node()->isSymlink()) {
310 syscallError(BadDescriptorState);
311 return result.finish(-1);
312 }
313 if (!anchor->node()->isDirectory() && (input.allowed & ~FileRights)) {
314 SYSCALL_ERROR(InvalidArgument);
315 return result.finish(-1);
316 }
317 LandlockRule rule{anchor, inodeIdentity(anchor->node()), input.allowed};
318 LockGuard<Mutex> guard(file->lock);
319 for (auto& existing : file->rules) {
320 if (sameObject(existing, anchor)) {
321 existing.access |= input.allowed;
322 return result.finish(0);
323 }
324 }
325 if (file->rules.count() >= MaximumRules || !file->rules.tryReserve(file->rules.count() + 1)) {
326 SYSCALL_ERROR(OutOfMemory);
327 return result.finish(-1);
328 }
329 file->rules.pushBack(pedigree_std::move(rule));
330 return result.finish(0);
331}
332
333int posix_landlock_restrict_self(int ruleset, unsigned flags) {
334 LandlockResult result;
335 if (flags) {
336 SYSCALL_ERROR(InvalidArgument);
337 return result.finish(-1);
338 }
339 if (!posix_no_new_privs()) {
340 SYSCALL_ERROR(NotEnoughPermissions);
341 return result.finish(-1);
342 }
343 DescriptorLease descriptor;
344 auto* file = acquireRuleset(ruleset, descriptor);
345 if (!file) {
346 return result.finish(-1);
347 }
348 auto previous = posix_sandbox_domain();
349 if (previous && previous->depth >= MaximumLayers) {
350 SYSCALL_ERROR(TooBig);
351 return result.finish(-1);
352 }
354 if (!domain) {
355 SYSCALL_ERROR(OutOfMemory);
356 return result.finish(-1);
357 }
358 domain->previous = previous;
359 domain->depth = previous ? previous->depth + 1 : 1;
360 // REFER is always denied unless explicitly granted, including ABI-1 rulesets.
361 domain->handled = file->handled | LandlockAccess::Refer;
362 {
363 LockGuard<Mutex> guard(file->lock);
364 if (!domain->rules.tryReserve(file->rules.count())) {
365 SYSCALL_ERROR(OutOfMemory);
366 return result.finish(-1);
367 }
368 for (const auto& rule : file->rules) {
369 domain->rules.pushBack(rule);
370 }
371 }
372 return result.finish(posix_sandbox_restrict(domain) ? 0 : -1);
373}
An in-RAM filesystem.
OpenFileDescriptionLease acquireOpenFileDescription() const
Definition File.h:75
virtual uint64_t readBytewise(uint64_t location, uint64_t size, uintptr_t buffer, bool bCanBlock=true)
Definition File.cc:1369
virtual bool isSeekable() const
Definition File.cc:820
bool supportsRegularFileOperations()
Definition File.cc:824
virtual bool isSocket() const
Definition File.cc:816
virtual bool isSymlink()
Definition File.cc:800
virtual bool isBytewise() const
Definition File.cc:1365
virtual bool isDirectory()
Definition File.cc:804
virtual bool isFifo() const
Definition File.cc:812
virtual bool isPipe() const
Definition File.cc:808
virtual bool allowMapping(bool shared, bool writeRequested, bool &mayWrite)
Definition File.cc:1177
virtual uint64_t writeBytewise(uint64_t location, uint64_t size, uintptr_t buffer, bool bCanBlock=true)
Definition File.cc:1376
Definition Mutex.h:56
static bool copyFromUser(void *destination, const void *source, size_t count, size_t elementSize=1)
Process * getParent()
Definition Process.h:620
static ProcessorInformation & information()
Definition RamFs.h:118
static SharedPointer< T > tryAdopt(T *ptr)
static VFS & instance()
Definition VFS.cc:311
A vector / dynamic array.
Definition Vector.h:33