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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
// Copyright (c) iliana destroyer of worlds <iliana@buttslol.net> // SPDX-License-Identifier: MIT //! pentacle is a library for executing programs as sealed anonymous files on Linux, using //! `memfd_create(2)`. //! //! This is useful for executing programs that execute untrusted programs with root permissions, or //! ensuring a cryptographically-verified program is not tampered with after verification but //! before execution. //! //! The library provides [a wrapper around `Command`][`SealedCommand`] as well as two helper //! functions for programs that execute sealed versions of themselves. //! //! ``` //! fn main() { //! pentacle::ensure_sealed().unwrap(); //! //! // The rest of your code //! } //! ``` #![deny( missing_copy_implementations, missing_debug_implementations, missing_docs, rust_2018_idioms, unstable_features )] #![warn(clippy::pedantic)] #![allow(clippy::must_use_candidate, clippy::needless_doctest_main)] #[cfg(not(any(target_os = "linux", target_os = "android")))] compile_error!("pentacle only works on linux or android"); mod syscall; use crate::syscall::{fcntl_add_seals, fcntl_get_seals, memfd_create}; use libc::{F_SEAL_GROW, F_SEAL_SEAL, F_SEAL_SHRINK, F_SEAL_WRITE, MFD_ALLOW_SEALING, MFD_CLOEXEC}; use std::ffi::CStr; use std::fmt::{self, Debug}; use std::fs::File; use std::io::{self, Read, Result, Write}; use std::ops::{Deref, DerefMut}; use std::os::unix::io::AsRawFd; use std::os::unix::process::CommandExt; use std::process::Command; const MEMFD_SEALS: libc::c_int = F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE; /// Ensure the currently running program is a sealed anonymous file. /// /// If `/proc/self/exe` is not a sealed anonymous file, a new anonymous file is created, /// `/proc/self/exe` is copied to it, the file is sealed, and [`CommandExt::exec`] is called. When /// the program begins again, this function will detect `/proc/self/exe` as a sealed anonymous /// file and return `Ok(())`. /// /// You should call this function at the beginning of `main`. This function has the same /// implications as [`CommandExt::exec`]: no destructors on the current stack or any other thread’s /// stack will be run. /// /// # Errors /// /// An error is returned if `/proc/self/exe` fails to open, `memfd_create(2)` fails, the `fcntl(2)` /// `F_ADD_SEALS` command fails, or copying from `/proc/self/exe` to the anonymous file fails. pub fn ensure_sealed() -> Result<()> { let mut file = File::open("/proc/self/exe")?; if is_sealed_inner(&file) { Ok(()) } else { let mut command = SealedCommand::new(&mut file)?; let mut args = std::env::args_os().fuse(); if let Some(arg0) = args.next() { command.arg0(arg0); } command.args(args); Err(command.exec()) } } /// Verify whether the currently running program is a sealed anonymous file. /// /// This function returns `false` if opening `/proc/self/exe` fails. pub fn is_sealed() -> bool { File::open("/proc/self/exe") .map(|f| is_sealed_inner(&f)) .unwrap_or(false) } fn is_sealed_inner(file: &File) -> bool { fcntl_get_seals(file) == MEMFD_SEALS } /// A [`Command`] wrapper that spawns sealed memory-backed programs. /// /// You can use the standard [`Command`] builder methods (such as [`spawn`][`Command::spawn`] and /// [`CommandExt::exec`]) via [`Deref` coercion][`DerefMut`]. pub struct SealedCommand { inner: Command, // we need to keep this memfd open for the lifetime of this struct _memfd: File, } impl SealedCommand { /// Constructs a new [`Command`] for launching the program data in `program` as a sealed /// memory-backed file, with the same default configuration as [`Command::new`]. /// /// The memory-backed file will close on `execve(2)` **unless** the program starts with `#!` /// (indicating that it is an interpreter script). /// /// `argv[0]` of the program will default to the file descriptor path in procfs (for example, /// `/proc/self/fd/3`). [`CommandExt::arg0`] can override this. /// /// # Errors /// /// An error is returned if `memfd_create(2)` fails, the `fcntl(2)` `F_ADD_SEALS` command /// fails, or copying from `program` to the anonymous file fails. pub fn new<R: Read>(program: &mut R) -> Result<Self> { let mut memfd_flags = MFD_ALLOW_SEALING; // If the program starts with `#!` (a shebang or hash-bang), the kernel will (almost // always; depends if `BINFMT_SCRIPT` is enabled) determine which interpreter to exec and // pass the script along as the first argument. In this case, the argument will be // `/proc/self/fd/{}`, which gets closed if MFD_CLOEXEC is set. We check for `#!` and only // set MFD_CLOEXEC if it's not there. let mut buf = [0; 8192]; let n = program.read(&mut buf)?; if !(n >= 2 && &buf[..2] == b"#!") { memfd_flags |= MFD_CLOEXEC; } let memfd_name = unsafe { CStr::from_bytes_with_nul_unchecked(b"pentacle_sealed\0") }; let mut memfd = memfd_create(memfd_name, memfd_flags)?; memfd.write_all(&buf[..n])?; io::copy(program, &mut memfd)?; fcntl_add_seals(&memfd, MEMFD_SEALS)?; Ok(Self { inner: Command::new(format!("/proc/self/fd/{}", memfd.as_raw_fd())), _memfd: memfd, }) } } impl Debug for SealedCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.inner.fmt(f) } } impl Deref for SealedCommand { type Target = Command; fn deref(&self) -> &Command { &self.inner } } impl DerefMut for SealedCommand { fn deref_mut(&mut self) -> &mut Command { &mut self.inner } }