Skip to main content

hermit_record_format/
script.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
9use std::ffi::OsStr;
10use std::ffi::OsString;
11use std::fs;
12use std::io::Read;
13use std::os::unix::ffi::OsStrExt;
14use std::path::Path;
15use std::path::PathBuf;
16
17// Defined in kernel include/linux/binfmts.h
18const BINPRM_BUF_SIZE: usize = 256;
19
20/// Unix shebang, see [Wikipedia](https://en.wikipedia.org/wiki/Shebang_(Unix)).
21#[derive(Debug, Eq, PartialEq)]
22pub struct Shebang {
23    program: PathBuf,
24    args: Vec<OsString>,
25}
26
27impl Shebang {
28    // Source of truth: fs/binfmt_script.c, function load_script().
29    pub fn from_buf(buf: &[u8]) -> Option<Self> {
30        if !buf.starts_with(b"#!") {
31            return None;
32        }
33
34        let mut i = 2;
35        while i < buf.len() {
36            if buf[i] == b' ' || buf[i] == b'\t' {
37                i += 1;
38            } else {
39                break;
40            }
41        }
42
43        let mut j = i;
44        while j < buf.len() {
45            if b" \t\r\n".contains(&buf[j]) {
46                break;
47            } else {
48                j += 1;
49            }
50        }
51
52        let program = PathBuf::from(OsStr::from_bytes(&buf[i..j]));
53
54        i = j;
55        while j < buf.len() {
56            if b"\n".contains(&buf[j]) {
57                break;
58            } else {
59                j += 1;
60            }
61        }
62
63        let args = String::from_utf8_lossy(&buf[i..j])
64            .split_ascii_whitespace()
65            .map(OsString::from)
66            .collect::<Vec<_>>();
67
68        Some(Shebang { program, args })
69    }
70
71    /// Extract the interpreter path without checking whether it exists.
72    ///
73    /// Callers must resolve the path in the filesystem namespace that will
74    /// execute the script.
75    pub fn interpreter_from_buf(buf: &[u8]) -> Option<PathBuf> {
76        Self::from_buf(buf).map(|shebang| shebang.program)
77    }
78
79    /// Parse exe/interpreter from a script contains a shebang, whenever possible.
80    pub fn new<P: AsRef<Path>>(path: P) -> Option<Self> {
81        let mut buf = Vec::new();
82        let nb = fs::File::open(path)
83            .and_then(|f| {
84                let mut handle = f.take(BINPRM_BUF_SIZE as u64);
85                handle.read_to_end(&mut buf)
86            })
87            .ok()?;
88        // Pass a slice up to the number of bytes read to the lookup function. We don't want to pass
89        // the extra zeroes.
90        let shebang = Self::from_buf(&buf[..nb])?;
91        let metadata = fs::metadata(&shebang.program).ok()?;
92        if metadata.is_file() {
93            Some(shebang)
94        } else {
95            None
96        }
97    }
98
99    /// Get interpreter from shebang
100    pub fn interpreter(&self) -> &Path {
101        &self.program
102    }
103
104    /// Get intepreter arguments from shebang.
105    pub fn args(&self) -> impl Iterator<Item = &OsStr> {
106        self.args.iter().map(OsStr::new)
107    }
108
109    /// Convert shebang into (intepreter, args).
110    pub fn into_parts(self) -> (PathBuf, Vec<OsString>) {
111        (self.program, self.args)
112    }
113}
114
115/// How the kernel's script handler reads the start of a file, for checks that
116/// must inspect exactly the interpreter the kernel executes.
117// Only the in-guest LiteInst program check uses it.
118#[derive(Debug, Eq, PartialEq)]
119pub enum KernelScript {
120    /// The file does not start with `#!`; the kernel tries its other handlers.
121    NotScript,
122    /// The script handler runs no interpreter: the `#!` line names none, or it
123    /// has no newline and the interpreter path runs to the end of the
124    /// `BINPRM_BUF_SIZE` (256) bytes the kernel reads, so it may be truncated.
125    /// Measured on Linux 7.1: `#!\n`, `#! \t\n` and a 254-byte path with no
126    /// newline fail with `ENOEXEC`, so the kernel tries its other handlers;
127    /// `#!` alone and `#!\0/bin/sh\n`, whose name a NUL leaves empty, fail the
128    /// `execve` with `EACCES`.
129    Declined,
130    /// The kernel executes this interpreter path, byte for byte.
131    Interpreter(PathBuf),
132}
133
134impl KernelScript {
135    /// Source of truth: `load_script()` in `fs/binfmt_script.c`; the errors
136    /// noted on [`KernelScript::Declined`] were measured on Linux 7.1. Unlike
137    /// [`Shebang::from_buf`], which its existing callers keep,
138    /// this ends the interpreter path only at a space, tab or NUL: a carriage
139    /// return is part of the path, as it is for the kernel.
140    pub fn from_buf(buf: &[u8]) -> Self {
141        // The kernel reads the file's first BINPRM_BUF_SIZE bytes into a buffer
142        // that is NUL-padded when the file is shorter.
143        let mut header = [0_u8; BINPRM_BUF_SIZE];
144        let len = buf.len().min(BINPRM_BUF_SIZE);
145        header[..len].copy_from_slice(&buf[..len]);
146        if !header.starts_with(b"#!") {
147            return Self::NotScript;
148        }
149        let last = BINPRM_BUF_SIZE - 1;
150        // `strnchr` stops at the first NUL.
151        let newline = header
152            .iter()
153            .take_while(|byte| **byte != 0)
154            .position(|byte| *byte == b'\n');
155        let line_end = match newline {
156            Some(newline) => newline,
157            None => {
158                // Without a newline the interpreter path must be followed by a
159                // space, tab or NUL within the buffer, or it may be truncated.
160                let Some(start) = (2..=last).find(|&index| !matches!(header[index], b' ' | b'\t'))
161                else {
162                    return Self::Declined;
163                };
164                if !header[start..=last]
165                    .iter()
166                    .any(|byte| matches!(byte, b' ' | b'\t' | 0))
167                {
168                    return Self::Declined;
169                }
170                // The kernel overwrites the buffer's last byte with a NUL.
171                last
172            }
173        };
174        let line = &header[2..line_end];
175        let start = line
176            .iter()
177            .position(|byte| !matches!(byte, b' ' | b'\t'))
178            .unwrap_or(line.len());
179        let name = &line[start..];
180        let name = &name[..name
181            .iter()
182            .position(|byte| matches!(byte, b' ' | b'\t' | 0))
183            .unwrap_or(name.len())];
184        if name.is_empty() {
185            Self::Declined
186        } else {
187            Self::Interpreter(PathBuf::from(OsStr::from_bytes(name)))
188        }
189    }
190}
191
192#[cfg(test)]
193mod test {
194    use super::*;
195
196    #[test]
197    fn shebang_invalid_shebang() {
198        assert_eq!(Shebang::from_buf(b"! /bin/bash"), None);
199    }
200
201    #[test]
202    fn shebang_empty_interpreter() {
203        for input in [b"#!".as_slice(), b"#!\n", b"#! \t\n"] {
204            assert_eq!(
205                Shebang::from_buf(input).map(|shebang| shebang.program),
206                Some(PathBuf::new()),
207                "input: {input:?}"
208            );
209        }
210    }
211
212    #[test]
213    fn shebang_bin_bash() {
214        assert_eq!(
215            Shebang::from_buf(b"#! /bin/bash").map(|s| s.program),
216            Some(PathBuf::from("/bin/bash"))
217        );
218        assert_eq!(
219            Shebang::from_buf(b"#!  \t/bin/bash").map(|s| s.program),
220            Some(PathBuf::from("/bin/bash"))
221        );
222        assert_eq!(
223            Shebang::from_buf(b"#!/bin/bash\nfoobar").map(|s| s.program),
224            Some(PathBuf::from("/bin/bash"))
225        );
226        assert_eq!(
227            Shebang::from_buf(b"#!/bin/bash").map(|s| s.program),
228            Some(PathBuf::from("/bin/bash"))
229        );
230    }
231
232    #[test]
233    fn shebang_env_python() {
234        assert_eq!(
235            Shebang::from_buf(b"#! /usr/bin/env python").map(|s| s.program),
236            Some(PathBuf::from("/usr/bin/env"))
237        );
238    }
239
240    #[test]
241    fn shebang_struct_parse() {
242        assert_eq!(
243            Shebang::from_buf(b"#! /usr/bin/env python3"),
244            Some(Shebang {
245                program: PathBuf::from("/usr/bin/env"),
246                args: vec![OsString::from("python3")]
247            })
248        );
249        assert_eq!(
250            Shebang::from_buf(b"#!/bin/bash\nfoobar"),
251            Some(Shebang {
252                program: PathBuf::from("/bin/bash"),
253                args: Vec::new(),
254            })
255        );
256    }
257
258    #[test]
259    fn shebang_struct_env_python_get_interpreter() {
260        assert_eq!(
261            Shebang::from_buf(b"#! /usr/bin/env python3"),
262            Some(Shebang {
263                program: PathBuf::from("/usr/bin/env"),
264                args: vec![OsString::from("python3")],
265            })
266        );
267
268        assert_eq!(
269            Shebang::from_buf(b"#! /usr/bin/env python3 -c  "),
270            Some(Shebang {
271                program: PathBuf::from("/usr/bin/env"),
272                args: vec![OsString::from("python3"), OsString::from("-c")],
273            })
274        );
275
276        assert_eq!(
277            Shebang::from_buf(b"#! /usr/bin/env python3  -c \nfoobar"),
278            Some(Shebang {
279                program: PathBuf::from("/usr/bin/env"),
280                args: vec![OsString::from("python3"), OsString::from("-c")],
281            })
282        );
283    }
284
285    fn kernel_interpreter(path: &[u8]) -> KernelScript {
286        KernelScript::Interpreter(PathBuf::from(OsStr::from_bytes(path)))
287    }
288
289    #[test]
290    fn kernel_script_reads_the_interpreter_as_load_script_does() {
291        let cases: [(&[u8], KernelScript); 7] = [
292            (b"\x7fELF\x02\x01\x01", KernelScript::NotScript),
293            (b"! /bin/bash", KernelScript::NotScript),
294            // A carriage return is part of the path, unlike in Shebang.
295            (b"#!/tmp/interp\r\n", kernel_interpreter(b"/tmp/interp\r")),
296            (
297                b"#! \t/usr/bin/env python3 -c\nbody",
298                kernel_interpreter(b"/usr/bin/env"),
299            ),
300            // A NUL ends the path.
301            (b"#!/bin/sh\0junk\n", kernel_interpreter(b"/bin/sh")),
302            // A short file is NUL-padded, which ends the path.
303            (b"#!/bin/bash", kernel_interpreter(b"/bin/bash")),
304            (b"#!relative arg\n", kernel_interpreter(b"relative")),
305        ];
306        for (input, expected) in cases {
307            assert_eq!(KernelScript::from_buf(input), expected, "input: {input:?}");
308        }
309    }
310
311    #[test]
312    fn kernel_script_declines_a_missing_or_truncated_interpreter() {
313        for input in [b"#!".as_slice(), b"#!\n", b"#! \t\n", b"#!\0/bin/sh\n"] {
314            assert_eq!(
315                KernelScript::from_buf(input),
316                KernelScript::Declined,
317                "input: {input:?}"
318            );
319        }
320        // No newline and no space, tab or NUL in the first 256 bytes: the path
321        // may be truncated, so the kernel declines it, whatever follows.
322        let mut long = b"#!/".to_vec();
323        long.resize(BINPRM_BUF_SIZE, b'a');
324        assert_eq!(KernelScript::from_buf(&long), KernelScript::Declined);
325        long.extend_from_slice(b" arg\n");
326        assert_eq!(KernelScript::from_buf(&long), KernelScript::Declined);
327        // A space in the last byte ends the path inside the buffer.
328        long.truncate(BINPRM_BUF_SIZE);
329        long[BINPRM_BUF_SIZE - 1] = b' ';
330        let expected = long[2..BINPRM_BUF_SIZE - 1].to_vec();
331        assert_eq!(KernelScript::from_buf(&long), kernel_interpreter(&expected));
332    }
333}