Logo

index : blog

---

  • summary
  • about
  • tree
  • log
  • branches
<< path: root/public/blog.git/html/src/search/liblandlock.jai blob: dc578ed7227eae952dcca6058933625f5a63e2c8 [raw] [clear marker]

        
0
1
2
3/** CAVE: Rulesets support only till ABI v7 !
4
5 Source
6 /usr/include/linux/landlock.h
7
8 Guide
9 https://docs.kernel.org/userspace-api/landlock.html
10
11 Example in C
12 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/samples/landlock/sandboxer.c
13
14
15 Very short example
16
17 rules := landlock_all_rules();
18 ok, ll_fd := landlock_create_ruleset(*rules);
19 assert(ok, "Create Ruleset");
20
21 ok = landlock_lock_privileges();
22 assert(ok, "Lock Privileges");
23
24 ok = landlock_restrict_self(ll_fd);
25 assert(ok, "Restrict Self");
26*/
27
28
29
30LL_Ruleset_Create :: enum_flags u64 {
31 NULL :: 0;
32 VERSION :: 1;
33
34 // CAVE: https://docs.kernel.org/userspace-api/landlock.html#landlock-errata
35 ERRATA;
36}
37
38LL_Restrict_Self :: enum_flags u64 {
39 LOG_SAME_EXEC_OFF :: 1;
40 LOG_NEW_EXEC_ON;
41 LOG_SUBDOMAINS_OFF;
42}
43
44landlock_rule_type :: enum #specified {
45 PATH_BENEATH :: 1;
46 NET_PORT :: 2;
47}
48
49/** Supports till ABI v7 */
50LL_Filesystem :: enum_flags u64 {
51 EXECUTE :: 1;
52 WRITE_FILE;
53 READ_FILE;
54 READ_DIR;
55 REMOVE_DIR;
56 REMOVE_FILE;
57 MAKE_CHAR;
58 MAKE_DIR;
59 MAKE_REG;
60 MAKE_SOCK;
61 MAKE_FIFO;
62 MAKE_BLOCK;
63 MAKE_SYM;
64 REFER; /** ABI v2+ */
65 TRUNCATE; /** ABI v3+ */
66 IOCTL_DEV; /** ABI v5+ */
67}
68
69LL_Network :: enum_flags u64 {
70 BIND_TCP :: 1;
71 CONNECT_TCP;
72}
73
74LL_Scoped :: enum_flags u64 {
75 ABSTRACT_UNIX_SOCKET :: 1;
76 SIGNAL;
77}
78
79
80landlock_ruleset_attr :: struct {
81 handled_access_fs: LL_Filesystem;
82 handled_access_net: LL_Network;
83 scoped: LL_Scoped;
84}
85
86landlock_path_beneath_attr :: struct {
87 allowed_access: LL_Filesystem;
88 parent_fd: s32;
89} #no_padding
90
91landlock_net_port_attr :: struct {
92 allowed_access: LL_Network;
93 port: u64;
94}
95
96
97/** https://man7.org/linux/man-pages/man2/landlock_create_ruleset.2.html
98
99 CAVE: Flags must be 0 if attr is used.
100 Otherwise flags can be set to: .VERSION
101
102 Quote from above man page:
103
104 If attr is NULL and size is 0, then the returned value is
105 the highest supported Landlock ABI version (starting at 1).
106 This version can be used for a best-effort security
107 approach, which is encouraged when user space is not pinned
108 to a specific kernel version. [...]
109
110*/
111landlock_create_ruleset :: (
112 ruleset: *landlock_ruleset_attr,
113 flags: LL_Ruleset_Create = .NULL,
114 loc := #caller_location
115)
116 -> (ok: bool, fd: int)
117{
118 assert(CPU == .X64 && OS == .LINUX, "Sorry, only support for x64 Linux!");
119
120 size := ifx ruleset == null then 0 else size_of(landlock_ruleset_attr);
121 fd := syscall(
122 SYS_LANDLOCK_CREATE_RULESET,
123 ruleset,
124 size,
125 cast(u64, flags)
126 );
127
128 if fd < 0 {
129 print_last_error(ERR_LL_CREATE_RULESET, loc);
130 return false, -1;
131 }
132
133 return true, fd;
134}
135
136
137/** https://man7.org/linux/man-pages/man2/landlock_restrict_self.2.html */
138landlock_restrict_self :: (ruleset_fd: int) -> ok: bool {
139 /** According to above man page, flags MUST be ZERO */
140 ok := syscall(SYS_LANDLOCK_RESTRICT_SELF, ruleset_fd, 0);
141 if ok < 0 {
142 print_last_error(ERR_LL_RESTRICT);
143 return false;
144 }
145 return true;
146}
147
148landlock_add_rule :: inline (
149 ruleset_fd: int,
150 rule_attr: *landlock_net_port_attr
151)
152 -> ok: bool
153{
154 return add_rule(ruleset_fd, .NET_PORT, rule_attr);
155}
156
157landlock_add_rule :: inline (
158 ruleset_fd: int,
159 rule_attr: *landlock_path_beneath_attr
160)
161 -> ok: bool
162{
163 return add_rule(ruleset_fd, .PATH_BENEATH, rule_attr);
164}
165
166
167/**
168 * The following procedures are non standard and added by me
169 */
170
171
172/** According to the man pages you must lock privileges
173 before applying any landlock rules.
174
175 https://man7.org/linux/man-pages/man7/landlock.7.html
176
177
178 'man prctl' on the return value:
179
180 On success, a nonnegative value is returned.
181 On error, -1 is returned, and errno is set to indicate the error.
182
183*/
184landlock_lock_privileges :: () -> ok: bool {
185 ok := prctl(PR_SET_NO_NEW_PRIVS, 1);
186 if ok < 0 {
187 print_last_error(ERR_PRCTL);
188 return false;
189 }
190 return true;
191}
192
193landlock_is_version_equal_or_higher :: (pinned_version: int, $print_version := false) -> bool {
194 ok, system_version := landlock_create_ruleset(null, .VERSION);
195 if !ok {
196 log_error("Could not determine version");
197 return false;
198 }
199
200 #if print_version then log("Landlock System Version: %", system_version);
201 return system_version >= pinned_version;
202}
203
204/** Returns _all_ available rules, essentially locking everything.
205
206 This might be useful if you want more a whitelist, rather than
207 a blacklist. Remove flags like this:
208
209 my_ruleset.handled_access_fs &= ~TRUNCATE;
210
211 Or if you want to remove items based on the supported ABI version.
212*/
213landlock_all_rules :: () -> landlock_ruleset_attr {
214 #insert -> string {
215 ll_fs := type_info(LL_Filesystem);
216 ll_net := type_info(LL_Network);
217 ll_scoped := type_info(LL_Scoped);
218
219 temp: [..]string;
220
221 sb: String_Builder;
222 append(*sb, "return .{ handled_access_fs = ");
223
224 for ll_fs.names array_add(*temp, tprint(".%", it));
225 fs_values := join(..temp, " | ");
226
227 append(*sb, fs_values);
228 append(*sb, ", handled_access_net = ");
229
230 array_reset(*temp);
231
232 for ll_net.names array_add(*temp, tprint(".%", it));
233 net_values := join(..temp, " | ");
234
235 append(*sb, net_values);
236 append(*sb, ", scoped = ");
237
238 array_reset(*temp);
239
240 for ll_scoped.names array_add(*temp, tprint(".%", it));
241 scoped_values := join(..temp, " | ");
242
243 append(*sb, scoped_values);
244 append(*sb, ", };");
245
246 code := builder_to_string(*sb);
247 return code;
248 };
249}
250
251
252#scope_file
253
254
255#import "POSIX";
256libc :: #library,system "libc";
257
258
259/** This assertion is important, since syscall numbers might shift when using
260 a different arch!
261*/
262#assert(CPU == .X64 && OS == .LINUX) "Only x64 Linux support!";
263
264
265/** 2026-04-23
266
267 Glibc didn't implented landlock functions yet, so we have to wrap
268 the syscalls ourselves.
269
270 Jai's Linux-faced stdlib does not have the syscall numbers for
271 the landlock functions, so we stole them from the Android lib.
272 Which are identical with x64 Linux anyway.
273
274*/
275
276SYS_LANDLOCK_CREATE_RULESET :: 444;
277SYS_LANDLOCK_ADD_RULE :: 445;
278SYS_LANDLOCK_RESTRICT_SELF :: 446;
279
280/** /usr/include/linux/prctl.h */
281PR_SET_NO_NEW_PRIVS :: 38;
282
283
284Error_Kind :: enum {
285 LANDLOCK;
286 PRCTL;
287}
288
289
290ERR_LL_CREATE_RULESET :: #code {
291 if err == {
292 case EOPNOTSUPP;
293 log_error("Landlock is supported by the kernel but disabled at boot time.");
294 case EINVAL;
295 log_error("Unknown flags, or unknown access, or too small size.");
296 case E2BIG;
297 log_error("size is too big.");
298 case EFAULT;
299 log_error("attr was not a valid address.");
300 case ENOMSG;
301 log_error("Empty accesses (i.e., attr did not specify any access rights to restrict).");
302 case; log_error("Uncovered error: %", err);
303 }
304}
305
306ERR_LL_ADD_RULE :: #code {
307 if err == {
308 case EAFNOSUPPORT;
309 log_error("rule_type is set correctly, but TCP is not supported by the kernel.");
310 case EOPNOTSUPP;
311 log_error("Landlock is supported, but this feature is disabled at boot time by the kernel.");
312 case EINVAL;
313 log_error("Flags is not 0 OR inalivid port number supplied.");
314 case ENOMSG;
315 log_error("Empty accesses (i.e., rule_attr.allowed_access is 0).");
316 case EBADF;
317 log_error("No valid file descriptor supplied in `ruleset_fd` or `rule_attr`.");
318 case EBADFD;
319 log_error("No valid file descriptor in `ruleset_fd`.");
320 case EPERM;
321 log_error("`ruleset_fd` has no write access to the underlying ruleset.");
322 case EFAULT;
323 log_error("`rule_attr` was not a valid address.");
324 }
325}
326
327ERR_LL_RESTRICT :: #code {
328 if err == {
329 case EOPNOTSUPP;
330 log_error("Landlock is supported by the kernel but disabled at boot time.");
331 case EINVAL;
332 log_error("flags is not 0.");
333 case EBADF;
334 log_error("ruleset_fd is not a file descriptor for the current thread.");
335 case EBADFD;
336 log_error("ruleset_fd is not a ruleset file descriptor.");
337 case EPERM;
338 log_error("ruleset_fd has no read access to the underlying ruleset.\nOr the calling thread is not running with no_new_privs,\nor it doesn't have the CAP_SYS_ADMIN in its user namespace.");
339 case E2BIG;
340 log_error("The maximum number of composed rulesets is reached for the calling thread. This limit is currently 64.");
341 }
342}
343
344ERR_PRCTL :: #code {
345 if err == {
346 case EINVAL;
347 log_error("OP value is not recognized/supported");
348 }
349}
350
351
352prctl :: (op: int, a2 := 0, a3 := 0, a4 := 0, a5 := 0) -> int #foreign libc;
353
354/** https://man7.org/linux/man-pages/man2/landlock_add_rule.2.html */
355add_rule :: (
356 ruleset_fd: int,
357 rule_type: landlock_rule_type,
358 rule_attr: *void
359)
360 -> ok: bool
361{
362 /** According to above man page, flags MUST be ZERO */
363 ok := syscall(SYS_LANDLOCK_ADD_RULE, ruleset_fd, rule_type, rule_attr, 0);
364 if ok < 0 {
365 print_last_error(ERR_LL_ADD_RULE);
366 return false;
367 }
368 return true;
369}
370
371print_last_error :: ($code: Code, loc := #caller_location) {
372 err := errno();
373 if err == 0 return;
374
375 log_error("-- Error --------------------");
376 log_error("%", loc);
377
378 #insert,scope() code;
379
380 log_error("------------------------------");
381}
382
383
384
Copyright 2026  E766CB298A6D1E64 | Git-Thing heavily inspired by cgit