File Coverage

blib/lib/Linux/Landlock/Direct.pm
Criterion Covered Total %
statement 58 91 63.7
branch 13 42 30.9
condition 5 18 27.7
subroutine 15 22 68.1
pod 10 11 90.9
total 101 184 54.8


line stmt bran cond sub pod time code
1             package Linux::Landlock::Direct;
2              
3             =head1 NAME
4              
5             Linux::Landlock::Direct - Direct, low-level interface to the Linux Landlock API
6              
7             =head1 DESCRIPTION
8              
9             This module provides a functional interface to the Linux Landlock API.
10             It is a relatively thin wrapper around the Landlock system calls.
11              
12             See L for a higher-level OO and exception based interface.
13              
14             See L for more information about Landlock.
15              
16             =head1 SYNOPSIS
17              
18             use Linux::Landlock::Direct qw(:functions :constants set_no_new_privs);
19              
20             # optional: restrict the Landlock functionality that can be used
21             ll_set_max_abi_version(4);
22             # create a new ruleset with all supported actions
23             my $ruleset_fd = ll_create_ruleset()
24             or die "ruleset creation failed: $!\n";
25             opendir my $dir, '/tmp';
26             # allow read and write access to files in /tmp, truncate is typically also needed, depending on the open call
27             ll_add_path_beneath_rule($ruleset_fd,
28             $LANDLOCK_RULE{PATH_BENEATH},
29             $LANDLOCK_ACCESS_FS{READ_FILE} | $LANDLOCK_ACCESS_FS{WRITE} | $LANDLOCK_ACCESS_FS{TRUNCATE},
30             $dir,
31             );
32             # NO_NEW_PRIVS is required for ll_restrict_self() to work, it can be set by any means, e.g. inherited or
33             # set via some other module; this implementation just exists for convenience. This cannot be undone.
34             set_no_new_privs();
35             # apply the ruleset to the current process and future children. This cannot be undone.
36             ll_restrict_self($ruleset_fd);
37              
38             =head1 FUNCTIONS
39              
40             =over 1
41              
42             =item ll_get_abi_version()
43              
44             Int, returns the ABI version of the Landlock implementation. Minimum version is 1.
45             Returns -1 on error.
46              
47             =item ll_set_max_abi_version($max_version)
48              
49             Sets the maximum ABI version to use. This prevents unexpected behaviour changes when
50             your application is running on a newer ABI version.
51              
52             This should be set to the highest version you tested with.
53              
54             If this is not used, or called with an undef argument, the maximum supported ABI
55             version will be used. This means future kernels could break your application.
56              
57             =item ll_create_scoped_ruleset(@actions)
58              
59             Int (file descriptor), creates a new Landlock ruleset that covers "scoped" actions.
60              
61             =item ll_create_ruleset($fs_actions, $net_actions, $scoped_actions)
62              
63             Int (file descriptor), creates a new Landlock ruleset that can cover file system,
64             network, and scoped actions at once. If no arguments are provided, it defaults to all possible
65             actions at the same time. Returns undef on error.
66              
67             =item ll_add_path_beneath_rule($ruleset_fd, $allowed_access, $parent)
68              
69             Add a rule of type C to the ruleset. C<$allowed_access> is a bitmask of allowed
70             accesses, C<$parent> is the filesystem object the rule applies to.
71              
72             It can be either a Perl file handle or a bare file descriptor and point to either a directory
73             or a file.
74              
75             If access rights are not supported by the running kernel, they are silently ignored, in line
76             with the "best effort" approach recommended by the Landlock documentation.
77              
78             Returns undef on error or the set of applied access rights on success.
79              
80             =item ll_add_net_port_rule($ruleset_fd, $allowed_access, $port)
81              
82             Add a rule of type C to the ruleset. C<$allowed_access> is a bitmask of allowed
83             accesses, C<$port> is the port the rule applies to.
84              
85             This requires an ABI version of at least 4.
86              
87             If access rights are not supported by the running kernel, they are silently ignored.
88             Returns undef on error or the set of applied access rights on success.
89              
90             =item ll_all_fs_access_supported()
91              
92             Returns a bitmask of all file system access rights that are known to this module and supported
93             by the running kernel.
94              
95             =item ll_all_net_access_supported()
96              
97             Returns a bitmask of all network access rights that are known to this module and supported
98             by the running kernel.
99              
100             =item ll_restrict_self($ruleset_fd)
101              
102             Apply the ruleset to the current process and all future children. This cannot be undone.
103             C must have been applied to the current process before calling this function.
104              
105             =item set_no_new_privs()
106              
107             Set the NO_NEW_PRIVS flag for the current process. This is required for L
108             to work. See L for more information.
109              
110             This is technically not part of Landlock and only added for convenience.
111              
112             =item get_no_new_privs()
113              
114             Get the current state of the NO_NEW_PRIVS flag.
115              
116             =back
117              
118             =head1 EXPORTS
119              
120             Nothing is exported by default. The following tags are available:
121              
122             =over 1
123              
124             =item :functions
125              
126             All functions, except for C.
127              
128             =item :constants
129              
130             All constants:
131              
132             C<%LANDLOCK_ACCESS_FS>
133              
134             # ABI version 1
135             EXECUTE
136             WRITE_FILE
137             READ_FILE
138             READ_DIR
139             REMOVE_DIR
140             REMOVE_FILE
141             MAKE_CHAR
142             MAKE_DIR
143             MAKE_REG
144             MAKE_SOCK
145             MAKE_FIFO
146             MAKE_BLOCK
147             MAKE_SYM
148             # ABI version 2
149             REFER
150             # ABI version 3
151             TRUNCATE
152             # ABI version 5
153             IOCTL_DEV
154              
155             C<%LANDLOCK_ACCESS_NET>
156              
157             # ABI version 4
158             BIND_TCP
159             CONNECT_TCP
160              
161             C<%%LANDLOCK_SCOPED>
162              
163             ABSTRACT_UNIX_SOCKET
164             SIGNAL
165              
166             C<%LANDLOCK_RULE>
167              
168             PATH_BENEATH
169             NET_PORT
170              
171             See L for more information.
172              
173             =item set_no_new_privs
174              
175             The C helper function.
176              
177             =back
178              
179             =head1 THREADS
180              
181             Landlock rules are per thread. So either apply them before spawning other threads or
182             ensure that the rules are applied in each thread.
183              
184             =cut
185              
186 2     2   253823 use strict;
  2         4  
  2         85  
