Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/hand/src/fence.rs

116 KiB, 1 run

created by r2519314175:915, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! What a command may touch, decided by the kernel rather than by this program.
2//!
3//! The app already has this rule. [`crate::wire::FenceSpec`] arrives in the
4//! shape `diamond_bounds` produces -- a set of roots the turn may read and
5//! write, a set it may only read, and a deny of Daimond's own directory -- and
6//! the whole of this module is that same rule enforced one layer down. Nothing
7//! new is being decided here. What changes is *who* enforces it: in the page a
8//! bound is checked at the tool dispatch door, and a command that ran outside
9//! the page would simply walk past that door. So the bound is handed to the
10//! kernel, which has no door to walk past.
11//!
12//! # Landlock is an allow-list, and that is the hard part
13//!
14//! The mechanism on Linux is Landlock. It is unprivileged, it is inherited
15//! across `execve`, and it is expressed as a set of *grants*: a path, and the
16//! access rights permitted at or beneath it. There is no deny rule. There is
17//! no rule ordering. Access to a file is decided by walking from the file
18//! upwards and taking the union of every rule found on the way, so a narrower
19//! rule placed deeper **cannot** subtract from a wider rule placed shallower.
20//!
21//! That was measured, not assumed. On Linux 7.0 (Landlock ABI 8), granting
22//! read+write on a workspace and then adding a read-only rule on a directory
23//! inside it leaves that directory writable; the crate refuses an empty-access
24//! rule outright ("empty access-right"), and even if it did not, an empty rule
25//! deeper in the tree would be a no-op for the same reason.
26//!
27//! The consequence runs through everything below. A `deny` of
28//! `/home/u/ws/.daimond` inside an `rw` of `/home/u/ws` **cannot be expressed by
29//! adding a rule**. It can only be expressed by never granting `/home/u/ws`
30//! at all, and instead granting each of its children *except* `.daimond`. That
31//! is what [`carve`] does, and it is the most important function in this file.
32//! Its costs are real and are stated at [`Listing`] and in [`Plan::caveats`];
33//! they are not hidden.
34//!
35//! The same reasoning applies to a `ro` path sitting inside an `rw` path, which
36//! is easy to miss: `diamond_bounds` expresses a read-only attachment as an
37//! allow plus a write fence, and if that attachment sits under a writable one
38//! then the read-only half is not enforceable by adding a rule either. It is
39//! carved the same way. A fence that quietly granted write there would be
40//! telling the user something untrue.
41//!
42//! # What this module refuses to do
43//!
44//! It never claims a fence it did not apply. [`Fence::detect`] asks the running
45//! kernel what it supports; [`Plan::apply`] asks for exactly that and treats
46//! anything short of full enforcement as a failure rather than as a degraded
47//! success. Where no fence is available the answer is a refusal with a sentence
48//! in it, not a command that runs unfenced. The opt-out exists -- see
49//! [`Unfenced`] -- but it is a required argument at every call site rather than
50//! a default somebody can forget.
51
52use crate::wire::FenceSpec;
53
54use oxedyne_fe2o3_core::prelude::*;
55
56use std::{
57 collections::BTreeMap,
58 ffi::OsString,
59 path::{
60 Path,
61 PathBuf,
62 },
63};
64
65#[cfg(target_os = "linux")]
66use landlock::{
67 Access,
68 AccessFs,
69 AccessNet,
70 BitFlags,
71 PathBeneath,
72 PathFd,
73 RestrictSelf,
74 RestrictSelfAttr,
75 Ruleset,
76 RulesetAttr,
77 RulesetCreatedAttr,
78 RulesetStatus,
79 Scope,
80 ABI,
81};
82
83// ┌───────────────────────────────────────────────────────────────┐
84// │ The Landlock ABI │
85// └───────────────────────────────────────────────────────────────┘
86
87/// Which Landlock ABI the running kernel offers.
88///
89/// A number rather than a boolean, because the answer is not "fenced or not":
90/// each level adds a category of thing that can be restrained, and a fence
91/// reporting only "on" would be claiming coverage it does not have on an older
92/// kernel. The page shows this level to the user, and [`Fence::holes`] turns it
93/// into the list of what is *still* reachable, which is the honest half of the
94/// same sentence.
95#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
96pub enum Abi {
97 /// No Landlock: either not built into the kernel or not enabled at boot.
98 None,
99 /// Filesystem rules (Linux 5.13).
100 V1,
101 /// Adds `REFER`, which governs linking and renaming across directories (5.19).
102 V2,
103 /// Adds `TRUNCATE`, without which a file can be emptied but not written (6.2).
104 V3,
105 /// Adds TCP bind and connect: the first level at which `net: false` means
106 /// anything at all (6.7).
107 V4,
108 /// Adds `IOCTL_DEV` (6.10).
109 V5,
110 /// Adds scoping: abstract unix sockets and signals (6.12).
111 V6,
112 /// Adds audit-log control (6.15).
113 V7,
114 /// Adds atomic enforcement across every thread of the process (7.0).
115 V8,
116 /// Adds `RESOLVE_UNIX`, which finally brings pathname unix sockets under the
117 /// filesystem rules (7.1).
118 V9,
119 /// Newer than this build knows about.
120 ///
121 /// Reported verbatim rather than rounded down silently, because "your kernel
122 /// is ahead of this build" and "your kernel is at the level this build tops
123 /// out at" are different facts and the user is entitled to both. The rights
124 /// asked for are still capped at [`Abi::V9`].
125 Newer(u32),
126}
127
128impl Abi {
129
130 /// The numeric level, as the kernel reports it.
131 pub fn level(&self) -> u32 {
132 match self {
133 Self::None => 0,
134 Self::V1 => 1,
135 Self::V2 => 2,
136 Self::V3 => 3,
137 Self::V4 => 4,
138 Self::V5 => 5,
139 Self::V6 => 6,
140 Self::V7 => 7,
141 Self::V8 => 8,
142 Self::V9 => 9,
143 Self::Newer(n) => *n,
144 }
145 }
146
147 /// The level for a number the kernel reported.
148 ///
149 /// # Arguments
150 /// * `n` - What `landlock_create_ruleset` returned when asked for the version.
151 pub fn of_level(n: u32) -> Self {
152 match n {
153 0 => Self::None,
154 1 => Self::V1,
155 2 => Self::V2,
156 3 => Self::V3,
157 4 => Self::V4,
158 5 => Self::V5,
159 6 => Self::V6,
160 7 => Self::V7,
161 8 => Self::V8,
162 9 => Self::V9,
163 n => Self::Newer(n),
164 }
165 }
166
167 /// Whether the filesystem can be fenced at all.
168 pub fn fences_files(&self) -> bool {
169 self.level() >= 1
170 }
171
172 /// Whether `net: false` can be honoured.
173 ///
174 /// Below this a caller asking for no network must be refused, rather than
175 /// given a fence that does not do what its name says.
176 pub fn fences_tcp(&self) -> bool {
177 self.level() >= 4
178 }
179
180 /// Whether abstract unix sockets and signals can be scoped to the sandbox.
181 pub fn scopes(&self) -> bool {
182 self.level() >= 6
183 }
184
185 /// Whether the restriction can be applied to every thread at once.
186 pub fn all_threads(&self) -> bool {
187 self.level() >= 8
188 }
189
190 /// Whether connecting to a *pathname* unix socket is governed by the
191 /// filesystem rules.
192 ///
193 /// Below this it is not, and that is the largest hole in the Linux fence.
194 /// See [`Fence::holes`].
195 pub fn fences_unix_sockets(&self) -> bool {
196 self.level() >= 9
197 }
198
199 /// The capability string the page shows, such as `landlock:abi-8`.
200 pub fn cap(&self) -> String {
201 match self {
202 Self::None => fmt!("landlock:none"),
203 other => fmt!("landlock:abi-{}", other.level()),
204 }
205 }
206}
207
208// ┌───────────────────────────────────────────────────────────────┐
209// │ Levels and grants │
210// └───────────────────────────────────────────────────────────────┘
211
212/// How much access a path carries, ordered so that "less" is unambiguous.
213///
214/// The ordering is the whole reason this is an enum with a derived `Ord`: the
215/// carve decision is exactly "is this descendant's level *below* its
216/// ancestor's", and a comparison that reads that way in the source is one fewer
217/// place for the rule to be written backwards.
218#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
219pub enum Level {
220 /// Nothing at all. Not granted, and carved out of any ancestor that is.
221 Deny,
222 /// Read, list and execute. Never write, create, delete or rename.
223 Ro,
224 /// Read and write.
225 Rw,
226}
227
228impl Level {
229
230 /// The word the report uses.
231 pub fn word(&self) -> &'static str {
232 match self {
233 Self::Deny => "deny",
234 Self::Ro => "ro",
235 Self::Rw => "rw",
236 }
237 }
238}
239
240/// One resolved rule: a real directory or file, and what may be done there.
241///
242/// Paths here are canonical -- symbolic links resolved, `.` and `..` gone --
243/// because path confusion is the classic way past a check of this kind, and
244/// comparing two spellings of one place is how it happens. `normalise` in the
245/// app's `tools.rs` does the lexical half of this for workspace-relative names;
246/// down here the paths are absolute and real, so the filesystem itself is asked.
247#[derive(Clone, Debug, Eq, PartialEq)]
248pub struct Grant {
249 /// The canonical path.
250 pub path: PathBuf,
251 /// What is permitted at and beneath it.
252 pub level: Level,
253}
254
255// ┌───────────────────────────────────────────────────────────────┐
256// │ Two decisions the carve forces │
257// └───────────────────────────────────────────────────────────────┘
258
259/// What happens to a directory that had to be carved rather than granted whole.
260///
261/// A carved directory is one holding a `deny` (or a narrower `ro`) and so cannot
262/// itself be granted -- see the module documentation. Its children are granted
263/// individually and the directory itself is left with nothing. That is airtight
264/// and it costs something real: `ls <workspace>` fails, because listing a
265/// directory needs `READ_DIR` *on that directory*.
266///
267/// The obvious repair is to grant `READ_DIR` on the carved directory alone. It
268/// works, and it leaks: a rule at `<workspace>` applies to everything beneath
269/// it, including the denied subtree, so the command can then list the *names*
270/// inside `.daimond` -- never the contents, since reading a file needs
271/// `READ_FILE`, which is not granted. Measured on ABI 8: with `READ_DIR` on the
272/// parent, `readdir` of the denied directory succeeds and `read` of a file in it
273/// is refused.
274///
275/// So this is a choice with no free answer, and it is spelled out rather than
276/// made silently. [`Listing::Sealed`] is the default, because a fence whose
277/// guarantee has an undocumented exception is worse than a fence that is
278/// inconvenient.
279#[derive(Clone, Copy, Debug, Eq, PartialEq)]
280pub enum Listing {
281 /// The carved directory cannot be listed. Airtight; `ls` on it fails.
282 Sealed,
283 /// The carved directory can be listed, and so can the denied subtrees inside
284 /// it -- entry *names* only. Convenient; leaky.
285 Names,
286}
287
288/// How much of the process the fence is applied to.
289///
290/// Landlock restricts the calling thread. Since ABI 8 it can restrict every
291/// thread of the process atomically instead, and that is what the launcher
292/// wants: a launcher that had grown a thread and fenced only the one calling
293/// [`Plan::apply`] would leave a sibling able to `fork` and `exec` outside the
294/// fence, which is a hole with no warning attached to it.
295///
296/// [`Reach::Thread`] exists because a fence covering the whole process cannot be
297/// tested from inside a test harness -- the first test to apply one would fence
298/// every test after it, including the ones that had not run yet. The rules are
299/// identical either way; only the set of tasks they bind to differs.
300#[derive(Clone, Copy, Debug, Eq, PartialEq)]
301pub enum Reach {
302 /// Every thread of the process, where the kernel can do it atomically.
303 Process,
304 /// The calling thread only, which is all Landlock does by default.
305 Thread,
306}
307
308/// Whether the fence adds the system paths a program needs in order to be a
309/// program at all.
310///
311/// A [`crate::wire::FenceSpec`] names the workspace. It does not name
312/// `/usr/bin/cargo`, the dynamic linker, the locale data or `/dev/null` -- and a
313/// fence granting only the workspace cannot run anything, because `execve` needs
314/// `EXECUTE` on the binary and the loader needs `READ_FILE` on the shared
315/// objects. Measured: with only `/usr` and `/etc` granted read-only, spawning
316/// `/bin/cat` fails with `EACCES`, because Rust's `Command` opens `/dev/null`
317/// for the stdio it was not given.
318///
319/// So the Linux fence adds a base, the base is **read-only**, and it is written
320/// out here rather than buried in a helper, since it is a deliberate widening of
321/// what the caller asked for. What it pointedly does *not* include:
322///
323/// * `/proc` -- because `/proc/<pid>/environ` of the user's *other* processes is
324/// readable by the same uid, and the browser's own environment is exactly the
325/// sort of thing a fenced command should not reach. Measured: with `/proc`
326/// left out, reading another process's environ is refused. A caller needing
327/// `/proc` must put it in `ro` explicitly and accept that.
328/// * `/tmp` -- shared with every other process the user runs. A command wanting
329/// scratch space does not need it: [`crate::exec::Scratch`] gives every run a
330/// private directory of its own, adds it to that run's `rw`, and points
331/// `TMPDIR` at it. That is not an optimisation -- with `/tmp` outside the
332/// fence and nothing in its place, a fenced `cargo test` dies part-way through
333/// with `couldn't create a temp dir: Permission denied`.
334/// * `/home`, `/var`, `/run`, `/sys`, `/mnt`, `/media` -- the user's data, the
335/// machine's state, and the sockets.
336#[derive(Clone, Copy, Debug, Eq, PartialEq)]
337pub enum SysBase {
338 /// Add the read-only system paths a program needs to start.
339 Minimal,
340 /// Add nothing. Only what the spec named is reachable, which for most
341 /// commands means they cannot run at all. Useful when the caller has listed
342 /// everything itself.
343 Bare,
344}
345
346impl SysBase {
347
348 /// The paths this base contributes, read-only.
349 ///
350 /// Absent entries are skipped: `/lib64` does not exist everywhere, and
351 /// skipping a path makes the fence tighter rather than looser, which is the
352 /// safe direction for a decision made without asking.
353 pub fn paths(&self) -> &'static [&'static str] {
354 match self {
355 Self::Bare => &[],
356 Self::Minimal => &[
357 "/usr",
358 "/bin",
359 "/sbin",
360 "/lib",
361 "/lib32",
362 "/lib64",
363 "/libx32",
364 "/etc",
365 "/opt",
366 "/dev/zero",
367 "/dev/random",
368 "/dev/urandom",
369 ],
370 }
371 }
372
373 /// The paths this base contributes READ-WRITE, because a program that cannot
374 /// write to them is a program that does not run.
375 ///
376 /// `/dev/null` is the whole of why this exists. It was in the read-only list,
377 /// and every git command inside the fence died with `fatal: could not open
378 /// '/dev/null' for reading and writing` -- git opens it for both, as does a
379 /// large share of Unix tooling, because discarding output IS a write. A base
380 /// described as "the system paths a program needs in order to be a program"
381 /// was missing the one device every program uses, and no test caught it
382 /// because nothing in the suite ran a program that writes to it.
383 ///
384 /// Writable is not a widening worth worrying about: writing to `/dev/null`
385 /// discards, and writing to `/dev/full` fails with ENOSPC by design. Neither
386 /// can carry a byte out of the compartment, which is what the fence is for.
387 /// `/dev/zero`, `/dev/random` and `/dev/urandom` stay read-only, because
388 /// nothing legitimate writes to them and a write there is a seeding attempt.
389 pub fn write_paths(&self) -> &'static [&'static str] {
390 match self {
391 Self::Bare => &[],
392 Self::Minimal => &["/dev/null", "/dev/full"],
393 }
394 }
395}
396
397// ┌───────────────────────────────────────────────────────────────┐
398// │ The refusal, and the way past it │
399// └───────────────────────────────────────────────────────────────┘
400
401/// What to do when no fence can be applied.
402///
403/// A required argument to [`Fence::plan`] rather than a field with a default,
404/// and that is the point. A default is a thing somebody forgets; an argument is
405/// a thing somebody has to write down. Every call site therefore says, in the
406/// source, what it wants to happen on a machine with no Landlock, and a reviewer
407/// can find all of them with one search.
408#[derive(Clone, Debug, Eq, PartialEq)]
409pub enum Unfenced {
410 /// Refuse to run the command. The hand's own answer, always.
411 Refuse,
412 /// Run it anyway, because the user was told what that means and said yes.
413 ///
414 /// The sentence they agreed to travels with the decision, so the journal
415 /// records what was actually on screen rather than merely that a flag was
416 /// set.
417 Allow {
418 /// What the user acknowledged, verbatim.
419 acknowledged: String,
420 },
421}
422
423// ┌───────────────────────────────────────────────────────────────┐
424// │ The fence │
425// └───────────────────────────────────────────────────────────────┘
426
427/// The compartment mechanism available on this machine.
428///
429/// An enum with one arm per platform, and the platforms that are not built yet
430/// are here from the first day rather than left to be discovered. The reason is
431/// not tidiness: an abstraction guessed from one implementation is a rewrite
432/// waiting for the second, and the second is macOS, whose sandbox is a *profile*
433/// applied to a process rather than a set of path rules, and the third is
434/// Windows, where the nearest equivalents are a Job Object and an AppContainer
435/// SID and neither is shaped like Landlock at all. Declaring them as arms that
436/// return a named refusal keeps the shape honest, and keeps the page able to say
437/// which guarantee it is offering on which machine.
438#[derive(Clone, Debug, Eq, PartialEq)]
439pub enum Fence {
440 /// Landlock, at the ABI level the running kernel reported.
441 Linux {
442 /// What the kernel supports.
443 abi: Abi,
444 /// What happens to a directory that had to be carved.
445 listing: Listing,
446 /// Whether the read-only system base is added.
447 base: SysBase,
448 },
449 /// Declared, not built.
450 MacOs,
451 /// Declared, not built.
452 Windows,
453 /// No compartment is available, and this is why.
454 None {
455 /// The sentence explaining what is missing.
456 why: String,
457 },
458}
459
460impl Fence {
461
462 /// Asks the running machine what it can actually do.
463 ///
464 /// On Linux this probes Landlock rather than reading a version number, and
465 /// does so on a throwaway thread: `landlock_restrict_self` restricts the
466 /// calling thread only, so a thread existing solely to ask "no rules, now
467 /// tell me what you supported" leaves the hand's own threads untouched.
468 /// Reading `/sys/kernel/security/lsm` would be cheaper and would be a guess:
469 /// it says Landlock is compiled in, not which ABI it offers.
470 pub fn detect() -> Self {
471 Self::detect_with(Listing::Sealed, SysBase::Minimal)
472 }
473
474 /// As [`Fence::detect`], with the two carve decisions made explicitly.
475 ///
476 /// # Arguments
477 /// * `listing` - What a carved directory may show.
478 /// * `base` - Whether the read-only system base is added.
479 pub fn detect_with(listing: Listing, base: SysBase) -> Self {
480 #[cfg(target_os = "linux")]
481 {
482 let abi = probe_abi();
483 if abi.fences_files() {
484 return Self::Linux { abi, listing, base };
485 }
486 Self::None {
487 why: fmt!(
488 "This kernel has no Landlock, so there is nothing to fence a \
489 command with. Landlock arrived in Linux 5.13 and must also be \
490 enabled at boot; check that \"landlock\" appears in \
491 /sys/kernel/security/lsm."),
492 }
493 }
494 #[cfg(target_os = "macos")]
495 {
496 let _ = (listing, base);
497 Self::MacOs
498 }
499 #[cfg(target_os = "windows")]
500 {
501 let _ = (listing, base);
502 Self::Windows
503 }
504 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
505 {
506 let _ = (listing, base);
507 Self::None {
508 why: fmt!(
509 "This build is for a platform the hand has no fence for, so it \
510 cannot say what a command would be prevented from touching."),
511 }
512 }
513 }
514
515 /// The mechanisms actually in force, for [`crate::wire::Resp::Hello`].
516 ///
517 /// The product's claim is that the compartment can be checked rather than
518 /// trusted, and a claim of that shape has to survive a machine where the
519 /// answer is "nothing". So on a kernel without Landlock this returns
520 /// `fence:none` and not an empty list: silence would read as "no answer
521 /// yet", and what the user needs to read is "no fence".
522 pub fn caps(&self) -> Vec<String> {
523 match self {
524 Self::Linux { abi, listing, base } => {
525 let mut out = vec![fmt!("fence:linux"), abi.cap()];
526 if abi.fences_tcp() {
527 out.push(fmt!("landlock:net-tcp"));
528 }
529 if abi.scopes() {
530 out.push(fmt!("landlock:scope-unix-abstract"));
531 out.push(fmt!("landlock:scope-signal"));
532 }
533 if abi.fences_unix_sockets() {
534 out.push(fmt!("landlock:unix-pathname"));
535 }
536 if abi.all_threads() {
537 out.push(fmt!("landlock:all-threads"));
538 }
539 // Withheld from every writable grant; see `writable`.
540 out.push(fmt!("landlock:no-make-sym"));
541 out.push(match listing {
542 Listing::Sealed => fmt!("carve:sealed"),
543 Listing::Names => fmt!("carve:names-visible"),
544 });
545 out.push(match base {
546 SysBase::Minimal => fmt!("sysbase:minimal"),
547 SysBase::Bare => fmt!("sysbase:bare"),
548 });
549 out
550 },
551 Self::MacOs => vec![fmt!("fence:none"), fmt!("fence:macos-unimplemented")],
552 Self::Windows => vec![fmt!("fence:none"), fmt!("fence:windows-unimplemented")],
553 Self::None { .. } => vec![fmt!("fence:none")],
554 }
555 }
556
557 /// What a command can still reach despite this fence.
558 ///
559 /// Written down because an undocumented hole is worse than a documented one:
560 /// a user who knows the shape of the gap can decide whether it matters, and
561 /// a user who does not has been misled. Every entry here was measured on a
562 /// running kernel, not inferred from documentation.
563 ///
564 /// # Why this takes the filter as an argument
565 ///
566 /// Two of Landlock's holes are closed by something that is not Landlock.
567 /// `chmod`, `chown`, `utimensat` and `setxattr` have no access right, and
568 /// `connect()` to a pathname unix socket is ungoverned below ABI 9 -- and
569 /// [`crate::seccomp`] refuses all of them at the system-call layer instead.
570 /// A `holes()` that could not see the filter would have to either overstate
571 /// the compartment or understate it, and this list is what `--report` prints,
572 /// so it must be neither.
573 ///
574 /// # What this is NOT
575 ///
576 /// It is not where the consent window's wording comes from, and this comment
577 /// used to say it was. That window's text is a fixed localised string --
578 /// `grant_hand_body` in `ext/_locales/*/messages.json` -- and the only thing
579 /// this end contributes to it is [`Fence::caps`], which `ext/grant.js` reads
580 /// to choose between "this machine can contain a command" and "it cannot".
581 /// Nothing in this function reaches a user's screen.
582 ///
583 /// That is deliberate rather than an omission waiting to be repaired. These
584 /// are paragraphs of kernel detail, and a consent window is the one dialog in
585 /// the product that has to stay short enough to be read: burying the decision
586 /// under six caveats is how a person learns to click through it. A user who
587 /// wants the whole account runs `--report`, which prints exactly this list.
588 ///
589 /// Passing `None` asks the honest question about Landlock alone, which is
590 /// what a machine with no filter gets -- and on such a machine no command
591 /// runs at all, because the filter is release gate 1's second half.
592 ///
593 /// # Arguments
594 /// * `sys` - The filter that will be installed on top of this fence, if any.
595 pub fn holes(&self, sys: Option<&crate::seccomp::Spec>) -> Vec<String> {
596 use crate::seccomp::{Meta, Unix};
597 let unix_shut = matches!(sys, Some(s) if s.unix == Unix::Refuse);
598 let meta_shut = matches!(sys, Some(s) if s.meta != Meta::Allow);
599 match self {
600 Self::Linux { abi, listing, base } => {
601 let mut out = Vec::new();
602 if !abi.fences_unix_sockets() && !unix_shut {
603 out.push(fmt!(
604 "A fenced command can step out of the fence entirely, \
605 by way of a pathname unix socket. Landlock does not \
606 govern connect() to a socket file until ABI 9 (Linux \
607 7.1), and this kernel is at ABI {}. Measured: with the \
608 network refused and the whole fence in force, connect() \
609 to /run/user/<uid>/bus succeeds, and one command through \
610 the session bus -- systemd-run --user -- starts a \
611 process that is NOT fenced and reads a file this fence \
612 denies. The same socket reaches ssh-agent, which can \
613 sign with your keys without the key ever being read. \
614 This is not a leak at the edge of the compartment; on \
615 this kernel it is a way out of it.", abi.level()));
616 }
617 if !meta_shut {
618 out.push(fmt!(
619 "A command can change a file's metadata anywhere it can \
620 name, including inside a denied subtree. Landlock has no \
621 access right covering chmod, chown, utimensat or setxattr, \
622 so none of the four is mediated at all. Measured on ABI {}: \
623 all four succeeded on a file outside every root, and chmod \
624 took a file inside the denied subtree from 600 to 777 -- \
625 although reading that same file is refused. A command cannot \
626 read your secrets through the fence; it can strip the \
627 permissions that were protecting them from everything else.",
628 abi.level()));
629 }
630 out.push(fmt!(
631 "Existence and metadata leak where contents do not. stat on \
632 a path outside the fence still answers, so sizes, \
633 timestamps, ownership and the mere presence or absence of a \
634 file are readable. The fence governs opening a file, not \
635 asking about one."));
636 if *base == SysBase::Minimal {
637 out.push(fmt!(
638 "The system base grants /usr, /etc and /opt read-only, \
639 and that is a wide grant. It is what makes a command \
640 able to run at all -- the interpreter, the linker, the \
641 shared objects -- but it also means every configuration \
642 file under /etc that is world-readable can be read, and \
643 every tool installed on this machine can be executed. \
644 /etc in particular is where a great deal of \
645 machine-identifying detail lives. SysBase::Bare removes \
646 this and leaves the caller to name what a command needs, \
647 which for most commands means naming the whole of a \
648 toolchain."));
649 }
650 out.push(fmt!(
651 "A path that is swapped for a symbolic link between the \
652 moment the rules are worked out and the moment they are \
653 opened would be granted as its target. The plan resolves \
654 every path and refuses any that is a link, and the open \
655 re-checks immediately before it acts, so the window is one \
656 statement wide rather than one turn wide -- but a fence \
657 built while another process is actively rearranging the \
658 workspace is not something this code can make safe."));
659 out.push(fmt!(
660 "UDP and raw sockets are not governed. Landlock's network \
661 rules cover TCP bind and connect only, so with net:false a \
662 command can still send UDP to a fixed address. Name lookup \
663 itself fails, because /etc/resolv.conf sits outside the \
664 fence, but a program carrying its own resolver address does \
665 not need it."));
666 if !abi.scopes() {
667 out.push(fmt!(
668 "Abstract unix sockets are reachable, and the command can \
669 signal processes outside the fence. Scoping arrived at \
670 ABI 6 (Linux 6.12) and this kernel is at ABI {}.",
671 abi.level()));
672 }
673 out.push(fmt!(
674 "File descriptors opened before the fence was applied keep \
675 working. Landlock checks the act of opening, not the use of \
676 something already open, so the fence must be applied before \
677 anything the command should not have is opened."));
678 out.push(fmt!(
679 "Every command can write in one place the workspace did not \
680 name. The hand adds a private temporary directory to each \
681 run's fence and points TMPDIR at it, because /tmp is outside \
682 the fence and a build that cannot write a temporary file \
683 fails part-way through for a reason nobody can read. So \
684 \"only inside the folders the workspace allows\" has exactly \
685 one exception, and this is it. It sits under the hand's own \
686 data directory rather than in the user's folder, so a \
687 build's leavings are never mistaken for the user's work; it \
688 is removed when the run ends; and no other run can reach it, \
689 since the directory holding them all carries no rule in any \
690 fence and each name carries 128 bits nobody can guess. See \
691 `exec::Scratch`."));
692 out.push(fmt!(
693 "A hard link made earlier is a second name for the same \
694 file. If a file inside a denied subtree already has a link \
695 inside a granted one, it is readable through that link. \
696 Landlock decides by the path walked; the command cannot \
697 create such a link, but it cannot undo one that exists."));
698 // A cost rather than a hole, and it is here because this is the
699 // list the consent window is drawn from and the one place a
700 // reader looks to find out why a command was refused.
701 out.push(fmt!(
702 "A command cannot create a SYMBOLIC link, anywhere, \
703 including in the folders it may write and in its own \
704 temporary directory. `ln -s` and `symlink(2)` answer \
705 \"Permission denied\". The right is withheld because a link \
706 is half of a leak: the command makes it, and whatever later \
707 follows it -- an archiver, a packager, a version control \
708 system recording the tree -- supplies the other half by \
709 reading a file the command itself could not open. Nothing \
710 else is narrowed by it."));
711 if *listing == Listing::Names {
712 out.push(fmt!(
713 "Entry names inside denied subtrees are visible, because \
714 the carved parent was granted READ_DIR so that it could \
715 be listed. Contents are not readable. Listing::Sealed \
716 closes this."));
717 }
718 out
719 },
720 Self::MacOs | Self::Windows | Self::None { .. } => vec![fmt!(
721 "Everything. There is no fence on this machine, and a command \
722 run here can touch whatever the user running the browser can \
723 touch.")],
724 }
725 }
726
727 /// The sentence a refusal carries, in the voice the file tools already use.
728 ///
729 /// # Arguments
730 /// * `what` - What was being attempted, named so the model can recover.
731 pub fn refusal(&self, what: &str) -> String {
732 match self {
733 Self::Linux { .. } => fmt!(
734 "{} was refused, although this machine can fence commands. That \
735 is a bug: the fence should have been applied instead.", what),
736 Self::MacOs => fmt!(
737 "{} was refused because the hand cannot fence a command on macOS \
738 yet. Doing it properly needs a sandbox profile applied through \
739 sandbox_exec, or the App Sandbox entitlements if the hand ships \
740 in a bundle; neither is built. Running the command unfenced \
741 would give it everything you can reach, so it was not run.",
742 what),
743 Self::Windows => fmt!(
744 "{} was refused because the hand cannot fence a command on \
745 Windows yet. Doing it properly needs a Job Object to bound the \
746 process tree and an AppContainer SID to bound what it may open; \
747 neither is built. Running the command unfenced would give it \
748 everything you can reach, so it was not run.", what),
749 Self::None { why } => fmt!(
750 "{} was refused because there is no fence on this machine. {} \
751 Running it anyway would give the command everything you can \
752 reach, so it was not run.", what, why),
753 }
754 }
755
756 /// Works out exactly what would be granted, without changing anything.
757 ///
758 /// Separated from [`Plan::apply`] on purpose. The plan is made in the
759 /// hand's own process, where an error can still become a
760 /// [`crate::wire::Resp::Refused`] the page can show; applying happens in the
761 /// doomed little process that is about to become the command, where the only
762 /// remaining move is to die. A spec that cannot be honoured must fail on
763 /// the near side of that line.
764 ///
765 /// # Arguments
766 /// * `spec` - What the caller asked for.
767 /// * `unfenced` - What to do if there is no fence. Required, not defaulted.
768 ///
769 /// # Returns
770 /// A plan, or an error naming the path or the capability that made the
771 /// request impossible.
772 pub fn plan(&self, spec: &FenceSpec, unfenced: &Unfenced) -> Outcome<Plan> {
773 match self {
774 Self::Linux { abi, listing, base } => {
775 if !spec.net && !abi.fences_tcp() {
776 return Err(err!(
777 "The command asked to run with no network, and this \
778 kernel's Landlock (ABI {}) has no network rules; those \
779 arrived at ABI 4 in Linux 6.7. Granting a filesystem \
780 fence and calling it a network fence would be a lie, so \
781 the command was refused.", abi.level();
782 Unimplemented, Network, Security));
783 }
784 let r = res!(resolve(spec, *base));
785 Ok(Plan {
786 abi: *abi,
787 listing: *listing,
788 base: *base,
789 reach: Reach::Process,
790 grants: r.grants,
791 sealed: r.sealed,
792 dropped: r.dropped,
793 net: spec.net,
794 waiver: None,
795 })
796 },
797 Self::MacOs | Self::Windows | Self::None { .. } => match unfenced {
798 Unfenced::Refuse => Err(err!(
799 "{}", self.refusal("This command");
800 Unimplemented, Security, Unauthorised)),
801 Unfenced::Allow { acknowledged } => Ok(Plan {
802 abi: Abi::None,
803 listing: Listing::Sealed,
804 base: SysBase::Bare,
805 reach: Reach::Process,
806 grants: Vec::new(),
807 sealed: Vec::new(),
808 dropped: Vec::new(),
809 net: true,
810 waiver: Some(acknowledged.clone()),
811 }),
812 },
813 }
814 }
815}
816
817// ┌───────────────────────────────────────────────────────────────┐
818// │ The plan │
819// └───────────────────────────────────────────────────────────────┘
820
821/// Everything the fence will do, resolved, before any of it is done.
822///
823/// Inspectable on purpose: the journal records it, the page can show it, and a
824/// test can assert on it without needing a kernel. A compartment nobody can
825/// read is a compartment nobody can check.
826#[derive(Clone, Debug, Eq, PartialEq)]
827pub struct Plan {
828 /// The ABI the rules were built for.
829 pub abi: Abi,
830 /// What a carved directory may show.
831 pub listing: Listing,
832 /// Which system base was added.
833 pub base: SysBase,
834 /// How much of the process the fence binds to.
835 pub reach: Reach,
836 /// The rules, canonical and de-duplicated.
837 pub grants: Vec<Grant>,
838 /// Directories that had to be carved, and so carry no grant of their own.
839 ///
840 /// Reported because the user will notice: these are the directories a
841 /// command cannot list and cannot create a file directly inside.
842 pub sealed: Vec<PathBuf>,
843 /// Children of a carved directory that were refused a grant because they do
844 /// not resolve to themselves.
845 ///
846 /// A symbolic link is the whole of this in practice. See [`carve`] for why
847 /// granting one is a complete escape from the fence rather than a nicety.
848 pub dropped: Vec<PathBuf>,
849 /// Whether the network is left alone.
850 pub net: bool,
851 /// Set only where the user knowingly waived the fence, carrying what they
852 /// were told.
853 pub waiver: Option<String>,
854}
855
856impl Plan {
857
858 /// Whether this plan is a fence at all.
859 pub fn is_fenced(&self) -> bool {
860 self.waiver.is_none() && self.abi.fences_files()
861 }
862
863 /// What the caller should be told about the shape of what they asked for.
864 ///
865 /// Distinct from [`Fence::holes`], which is about the mechanism. These are
866 /// consequences of *this* spec: they exist because something had to be
867 /// carved, and they would not exist for a spec with no `deny` in it.
868 pub fn caveats(&self) -> Vec<String> {
869 let mut out = Vec::new();
870 if let Some(ack) = &self.waiver {
871 out.push(fmt!(
872 "This command is running with no fence at all, because that was \
873 acknowledged: {}", ack));
874 return out;
875 }
876 for dir in &self.sealed {
877 match self.listing {
878 Listing::Sealed => out.push(fmt!(
879 "{} cannot be listed, and a file cannot be created directly \
880 in it. It holds something the command may not touch, and \
881 Landlock has no way to grant a directory and withhold part \
882 of it, so its children were granted one by one and the \
883 directory itself was not.", dir.display())),
884 Listing::Names => out.push(fmt!(
885 "{} can be listed but a file cannot be created directly in \
886 it, and the listing includes the names inside the parts the \
887 command may not read.", dir.display())),
888 }
889 }
890 if !self.sealed.is_empty() {
891 out.push(fmt!(
892 "Anything created inside a carved directory after the command \
893 started is invisible to it. The rules name the children that \
894 existed when the fence was built, and a name appearing \
895 afterwards has no rule."));
896 }
897 for p in &self.dropped {
898 out.push(fmt!(
899 "{} was not granted, because it is a symbolic link rather than \
900 the thing it names. Granting it would grant whatever it points \
901 at -- which is what a command inside the fence would use it for. \
902 Reach the target by its own path, if that path is inside the \
903 fence.", p.display()));
904 }
905 out
906 }
907
908 /// Whether this plan permits `want` at `path`.
909 ///
910 /// The same walk the kernel does -- from the path upwards, taking the union
911 /// -- so a caller can check the working directory *before* spawning rather
912 /// than watching the command fail obscurely. A convenience, not the
913 /// guarantee: the guarantee is the kernel's.
914 ///
915 /// # Arguments
916 /// * `path` - An absolute path. Lexically normalised rather than resolved
917 /// against the filesystem, since the caller may be asking about something
918 /// that does not exist yet.
919 /// * `want` - The access being asked about.
920 pub fn permits(&self, path: &Path, want: Level) -> bool {
921 if self.waiver.is_some() {
922 return true;
923 }
924 let p = lexical(path);
925 let mut best = Level::Deny;
926 for g in &self.grants {
927 if p == g.path || p.starts_with(&g.path) {
928 if g.level > best {
929 best = g.level;
930 }
931 }
932 }
933 best >= want
934 }
935
936 /// Applies the fence to **the current process**, then returns what took hold.
937 ///
938 /// # Where this must be called
939 ///
940 /// Landlock restricts the caller and is inherited across `execve`; there is
941 /// no way to hand a ruleset to somebody else's child. Rust's one hook for
942 /// running code between fork and exec, `CommandExt::pre_exec`, is an
943 /// `unsafe` function, and this project does not write `unsafe`.
944 ///
945 /// So the sequence is: the hand re-executes *itself* as a small launcher,
946 /// the launcher calls this on itself while it is still single-threaded and
947 /// has opened nothing, and then it `exec`s the real command --
948 /// `CommandExt::exec` is safe, and the fence carries across. Calling this
949 /// in the hand's own process would fence the hand, which serves the page.
950 ///
951 /// # Returns
952 /// What was actually enforced, or an error. Anything short of full
953 /// enforcement is an error rather than a quieter success: the rules asked
954 /// for exactly what this kernel said it supports, so a partial result means
955 /// something is wrong rather than merely old.
956 pub fn apply(&self) -> Outcome<Applied> {
957 if let Some(ack) = &self.waiver {
958 return Ok(Applied {
959 abi: Abi::None,
960 fenced: false,
961 caps: vec![fmt!("fence:none"), fmt!("fence:waived")],
962 waiver: Some(ack.clone()),
963 });
964 }
965 #[cfg(target_os = "linux")]
966 {
967 self.apply_linux()
968 }
969 #[cfg(not(target_os = "linux"))]
970 {
971 Err(err!(
972 "There is no fence to apply on this platform, and the plan was \
973 not marked as knowingly unfenced. Nothing was run.";
974 Unimplemented, Security))
975 }
976 }
977
978 /// The Landlock half of [`Plan::apply`].
979 #[cfg(target_os = "linux")]
980 fn apply_linux(&self) -> Outcome<Applied> {
981 let abi = ll_abi(self.abi);
982
983 // Handle every filesystem right this ABI knows. Handling a right is what
984 // switches it from "unrestricted" to "denied unless a rule grants it",
985 // so anything left unhandled is a whole category of access the fence
986 // would not be governing.
987 let mut rs = res!(Ruleset::default().handle_access(AccessFs::from_all(abi)));
988
989 // Network. Handling the rights and then adding no port rules is what
990 // denies TCP outright; leaving them unhandled is what leaves the network
991 // alone. There is no third state, which is why `net` is a boolean.
992 if !self.net && self.abi.fences_tcp() {
993 rs = res!(rs.handle_access(AccessNet::from_all(ABI::V4)));
994 }
995
996 // Scoping. Signals are scoped whatever the network setting, because a
997 // command reaching out to signal the browser is a containment failure
998 // and not a networking question. Abstract unix sockets are scoped only
999 // when the network is refused, since scoping them breaks X11 and the
1000 // session bus for a command that was allowed to talk to the world
1001 // anyway.
1002 if self.abi.scopes() {
1003 let mut sc: BitFlags<Scope> = Scope::Signal.into();
1004 if !self.net {
1005 sc |= Scope::AbstractUnixSocket;
1006 }
1007 rs = res!(rs.scope(sc));
1008 }
1009
1010 let mut created = res!(rs.create());
1011
1012 // Each grant is opened here rather than through `path_beneath_rules`,
1013 // which silently drops a path it cannot open. Dropping fails in the safe
1014 // direction -- the path ends up denied -- but silently, and a fence that
1015 // quietly did less than it was told is the failure this file exists to
1016 // avoid.
1017 for g in &self.grants {
1018 let mut access = match g.level {
1019 Level::Rw => writable(abi),
1020 Level::Ro => AccessFs::from_read(abi),
1021 // A denied path carries no rule at all; it is absent from
1022 // `grants` by construction, and this arm is here so that adding
1023 // a level later cannot silently grant it.
1024 Level::Deny => continue,
1025 };
1026 // A regular file cannot carry the rights that only make sense for a
1027 // directory -- MAKE_REG, MAKE_DIR, REMOVE_FILE and the rest -- and
1028 // asking for them anyway is not merely useless: the ruleset comes
1029 // back PARTIALLY enforced, which this code correctly treats as a
1030 // failure, so a single granted file would refuse every command. The
1031 // carve makes granted files common rather than rare, since carving a
1032 // directory grants each of its children by name.
1033 if !g.path.is_dir() {
1034 access &= AccessFs::from_file(abi);
1035 }
1036 // `PathFd::new` follows symbolic links, so a rule is bound to
1037 // whatever the last component resolves to rather than to the path
1038 // that was planned. Every path in a plan is canonical by
1039 // construction, which means none of them is a link -- so if one is a
1040 // link now, the tree changed between the plan and this moment and
1041 // the fence is refused. The check does not close the race, since the
1042 // swap can happen between the lstat and the open; it shortens it
1043 // from "since the plan was made" to "within this statement", and
1044 // `Fence::holes` says the remainder out loud.
1045 res!(not_a_link(&g.path));
1046 let fd = match PathFd::new(&g.path) {
1047 Ok(fd) => fd,
1048 Err(e) => return Err(err!(
1049 "The fence cannot be built: {} was to be granted {} access \
1050 and could not be opened ({}). Nothing was applied.",
1051 g.path.display(), g.level.word(), e;
1052 IO, Path, Security)),
1053 };
1054 created = res!(created.add_rule(PathBeneath::new(fd, access)));
1055 }
1056
1057 // A carved directory gets a listing right and nothing else, where that
1058 // was asked for. See `Listing` for what it costs.
1059 if self.listing == Listing::Names {
1060 for dir in &self.sealed {
1061 res!(not_a_link(dir));
1062 let fd = match PathFd::new(dir) {
1063 Ok(fd) => fd,
1064 Err(e) => return Err(err!(
1065 "The fence cannot be built: the carved directory {} \
1066 could not be opened ({}).", dir.display(), e;
1067 IO, Path, Security)),
1068 };
1069 created = res!(created.add_rule(
1070 PathBeneath::new(fd, BitFlags::from(AccessFs::ReadDir))));
1071 }
1072 }
1073
1074 // Every thread, where that was asked for and the kernel can do it
1075 // atomically. See `Reach`.
1076 if self.reach == Reach::Process && self.abi.all_threads() {
1077 created = res!(created.all_threads(true));
1078 }
1079
1080 let status = res!(created.restrict_self());
1081 match status.ruleset {
1082 RulesetStatus::FullyEnforced => (),
1083 RulesetStatus::PartiallyEnforced => return Err(err!(
1084 "The kernel applied only part of the fence. The rules were built \
1085 for Landlock ABI {}, which is what this kernel reported it \
1086 supports, so a partial result means the two disagree. The \
1087 command was not run, rather than run behind a fence of unknown \
1088 shape.", self.abi.level();
1089 Mismatch, Security, System)),
1090 RulesetStatus::NotEnforced => return Err(err!(
1091 "The kernel applied none of the fence, although it reported \
1092 Landlock ABI {}. The command was not run.", self.abi.level();
1093 Mismatch, Security, System)),
1094 }
1095 if !status.no_new_privs {
1096 return Err(err!(
1097 "no_new_privs could not be set, so a setuid program inside the \
1098 fence could still gain privileges the fence does not bound. The \
1099 command was not run.";
1100 Security, System));
1101 }
1102
1103 let mut caps = vec![fmt!("fence:linux"), self.abi.cap()];
1104 if !self.net {
1105 caps.push(fmt!("net:denied-tcp"));
1106 }
1107 if self.abi.scopes() {
1108 caps.push(fmt!("scope:signal"));
1109 if !self.net {
1110 caps.push(fmt!("scope:unix-abstract"));
1111 }
1112 }
1113 if status.all_threads {
1114 caps.push(fmt!("landlock:all-threads"));
1115 }
1116 Ok(Applied {
1117 abi: self.abi,
1118 fenced: true,
1119 caps,
1120 waiver: None,
1121 })
1122 }
1123}
1124
1125/// What actually took hold, as opposed to what was asked for.
1126///
1127/// The two are kept apart because the difference is the only thing worth
1128/// reporting: a [`Plan`] is a wish and this is the answer.
1129#[derive(Clone, Debug, Eq, PartialEq)]
1130pub struct Applied {
1131 /// The ABI the rules were built for.
1132 pub abi: Abi,
1133 /// Whether a fence is in force at all.
1134 pub fenced: bool,
1135 /// The capability strings for the journal and the page.
1136 pub caps: Vec<String>,
1137 /// What the user acknowledged, where they waived the fence.
1138 pub waiver: Option<String>,
1139}
1140
1141// ┌───────────────────────────────────────────────────────────────┐
1142// │ Resolving a spec into rules │
1143// └───────────────────────────────────────────────────────────────┘
1144
1145/// What [`resolve`] worked out, before it becomes a [`Plan`].
1146///
1147/// Three lists rather than a tuple, because the third arrived later and a tuple
1148/// of three `Vec<PathBuf>`-shaped things is exactly where an argument gets
1149/// passed in the wrong order.
1150struct Resolved {
1151 /// The rules, canonical and de-duplicated.
1152 grants: Vec<Grant>,
1153 /// Directories that had to be carved, and so carry no grant of their own.
1154 sealed: Vec<PathBuf>,
1155 /// Children of a carved directory that were refused a grant because they do
1156 /// not resolve to themselves.
1157 dropped: Vec<PathBuf>,
1158}
1159
1160/// Turns a spec into the grants that express it, carving where it must.
1161///
1162/// # Arguments
1163/// * `spec` - What the caller asked for.
1164/// * `base` - Whether the read-only system base is added.
1165///
1166/// # Returns
1167/// The grants, the directories that had to be carved and the children that were
1168/// refused, or an error naming the path that could not be resolved.
1169fn resolve(spec: &FenceSpec, base: SysBase) -> Outcome<Resolved> {
1170 // Every path in one canonical form first. Two spellings of one directory
1171 // would defeat the ancestor comparisons the whole carve rests on, and a
1172 // symbolic link left unresolved would grant its target rather than itself.
1173 let mut want: BTreeMap<PathBuf, Level> = BTreeMap::new();
1174 for (paths, level) in [
1175 (&spec.rw, Level::Rw),
1176 (&spec.ro, Level::Ro),
1177 (&spec.deny, Level::Deny),
1178 ] {
1179 for raw in paths.iter() {
1180 let p = res!(canonical(raw, level));
1181 // The most restrictive wins where a path is named twice. A caller
1182 // listing a path as both writable and denied has contradicted
1183 // itself, and the reading that cannot leak is the strict one.
1184 match want.get(&p) {
1185 Some(existing) if *existing <= level => (),
1186 _ => { want.insert(p, level); },
1187 }
1188 }
1189 }
1190
1191 // The system base, added only where the caller did not speak about the path
1192 // itself. An explicit entry always wins: the base is a convenience and must
1193 // never quietly widen or narrow what was actually asked for.
1194 for raw in base.paths() {
1195 let real = match Path::new(raw).canonicalize() {
1196 Ok(r) => r,
1197 // Absent from this machine. Skipping tightens the fence, which is
1198 // the safe direction for something nobody asked for by name.
1199 Err(_) => continue,
1200 };
1201 want.entry(real).or_insert(Level::Ro);
1202 }
1203 // The writable half of the base, after the read-only half, so a device named
1204 // in both lists ends up writable. Same rule as above: an explicit entry from
1205 // the caller still wins, because `or_insert` does not overwrite one.
1206 for raw in base.write_paths() {
1207 let real = match Path::new(raw).canonicalize() {
1208 Ok(r) => r,
1209 Err(_) => continue,
1210 };
1211 want.entry(real).or_insert(Level::Rw);
1212 }
1213
1214 // Which paths have to be cut out of which. A path is cut out of its nearest
1215 // named ancestor whenever it carries less access than that ancestor does,
1216 // because Landlock takes the union walking upwards and a narrower rule
1217 // deeper down would read as an addition rather than a subtraction.
1218 let mut cuts: BTreeMap<PathBuf, Vec<PathBuf>> = BTreeMap::new();
1219 for (p, level) in want.iter() {
1220 if let Some(owner) = nearest_owner(&want, p) {
1221 let owner_level = match want.get(&owner) {
1222 Some(l) => *l,
1223 None => return Err(err!(
1224 "The fence's own bookkeeping lost {}.", owner.display(); Bug)),
1225 };
1226 if *level < owner_level {
1227 cuts.entry(owner).or_default().push(p.clone());
1228 }
1229 }
1230 }
1231
1232 let mut grants: Vec<Grant> = Vec::new();
1233 let mut sealed: Vec<PathBuf> = Vec::new();
1234 let mut dropped: Vec<PathBuf> = Vec::new();
1235 let none: Vec<PathBuf> = Vec::new();
1236 for (p, level) in want.iter() {
1237 if *level == Level::Deny {
1238 continue; // A denied path is expressed by the absence of a rule.
1239 }
1240 let mine = match cuts.get(p) {
1241 Some(v) => v.as_slice(),
1242 None => none.as_slice(),
1243 };
1244 res!(carve(p, p, mine, *level, &mut grants, &mut sealed, &mut dropped));
1245 }
1246
1247 // One rule per path, at the widest level anything asked for. The kernel
1248 // would union them anyway; doing it here makes the plan readable and keeps
1249 // the rule count down.
1250 let mut best: BTreeMap<PathBuf, Level> = BTreeMap::new();
1251 for g in grants {
1252 match best.get(&g.path) {
1253 Some(l) if *l >= g.level => (),
1254 _ => { best.insert(g.path, g.level); },
1255 }
1256 }
1257 let out = best.into_iter()
1258 .map(|(path, level)| Grant { path, level })
1259 .collect::<Vec<_>>();
1260 sealed.sort();
1261 sealed.dedup();
1262 dropped.sort();
1263 dropped.dedup();
1264 Ok(Resolved { grants: out, sealed, dropped })
1265}
1266
1267/// Grants `root`, or -- where something inside it must be withheld -- grants its
1268/// children one at a time instead.
1269///
1270/// This is the answer to the problem the module documentation states: Landlock
1271/// cannot subtract, so a directory holding something the command may not touch
1272/// cannot be granted at all. What can be granted is each of its children except
1273/// the one leading to the withheld thing, and then the same question again one
1274/// level down, until the withheld thing is reached.
1275///
1276/// # A carved child is never granted under the name it was found by
1277///
1278/// This is the single most dangerous line in the file, and it was wrong. The
1279/// enumeration below finds *names*; `PathFd::new` in [`Plan::apply_linux`] then
1280/// **follows symbolic links** and binds the rule to the inode it lands on. A
1281/// child called `escape` that is a link to `/home/u` therefore grants the whole
1282/// home directory, at whatever level the carved parent carries.
1283///
1284/// That is not a corner case. Every real fence carves the workspace, because
1285/// the spec always denies `.daimond` inside it -- and the workspace is exactly
1286/// the directory a command is *allowed to write to*. So a command need only
1287/// leave a symbolic link behind on one turn to be granted its target on the
1288/// next: deterministic, persistent, and chosen by the thing being fenced.
1289///
1290/// The rule here is therefore that a carved child is granted only if it resolves
1291/// to itself: `canonicalize` must return the same path it was given, and that
1292/// path must still lie under the root the carve started from. Anything else is
1293/// dropped and reported through [`Plan::caveats`]. Dropping rather than
1294/// resolving is deliberate -- granting a link's *target* would be granting a
1295/// path the spec never named, which is the same escape wearing a tidier hat.
1296///
1297/// Four things it does not cover, all consequences of enumerating a directory
1298/// at one moment in time, and all reported through [`Plan::caveats`] rather than
1299/// left for the user to discover:
1300///
1301/// * A child created after the fence was built has no rule, so it is
1302/// unreachable -- including by the command that just tried to create it.
1303/// * The carved directory itself cannot be listed. See [`Listing`].
1304/// * A file cannot be created directly in the carved directory, because creating
1305/// one needs `MAKE_REG` *on that directory*, and granting that would grant it
1306/// beneath the carved directory too -- which is exactly what the carve exists
1307/// to prevent.
1308/// * A child could be replaced by a symbolic link *between* this check and the
1309/// `PathFd::new` that opens it. The window is narrowed at both ends -- the
1310/// open re-checks with `symlink_metadata` first -- but it is not closed, and
1311/// [`Fence::holes`] says so.
1312///
1313/// # Arguments
1314/// * `root` - The path to grant.
1315/// * `top` - The outermost root this carve descends from; nothing may be granted
1316/// outside it.
1317/// * `cuts` - Paths strictly beneath it that must be withheld from this grant.
1318/// * `level` - What to grant.
1319/// * `grants` - Where the rules accumulate.
1320/// * `sealed` - Where carved directories are recorded.
1321/// * `dropped` - Where children that do not resolve to themselves are recorded.
1322fn carve(
1323 root: &Path,
1324 top: &Path,
1325 cuts: &[PathBuf],
1326 level: Level,
1327 grants: &mut Vec<Grant>,
1328 sealed: &mut Vec<PathBuf>,
1329 dropped: &mut Vec<PathBuf>,
1330)
1331 -> Outcome<()>
1332{
1333 if cuts.is_empty() {
1334 grants.push(Grant { path: root.to_path_buf(), level });
1335 return Ok(());
1336 }
1337 if !root.is_dir() {
1338 return Err(err!(
1339 "{} must be carved around {} path(s) inside it, but it is not a \
1340 directory, so there is nothing inside it to carve.",
1341 root.display(), cuts.len();
1342 Invalid, Path, Bug));
1343 }
1344 sealed.push(root.to_path_buf());
1345
1346 // Group the cuts by the child of `root` leading to each of them, so the walk
1347 // descends once per branch rather than once per cut.
1348 let mut branch: BTreeMap<OsString, Vec<PathBuf>> = BTreeMap::new();
1349 for c in cuts {
1350 let rel = match c.strip_prefix(root) {
1351 Ok(r) => r,
1352 Err(_) => return Err(err!(
1353 "{} was to be cut out of {}, which does not contain it.",
1354 c.display(), root.display();
1355 Bug, Path)),
1356 };
1357 let first = match rel.components().next() {
1358 Some(comp) => comp.as_os_str().to_os_string(),
1359 None => return Err(err!(
1360 "{} was to be cut out of itself.", root.display(); Bug, Path)),
1361 };
1362 branch.entry(first).or_default().push(c.clone());
1363 }
1364
1365 let entries = match std::fs::read_dir(root) {
1366 Ok(it) => it,
1367 Err(e) => return Err(err!(
1368 "{} could not be listed while building the fence around it ({}). \
1369 Nothing was applied.", root.display(), e;
1370 IO, Path)),
1371 };
1372 for entry in entries {
1373 let entry = res!(entry);
1374 let name = entry.file_name();
1375 let child = root.join(&name);
1376 match branch.get(&name) {
1377 // Nothing withheld down here: grant the child whole, but only if
1378 // the child is itself and not a pointer at something else.
1379 None => {
1380 if resolves_to_itself(&child, top) {
1381 grants.push(Grant { path: child, level });
1382 } else {
1383 dropped.push(child);
1384 }
1385 },
1386 Some(sub) => {
1387 // The cut itself. It carries its own level, applied where the
1388 // caller's own entry for it is handled, so nothing is granted
1389 // here -- and for a deny, nothing is granted anywhere.
1390 if sub.iter().any(|c| c.as_path() == child.as_path()) {
1391 continue;
1392 }
1393 // An intermediate directory on the way down. A cut is a
1394 // canonical path, so every component above it is already known
1395 // not to be a link; a failure here means the tree changed while
1396 // it was being read, and the fence is refused rather than built
1397 // around a guess.
1398 if !resolves_to_itself(&child, top) {
1399 return Err(err!(
1400 "{} lies on the way to something this fence must \
1401 withhold, and it stopped resolving to itself while the \
1402 rules were being built. Nothing was applied.",
1403 child.display();
1404 Conflict, Path, Security));
1405 }
1406 res!(carve(&child, top, sub, level, grants, sealed, dropped));
1407 },
1408 }
1409 }
1410 Ok(())
1411}
1412
1413/// Whether `p` is the thing it names, and still lies under `top`.
1414///
1415/// The test is deliberately strict: `canonicalize` resolves every symbolic link
1416/// and every `..` in the path, so a path that comes back unchanged is one that
1417/// no link is involved in. A link is the interesting case and the answer for it
1418/// is no; a path that has vanished since it was listed answers no as well, which
1419/// is the safe direction for something nobody can look at.
1420///
1421/// # Arguments
1422/// * `p` - The candidate, built by joining a canonical directory with one name.
1423/// * `top` - The root the carve started from.
1424fn resolves_to_itself(p: &Path, top: &Path) -> bool {
1425 match std::fs::canonicalize(p) {
1426 Ok(real) => real == p && real.starts_with(top),
1427 Err(_) => false,
1428 }
1429}
1430
1431/// The nearest strictly-enclosing path the spec named, if any.
1432///
1433/// "Nearest" and not "any", because the levels form a chain: a deny inside a
1434/// read-only attachment inside a writable workspace has to be cut out of the
1435/// read-only attachment, which is in turn cut out of the workspace. Comparing
1436/// against the outermost only would carve the wrong directory.
1437///
1438/// # Arguments
1439/// * `want` - Every path the spec named, with its level.
1440/// * `p` - The path whose owner is wanted.
1441fn nearest_owner(want: &BTreeMap<PathBuf, Level>, p: &Path) -> Option<PathBuf> {
1442 let mut at = p.parent();
1443 while let Some(dir) = at {
1444 if want.contains_key(dir) {
1445 return Some(dir.to_path_buf());
1446 }
1447 at = dir.parent();
1448 }
1449 None
1450}
1451
1452/// The canonical form of a path the caller named, or an error saying why not.
1453///
1454/// Symbolic links are resolved, so a granted path that is a link grants the
1455/// place it points at under the name it points at rather than under the name it
1456/// was written as -- which matters, because Landlock binds a rule to an inode
1457/// and comparing the written spellings would put the ancestor tests on the wrong
1458/// tree.
1459///
1460/// A missing path is an error for `rw` and `ro` and not for `deny`. A grant of
1461/// something absent would silently narrow the fence and leave the command
1462/// failing for a reason nobody can see; a deny of something absent is simply
1463/// satisfied, and its parent is carved regardless, so a thing of that name
1464/// cannot be created there later either.
1465///
1466/// # Arguments
1467/// * `raw` - The path as the spec spelled it.
1468/// * `level` - What it was named for.
1469fn canonical(raw: &str, level: Level) -> Outcome<PathBuf> {
1470 let p = Path::new(raw);
1471 if !p.is_absolute() {
1472 return Err(err!(
1473 "The fence was given the path {:?}, which is not absolute. The hand \
1474 does not interpret workspace-relative spellings; whatever resolved \
1475 them should send the result.", raw;
1476 Invalid, Input, Path));
1477 }
1478 match p.canonicalize() {
1479 Ok(c) => Ok(c),
1480 Err(e) => match level {
1481 // Nothing to resolve, so the lexical form stands. Its parent is
1482 // still carved, which is what makes the deny hold.
1483 Level::Deny => Ok(lexical(p)),
1484 _ => Err(err!(
1485 "The fence was told to grant {} access to {:?}, which cannot be \
1486 resolved ({}). Granting nothing there would leave the command \
1487 failing for a reason nobody could see, so the fence was refused \
1488 instead.", level.word(), raw, e;
1489 Invalid, Input, Path, NotFound)),
1490 },
1491 }
1492}
1493
1494/// A path with `.` dropped and `..` resolved against the text rather than the
1495/// filesystem.
1496///
1497/// Used only where the filesystem cannot answer: a path that does not exist, and
1498/// [`Plan::permits`] asking about one that may not exist yet. The lexical rule
1499/// is the one the app's `normalise` uses, and it is here for the same reason --
1500/// without it, `ws/.daimond/../attached` reads as being under `.daimond` when it
1501/// is not, and `ws/attached/../.daimond` reads as not being under `.daimond`
1502/// when it is.
1503///
1504/// # Arguments
1505/// * `p` - The path to normalise.
1506fn lexical(p: &Path) -> PathBuf {
1507 let mut out = PathBuf::new();
1508 for comp in p.components() {
1509 match comp {
1510 std::path::Component::CurDir => (),
1511 std::path::Component::ParentDir => { out.pop(); },
1512 other => out.push(other.as_os_str()),
1513 }
1514 }
1515 out
1516}
1517
1518/// Refuses a path that is a symbolic link, immediately before it is opened.
1519///
1520/// `symlink_metadata` is the `lstat` half of the pair: it describes the link
1521/// rather than what it points at, which is the whole question here.
1522///
1523/// # Arguments
1524/// * `p` - The path about to be opened for a rule.
1525#[cfg(target_os = "linux")]
1526fn not_a_link(p: &Path) -> Outcome<()> {
1527 let md = match std::fs::symlink_metadata(p) {
1528 Ok(md) => md,
1529 Err(e) => return Err(err!(
1530 "The fence cannot be built: {} could not be examined ({}) \
1531 immediately before it was to be opened. Nothing was applied.",
1532 p.display(), e;
1533 IO, Path, Security)),
1534 };
1535 if md.file_type().is_symlink() {
1536 return Err(err!(
1537 "The fence cannot be built: {} is a symbolic link, and a rule opened \
1538 through a link would be bound to whatever it points at rather than \
1539 to the path the fence planned. Every path in a plan is canonical, so \
1540 this one changed after the plan was made. Nothing was applied.",
1541 p.display();
1542 Conflict, Path, Security));
1543 }
1544 Ok(())
1545}
1546
1547// ┌───────────────────────────────────────────────────────────────┐
1548// │ Talking to Landlock │
1549// └───────────────────────────────────────────────────────────────┘
1550
1551/// Asks the kernel which Landlock ABI it offers, restricting nothing.
1552///
1553/// `RestrictSelf` with no flags set makes no `landlock_restrict_self` call at
1554/// all -- it carries the answer from the version query the kernel was asked when
1555/// the builder was made -- so this is a question and not a change.
1556///
1557/// The throwaway thread is for the one side effect that remains. The builder
1558/// sets `PR_SET_NO_NEW_PRIVS`, which is per-thread and inherited by anything
1559/// forked from that thread, and setting it on the hand's own thread would leave
1560/// every later child unable to gain privileges through a setuid program -- which
1561/// would silently break `sudo` for a command that had every right to use it. On
1562/// a thread that exists for the length of one question, it changes nothing.
1563///
1564/// The first draft of this probe built a real ruleset and applied it with
1565/// `no_new_privs(false)`, reasoning that a probe should not change what it
1566/// probes. The kernel refuses that outright with `EPERM`: Landlock will not
1567/// restrict a thread that has not set `no_new_privs` first. Every test that
1568/// needed a kernel then skipped, loudly, which is the only reason it was found.
1569#[cfg(target_os = "linux")]
1570fn probe_abi() -> Abi {
1571 let probe = std::thread::spawn(|| -> Abi {
1572 let status = match RestrictSelf::default().apply() {
1573 Ok(s) => s,
1574 Err(_) => return Abi::None,
1575 };
1576 match status.landlock {
1577 landlock::LandlockStatus::Available { effective_abi, kernel_abi } =>
1578 Abi::of_level(match kernel_abi {
1579 // The kernel is ahead of the crate; report its own number.
1580 Some(v) if v > 0 => v as u32,
1581 _ => effective_abi as u32,
1582 }),
1583 // Landlock is either absent or switched off; both mean no fence.
1584 _ => Abi::None,
1585 }
1586 });
1587 match probe.join() {
1588 Ok(abi) => abi,
1589 // A probe that could not finish is reported as no Landlock, which makes
1590 // the hand refuse. Guessing upwards here would be guessing in the one
1591 // direction that lets a command run unfenced.
1592 Err(_) => Abi::None,
1593 }
1594}
1595
1596// ── A writable grant does not include the right to make a symbolic link ─────
1597//
1598// A link is half of a leak, and a fenced command supplies exactly that half. It
1599// costs one call inside a folder the command may write, and the other half is
1600// supplied by whatever later reads the link -- an archiver, a packager, an
1601// uploader, a version control system. Ore is the case that was measured: it
1602// absorbs the CONTENT of a link that leaves the working copy, under the link's
1603// own path, into a signed history with no forget, and a global `post-commit`
1604// hook runs it from outside the fence on the owner's key. `ln -s
1605// ../../../outside/private.txt leak.txt` was enough, and the daimon needed no
1606// access to Ore at all.
1607//
1608// The obvious repair is to check what a link points at, and it is weaker twice
1609// over. It races a repoint between the check and the read, and it cannot see a
1610// `symlink(2)` a compiler makes rather than an `ln` a model runs. Withholding
1611// the capability has neither weakness, because there is no call left to make.
1612//
1613// The right is still HANDLED at the ruleset -- `AccessFs::from_all` covers it --
1614// which is what turns it from unrestricted into denied-unless-granted. What
1615// changes here is that no grant carries it, so `symlink(2)` answers
1616// `EACCES` everywhere, including in the command's own scratch directory.
1617//
1618// Nothing else narrows: reading, writing, creating, removing, renaming, hard
1619// linking and truncating are all as they were. The cost is stated in
1620// `Fence::holes`, because a command meeting "Permission denied" from `ln -s`
1621// deserves to find out why somewhere other than here.
1622
1623/// The rights a writable grant carries.
1624///
1625/// # Arguments
1626/// * `abi` - The ABI the rules are being built for.
1627#[cfg(target_os = "linux")]
1628fn writable(abi: ABI) -> BitFlags<AccessFs> {
1629 AccessFs::from_all(abi) & !BitFlags::from(AccessFs::MakeSym)
1630}
1631
1632/// The crate's ABI constant for a detected level, capped at what it knows.
1633///
1634/// A kernel newer than this build is asked only for the rights this build
1635/// understands. Asking for a right by a number nobody has checked is how a
1636/// fence acquires behaviour nobody intended.
1637///
1638/// # Arguments
1639/// * `abi` - The detected level.
1640#[cfg(target_os = "linux")]
1641fn ll_abi(abi: Abi) -> ABI {
1642 match abi {
1643 Abi::None => ABI::Unsupported,
1644 Abi::V1 => ABI::V1,
1645 Abi::V2 => ABI::V2,
1646 Abi::V3 => ABI::V3,
1647 Abi::V4 => ABI::V4,
1648 Abi::V5 => ABI::V5,
1649 Abi::V6 => ABI::V6,
1650 Abi::V7 => ABI::V7,
1651 Abi::V8 => ABI::V8,
1652 Abi::V9 => ABI::V9,
1653 Abi::Newer(_) => ABI::V9,
1654 }
1655}
1656
1657// ┌───────────────────────────────────────────────────────────────┐
1658// │ Tests │
1659// └───────────────────────────────────────────────────────────────┘
1660//
1661// Every kernel test does the forbidden thing FIRST, unfenced, and asserts that
1662// it worked. Only then is the fence applied, in the same thread, on the same
1663// file, and the same attempt asserted to fail. A test showing only the refusal
1664// would pass just as well against a path that never existed, a permission bit
1665// nobody set, or a fence that refuses everything -- which is to say it would
1666// prove nothing.
1667//
1668// Each kernel test runs its body on a thread it spawns itself, because
1669// `landlock_restrict_self` restricts the calling thread and there is no way to
1670// take a restriction off again. libtest gives a test its own thread only while
1671// it is running tests concurrently; under `--test-threads=1` it runs them on the
1672// main thread, where the first fence applied would silently be inherited by
1673// every test after it. Owning the thread makes the tests mean the same thing
1674// however the harness is invoked.
1675
1676#[cfg(test)]
1677mod tests {
1678 use super::*;
1679
1680 use std::{
1681 fs,
1682 net::{
1683 TcpListener,
1684 TcpStream,
1685 },
1686 };
1687
1688 /// Where the fixtures go.
1689 ///
1690 /// Under the home cache and never `/tmp`: that is a tmpfs here, its pages
1691 /// are charged to whoever wrote them, and filling it has taken this machine
1692 /// down before.
1693 fn root() -> Outcome<PathBuf> {
1694 let home = match std::env::var("HOME") {
1695 Ok(h) => h,
1696 Err(e) => return Err(err!(
1697 "The fence tests need HOME to know where to put fixtures: {}", e;
1698 Test, Configuration)),
1699 };
1700 Ok(PathBuf::from(home).join(".cache/daimond-hand-fence-tests"))
1701 }
1702
1703 /// A fresh workspace: an `rw` root, a `deny` subtree inside it, an `ro`
1704 /// attachment, and a directory outside the fence entirely.
1705 ///
1706 /// # Arguments
1707 /// * `name` - A name unique to the calling test, so tests do not share state.
1708 fn fixture(name: &str) -> Outcome<PathBuf> {
1709 let base = res!(root()).join(name);
1710 let _ = fs::remove_dir_all(&base);
1711 res!(fs::create_dir_all(base.join("ws/.daimond")));
1712 res!(fs::create_dir_all(base.join("ws/sub/deep")));
1713 res!(fs::create_dir_all(base.join("ws/refs")));
1714 res!(fs::create_dir_all(base.join("outside")));
1715 res!(fs::write(base.join("ws/ok.txt"), "ok"));
1716 res!(fs::write(base.join("ws/.daimond/secret.txt"), "secret"));
1717 res!(fs::write(base.join("ws/sub/deep/x.txt"), "deep"));
1718 res!(fs::write(base.join("ws/refs/note.md"), "note"));
1719 res!(fs::write(base.join("outside/other.txt"), "other"));
1720 Ok(base)
1721 }
1722
1723 /// The spec the fixture is built for: workspace writable, `refs` read-only,
1724 /// `.daimond` denied, no network.
1725 ///
1726 /// # Arguments
1727 /// * `base` - The fixture root.
1728 fn spec(base: &Path) -> FenceSpec {
1729 FenceSpec {
1730 rw: vec![fmt!("{}", base.join("ws").display())],
1731 ro: vec![fmt!("{}", base.join("ws/refs").display())],
1732 deny: vec![fmt!("{}", base.join("ws/.daimond").display())],
1733 net: false,
1734 }
1735 }
1736
1737 /// A Linux fence at a stated ABI, for the tests that decide rules rather
1738 /// than apply them.
1739 ///
1740 /// # Arguments
1741 /// * `abi` - The level to pretend to.
1742 fn planner(abi: Abi) -> Fence {
1743 Fence::Linux {
1744 abi,
1745 listing: Listing::Sealed,
1746 base: SysBase::Bare,
1747 }
1748 }
1749
1750 /// The real fence, or `None` with a printed reason where this kernel cannot
1751 /// run the test.
1752 ///
1753 /// Loud rather than silent: a kernel test that quietly passes on a machine
1754 /// that never ran it is a test that will quietly pass forever.
1755 ///
1756 /// # Arguments
1757 /// * `what` - The test's name, for the message.
1758 fn kernel_fence(what: &str) -> Option<Fence> {
1759 let f = Fence::detect();
1760 match &f {
1761 Fence::Linux { abi, .. } => {
1762 println!("[{}] running against {}", what, abi.cap());
1763 Some(f)
1764 },
1765 other => {
1766 println!(
1767 "[{}] SKIPPED: this machine has no fence to test. The hand \
1768 reports caps {:?}. Run this on Linux 5.13 or later with \
1769 Landlock enabled.", what, other.caps());
1770 None
1771 },
1772 }
1773 }
1774
1775 /// Runs a test body on a thread of its own, so the fence it applies dies
1776 /// with it.
1777 ///
1778 /// A panic inside becomes an error rather than a lost thread, so a failed
1779 /// assertion still fails the test it belongs to.
1780 ///
1781 /// # Arguments
1782 /// * `what` - The test's name, for the error.
1783 /// * `body` - The unfenced half, the fence, and the fenced half.
1784 fn own_thread<F>(what: &'static str, body: F) -> Outcome<()>
1785 where
1786 F: FnOnce() -> Outcome<()> + Send + 'static,
1787 {
1788 match std::thread::spawn(body).join() {
1789 Ok(result) => result,
1790 Err(panic) => {
1791 let msg = match panic.downcast_ref::<&str>() {
1792 Some(s) => s.to_string(),
1793 None => match panic.downcast_ref::<String>() {
1794 Some(s) => s.clone(),
1795 None => fmt!("(the panic carried no message)"),
1796 },
1797 };
1798 Err(err!("{}: {}", what, msg; Test))
1799 },
1800 }
1801 }
1802
1803 /// Applies the spec to this thread, failing loudly rather than continuing
1804 /// unfenced.
1805 ///
1806 /// # Arguments
1807 /// * `f` - The fence.
1808 /// * `s` - What to apply.
1809 fn engage(f: &Fence, s: &FenceSpec) -> Outcome<Applied> {
1810 let mut plan = res!(f.plan(s, &Unfenced::Refuse));
1811 // The rules are exactly the production ones; only their reach is
1812 // narrowed. `Reach::Process` is what the launcher uses and it is
1813 // untestable from inside a harness -- the first test to apply it would
1814 // fence every test that had not run yet, including this one's siblings.
1815 plan.reach = Reach::Thread;
1816 let applied = res!(plan.apply());
1817 if !applied.fenced {
1818 return Err(err!(
1819 "The fence reported that it was not applied, so the rest of this \
1820 test would prove nothing."; Test, Security));
1821 }
1822 Ok(applied)
1823 }
1824
1825 // ── The carve, decided without a kernel ─────────────────────────
1826
1827 /// A deny inside an `rw` root becomes an absence of a rule on the parent and
1828 /// a rule on each of its other children.
1829 ///
1830 /// This is the shape the whole file exists for, so it is asserted directly
1831 /// rather than only through its effects.
1832 #[test]
1833 fn deny_inside_rw_carves_the_parent() -> Outcome<()> {
1834 let base = res!(fixture("carve-shape"));
1835 let ws = res!(base.join("ws").canonicalize());
1836 let plan = res!(planner(Abi::V5).plan(&spec(&base), &Unfenced::Refuse));
1837
1838 // The workspace itself must NOT be granted. Granting it and then adding
1839 // a narrower rule underneath is the mistake this file is about.
1840 assert!(
1841 !plan.grants.iter().any(|g| g.path == ws),
1842 "the carved parent was granted whole: {:?}", plan.grants);
1843 assert!(plan.sealed.contains(&ws), "the carved parent was not reported");
1844
1845 // Its children are granted one by one, except the denied one.
1846 let granted = |rel: &str| -> bool {
1847 plan.grants.iter().any(|g| g.path == ws.join(rel))
1848 };
1849 assert!(granted("ok.txt"), "{:?}", plan.grants);
1850 assert!(granted("sub"), "{:?}", plan.grants);
1851 assert!(granted("refs"), "{:?}", plan.grants);
1852 assert!(!granted(".daimond"), "the denied subtree was granted");
1853
1854 // And nothing anywhere grants anything inside the denied subtree.
1855 let deny = ws.join(".daimond");
1856 assert!(
1857 !plan.grants.iter().any(|g| g.path.starts_with(&deny)),
1858 "a rule reaches inside the denied subtree: {:?}", plan.grants);
1859 Ok(())
1860 }
1861
1862 /// A read-only path inside a writable one is carved too.
1863 ///
1864 /// The easy bug: `diamond_bounds` expresses a read-only attachment as an
1865 /// allow plus a write fence, and if that attachment sits inside a writable
1866 /// one then adding a read-only rule achieves nothing, because Landlock takes
1867 /// the union walking upwards. The read-only half would silently not hold.
1868 #[test]
1869 fn ro_inside_rw_is_carved_not_merely_added() -> Outcome<()> {
1870 let base = res!(fixture("carve-ro"));
1871 let ws = res!(base.join("ws").canonicalize());
1872 let refs = ws.join("refs");
1873 let plan = res!(planner(Abi::V5).plan(&spec(&base), &Unfenced::Refuse));
1874
1875 // `refs` is granted, and only read-only.
1876 let g = match plan.grants.iter().find(|g| g.path == refs) {
1877 Some(g) => g,
1878 None => return Err(err!("refs was not granted at all"; Test)),
1879 };
1880 assert_eq!(Level::Ro, g.level);
1881
1882 // No rule above it grants write over it. That is the property; the rule
1883 // on `refs` alone would not deliver it.
1884 for other in &plan.grants {
1885 if other.path != refs && refs.starts_with(&other.path) {
1886 assert!(
1887 other.level < Level::Rw,
1888 "{} grants {} over the read-only {}",
1889 other.path.display(), other.level.word(), refs.display());
1890 }
1891 }
1892 Ok(())
1893 }
1894
1895 /// A relative path, and a grant of something absent, are refused rather than
1896 /// quietly dropped.
1897 #[test]
1898 fn bad_specs_are_refused() -> Outcome<()> {
1899 let f = planner(Abi::V5);
1900 let relative = FenceSpec {
1901 rw: vec![fmt!("ws")],
1902 ..Default::default()
1903 };
1904 assert!(f.plan(&relative, &Unfenced::Refuse).is_err(),
1905 "a relative path was accepted");
1906
1907 let missing = FenceSpec {
1908 rw: vec![fmt!("/nowhere/at/all/{}", 0)],
1909 ..Default::default()
1910 };
1911 assert!(f.plan(&missing, &Unfenced::Refuse).is_err(),
1912 "a grant of a path that does not exist was accepted");
1913
1914 // A deny of something absent is fine: there is nothing to resolve, and
1915 // the carve of its parent is what makes it hold.
1916 let base = res!(fixture("absent-deny"));
1917 let absent = FenceSpec {
1918 rw: vec![fmt!("{}", base.join("ws").display())],
1919 deny: vec![fmt!("{}", base.join("ws/never-made").display())],
1920 ..Default::default()
1921 };
1922 let plan = res!(f.plan(&absent, &Unfenced::Refuse));
1923 let ws = res!(base.join("ws").canonicalize());
1924 assert!(plan.sealed.contains(&ws),
1925 "a deny of an absent path did not carve its parent");
1926 Ok(())
1927 }
1928
1929 /// No fence means no command, and the way past it has to be written down.
1930 #[test]
1931 fn no_fence_refuses_by_default() -> Outcome<()> {
1932 let s = FenceSpec { net: true, ..Default::default() };
1933 for f in [
1934 Fence::None { why: fmt!("Nothing here.") },
1935 Fence::MacOs,
1936 Fence::Windows,
1937 ] {
1938 assert!(f.plan(&s, &Unfenced::Refuse).is_err(),
1939 "{:?} ran a command with no fence", f);
1940
1941 // The refusal names what is missing, so the sentence is usable.
1942 let words = f.refusal("A build");
1943 assert!(words.contains("A build"), "{}", words);
1944 match f {
1945 Fence::MacOs => assert!(words.contains("sandbox_exec"), "{}", words),
1946 Fence::Windows => assert!(
1947 words.contains("Job Object") && words.contains("AppContainer"),
1948 "{}", words),
1949 _ => (),
1950 }
1951
1952 // And the opt-out works, carries what the user agreed to, and says
1953 // plainly that nothing is fenced.
1954 let plan = res!(f.plan(&s, &Unfenced::Allow {
1955 acknowledged: fmt!("I know this runs unfenced."),
1956 }));
1957 assert!(!plan.is_fenced());
1958 let applied = res!(plan.apply());
1959 assert!(!applied.fenced);
1960 assert!(applied.caps.contains(&fmt!("fence:none")));
1961 assert!(plan.caveats().iter().any(|c| c.contains("no fence at all")));
1962 }
1963 Ok(())
1964 }
1965
1966 /// Asking for no network on a kernel too old for network rules is refused,
1967 /// rather than answered with a filesystem fence wearing the wrong label.
1968 #[test]
1969 fn no_net_on_an_old_abi_is_refused() -> Outcome<()> {
1970 let base = res!(fixture("old-abi"));
1971 let f = planner(Abi::V3); // no network rules before ABI 4
1972 assert!(f.plan(&spec(&base), &Unfenced::Refuse).is_err(),
1973 "net:false was accepted on an ABI that cannot honour it");
1974
1975 // The same kernel is fine for a command that wanted the network anyway.
1976 let open = FenceSpec { net: true, ..spec(&base) };
1977 assert!(f.plan(&open, &Unfenced::Refuse).is_ok());
1978 Ok(())
1979 }
1980
1981 /// The capability report says "no fence" out loud rather than saying nothing.
1982 #[test]
1983 fn caps_never_stay_silent() -> Outcome<()> {
1984 for f in [
1985 Fence::None { why: fmt!("x") },
1986 Fence::MacOs,
1987 Fence::Windows,
1988 ] {
1989 let caps = f.caps();
1990 assert!(caps.contains(&fmt!("fence:none")), "{:?}", caps);
1991 assert!(!f.holes(None).is_empty(), "{:?}", f);
1992 }
1993 let linux = Fence::Linux {
1994 abi: Abi::V8,
1995 listing: Listing::Sealed,
1996 base: SysBase::Minimal,
1997 };
1998 assert!(linux.caps().contains(&fmt!("landlock:abi-8")));
1999
2000 // Each of these is a measured hole, and each was missing from the list
2001 // at some point while the list was being believed. Naming them
2002 // individually is the point: `holes()` is the honest half of `caps()`,
2003 // and a hole nobody wrote down is indistinguishable from a hole nobody
2004 // has.
2005 let holes = linux.holes(None);
2006 let said = |needle: &str| -> bool {
2007 holes.iter().any(|h| h.contains(needle))
2008 };
2009 assert!(said("pathname unix socket"), "{:?}", holes);
2010 // And it must say what that actually costs, not merely that it exists.
2011 assert!(said("systemd-run"), "the unix-socket hole is understated: {:?}", holes);
2012 assert!(said("ssh-agent"), "{:?}", holes);
2013 assert!(said("chmod"), "metadata syscalls are not mentioned: {:?}", holes);
2014 assert!(said("setxattr"), "{:?}", holes);
2015 assert!(said("stat"), "metadata reads are not mentioned: {:?}", holes);
2016 assert!(said("/etc"), "the breadth of the system base is not stated: {:?}", holes);
2017 assert!(said("symbolic link"), "the open-time race is not stated: {:?}", holes);
2018
2019 // The two the filter closes must STOP being claimed once it is installed,
2020 // and only those two. A list that goes on describing a shut hole is as
2021 // dishonest as one that leaves an open one out, and this list is what the
2022 // consent window's wording is drawn from.
2023 let shut = linux.holes(Some(&crate::seccomp::Spec::for_command()));
2024 let now = |needle: &str| -> bool {
2025 shut.iter().any(|h| h.contains(needle))
2026 };
2027 assert!(!now("systemd-run"),
2028 "the session bus is refused and holes() still claims it: {:?}", shut);
2029 assert!(!now("chmod"),
2030 "the metadata calls are refused and holes() still claims them: {:?}", shut);
2031 // Everything the filter does NOT close is still said, in the same words.
2032 assert!(now("stat"), "metadata READS are not the filter's to close: {:?}", shut);
2033 assert!(now("/etc"), "{:?}", shut);
2034 assert!(now("symbolic link"), "{:?}", shut);
2035 assert!(now("UDP"), "{:?}", shut);
2036 assert!(now("File descriptors opened before"), "{:?}", shut);
2037 assert_eq!(holes.len(), shut.len() + 2,
2038 "exactly two entries should have gone: {:?} -> {:?}", holes, shut);
2039
2040 // A spec that refuses nothing closes nothing, so the list is the full one
2041 // again. The filter's presence is not what matters; what it refuses is.
2042 let idle = crate::seccomp::Spec {
2043 meta: crate::seccomp::Meta::Allow,
2044 unix: crate::seccomp::Unix::Allow,
2045 ring: crate::seccomp::Ring::Refuse,
2046 poke: crate::seccomp::Poke::Refuse,
2047 };
2048 assert_eq!(holes.len(), linux.holes(Some(&idle)).len(),
2049 "a filter that refuses neither still shortened the list");
2050 Ok(())
2051 }
2052
2053 // ── The symlink escape ──────────────────────────────────────────
2054
2055 /// A symbolic link in a carved directory must not be granted.
2056 ///
2057 /// The escape this proves, in the order it happens on a real machine: the
2058 /// spec always denies `.daimond` inside the workspace, so the workspace is
2059 /// always carved and its children granted one by one. The workspace is also
2060 /// the one place a command may write. So a command drops a link there on one
2061 /// turn -- `ln -s /home/you ws/escape` -- and on the next turn the carve
2062 /// enumerates it, `PathFd::new` follows it, and the rule binds to the home
2063 /// directory's inode at the workspace's own level. Read and write, on
2064 /// everything, chosen by the thing being fenced.
2065 ///
2066 /// Asserted at both ends. The plan must not carry the link, and the kernel
2067 /// must refuse the target -- because a plan that looks right and a fence that
2068 /// is wrong is the failure mode the whole file is written against.
2069 #[test]
2070 fn a_symlink_in_a_carved_directory_is_not_granted() -> Outcome<()> {
2071 let base = res!(fixture("symlink-carve"));
2072 let ws = res!(base.join("ws").canonicalize());
2073 let outside = res!(base.join("outside").canonicalize());
2074
2075 // The link a command could leave behind on any turn it can write.
2076 let link = base.join("ws/escape");
2077 res!(std::os::unix::fs::symlink(&outside, &link));
2078
2079 let plan = res!(planner(Abi::V5).plan(&spec(&base), &Unfenced::Refuse));
2080
2081 // Nothing is granted under the name of the link.
2082 assert!(
2083 !plan.grants.iter().any(|g| g.path == ws.join("escape")),
2084 "the link itself was granted: {:?}", plan.grants);
2085 // And nothing is granted at what it points at, which is the form the
2086 // bug actually took: the rule is bound to the target's inode.
2087 assert!(
2088 !plan.grants.iter().any(|g| g.path == outside || outside.starts_with(&g.path)),
2089 "the link's target was granted: {:?}", plan.grants);
2090 // The user is told, rather than left to wonder why the link is dead.
2091 assert!(plan.dropped.contains(&ws.join("escape")),
2092 "the dropped link was not reported: {:?}", plan.dropped);
2093 assert!(plan.caveats().iter().any(|c| c.contains("escape")),
2094 "the caveat did not name the link: {:?}", plan.caveats());
2095 Ok(())
2096 }
2097
2098 /// The same escape, refused by the kernel rather than by the plan.
2099 #[test]
2100 fn a_symlink_escape_is_refused_by_the_kernel() -> Outcome<()> {
2101 let f = match kernel_fence("a_symlink_escape_is_refused_by_the_kernel") {
2102 Some(f) => f,
2103 None => return Ok(()),
2104 };
2105 own_thread("a_symlink_escape_is_refused_by_the_kernel", move || {
2106 let base = res!(fixture("symlink-kernel"));
2107 let outside = res!(base.join("outside").canonicalize());
2108 let link = base.join("ws/escape");
2109 res!(std::os::unix::fs::symlink(&outside, &link));
2110
2111 let direct = base.join("outside/other.txt");
2112 let through = link.join("other.txt");
2113
2114 // Broken first: unfenced, the file reads both ways round. Without
2115 // this half the test would pass against a link that never worked.
2116 assert_eq!("other", res!(fs::read_to_string(&direct)));
2117 assert_eq!("other", res!(fs::read_to_string(&through)));
2118
2119 res!(engage(&f, &spec(&base)));
2120
2121 assert!(fs::read_to_string(&through).is_err(),
2122 "a symbolic link in the workspace still reached outside the \
2123 fence: this is the escape, and it is open");
2124 assert!(fs::read_to_string(&direct).is_err(),
2125 "the link's target was granted under its own name");
2126 assert!(fs::write(link.join("planted.txt"), "x").is_err(),
2127 "a symbolic link in the workspace granted write outside the fence");
2128
2129 // The rest of the workspace still works, so the refusals above are
2130 // the fence holding rather than the fence refusing everything.
2131 assert_eq!("ok", res!(fs::read_to_string(base.join("ws/ok.txt"))));
2132 Ok(())
2133 })
2134 }
2135
2136 /// A link is dropped even when it points somewhere the fence already allows.
2137 ///
2138 /// The safe direction, and it costs something: the target is reachable by
2139 /// its own path and not by the link's. Asserted so that a later change
2140 /// "fixing" the inconvenience has to argue with a test rather than with a
2141 /// comment.
2142 #[test]
2143 fn a_link_pointing_inside_the_fence_is_dropped_too() -> Outcome<()> {
2144 let base = res!(fixture("symlink-inward"));
2145 let ws = res!(base.join("ws").canonicalize());
2146 res!(std::os::unix::fs::symlink(ws.join("sub"), base.join("ws/shortcut")));
2147
2148 let plan = res!(planner(Abi::V5).plan(&spec(&base), &Unfenced::Refuse));
2149 assert!(!plan.grants.iter().any(|g| g.path == ws.join("shortcut")),
2150 "an inward link was granted: {:?}", plan.grants);
2151 assert!(plan.dropped.contains(&ws.join("shortcut")));
2152 // The target keeps its own grant, so nothing real was lost.
2153 assert!(plan.grants.iter().any(|g| g.path == ws.join("sub")),
2154 "the link's target lost its own grant: {:?}", plan.grants);
2155 Ok(())
2156 }
2157
2158 /// An uncarved root that *is* a link is resolved, not dropped.
2159 ///
2160 /// The distinction matters and is easy to collapse. A path the *spec* named
2161 /// goes through `canonical`, which resolves it, because the user chose it. A
2162 /// path found by *enumerating* a carved directory was chosen by whatever
2163 /// could write there, which is the command. Same mechanism, opposite
2164 /// answers.
2165 #[test]
2166 fn a_spec_named_link_is_still_resolved() -> Outcome<()> {
2167 let base = res!(fixture("symlink-spec"));
2168 let real = res!(base.join("ws/sub").canonicalize());
2169 let named = base.join("link-to-sub");
2170 res!(std::os::unix::fs::symlink(&real, &named));
2171
2172 let s = FenceSpec {
2173 rw: vec![fmt!("{}", named.display())],
2174 ..Default::default()
2175 };
2176 let plan = res!(planner(Abi::V5).plan(&s, &Unfenced::Refuse));
2177 assert!(plan.grants.iter().any(|g| g.path == real && g.level == Level::Rw),
2178 "a spec-named link was not resolved to its target: {:?}", plan.grants);
2179 assert!(plan.dropped.is_empty(), "{:?}", plan.dropped);
2180 Ok(())
2181 }
2182
2183 // ── The same rules, proved against the kernel ───────────────────
2184
2185 /// A file outside the fence: readable now, refused once fenced.
2186 #[test]
2187 fn outside_the_fence_becomes_unreadable() -> Outcome<()> {
2188 let f = match kernel_fence("outside_the_fence_becomes_unreadable") {
2189 Some(f) => f,
2190 None => return Ok(()),
2191 };
2192 own_thread("outside_the_fence_becomes_unreadable", move || {
2193 let base = res!(fixture("outside"));
2194 let target = base.join("outside/other.txt");
2195
2196 // Broken first: unfenced, this works.
2197 assert_eq!("other", res!(fs::read_to_string(&target)));
2198
2199 res!(engage(&f, &spec(&base)));
2200 assert!(fs::read_to_string(&target).is_err(),
2201 "a file outside every root was still readable");
2202 Ok(())
2203 })
2204 }
2205
2206 /// A read-only root: writable now, refused once fenced, still readable.
2207 #[test]
2208 fn a_read_only_root_stops_accepting_writes() -> Outcome<()> {
2209 let f = match kernel_fence("a_read_only_root_stops_accepting_writes") {
2210 Some(f) => f,
2211 None => return Ok(()),
2212 };
2213 own_thread("a_read_only_root_stops_accepting_writes", move || {
2214 let base = res!(fixture("read-only"));
2215 let target = base.join("ws/refs/note.md");
2216
2217 // Broken first.
2218 res!(fs::write(&target, "written before the fence"));
2219
2220 res!(engage(&f, &spec(&base)));
2221 assert!(fs::write(&target, "written after").is_err(),
2222 "a read-only root accepted a write");
2223 // And it is genuinely read-only rather than simply unreachable,
2224 // which is the difference between a fence and a mistake.
2225 assert!(fs::read_to_string(&target).is_ok(),
2226 "a read-only root stopped being readable");
2227 Ok(())
2228 })
2229 }
2230
2231 /// The one most likely to be quietly broken: a denied subtree inside a
2232 /// writable parent.
2233 ///
2234 /// Landlock cannot subtract, so this holds only if the parent was never
2235 /// granted. Both directions are asserted -- the denied subtree is refused
2236 /// and the rest of the same parent still works -- because a fence that
2237 /// refused everything would pass the first half on its own.
2238 #[test]
2239 fn a_denied_subtree_inside_a_writable_parent_is_refused() -> Outcome<()> {
2240 let f = match kernel_fence(
2241 "a_denied_subtree_inside_a_writable_parent_is_refused") {
2242 Some(f) => f,
2243 None => return Ok(()),
2244 };
2245 own_thread("a_denied_subtree_inside_a_writable_parent_is_refused", move || {
2246 let base = res!(fixture("deny-in-rw"));
2247 let secret = base.join("ws/.daimond/secret.txt");
2248 let ok = base.join("ws/ok.txt");
2249 let deep = base.join("ws/sub/deep/x.txt");
2250
2251 // Broken first: unfenced, the denied file reads and writes.
2252 assert_eq!("secret", res!(fs::read_to_string(&secret)));
2253 res!(fs::write(&secret, "secret"));
2254
2255 res!(engage(&f, &spec(&base)));
2256 assert!(fs::read_to_string(&secret).is_err(),
2257 "the denied subtree was still readable inside a writable parent");
2258 assert!(fs::write(&secret, "z").is_err(),
2259 "the denied subtree was still writable inside a writable parent");
2260 assert!(fs::read_dir(base.join("ws/.daimond")).is_err(),
2261 "the denied subtree could still be listed");
2262
2263 // The rest of the workspace is untouched, so the refusals above are
2264 // the fence working rather than the fence breaking.
2265 assert_eq!("ok", res!(fs::read_to_string(&ok)));
2266 res!(fs::write(&ok, "ok"));
2267 assert_eq!("deep", res!(fs::read_to_string(&deep)));
2268 Ok(())
2269 })
2270 }
2271
2272 /// A read-only attachment inside a writable workspace really is read-only.
2273 ///
2274 /// Proving the carve rather than the rule: adding a read-only rule under a
2275 /// writable one does nothing, so a fence taking the easy route would fail
2276 /// here and nowhere else.
2277 #[test]
2278 fn a_read_only_attachment_inside_a_writable_workspace_holds() -> Outcome<()> {
2279 let f = match kernel_fence(
2280 "a_read_only_attachment_inside_a_writable_workspace_holds") {
2281 Some(f) => f,
2282 None => return Ok(()),
2283 };
2284 own_thread("a_read_only_attachment_inside_a_writable_workspace_holds", move || {
2285 let base = res!(fixture("ro-in-rw"));
2286 let note = base.join("ws/refs/note.md");
2287
2288 // Broken first.
2289 res!(fs::write(&note, "note"));
2290
2291 res!(engage(&f, &spec(&base)));
2292 assert!(fs::write(&note, "changed").is_err(),
2293 "a read-only attachment inside a writable workspace accepted a \
2294 write");
2295 assert_eq!("note", res!(fs::read_to_string(&note)));
2296 Ok(())
2297 })
2298 }
2299
2300 /// A carved directory cannot be listed, and nothing can be made in it.
2301 ///
2302 /// Asserted rather than merely documented, because these are the costs of
2303 /// the carve, and a change quietly removing them would have quietly opened
2304 /// the denied subtree.
2305 #[test]
2306 fn a_carved_directory_is_sealed() -> Outcome<()> {
2307 let f = match kernel_fence("a_carved_directory_is_sealed") {
2308 Some(f) => f,
2309 None => return Ok(()),
2310 };
2311 own_thread("a_carved_directory_is_sealed", move || {
2312 let base = res!(fixture("sealed"));
2313 let ws = base.join("ws");
2314
2315 // Broken first.
2316 assert!(res!(fs::read_dir(&ws)).count() > 0);
2317 res!(fs::write(ws.join("made-before.txt"), "x"));
2318
2319 let mut plan = res!(f.plan(&spec(&base), &Unfenced::Refuse));
2320 plan.reach = Reach::Thread; // see `engage`
2321 let caveats = plan.caveats();
2322 assert!(caveats.iter().any(|c| c.contains("cannot be listed")),
2323 "the cost of the carve was not reported: {:?}", caveats);
2324 assert!(caveats.iter().any(|c| c.contains("invisible")),
2325 "the after-the-fact child caveat was not reported: {:?}", caveats);
2326 let applied = res!(plan.apply());
2327 assert!(applied.fenced);
2328
2329 assert!(fs::read_dir(&ws).is_err(),
2330 "a carved directory could still be listed");
2331 assert!(fs::write(ws.join("made-after.txt"), "x").is_err(),
2332 "a file could be created directly in a carved directory");
2333 // A child that existed when the fence was built is still fine.
2334 assert_eq!("x", res!(fs::read_to_string(ws.join("made-before.txt"))));
2335 Ok(())
2336 })
2337 }
2338
2339 /// With `net: false`, a TCP connection that worked a moment ago is refused.
2340 ///
2341 /// Loopback, and to a listener this test owns, so the result does not depend
2342 /// on the machine having a route to anywhere.
2343 #[test]
2344 fn net_false_stops_tcp() -> Outcome<()> {
2345 let f = match kernel_fence("net_false_stops_tcp") {
2346 Some(f) => f,
2347 None => return Ok(()),
2348 };
2349 if let Fence::Linux { abi, .. } = &f {
2350 if !abi.fences_tcp() {
2351 println!(
2352 "[net_false_stops_tcp] SKIPPED: Landlock ABI {} has no \
2353 network rules; they arrived at ABI 4 in Linux 6.7.",
2354 abi.level());
2355 return Ok(());
2356 }
2357 }
2358 own_thread("net_false_stops_tcp", move || {
2359 let base = res!(fixture("net"));
2360 // The listener lives on another thread, unfenced, so that what is
2361 // being tested is the fenced thread's ability to reach it.
2362 let listener = res!(TcpListener::bind("127.0.0.1:0"));
2363 let port = res!(listener.local_addr()).port();
2364 std::thread::spawn(move || {
2365 for stream in listener.incoming() {
2366 drop(stream);
2367 }
2368 });
2369
2370 // Broken first: unfenced, the connection is made.
2371 let first = TcpStream::connect(("127.0.0.1", port));
2372 assert!(first.is_ok(),
2373 "the test's own listener was unreachable: {:?}", first);
2374 drop(first);
2375
2376 res!(engage(&f, &spec(&base)));
2377 assert!(TcpStream::connect(("127.0.0.1", port)).is_err(),
2378 "a fenced command with net:false still opened a TCP connection");
2379 assert!(TcpListener::bind("127.0.0.1:0").is_err(),
2380 "a fenced command with net:false still bound a TCP port");
2381 Ok(())
2382 })
2383 }
2384
2385 /// The fence survives `execve`, which is the whole reason it can be applied
2386 /// by a launcher that then becomes the command.
2387 #[test]
2388 fn the_fence_is_inherited_by_a_real_program() -> Outcome<()> {
2389 let f = match kernel_fence("the_fence_is_inherited_by_a_real_program") {
2390 Some(f) => f,
2391 None => return Ok(()),
2392 };
2393 let cat = Path::new("/bin/cat");
2394 if !cat.exists() {
2395 println!(
2396 "[the_fence_is_inherited_by_a_real_program] SKIPPED: no \
2397 /bin/cat on this machine to run inside the fence.");
2398 return Ok(());
2399 }
2400 own_thread("the_fence_is_inherited_by_a_real_program", move || {
2401 let base = res!(fixture("exec"));
2402 let inside = base.join("ws/ok.txt");
2403 let secret = base.join("ws/.daimond/secret.txt");
2404 let outside = base.join("outside/other.txt");
2405
2406 // Broken first: unfenced, `cat` reads all three.
2407 for p in [&inside, &secret, &outside] {
2408 let out = res!(std::process::Command::new("/bin/cat").arg(p).output());
2409 assert!(out.status.success(),
2410 "cat {} failed before the fence", p.display());
2411 }
2412
2413 // The system base is what makes there be a program to run at all.
2414 let fenced = Fence::Linux {
2415 abi: match &f {
2416 Fence::Linux { abi, .. } => *abi,
2417 _ => Abi::None,
2418 },
2419 listing: Listing::Sealed,
2420 base: SysBase::Minimal,
2421 };
2422 res!(engage(&fenced, &spec(&base)));
2423
2424 let out = res!(std::process::Command::new("/bin/cat").arg(&inside).output());
2425 assert!(out.status.success(),
2426 "the fenced command could not read a file it was granted: {}",
2427 String::from_utf8_lossy(&out.stderr));
2428 assert_eq!("ok", String::from_utf8_lossy(&out.stdout));
2429
2430 for p in [&secret, &outside] {
2431 let out = res!(std::process::Command::new("/bin/cat").arg(p).output());
2432 assert!(!out.status.success(),
2433 "an exec'd program read {}, so the fence was not inherited",
2434 p.display());
2435 }
2436 Ok(())
2437 })
2438 }
2439
2440 /// A plan built for a newer ABI than the kernel has is refused, not quietly
2441 /// applied with the unsupported parts dropped.
2442 ///
2443 /// This is the "never silently degrade" rule, and it is the one property
2444 /// here that cannot be reached by any correct spec: the fence always asks
2445 /// for exactly what the kernel reported. Asking for more is therefore the
2446 /// only way to make the kernel answer `PartiallyEnforced` and see what this
2447 /// code does with that answer. Without this test, a change accepting a
2448 /// partial result would break nothing else in the suite.
2449 #[test]
2450 fn a_partly_applied_fence_is_an_error() -> Outcome<()> {
2451 let f = match kernel_fence("a_partly_applied_fence_is_an_error") {
2452 Some(f) => f,
2453 None => return Ok(()),
2454 };
2455 let abi = match &f {
2456 Fence::Linux { abi, .. } => *abi,
2457 _ => return Ok(()),
2458 };
2459 if abi >= Abi::V9 {
2460 println!(
2461 "[a_partly_applied_fence_is_an_error] SKIPPED: this kernel is at \
2462 ABI {}, which is everything this build knows how to ask for, so \
2463 there is no way to ask for more and see it refused.",
2464 abi.level());
2465 return Ok(());
2466 }
2467 own_thread("a_partly_applied_fence_is_an_error", move || {
2468 let base = res!(fixture("partial"));
2469 let mut plan = res!(f.plan(&spec(&base), &Unfenced::Refuse));
2470 plan.reach = Reach::Thread; // see `engage`
2471
2472 // Broken first: as planned, against the ABI the kernel reported, it
2473 // applies cleanly. So the refusal below is about the ABI and not
2474 // about the spec.
2475 let honest = res!(plan.clone().apply());
2476 assert!(honest.fenced);
2477
2478 // Now ask for rights this kernel does not have. Landlock's
2479 // best-effort mode will drop them and report a partial result, and a
2480 // partial result must not read as a fence.
2481 let mut ahead = plan.clone();
2482 ahead.abi = Abi::V9;
2483 match ahead.apply() {
2484 Ok(applied) => return Err(err!(
2485 "A fence built for ABI 9 on an ABI {} kernel reported \
2486 success ({:?}). Landlock dropped what it could not do, and \
2487 this code called the remainder a fence.", abi.level(), applied;
2488 Test, Security)),
2489 Err(e) => {
2490 let said = e.msgs().join(" ");
2491 assert!(said.contains("only part of the fence"),
2492 "the refusal did not say what went wrong: {}", said);
2493 },
2494 }
2495 Ok(())
2496 })
2497 }
2498
2499 /// `permits` agrees with the rules about the interesting places.
2500 ///
2501 /// It is only a pre-flight check, so the point is that it does not tell the
2502 /// caller something the kernel will contradict a moment later.
2503 #[test]
2504 fn permits_agrees_with_the_rules() -> Outcome<()> {
2505 let base = res!(fixture("permits"));
2506 let plan = res!(planner(Abi::V5).plan(&spec(&base), &Unfenced::Refuse));
2507 let ws = res!(base.join("ws").canonicalize());
2508
2509 assert!(plan.permits(&ws.join("ok.txt"), Level::Rw));
2510 assert!(plan.permits(&ws.join("sub/deep/x.txt"), Level::Rw));
2511 assert!(plan.permits(&ws.join("refs/note.md"), Level::Ro));
2512 assert!(!plan.permits(&ws.join("refs/note.md"), Level::Rw));
2513 assert!(!plan.permits(&ws.join(".daimond/secret.txt"), Level::Ro));
2514 assert!(!plan.permits(&base.join("outside/other.txt"), Level::Ro));
2515
2516 // The `..` route into the denied subtree is the classic one, and it is
2517 // resolved before the comparison rather than after.
2518 assert!(!plan.permits(&ws.join("sub/../.daimond/secret.txt"), Level::Ro),
2519 "a `..` path walked into the denied subtree");
2520 Ok(())
2521 }
2522 /// `/dev/null` must be WRITABLE, or nothing that discards output runs.
2523 ///
2524 /// This is the shape the whole file exists to catch and did not: the base is
2525 /// described as "the system paths a program needs in order to be a program",
2526 /// and it granted the one device every program uses read-only. Every git
2527 /// command inside the fence died with `fatal: could not open '/dev/null' for
2528 /// reading and writing`. Nothing in the suite ran a program that WRITES to
2529 /// it, so a base that could not run git passed every test it had.
2530 ///
2531 /// It runs real `git` where there is one, because a write to `/dev/null` by
2532 /// hand proves the device and the fence agree while the failure this is
2533 /// written against was a whole tool refusing to start.
2534 #[test]
2535 fn a_fenced_command_can_discard_its_output() -> Outcome<()> {
2536 let f = match kernel_fence("a_fenced_command_can_discard_its_output") {
2537 Some(f) => f,
2538 None => return Ok(()),
2539 };
2540 own_thread("a_fenced_command_can_discard_its_output", move || {
2541 let base = res!(fixture("devnull"));
2542 res!(engage(&f, &spec(&base)));
2543
2544 // The device itself, opened the way a shell redirection opens it.
2545 res!(fs::OpenOptions::new().write(true).open("/dev/null")
2546 .map_err(|e| err!("/dev/null is not writable behind the fence: {}", e;
2547 IO, Write)));
2548 // And read-write, which is what git asks for and what failed.
2549 res!(fs::OpenOptions::new().read(true).write(true).open("/dev/null")
2550 .map_err(|e| err!("/dev/null could not be opened read-write: {}", e;
2551 IO, Write)));
2552
2553 // Reading it still works, so the fix did not trade one direction for
2554 // the other.
2555 assert_eq!("", res!(fs::read_to_string("/dev/null")));
2556
2557 // Seeding the random devices stays refused: nothing legitimate writes
2558 // there, and a write is an attempt to make randomness predictable.
2559 assert!(fs::OpenOptions::new().write(true).open("/dev/urandom").is_err(),
2560 "/dev/urandom was writable: the base promoted more than it needed");
2561
2562 // The tool that could not start. Skipped rather than faked where git
2563 // is absent, because a claimed proof is worse than an honest gap.
2564 if let Ok(out) = std::process::Command::new("git")
2565 .args(["--version"]).output()
2566 {
2567 assert!(out.status.success(),
2568 "git could not run behind the fence: {}",
2569 String::from_utf8_lossy(&out.stderr));
2570 }
2571 Ok(())
2572 })
2573 }
2574
2575}