1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//! The bounded, fail-closed read of a script body for
//! `pm_guard_secret_script` (#8879), split out for the 500-SLOC cap.
//!
//! Why: a body the guard cannot read in full is a body it cannot clear, so
//! every class of read it cannot finish is an [`Unread`] refusal.
//! What: [`read_script`] and the types it answers with.
//! Test: `pm_guard_secret_script::tests::read_script_reports_each_unread_class`.
use std::path::Path;
/// Largest script body the guard reads; a larger one is not inspected.
pub(crate) const MAX_SCRIPT_BYTES: u64 = 256 * 1024;
/// Why a script body was not judged. Every class refuses: the guard fails
/// closed on a body it cannot read in full (#8879).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Unread {
/// An open, stat or read error: permission denied, gone mid-check.
Unreadable,
/// Not a regular file: a directory, a FIFO, a device.
NotRegular,
/// A script run past the `MAX_SCRIPT_DEPTH` nesting bound: its body is
/// not followed (#9037).
TooDeep,
/// Larger than [`MAX_SCRIPT_BYTES`].
TooLarge,
/// Not UTF-8 text, or carrying a NUL byte.
NotText,
/// A literal script that does not exist and that nothing else in the
/// command names, so no earlier stage can have written it.
Missing,
/// An interpreter's script named through a variable, a substitution or a
/// glob: no path to read.
Unresolvable,
}
/// What [`read_script`] found at a path.
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Body {
/// A script body, judged as text.
Script(String),
/// A compiled executable (ELF or Mach-O magic): not a script.
Executable,
}
/// The first bytes of a compiled executable: ELF, then the Mach-O thin
/// (32/64-bit, both byte orders) and fat magics.
const EXECUTABLE_MAGIC: &[[u8; 4]] = &[
[0x7f, b'E', b'L', b'F'],
[0xfe, 0xed, 0xfa, 0xce],
[0xfe, 0xed, 0xfa, 0xcf],
[0xce, 0xfa, 0xed, 0xfe],
[0xcf, 0xfa, 0xed, 0xfe],
[0xca, 0xfe, 0xba, 0xbe],
[0xbe, 0xba, 0xfe, 0xca],
];
/// Read a bounded prefix of a script body, failing closed.
///
/// Why: #8879 — a body the guard cannot read in full is a body it cannot
/// clear, so every unread class refuses instead of allowing.
/// What: opens the path `O_NONBLOCK` (a FIFO cannot block the hook) and
/// requires the OPENED handle to be a regular file; reads through
/// `take(MAX_SCRIPT_BYTES + 1)` so no size report is trusted. A compiled
/// executable is [`Body::Executable`]. A symlink is judged by the target the
/// open reached (#9037: `node_modules/.bin` and Homebrew shims); a symlink
/// loop fails the open and is [`Unread::Unreadable`]. An over-bound body and
/// non-UTF-8 or NUL-bearing bytes are each an [`Unread`] error. Nothing the
/// body names is opened here.
/// Test: `read_script_reports_each_unread_class`,
/// `every_unread_class_refuses_8879`, `a_symlinked_script_is_judged_by_its_target_9037`.
pub(crate) fn read_script(path: &Path) -> Result<Body, Unread> {
use std::io::Read;
use std::os::unix::fs::OpenOptionsExt;
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NONBLOCK)
.open(path)
.map_err(|_| Unread::Unreadable)?;
let opened = file.metadata().map_err(|_| Unread::Unreadable)?;
if !opened.file_type().is_file() {
return Err(Unread::NotRegular);
}
let mut bytes = Vec::new();
file.take(MAX_SCRIPT_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|_| Unread::Unreadable)?;
// #8879: a compiled program run by path is not a script; the kernel runs it.
if EXECUTABLE_MAGIC.iter().any(|m| bytes.starts_with(m)) {
return Ok(Body::Executable);
}
if bytes.len() as u64 > MAX_SCRIPT_BYTES {
return Err(Unread::TooLarge);
}
if bytes.contains(&0) {
return Err(Unread::NotText);
}
String::from_utf8(bytes)
.map(Body::Script)
.map_err(|_| Unread::NotText)
}