187 2     2   11 use warnings;
  2         4  
  2         139  
188 2     2   15 use Exporter 'import';
  2         4  
  2         114  
189 2     2   14 use List::Util qw(reduce min);
  2         4  
  2         186  
190 2     2   1181 use POSIX qw();
  2         21300  
  2         86  
191 2     2   1054 use Linux::Landlock::Syscalls qw(NR Q_pack);
  2         8  
  2         158  
192 2     2   3497 use Math::BigInt;
  2         99921  
  2         36  
193              
194             our $MAX_ABI_VERSION = 6;
195              
196             # adapted from linux/landlock.ph, architecture independent consts
197             my $LANDLOCK_CREATE_RULESET_VERSION = (1 << 0);
198             our %LANDLOCK_ACCESS_FS = (
199             # ABI version 1
200             EXECUTE => Math::BigInt->bone->blsft(0),
201             WRITE_FILE => Math::BigInt->bone->blsft(1),
202             READ_FILE => Math::BigInt->bone->blsft(2),
203             READ_DIR => Math::BigInt->bone->blsft(3),
204             REMOVE_DIR => Math::BigInt->bone->blsft(4),
205             REMOVE_FILE => Math::BigInt->bone->blsft(5),
206             MAKE_CHAR => Math::BigInt->bone->blsft(6),
207             MAKE_DIR => Math::BigInt->bone->blsft(7),
208             MAKE_REG => Math::BigInt->bone->blsft(8),
209             MAKE_SOCK => Math::BigInt->bone->blsft(9),
210             MAKE_FIFO => Math::BigInt->bone->blsft(10),
211             MAKE_BLOCK => Math::BigInt->bone->blsft(11),
212             MAKE_SYM => Math::BigInt->bone->blsft(12),
213             # ABI version 2
214             REFER => Math::BigInt->bone->blsft(13),
215             # ABI version 3
216             TRUNCATE => Math::BigInt->bone->blsft(14),
217             # ABI version 5
218             IOCTL_DEV => Math::BigInt->bone->blsft(15),
219             );
220             our %LANDLOCK_ACCESS_NET = (
221             # ABI version 4
222             BIND_TCP => Math::BigInt->bone->blsft(0),
223             CONNECT_TCP => Math::BigInt->bone->blsft(1),
224             );
225             our %LANDLOCK_SCOPED = (
226             # ABI version 6
227             ABSTRACT_UNIX_SOCKET => Math::BigInt->bone->blsft(0),
228             SIGNAL => Math::BigInt->bone->blsft(1),
229             );
230             our %LANDLOCK_RULE = (
231             PATH_BENEATH => 1,
232             NET_PORT => 2,
233             );
234             our @EXPORT_OK = qw(
235             get_no_new_privs
236             ll_get_abi_version
237             ll_create_ruleset
238             ll_add_path_beneath_rule
239             ll_add_net_port_rule
240             ll_all_fs_access_supported
241             ll_all_net_access_supported
242             ll_all_scoped_supported
243             ll_restrict_self
244             ll_set_max_abi_version
245             set_no_new_privs
246             %LANDLOCK_ACCESS_FS
247             %LANDLOCK_ACCESS_NET
248             %LANDLOCK_SCOPED
249             %LANDLOCK_RULE
250             );
251             our %EXPORT_TAGS = (
252             functions => [grep { /^ll_/x } @EXPORT_OK],
253             constants => [grep { /^%/x } @EXPORT_OK],
254             );
255              
256             my %MAX_FS_SUPPORTED = (
257             1 => $LANDLOCK_ACCESS_FS{MAKE_SYM},
258             2 => $LANDLOCK_ACCESS_FS{REFER},
259             3 => $LANDLOCK_ACCESS_FS{TRUNCATE},
260             4 => $LANDLOCK_ACCESS_FS{TRUNCATE},
261             5 => $LANDLOCK_ACCESS_FS{IOCTL_DEV},
262             6 => $LANDLOCK_ACCESS_FS{IOCTL_DEV},
263             );
264             my %MAX_NET_SUPPORTED = (4 => $LANDLOCK_ACCESS_NET{CONNECT_TCP},);
265             my %MAX_SCOPED_SUPPORTED = (6 => $LANDLOCK_SCOPED{SIGNAL},);
266              
267             my ($abi_version, $max_abi_version, $fs_access_supported, $net_port_supported, $scoped_supported);
268              
269             sub ll_set_max_abi_version {
270 1     1 1 663 my ($max_version) = @_;
271 1         2 undef $abi_version;
272 1         2 return $max_abi_version = $max_version;
273             }
274              
275             sub ll_all_fs_access_supported {
276 3 100   3 1 392 if (!defined $fs_access_supported) {
277 1         3 my $version = min($MAX_ABI_VERSION, ll_get_abi_version());
278 13     13   1763 $fs_access_supported = reduce { $a | $b } Math::BigInt->new(0),
279 1   50     12 grep { $_ <= $MAX_FS_SUPPORTED{$version} // 0 } values %LANDLOCK_ACCESS_FS;
  16         563  
280             }
281 3         138 return $fs_access_supported;
282             }
283              
284             sub ll_all_net_access_supported {
285 0 0   0 1 0 if (!defined $net_port_supported) {
286 0         0 my $version = ll_get_abi_version();
287 0         0 $version = min(4, $version);
288             $net_port_supported =
289 0     0   0 reduce { $a | $b } Math::BigInt->new(0),
290 0   0     0 grep { $_ <= $MAX_NET_SUPPORTED{$version} // 0 } values %LANDLOCK_ACCESS_NET;
  0         0  
291             }
292 0         0 return $net_port_supported;
293             }
294              
295             sub ll_all_scoped_supported {
296 1 50   1 0 9 if (!defined $scoped_supported) {
297 1         3 my $version = ll_get_abi_version();
298 1         4 $version = min(6, $version);
299             $scoped_supported =
300 0     0   0 reduce { $a | $b } Math::BigInt->new(0),
301 1   50     16 grep { $_ <= $MAX_SCOPED_SUPPORTED{$version} // 0 } values %LANDLOCK_SCOPED;
  2         576  
302             }
303 1         212 return $scoped_supported;
304             }
305              
306             sub ll_get_abi_version {
307 8 100   8 1 376747 if (!defined $abi_version) {
308 3 50       17 if (my $nr = NR('landlock_create_ruleset')) {
309 3         19 $abi_version = syscall($nr, undef, 0, $LANDLOCK_CREATE_RULESET_VERSION);
310 3   66     19 $abi_version = min($abi_version, $max_abi_version // $abi_version);
311             } else {
312 0         0 $abi_version = -1;
313             }
314             }
315 8         69 return $abi_version;
316             }
317              
318             sub _get_supported_actions {
319 1     1   2 my ($fs_actions, $net_actions, $scoped_actions) = @_;
320 1   33     5 my $allowed = Q_pack($fs_actions // ll_all_fs_access_supported());
321 1 50       45 if (ll_get_abi_version() >= 4) {
322 0   0     0 $allowed .= Q_pack($net_actions // ll_all_net_access_supported());
323             }
324 1 50       4 if (ll_get_abi_version() >= 6) {
325 0   0     0 $allowed .= Q_pack($scoped_actions // ll_all_scoped_supported());
326             }
327 1         2 return $allowed;
328             }
329              
330             sub ll_create_ruleset {
331 1     1 1 1040 my ($fs_actions, $net_actions, $scoped_actions) = @_;
332 1         4 my $allowed = _get_supported_actions($fs_actions, $net_actions, $scoped_actions);
333 1 50       3 my $nr = NR('landlock_create_ruleset') or return;
334 1         17 my $fd = syscall($nr, $allowed, length $allowed, 0);
335 1 50       3 if ($fd >= 0) {
336 1         4 return $fd;
337             } else {
338 0 0       0 if (ll_get_abi_version() >= 6) {
339             # RHEL 9 (and derivatives) ship a broken backport of ABI 6 (https://issues.redhat.com/browse/RHEL-125143)
340 0         0 $abi_version -= 1;
341 0         0 $allowed = _get_supported_actions($fs_actions, $net_actions, $scoped_actions);
342 0         0 $fd = syscall($nr, $allowed, length $allowed, 0);
343 0 0       0 if ($fd > 0) {
344 0         0 warn "Working around buggy kernel, scoped actions not restricted\n";
345 0         0 return $fd;
346             }
347             }
348 0         0 return;
349             }
350             }
351              
352             sub ll_add_path_beneath_rule {
353 1     1 1 593 my ($ruleset_fd, $allowed_access, $parent) = @_;
354              
355 1 50       5 my $fd = ref $parent ? fileno $parent : $parent;
356 1         3 my $applied = $allowed_access & ll_all_fs_access_supported();
357 1 50       147 my $nr = NR('landlock_add_rule') or return;
358 1         5 my $result = syscall($nr, $ruleset_fd, $LANDLOCK_RULE{PATH_BENEATH}, Q_pack($applied) . pack('l', $fd), 0);
359 1 50       43 return ($result == 0) ? $applied : undef;
360             }
361              
362             sub ll_add_net_port_rule {
363 0     0 1   my ($ruleset_fd, $allowed_access, $port) = @_;
364              
365 0           my $applied = $allowed_access & ll_all_net_access_supported();
366 0 0         my $nr = NR('landlock_add_rule') or return;
367             my $result =
368 0           syscall($nr, $ruleset_fd, $LANDLOCK_RULE{NET_PORT}, Q_pack($applied) . Q_pack(Math::BigInt->new($port)), 0);
369 0 0         return ($result == 0) ? $applied : undef;
370             }
371              
372             sub set_no_new_privs {
373 0     0 1   my $PR_SET_NO_NEW_PRIVS = 38;
374 0 0         my $nr = NR('prctl') or return;
375 0 0         return (syscall($nr, $PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0) ? 1 : undef;
376             }
377              
378             sub get_no_new_privs {
379 0     0 1   my $PR_GET_NO_NEW_PRIVS = 39;
380 0 0         my $nr = NR('prctl') or return;
381 0           return syscall($nr, $PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0);
382             }
383              
384             sub ll_restrict_self {
385 0     0 1   my ($ruleset_fd) = @_;
386 0 0         my $nr = NR('landlock_restrict_self') or return;
387 0 0         return (syscall($nr, $ruleset_fd, 0) == 0) ? 1 : undef;
388             }
389              
390             1;