Skip to main content

detcore/
exec_record.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! The image a process executes, which its `/proc/<pid>/exe` link names.
10//!
11//! Detcore records it at each exec, on every backend, and checks it against
12//! the kernel's own link when the exec completes (`tool_global`). Where the
13//! backend's own procfs is not the guest's kernel procfs (KVM), the caller's
14//! own link (`/proc/self/exe` and its other spellings) is answered from the
15//! record while it still names exactly the image the process executes
16//! ([`ExecRecord::names_path_exactly`]; `Detcore::readlink_own_task_link`,
17//! package K slice E1); otherwise it is answered as before. Where the
18//! kernel's link names a launcher in the guest's place (SaBRe's loader,
19//! DynamoRIO's library; `Detcore::launched_exe`), the record answers every
20//! process's link while the image is at its path, as before. The kernel's
21//! link stands elsewhere.
22//!
23//! Only the path is recorded. Where the image ends, which a program-break
24//! model would need, is known only once the image is loaded, and nothing
25//! reads it yet.
26
27use std::fs;
28use std::io::Read;
29use std::os::unix::ffi::OsStrExt;
30use std::os::unix::fs::MetadataExt;
31use std::os::unix::fs::OpenOptionsExt;
32use std::path::Path;
33use std::path::PathBuf;
34
35use serde::Deserialize;
36use serde::Serialize;
37
38/// The bytes of a file the kernel reads to find its format
39/// (`BINPRM_BUF_SIZE`, include/linux/binfmts.h).
40const BINPRM_BUF_SIZE: usize = 256;
41
42/// The interpreter levels the kernel follows before it refuses an exec with
43/// `ELOOP` (`exec_binprm`'s `depth > 5`, fs/exec.c).
44const MAX_INTERPRETER_DEPTH: usize = 5;
45
46/// The image a process executes.
47#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
48pub struct ExecRecord {
49    /// The image's canonical path, as the process's root directory shows it:
50    /// what `readlink("/proc/self/exe")` returns while the file is there.
51    pub exe: PathBuf,
52    /// The image file's device and inode when it was exec'd, which tell
53    /// whether the file is still at `exe` ([`Self::is_present`]).
54    dev: u64,
55    ino: u64,
56    /// Its link count and inode change time then: a rename, link or unlink
57    /// of the file since changes both, even where the same inode is put back
58    /// at `exe`, which Linux's link would no longer name.
59    nlink: u64,
60    ctime: (i64, i64),
61    /// The device and inode of each directory above the image, from its
62    /// parent up to the process's root directory, when it was exec'd
63    /// ([`Self::names_path_exactly`]).
64    directories: Vec<(u64, u64)>,
65}
66
67/// The device and inode of each directory above `path`, a canonical path
68/// below `root`, from its parent up to `root`, not following a symbolic link
69/// at any of them, read with raw syscalls (in-guest Detcore must not call
70/// libc's wrappers: `crate::util::raw_syscall`). `None` where one cannot be
71/// read or is no directory ([`directory_sample`]).
72fn directory_chain(root: &Path, path: &Path) -> Option<Vec<(u64, u64)>> {
73    path.ancestors()
74        .skip(1)
75        .take_while(|directory| directory.starts_with(root))
76        .map(|directory| directory_sample(&crate::util::raw_lstat(directory).ok()?))
77        .collect()
78}
79
80/// The device and inode of a directory, from its unfollowed status: `None`
81/// for anything but a directory, so that another kind of file put in a
82/// directory's place never matches it, even where it reuses the freed
83/// directory's inode number (a symbolic link to where the directory went).
84fn directory_sample(stat: &libc::stat) -> Option<(u64, u64)> {
85    (stat.st_mode & libc::S_IFMT == libc::S_IFDIR).then_some((stat.st_dev, stat.st_ino))
86}
87
88impl ExecRecord {
89    /// The image an `execve` of `path` runs, as the kernel finds it: `path`
90    /// taken from the working directory `cwd`, every symbolic link followed,
91    /// and for a script the interpreter its `#!` line names, level by level,
92    /// as `/proc/<pid>/exe` names the interpreter. `root` is the process's
93    /// root directory as the caller sees it, and `cwd` is the working
94    /// directory below it, absolute.
95    ///
96    /// `None` where the kernel would refuse the exec (no such file, a
97    /// malformed `#!` line, too many interpreter levels), where the caller
98    /// cannot look the path up, and for a file the caller cannot read, which
99    /// the kernel may still run (an execute-only binary, or a script whose
100    /// `#!` line the kernel reads without a permission check): the link is
101    /// then the kernel's to answer. A resolution can still be wrong where
102    /// the kernel runs a file differently (a `binfmt_misc` handler) or the
103    /// caller sees another filesystem; `tool_global` checks every record
104    /// against the kernel's own link after the exec.
105    pub fn resolve(root: &Path, cwd: &Path, path: &Path) -> Option<Self> {
106        Self::resolve_from(root, Some(cwd), path)
107    }
108
109    /// [`Self::resolve`] for a caller whose working directory may be unknown
110    /// (`cwd` is `None`): then `None` wherever the kernel would look a
111    /// relative path up from it (a relative `path`, or a script whose `#!`
112    /// line names a relative interpreter).
113    pub fn resolve_from(root: &Path, cwd: Option<&Path>, path: &Path) -> Option<Self> {
114        let from_cwd = |path: &Path| {
115            if path.is_absolute() {
116                Some(path.to_path_buf())
117            } else {
118                Some(cwd?.join(path))
119            }
120        };
121        let mut spelled = from_cwd(path)?;
122        for _ in 0..=MAX_INTERPRETER_DEPTH {
123            let found = crate::util::canonicalize(&below(root, &spelled)?).ok()?;
124            // Linux execs only a regular file, and refuses anything else
125            // (EACCES) before it opens it.
126            if !fs::metadata(&found).ok()?.is_file() {
127                return None;
128            }
129            match script_interpreter(&found) {
130                Script::Not => return Self::at(root, &found),
131                // The kernel opens the interpreter from the caller's working
132                // directory, as it opened the script.
133                Script::Interpreter(interpreter) => spelled = from_cwd(&interpreter)?,
134                Script::Declined | Script::Unread => return None,
135            }
136        }
137        None
138    }
139
140    /// The record of the image at `found`, a canonical path the caller sees
141    /// below `root`, as that file is now.
142    pub fn at(root: &Path, found: &Path) -> Option<Self> {
143        let metadata = fs::metadata(found).ok()?;
144        Some(Self {
145            exe: Path::new("/").join(found.strip_prefix(root).ok()?),
146            dev: metadata.dev(),
147            ino: metadata.ino(),
148            nlink: metadata.nlink(),
149            ctime: (metadata.ctime(), metadata.ctime_nsec()),
150            directories: directory_chain(root, found).unwrap_or_default(),
151        })
152    }
153
154    /// The image file's device and inode when it was exec'd.
155    pub fn identity(&self) -> (u64, u64) {
156        (self.dev, self.ino)
157    }
158
159    /// Whether the image's file is still at its path, untouched, for a
160    /// process whose root directory the caller sees at `root`: the same
161    /// inode, with the link count and change time it had at the exec. A
162    /// rename or link of the image since, or a change of its mode, reads as
163    /// absent, so the caller refuses rather than name a path Linux's link
164    /// might not.
165    pub fn is_present(&self, root: &Path) -> bool {
166        below(root, &self.exe)
167            .and_then(|path| fs::symlink_metadata(path).ok())
168            .is_some_and(|now| {
169                (
170                    now.dev(),
171                    now.ino(),
172                    now.nlink(),
173                    (now.ctime(), now.ctime_nsec()),
174                ) == (self.dev, self.ino, self.nlink, self.ctime)
175            })
176    }
177
178    /// Whether the image's path still names exactly the file the process
179    /// executes, as Linux's `exe` link renders it (`d_path` of the image's
180    /// dentry), for a process whose root directory the caller sees at
181    /// `root`: the file is untouched at its path ([`Self::is_present`]), and
182    /// every directory above it is the one that was there at the exec (its
183    /// device and inode, not following a symbolic link). A directory has one
184    /// dentry, so the same directory inode at a path is that path's dentry,
185    /// and the same untouched file in the same directory under the same name
186    /// is the same dentry: a rename of the image or of any directory above
187    /// it, another directory or a symbolic link put at one of their paths,
188    /// and a hard link of the image put back at its path all read as not
189    /// exact. (Mounts are refused in the guest.)
190    pub fn names_path_exactly(&self, root: &Path) -> bool {
191        self.is_present(root)
192            && !self.directories.is_empty()
193            && below(root, &self.exe)
194                .and_then(|path| directory_chain(root, &path))
195                .is_some_and(|now| now == self.directories)
196    }
197}
198
199/// The program interpreter (`PT_INTERP`) the ELF64 image at `path` names,
200/// which the kernel opens and maps at the exec (`load_elf_binary`,
201/// fs/binfmt_elf.c), or `None` for any other file, an image without one, and
202/// one the caller cannot read. Like the kernel, this refuses a name shorter
203/// than 2 or longer than `PATH_MAX` bytes or one whose last byte is not NUL,
204/// and the name is the string up to its first NUL, which `open_exec` opens.
205pub fn program_interpreter(path: &Path) -> Option<PathBuf> {
206    let file = crate::util::RawFd::open(path, libc::O_RDONLY).ok()?;
207    let read_exact = |offset: u64, buffer: &mut [u8]| {
208        (file.read_at(buffer, offset).ok()? == buffer.len()).then_some(())
209    };
210    let mut header = [0_u8; 64];
211    read_exact(0, &mut header)?;
212    if header[..4] != *b"\x7fELF" || header[4] != 2 || header[5] != 1 {
213        return None;
214    }
215    let u16_at = |at: usize| u16::from_le_bytes([header[at], header[at + 1]]);
216    let phoff = u64::from_le_bytes(header[32..40].try_into().unwrap());
217    let (phentsize, phnum) = (usize::from(u16_at(54)), u16_at(56));
218    if phentsize < 56 {
219        return None;
220    }
221    let mut entry = vec![0_u8; phentsize];
222    for index in 0..u64::from(phnum) {
223        read_exact(phoff.checked_add(index * phentsize as u64)?, &mut entry)?;
224        if u32::from_le_bytes(entry[..4].try_into().unwrap()) != 3 {
225            continue; // not PT_INTERP
226        }
227        let field = |at: usize| u64::from_le_bytes(entry[at..at + 8].try_into().unwrap());
228        let (offset, size) = (field(8), field(32));
229        if size < 2 || size > libc::PATH_MAX as u64 {
230            return None;
231        }
232        let mut name = vec![0_u8; size as usize];
233        read_exact(offset, &mut name)?;
234        if name.last() != Some(&0) {
235            return None;
236        }
237        let end = name.iter().position(|byte| *byte == 0)?;
238        return Some(PathBuf::from(std::ffi::OsStr::from_bytes(&name[..end])));
239    }
240    None
241}
242
243/// What decides where Linux starts a process's program break after it execs
244/// an ELF image (`load_elf_binary`, fs/binfmt_elf.c), from the image's file
245/// header and program headers.
246#[derive(Clone, Copy, Debug, PartialEq, Eq)]
247pub struct ElfLayout {
248    /// Whether the image is `ET_DYN` (position-independent) rather than
249    /// `ET_EXEC`.
250    pub position_independent: bool,
251    /// Whether the image names a program interpreter (`PT_INTERP`).
252    pub interpreter: bool,
253    /// The virtual address of the image's program headers before the load
254    /// bias, as Linux finds it for `AT_PHDR` (since Linux 5.18): `e_phoff` in
255    /// the last `PT_LOAD` segment whose file range holds it, or 0 where none
256    /// does (`AT_PHDR` is then the bias itself).
257    pub phdr: u64,
258    /// The end of the highest `PT_LOAD` segment in memory, `p_vaddr +
259    /// p_memsz`, before the load bias: the end of its bss.
260    pub end: u64,
261    /// The first executable `PT_LOAD` segment's file offset and virtual
262    /// address, each page-aligned down: where its text mapping starts in the
263    /// file and, before the load bias, in memory. `None` without one.
264    pub text: Option<(u64, u64)>,
265    /// `mm->end_data - mm->start_data` as Linux sets them: the highest
266    /// segment end in the file (`p_vaddr + p_filesz`) less the highest
267    /// segment start, which `RLIMIT_DATA` counts with the heap
268    /// (`check_data_rlimit`).
269    pub data_span: u64,
270    /// The page size Linux maps the segments with and aligns the program
271    /// break to: `ELF_MIN_ALIGN`, the larger of `ELF_EXEC_PAGESIZE` and
272    /// `PAGE_SIZE` (fs/binfmt_elf.c), which is the host's page size, since
273    /// `ELF_EXEC_PAGESIZE` is `PAGE_SIZE` on arm64 and 4096 on x86_64.
274    pub page: u64,
275}
276
277impl ElfLayout {
278    /// The layout of the ELF64 image at `path` (`ET_EXEC` or `ET_DYN`), or
279    /// `None` for any other file, one without a loadable segment, and one the
280    /// caller cannot read.
281    pub fn read(path: &Path) -> Option<Self> {
282        Self::read_file(&crate::util::RawFd::open(path, libc::O_RDONLY).ok()?)
283    }
284
285    /// [`Self::read`] of an open file, read with raw syscalls: in-guest
286    /// Detcore must not call libc's wrappers (`crate::util::raw_syscall`).
287    pub fn read_file(file: &crate::util::RawFd) -> Option<Self> {
288        let read_exact = |offset: u64, buffer: &mut [u8]| {
289            (file.read_at(buffer, offset).ok()? == buffer.len()).then_some(())
290        };
291        let mut header = [0_u8; 64];
292        read_exact(0, &mut header)?;
293        Self::parse(&header, read_exact, crate::page::page_size())
294    }
295
296    /// [`Self::read`] of an image whose 64-byte file header is `header`,
297    /// reading its program headers through `read_at(offset, buffer)`, on a
298    /// host whose pages are `page` bytes.
299    fn parse(
300        header: &[u8; 64],
301        mut read_at: impl FnMut(u64, &mut [u8]) -> Option<()>,
302        page: u64,
303    ) -> Option<Self> {
304        let u16_at = |at: usize| u16::from_le_bytes([header[at], header[at + 1]]);
305        let u64_at = |at: usize| u64::from_le_bytes(header[at..at + 8].try_into().unwrap());
306        // ELF magic, 64-bit class, little-endian data, ET_EXEC or ET_DYN.
307        if header[..4] != *b"\x7fELF" || header[4] != 2 || header[5] != 1 {
308            return None;
309        }
310        let position_independent = match u16_at(16) {
311            2 => false, // ET_EXEC
312            3 => true,  // ET_DYN
313            _ => return None,
314        };
315        let (phoff, phentsize, phnum) = (u64_at(32), usize::from(u16_at(54)), u16_at(56));
316        if phentsize < 56 {
317            return None;
318        }
319        let mut entry = vec![0_u8; phentsize];
320        let (mut phdr, mut end, mut interpreter) = (0, None::<u64>, false);
321        let (mut text, mut start_data, mut end_data) = (None::<(u64, u64)>, 0_u64, 0_u64);
322        for index in 0..u64::from(phnum) {
323            read_at(phoff.checked_add(index * phentsize as u64)?, &mut entry)?;
324            let field = |at: usize| u64::from_le_bytes(entry[at..at + 8].try_into().unwrap());
325            match u32::from_le_bytes(entry[..4].try_into().unwrap()) {
326                1 => {} // PT_LOAD
327                3 => {
328                    // PT_INTERP
329                    interpreter = true;
330                    continue;
331                }
332                _ => continue,
333            }
334            let (offset, vaddr, filesz, memsz) = (field(8), field(16), field(32), field(40));
335            // PF_X.
336            if text.is_none() && u32::from_le_bytes(entry[4..8].try_into().unwrap()) & 1 != 0 {
337                text = Some((offset & !(page - 1), vaddr & !(page - 1)));
338            }
339            if offset <= phoff && phoff < offset.checked_add(filesz)? {
340                phdr = (phoff - offset).checked_add(vaddr)?;
341            }
342            start_data = start_data.max(vaddr);
343            end_data = end_data.max(vaddr.checked_add(filesz)?);
344            let segment_end = vaddr.checked_add(memsz)?;
345            end = Some(end.map_or(segment_end, |end| end.max(segment_end)));
346        }
347        Some(Self {
348            position_independent,
349            interpreter,
350            phdr,
351            end: end?,
352            text,
353            data_span: end_data.saturating_sub(start_data),
354            page,
355        })
356    }
357
358    /// Where Linux starts the program break of a process that exec'd this
359    /// image, given the `AT_PHDR` its exec reported, with address
360    /// randomization off: the end of the highest loaded segment at the load
361    /// bias, page-aligned up (`mm->start_brk = mm->brk =
362    /// ELF_PAGEALIGN(elf_brk)`), or why there is no answer.
363    pub fn start_brk(&self, at_phdr: u64) -> Result<u64, &'static str> {
364        let bias = at_phdr
365            .checked_sub(self.phdr)
366            .ok_or("AT_PHDR below the image's program headers")?;
367        self.start_brk_at_bias(bias)
368    }
369
370    /// [`Self::start_brk`] for an image loaded at load bias `bias`.
371    pub fn start_brk_at_bias(&self, bias: u64) -> Result<u64, &'static str> {
372        if self.position_independent && !self.interpreter {
373            // Recent kernels (6.17's load_elf_binary) move it to
374            // ELF_ET_DYN_BASE even without randomization, unless built with
375            // CONFIG_COMPAT_BRK.
376            return Err(
377                "a position-independent image without an interpreter, whose break depends on the kernel's build",
378            );
379        }
380        bias.checked_add(self.end)
381            .and_then(|end| end.checked_add(self.page - 1))
382            .map(|end| end & !(self.page - 1))
383            .ok_or("an image ending past the address space")
384    }
385}
386
387/// The load bias of the image file at `path` with inode `ino`, in a
388/// process whose `/proc/<pid>/maps` reads `maps`: the start of the file's
389/// executable mapping at the text segment's offset less the text segment's
390/// address ([`ElfLayout::text`]), or why there is none. The executable
391/// mapping, because a launcher may also map the file to read it (SaBRe's
392/// rewriter maps it read-only at offset 0) and may not map the image's first
393/// page at all. The file is matched by inode and its whole pathname as the
394/// reader sees it, not by device: maps shows the superblock's device, which
395/// on btrfs and overlay is not the `st_dev` a `stat` of the file reports.
396/// Where several such mappings give different biases, there is no answer.
397/// This is
398/// how Detcore finds an image's bias where the kernel exec'd a launcher,
399/// whose `AT_PHDR` the process's auxiliary vector holds.
400pub fn load_bias(
401    maps: &[u8],
402    path: &Path,
403    ino: u64,
404    (text_offset, text_page): (u64, u64),
405) -> Result<u64, &'static str> {
406    // Bytes, not text: a pathname in the mappings is whatever bytes the file
407    // was named with.
408    fn text(field: &[u8]) -> Option<&str> {
409        std::str::from_utf8(field).ok()
410    }
411    // As the mappings show it: Linux writes a newline in a pathname as
412    // `\012` (`show_map_vma`, `seq_file_path(m, file, "\n")`).
413    let mut displayed = Vec::new();
414    for &byte in path.as_os_str().as_bytes() {
415        if byte == b'\n' {
416            displayed.extend_from_slice(b"\\012");
417        } else {
418            displayed.push(byte);
419        }
420    }
421    let mut biases = maps.split(|&byte| byte == b'\n').filter_map(|line| {
422        let mut fields = line.splitn(6, |byte| byte.is_ascii_whitespace());
423        let range = text(fields.next()?)?;
424        let permissions = fields.next()?;
425        let offset = u64::from_str_radix(text(fields.next()?)?, 16).ok()?;
426        let _device = fields.next()?;
427        let map_ino: u64 = text(fields.next()?)?.parse().ok()?;
428        let pathname = fields.next()?.trim_ascii_start();
429        if !permissions.contains(&b'x')
430            || offset != text_offset
431            || map_ino != ino
432            || pathname != displayed
433        {
434            return None;
435        }
436        let start = u64::from_str_radix(range.split_once('-')?.0, 16).ok()?;
437        start.checked_sub(text_page)
438    });
439    let bias = biases
440        .next()
441        .ok_or("no executable mapping of the image's text")?;
442    if biases.any(|other| other != bias) {
443        return Err("executable mappings of the image's text at different biases");
444    }
445    Ok(bias)
446}
447
448/// The kernel's `start_brk` in a `/proc/<pid>/stat` line: field 47, counted
449/// past the command, which may hold spaces and parentheses.
450pub fn stat_start_brk(stat: &str) -> Option<u64> {
451    stat[stat.rfind(')')? + 1..]
452        .split_ascii_whitespace()
453        .nth(47 - 3)?
454        .parse()
455        .ok()
456}
457
458/// What Detcore found when it checked its model of a process's program break
459/// ([`ElfLayout::start_brk`]) against the kernel's `start_brk`.
460#[derive(Clone, Copy, Debug, PartialEq, Eq)]
461pub enum BrkCheck {
462    /// The model equals the kernel's start.
463    Equal(u64),
464    /// The model differs from the kernel's start: a Detcore defect.
465    Differs {
466        /// Detcore's start.
467        model: u64,
468        /// The kernel's start.
469        kernel: u64,
470    },
471    /// No model, for the reason given.
472    Unmodeled(&'static str),
473    /// Not compared: the kernel randomized the break, which Linux's rule then
474    /// does not give.
475    Randomized(u64),
476    /// Not compared: whether the kernel randomized the break is unknown.
477    RandomizationUnknown(u64),
478    /// Not compared: the kernel's start cannot be read.
479    KernelUnread(u64),
480}
481
482impl BrkCheck {
483    /// The outcome for a model (or why there is none), whether the kernel
484    /// randomized the break (`None`: unknown), and the kernel's start (`None`:
485    /// unread).
486    pub fn of(
487        model: Result<u64, &'static str>,
488        randomized: Option<bool>,
489        kernel: Option<u64>,
490    ) -> Self {
491        let model = match model {
492            Ok(model) => model,
493            Err(reason) => return Self::Unmodeled(reason),
494        };
495        match (randomized, kernel) {
496            (Some(true), _) => Self::Randomized(model),
497            (None, _) => Self::RandomizationUnknown(model),
498            (Some(false), None) => Self::KernelUnread(model),
499            (Some(false), Some(kernel)) if kernel == model => Self::Equal(model),
500            (Some(false), Some(kernel)) => Self::Differs { model, kernel },
501        }
502    }
503}
504
505impl std::fmt::Display for BrkCheck {
506    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507        match self {
508            Self::Equal(model) => write!(f, "model {model:#x} equals the kernel's start_brk"),
509            Self::Differs { model, kernel } => write!(
510                f,
511                "model {model:#x} differs from the kernel's start_brk {kernel:#x}"
512            ),
513            Self::Unmodeled(reason) => write!(f, "not modeled: {reason}"),
514            Self::Randomized(model) => write!(
515                f,
516                "model {model:#x} not compared: the kernel randomized the break"
517            ),
518            Self::RandomizationUnknown(model) => write!(
519                f,
520                "model {model:#x} not compared: whether the kernel randomized the break is unknown"
521            ),
522            Self::KernelUnread(model) => write!(
523                f,
524                "model {model:#x} not compared: the kernel's start_brk is unreadable"
525            ),
526        }
527    }
528}
529
530/// The value of entry `key` in an auxiliary vector read from
531/// `/proc/<pid>/auxv` as `auxv`, native-endian pairs ended by `AT_NULL`.
532pub fn auxv_entry(auxv: &[u8], key: u64) -> Option<u64> {
533    auxv.as_chunks::<16>()
534        .0
535        .iter()
536        .map(|pair| {
537            let word = |at: usize| u64::from_ne_bytes(pair[at..at + 8].try_into().unwrap());
538            (word(0), word(8))
539        })
540        .take_while(|&(entry, _)| entry != 0)
541        .find_map(|(entry, value)| (entry == key).then_some(value))
542}
543
544/// `path`, absolute in a root directory the caller sees at `root`, as the
545/// caller spells it.
546fn below(root: &Path, path: &Path) -> Option<PathBuf> {
547    Some(root.join(path.strip_prefix("/").ok()?))
548}
549
550/// What the kernel's script handler makes of a file.
551#[derive(Debug, PartialEq, Eq)]
552enum Script {
553    /// Not a script.
554    Not,
555    /// A file the caller may not read, which the kernel may still execute.
556    Unread,
557    /// A script; the kernel executes this interpreter path.
558    Interpreter(PathBuf),
559    /// A `#!` line that names no interpreter the kernel would run.
560    Declined,
561}
562
563/// What the kernel's script handler makes of the file at `path`.
564fn script_interpreter(path: &Path) -> Script {
565    let mut header = Vec::with_capacity(BINPRM_BUF_SIZE);
566    // Nonblocking and without a controlling terminal, so that a file
567    // replaced by a FIFO or device since it was checked cannot wait.
568    let read = fs::OpenOptions::new()
569        .read(true)
570        .custom_flags(libc::O_NONBLOCK | libc::O_NOCTTY)
571        .open(path)
572        .and_then(|file| file.take(BINPRM_BUF_SIZE as u64).read_to_end(&mut header));
573    match read {
574        Ok(_) => parse_script(&header),
575        Err(_) => Script::Unread,
576    }
577}
578
579/// Source of truth: `load_script()` in fs/binfmt_script.c. The kernel reads
580/// the first [`BINPRM_BUF_SIZE`] bytes into a zeroed buffer. The line ends at
581/// the first newline, and the interpreter path at a space, tab or NUL (so a
582/// short file with no newline ends at the zeroes). A line with no newline
583/// whose interpreter path runs to the end of the buffer may be truncated, and
584/// the kernel declines it.
585fn parse_script(header: &[u8]) -> Script {
586    let mut buffer = [0_u8; BINPRM_BUF_SIZE];
587    let read = header.len().min(BINPRM_BUF_SIZE);
588    buffer[..read].copy_from_slice(&header[..read]);
589    let Some(line) = buffer.strip_prefix(b"#!") else {
590        return Script::Not;
591    };
592    let (line, ended) = match line.iter().position(|&byte| byte == b'\n') {
593        Some(end) => (&line[..end], true),
594        None => (line, false),
595    };
596    let start = line
597        .iter()
598        .position(|&byte| byte != b' ' && byte != b'\t')
599        .unwrap_or(line.len());
600    let rest = &line[start..];
601    let end = rest
602        .iter()
603        .position(|&byte| matches!(byte, b' ' | b'\t' | 0));
604    if end.is_none() && !ended {
605        return Script::Declined;
606    }
607    let interpreter = &rest[..end.unwrap_or(rest.len())];
608    if interpreter.is_empty() {
609        return Script::Declined;
610    }
611    Script::Interpreter(PathBuf::from(std::ffi::OsStr::from_bytes(interpreter)))
612}
613
614#[cfg(test)]
615mod tests {
616    use std::os::unix::fs::PermissionsExt;
617
618    use super::*;
619
620    #[test]
621    fn an_elf_layout_gives_linuxs_start_of_the_program_break() {
622        // A real image: this test binary, linked by the toolchain.
623        let layout = ElfLayout::read(&std::env::current_exe().unwrap()).unwrap();
624        assert!(layout.end > layout.phdr);
625        // Its text segment is executable and page-aligned in file and memory.
626        let (text_offset, text_page) = layout.text.unwrap();
627        assert_eq!((text_offset % layout.page, text_page % layout.page), (0, 0));
628        assert_eq!(layout.page, crate::page::page_size());
629        // A non-ELF file and a 32-bit or big-endian header have none.
630        assert_eq!(ElfLayout::read(Path::new("/proc/self/status")), None);
631        let mut header = [0_u8; 64];
632        header[..4].copy_from_slice(b"\x7fELF");
633        header[4] = 1;
634        header[5] = 1;
635        assert_eq!(ElfLayout::parse(&header, |_, _| None, 4096), None);
636
637        // A synthetic ET_EXEC image with two loads: text at 0x400000 holding
638        // the program headers at file offset 64, and data at 0x403e10 whose
639        // bss runs to 0x4050b0.
640        let mut header = [0_u8; 64];
641        header[..4].copy_from_slice(b"\x7fELF");
642        header[4] = 2;
643        header[5] = 1;
644        header[16..18].copy_from_slice(&2_u16.to_le_bytes());
645        header[32..40].copy_from_slice(&64_u64.to_le_bytes());
646        header[54..56].copy_from_slice(&56_u16.to_le_bytes());
647        header[56..58].copy_from_slice(&3_u16.to_le_bytes());
648        let program_header = |kind: u32, offset: u64, vaddr: u64, filesz: u64, memsz: u64| {
649            let mut entry = [0_u8; 56];
650            entry[..4].copy_from_slice(&kind.to_le_bytes());
651            entry[8..16].copy_from_slice(&offset.to_le_bytes());
652            entry[16..24].copy_from_slice(&vaddr.to_le_bytes());
653            entry[32..40].copy_from_slice(&filesz.to_le_bytes());
654            entry[40..48].copy_from_slice(&memsz.to_le_bytes());
655            entry
656        };
657        let entries = [
658            program_header(6, 64, 0x400040, 0xa8, 0xa8), // PT_PHDR, not loaded
659            program_header(1, 0, 0x400000, 0x1000, 0x1000),
660            program_header(1, 0x2e10, 0x403e10, 0x290, 0x12a0),
661        ];
662        let read_at = |offset: u64, buffer: &mut [u8]| {
663            let index = usize::try_from((offset - 64) / 56).ok()?;
664            buffer.copy_from_slice(entries.get(index)?);
665            Some(())
666        };
667        let layout = ElfLayout::parse(&header, read_at, 4096).unwrap();
668        assert_eq!(
669            layout,
670            ElfLayout {
671                position_independent: false,
672                interpreter: false,
673                phdr: 0x400040,
674                end: 0x4050b0,
675                // The synthetic headers carry no flags, so no executable
676                // segment.
677                text: None,
678                data_span: 0x290,
679                page: 4096,
680            }
681        );
682        assert_eq!(layout.start_brk(0x400040), Ok(0x406000));
683        assert!(layout.start_brk(0x400000).is_err());
684        // On a host with 64 KiB pages the same image's break starts at the
685        // next 64 KiB boundary.
686        let large_pages = ElfLayout::parse(&header, read_at, 0x10000).unwrap();
687        assert_eq!(
688            large_pages,
689            ElfLayout {
690                page: 0x10000,
691                ..layout
692            }
693        );
694        assert_eq!(large_pages.start_brk(0x400040), Ok(0x410000));
695        // An end already on a page boundary stays there, at any bias.
696        let aligned = ElfLayout {
697            end: 0x406000,
698            ..layout
699        };
700        assert_eq!(aligned.start_brk(0x400040), Ok(0x406000));
701        // A position-independent image with an interpreter at its bias.
702        let dynamic = ElfLayout {
703            position_independent: true,
704            interpreter: true,
705            ..aligned
706        };
707        assert_eq!(
708            dynamic.start_brk(0x5555_5555_4000 + 0x400040),
709            Ok(0x5555_5595_a000)
710        );
711        // Without an interpreter (a static PIE, or the dynamic loader run
712        // directly), the kernel's build decides: no model.
713        let static_pie = ElfLayout {
714            interpreter: false,
715            ..dynamic
716        };
717        assert!(static_pie.start_brk(0x5555_5555_4000 + 0x400040).is_err());
718        // Program headers outside every load: Linux's AT_PHDR is the bias.
719        let unloaded = ElfLayout { phdr: 0, ..layout };
720        assert_eq!(unloaded.start_brk(0), Ok(0x406000));
721        // A PT_INTERP entry and ET_DYN are read from the headers.
722        header[16..18].copy_from_slice(&3_u16.to_le_bytes());
723        header[56..58].copy_from_slice(&4_u16.to_le_bytes());
724        let with_interp = [
725            entries[0],
726            program_header(3, 0x2a8, 0x4002a8, 0x1c, 0x1c), // PT_INTERP
727            entries[1],
728            entries[2],
729        ];
730        let read_at = |offset: u64, buffer: &mut [u8]| {
731            let index = usize::try_from((offset - 64) / 56).ok()?;
732            buffer.copy_from_slice(with_interp.get(index)?);
733            Some(())
734        };
735        assert_eq!(
736            ElfLayout::parse(&header, read_at, 4096).unwrap(),
737            ElfLayout {
738                position_independent: true,
739                interpreter: true,
740                ..layout
741            }
742        );
743    }
744
745    /// The kernel's own `start_brk` for a real image, a shell exec'd without
746    /// address randomization, is what its layout and `AT_PHDR` give.
747    #[test]
748    fn the_kernels_start_of_the_break_is_what_the_layout_gives() {
749        use std::os::unix::process::CommandExt;
750        let mut command = std::process::Command::new("/bin/sh");
751        command
752            .args(["-c", "read line"])
753            .stdin(std::process::Stdio::piped());
754        // SAFETY: personality(2) is async-signal-safe.
755        unsafe {
756            command.pre_exec(|| {
757                if libc::personality(libc::ADDR_NO_RANDOMIZE as libc::c_ulong) == -1 {
758                    return Err(std::io::Error::last_os_error());
759                }
760                Ok(())
761            });
762        }
763        // `spawn` returns once the exec has happened.
764        let mut child = command.spawn().unwrap();
765        let process = PathBuf::from(format!("/proc/{}", child.id()));
766        let image = fs::read_link(process.join("exe")).unwrap();
767        let at_phdr = auxv_entry(&fs::read(process.join("auxv")).unwrap(), libc::AT_PHDR).unwrap();
768        let start_brk = stat_start_brk(&fs::read_to_string(process.join("stat")).unwrap()).unwrap();
769        let maps = fs::read(process.join("maps")).unwrap();
770        drop(child.stdin.take());
771        child.wait().unwrap();
772        let layout = ElfLayout::read(&image).unwrap();
773        assert_eq!(layout.start_brk(at_phdr), Ok(start_brk), "{image:?}");
774        // The same start from the image's executable mapping, as Detcore
775        // finds it where the kernel exec'd a launcher.
776        let ino = std::os::unix::fs::MetadataExt::ino(&fs::metadata(&image).unwrap());
777        let bias = load_bias(&maps, &image, ino, layout.text.unwrap()).unwrap();
778        assert_eq!(layout.start_brk_at_bias(bias), Ok(start_brk), "{image:?}");
779    }
780
781    #[test]
782    fn the_start_of_the_break_is_field_47_past_the_command() {
783        let fields: Vec<String> = (3..=52).map(|field| field.to_string()).collect();
784        let stat = format!("12 (a) b) (c) {}\n", fields.join(" "));
785        assert_eq!(stat_start_brk(&stat), Some(47));
786        assert_eq!(stat_start_brk("12 (a) S 1 2"), None);
787        assert_eq!(stat_start_brk("12 a S"), None);
788    }
789
790    #[test]
791    fn a_break_check_compares_only_where_linuxs_rule_gives_the_kernels_start() {
792        assert_eq!(
793            BrkCheck::of(Ok(0x406000), Some(false), Some(0x406000)),
794            BrkCheck::Equal(0x406000)
795        );
796        let differs = BrkCheck::of(Ok(0x406000), Some(false), Some(0x407000));
797        assert_eq!(
798            differs,
799            BrkCheck::Differs {
800                model: 0x406000,
801                kernel: 0x407000
802            }
803        );
804        assert_eq!(
805            differs.to_string(),
806            "model 0x406000 differs from the kernel's start_brk 0x407000"
807        );
808        assert_eq!(
809            BrkCheck::of(Ok(0x406000), Some(true), Some(0x1234000)),
810            BrkCheck::Randomized(0x406000)
811        );
812        assert_eq!(
813            BrkCheck::of(Ok(0x406000), None, Some(0x406000)),
814            BrkCheck::RandomizationUnknown(0x406000)
815        );
816        assert_eq!(
817            BrkCheck::of(Ok(0x406000), Some(false), None),
818            BrkCheck::KernelUnread(0x406000)
819        );
820        assert_eq!(
821            BrkCheck::of(Err("no exec record"), Some(false), Some(0x406000)),
822            BrkCheck::Unmodeled("no exec record")
823        );
824    }
825
826    #[test]
827    fn a_load_bias_comes_from_the_images_executable_mapping() {
828        // A non-PIE image's text and data, another file's text, a read-only
829        // mapping of the whole image at offset 0 (as SaBRe's rewriter makes),
830        // and a position-independent image whose name has a space.
831        let listed = "\
83200401000-00402000 r-xp 00001000 00:20 1234 /dir/image
83300402000-00403000 r--p 00002000 00:20 1234 /dir/image
834555555553000-555555554000 r-xp 00001000 00:20 99 /other
8357ffff7bac000-7ffff7bb1000 r--p 00000000 00:20 1234 /dir/image
8367ffff7fc6000-7ffff7fc7000 r-xp 00001000 00:21 5678 /with spaces
837";
838        let maps = listed.as_bytes();
839        let path = |path: &'static str| Path::new(path);
840        let text = (0x1000, 0x401000);
841        assert_eq!(load_bias(maps, path("/dir/image"), 1234, text), Ok(0));
842        assert!(load_bias(maps, path("/dir/image"), 4321, text).is_err());
843        assert!(load_bias(maps, path("/dir/ima"), 1234, text).is_err());
844        // The same inode and name under another directory (another
845        // filesystem's file) is not the image.
846        assert!(load_bias(maps, path("/elsewhere/image"), 1234, text).is_err());
847        // Two executable mappings of the text at different biases: no answer.
848        let twice =
849            format!("{listed}7ffff0001000-7ffff0002000 r-xp 00001000 00:20 1234 /dir/image\n");
850        assert!(load_bias(twice.as_bytes(), path("/dir/image"), 1234, text).is_err());
851        // The same bias twice is still one answer.
852        let again = format!("{listed}00401000-00402000 r-xp 00001000 00:20 1234 /dir/image\n");
853        assert_eq!(
854            load_bias(again.as_bytes(), path("/dir/image"), 1234, text),
855            Ok(0)
856        );
857        // A text segment at another offset has no executable mapping here.
858        assert_eq!(
859            load_bias(maps, path("/dir/image"), 1234, (0x2000, 0x402000)).ok(),
860            None
861        );
862        // A position-independent image at its bias.
863        assert_eq!(
864            load_bias(maps, path("/with spaces"), 5678, (0x1000, 0x1000)),
865            Ok(0x7ffff7fc5000)
866        );
867        // A name that is not UTF-8, anywhere in the mappings, is still bytes
868        // to compare.
869        let mut named = maps.to_vec();
870        named.extend_from_slice(b"7ffff7fd0000-7ffff7fd1000 r-xp 00001000 00:21 42 /n\xffme\n");
871        let unnamed = Path::new(std::ffi::OsStr::from_bytes(b"/n\xffme"));
872        assert_eq!(
873            load_bias(&named, unnamed, 42, (0x1000, 0x1000)),
874            Ok(0x7ffff7fcf000)
875        );
876        assert_eq!(load_bias(&named, path("/dir/image"), 1234, text), Ok(0));
877        // A newline in the name, which the mappings show as `\012`.
878        named.extend_from_slice(
879            b"7ffff7fe0000-7ffff7fe1000 r-xp 00001000 00:21 43 /line\\012break\n",
880        );
881        let broken = Path::new("/line\nbreak");
882        assert_eq!(
883            load_bias(&named, broken, 43, (0x1000, 0x1000)),
884            Ok(0x7ffff7fdf000)
885        );
886        // The model at a bias from the mapping: the synthetic image's end
887        // page-aligned up from the bias.
888        let layout = ElfLayout {
889            position_independent: true,
890            interpreter: true,
891            phdr: 0x40,
892            end: 0x50b0,
893            text: Some((0x1000, 0x1000)),
894            data_span: 0x290,
895            page: 4096,
896        };
897        assert_eq!(layout.start_brk_at_bias(0x555555556000), Ok(0x55555555c000));
898    }
899
900    #[test]
901    fn an_auxiliary_vector_entry_is_read_up_to_its_end() {
902        let auxv: Vec<u8> = [(3_u64, 0x400040_u64), (6, 4096), (0, 0), (9, 1)]
903            .iter()
904            .flat_map(|(key, value)| [key.to_ne_bytes(), value.to_ne_bytes()].concat())
905            .collect();
906        assert_eq!(auxv_entry(&auxv, 3), Some(0x400040));
907        assert_eq!(auxv_entry(&auxv, 6), Some(4096));
908        assert_eq!(auxv_entry(&auxv, 9), None);
909        assert_eq!(auxv_entry(&auxv[..20], 6), None);
910    }
911
912    #[test]
913    fn a_script_line_names_its_interpreter_as_the_kernel_reads_it() {
914        let interpreter = |path: &str| Script::Interpreter(PathBuf::from(path));
915        assert_eq!(parse_script(b"\x7fELF"), Script::Not);
916        assert_eq!(parse_script(b"#!/bin/sh\n"), interpreter("/bin/sh"));
917        assert_eq!(parse_script(b"#! \t/bin/sh -e\n"), interpreter("/bin/sh"));
918        // A carriage return is part of the path to the kernel.
919        assert_eq!(parse_script(b"#!/bin/sh\r\n"), interpreter("/bin/sh\r"));
920        assert_eq!(parse_script(b"#!/bin/sh -x"), interpreter("/bin/sh"));
921        // A short file with no newline: the path ends at the zeroed buffer.
922        assert_eq!(parse_script(b"#!/bin/sh"), interpreter("/bin/sh"));
923        // One that fills the buffer may be truncated.
924        let long = [b"#!/".as_slice(), &[b'a'; BINPRM_BUF_SIZE]].concat();
925        assert_eq!(parse_script(&long), Script::Declined);
926        assert_eq!(parse_script(b"#!\n"), Script::Declined);
927        assert_eq!(parse_script(b"#!\0/bin/sh\n"), Script::Declined);
928    }
929
930    #[test]
931    #[allow(clippy::disallowed_methods)] // std::fs::canonicalize is the independent oracle
932    fn a_script_resolves_to_its_interpreter_and_a_link_to_its_file() {
933        let directory = tempfile::tempdir().unwrap();
934        let root = std::fs::canonicalize(directory.path()).unwrap();
935        let interpreter = std::env::current_exe().unwrap();
936        let interpreter = std::fs::canonicalize(&interpreter).unwrap();
937        let script = root.join("script");
938        fs::write(&script, format!("#!{}\n", interpreter.display())).unwrap();
939        fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
940        std::os::unix::fs::symlink("script", root.join("link")).unwrap();
941
942        let record = ExecRecord::resolve(Path::new("/"), &root, Path::new("link")).unwrap();
943        assert_eq!(record.exe, interpreter);
944        assert!(record.is_present(Path::new("/")));
945
946        // The same file seen below another root is named from that root.
947        let image = root.join("image");
948        fs::copy(&interpreter, &image).unwrap();
949        let record = ExecRecord::resolve(&root, Path::new("/"), Path::new("/image")).unwrap();
950        assert_eq!(record.exe, Path::new("/image"));
951        assert!(record.is_present(&root));
952
953        // A file removed or replaced at the path is no longer the image.
954        fs::remove_file(&image).unwrap();
955        assert!(!record.is_present(&root));
956        fs::copy(&interpreter, &image).unwrap();
957        assert!(!record.is_present(&root));
958
959        // The same inode put back at the path after a rename is not the
960        // image's link any more: Linux's link follows the renamed name.
961        let image = root.join("image");
962        let record = ExecRecord::resolve(&root, Path::new("/"), Path::new("/image")).unwrap();
963        fs::hard_link(&image, root.join("alias")).unwrap();
964        fs::rename(&image, root.join("moved")).unwrap();
965        fs::hard_link(root.join("alias"), &image).unwrap();
966        assert!(!record.is_present(&root));
967
968        // With its link count back where it was, a rename still changed
969        // the inode's change time.
970        let image = root.join("relinked");
971        fs::copy(&interpreter, &image).unwrap();
972        fs::hard_link(&image, root.join("second")).unwrap();
973        let record = ExecRecord::at(&root, &image).unwrap();
974        fs::rename(&image, root.join("away")).unwrap();
975        fs::rename(root.join("second"), &image).unwrap();
976        assert_eq!(fs::metadata(&image).unwrap().nlink(), 2);
977        assert!(!record.is_present(&root));
978
979        // Linux refuses to exec anything but a regular file (EACCES) before
980        // it opens it, and so does the resolution: an open of a FIFO with no
981        // writer would wait forever.
982        let fifo = root.join("fifo");
983        let fifo_name = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap();
984        assert_eq!(unsafe { libc::mkfifo(fifo_name.as_ptr(), 0o755) }, 0);
985        assert_eq!(
986            ExecRecord::resolve(&root, Path::new("/"), Path::new("/fifo")),
987            None
988        );
989        assert_eq!(
990            ExecRecord::resolve(Path::new("/"), Path::new("/"), Path::new("/dev/null")),
991            None
992        );
993
994        assert_eq!(
995            ExecRecord::resolve(&root, Path::new("/"), Path::new("/missing")),
996            None
997        );
998        // A file whose header the caller cannot read is the kernel's to
999        // resolve (Linux runs an execute-only script's interpreter): its
1000        // header reads as unread, never as not a script.
1001        assert_eq!(script_interpreter(&root), Script::Unread);
1002    }
1003
1004    #[test]
1005    fn only_a_directory_samples_as_one() {
1006        // SAFETY: an all-zero `stat` is a valid value.
1007        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1008        stat.st_dev = 7;
1009        stat.st_ino = 42;
1010        stat.st_mode = libc::S_IFDIR | 0o755;
1011        assert_eq!(directory_sample(&stat), Some((7, 42)));
1012        // A symbolic link, or any other file, with the recorded directory's
1013        // device and inode (a freed inode number reused) is not that
1014        // directory.
1015        for kind in [
1016            libc::S_IFLNK,
1017            libc::S_IFREG,
1018            libc::S_IFIFO,
1019            libc::S_IFSOCK,
1020            libc::S_IFCHR,
1021            libc::S_IFBLK,
1022        ] {
1023            stat.st_mode = kind | 0o777;
1024            assert_eq!(directory_sample(&stat), None, "{kind:o}");
1025        }
1026        // And a chain through such a file is no chain.
1027        let directory = tempfile::tempdir().unwrap();
1028        let root = crate::util::canonicalize(directory.path()).unwrap();
1029        fs::create_dir(root.join("away")).unwrap();
1030        std::os::unix::fs::symlink("away", root.join("a")).unwrap();
1031        assert_eq!(directory_chain(&root, &root.join("a/image")), None);
1032        assert!(directory_chain(&root, &root.join("away/image")).is_some());
1033    }
1034
1035    #[test]
1036    fn a_renamed_directory_above_the_image_is_not_its_path() {
1037        let directory = tempfile::tempdir().unwrap();
1038        let root = crate::util::canonicalize(directory.path()).unwrap();
1039        fs::create_dir(root.join("a")).unwrap();
1040        fs::create_dir(root.join("b")).unwrap();
1041        let image = root.join("a/image");
1042        fs::write(&image, b"image").unwrap();
1043        fs::hard_link(&image, root.join("b/image")).unwrap();
1044        let record = ExecRecord::at(&root, &image).unwrap();
1045        assert_eq!(record.exe, Path::new("/a/image"));
1046        assert!(record.names_path_exactly(&root));
1047        // The image's directory renamed away and another directory holding a
1048        // link of the same file renamed into its place: the file is untouched
1049        // at the path, but Linux's link names /away/image.
1050        fs::rename(root.join("a"), root.join("away")).unwrap();
1051        fs::rename(root.join("b"), root.join("a")).unwrap();
1052        assert!(record.is_present(&root));
1053        assert!(!record.names_path_exactly(&root));
1054        // Renamed back: the same directories at the same paths.
1055        fs::rename(root.join("a"), root.join("b")).unwrap();
1056        fs::rename(root.join("away"), root.join("a")).unwrap();
1057        assert!(record.names_path_exactly(&root));
1058        // A symbolic link in a directory's place.
1059        fs::rename(root.join("a"), root.join("away")).unwrap();
1060        std::os::unix::fs::symlink("away", root.join("a")).unwrap();
1061        assert!(!record.names_path_exactly(&root));
1062    }
1063
1064    /// The test binary, a dynamically linked executable, names its program
1065    /// interpreter, which exists where the kernel found it; a file that is
1066    /// not ELF names none.
1067    #[test]
1068    fn an_image_names_its_program_interpreter_as_the_kernel_reads_it() {
1069        let interpreter = program_interpreter(Path::new("/proc/self/exe"))
1070            .expect("the test binary is dynamically linked");
1071        assert!(interpreter.is_absolute(), "{}", interpreter.display());
1072        assert!(interpreter.exists(), "{}", interpreter.display());
1073        let directory = tempfile::tempdir().unwrap();
1074        let script = directory.path().join("script");
1075        fs::write(&script, "#!/bin/sh\n").unwrap();
1076        assert_eq!(program_interpreter(&script), None);
1077    }
1078}