├── .gitignore ├── tests ├── ensure_sealed.rs └── spawn.rs ├── README.md ├── Cargo.toml ├── Cargo.lock ├── LICENSE ├── .github └── workflows │ └── test.yml ├── CHANGELOG.md └── src ├── lib.rs ├── syscall.rs └── file.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /tests/ensure_sealed.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) iliana destroyer of worlds 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Smoke test for the two pub functions in pentacle. 5 | // 6 | // Additional test functions should not be added here due to the test calling CommandExt::exec the 7 | // first time around. 8 | 9 | #![warn(clippy::pedantic)] 10 | 11 | #[test] 12 | fn main() { 13 | pentacle::ensure_sealed().unwrap(); 14 | assert!(pentacle::is_sealed()); 15 | assert!(std::env::args().next().unwrap().contains("ensure_sealed")); 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pentacle 2 | 3 | pentacle is a library for executing programs as sealed anonymous files on Linux, using `memfd_create(2)`. It also has a lower-level interface for creating and sealing anonymous files with various flags. 4 | 5 | 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. 6 | 7 | This library is based on [runc's cloned_binary.c](https://github.com/opencontainers/runc/blob/master/libcontainer/nsenter/cloned_binary.c). 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pentacle" 3 | version = "1.1.0" 4 | edition = "2021" 5 | rust-version = "1.59" 6 | description = "Executes programs as sealed anonymous files on Linux" 7 | readme = "README.md" 8 | repository = "https://github.com/haha-business/pentacle" 9 | license = "MIT" 10 | keywords = ["command", "exec", "memfd", "memfd_create", "seal"] 11 | exclude = [".github", ".gitignore"] 12 | 13 | [dependencies] 14 | libc = "0.2.153" 15 | log = { version = "0.4.4", optional = true } 16 | 17 | [features] 18 | default = ["log"] 19 | 20 | [lints.rust.unexpected_cfgs] 21 | level = "warn" 22 | check-cfg = ["cfg(coverage_nightly)"] 23 | 24 | [package.metadata.docs.rs] 25 | # https://docs.rs/about/metadata 26 | targets = ["x86_64-unknown-linux-gnu", "i686-unknown-linux-gnu"] 27 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "cfg-if" 7 | version = "0.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" 10 | 11 | [[package]] 12 | name = "libc" 13 | version = "0.2.153" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 16 | 17 | [[package]] 18 | name = "log" 19 | version = "0.4.4" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "cba860f648db8e6f269df990180c2217f333472b4a6e901e97446858487971e2" 22 | dependencies = [ 23 | "cfg-if", 24 | ] 25 | 26 | [[package]] 27 | name = "pentacle" 28 | version = "1.1.0" 29 | dependencies = [ 30 | "libc", 31 | "log", 32 | ] 33 | -------------------------------------------------------------------------------- /tests/spawn.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) iliana destroyer of worlds 2 | // SPDX-License-Identifier: MIT 3 | 4 | #![warn(clippy::pedantic)] 5 | 6 | use pentacle::SealedCommand; 7 | use std::fs::File; 8 | use std::io::Result; 9 | 10 | // Test that a SealedCommand can be spawned more than once. 11 | #[test] 12 | fn so_nice_we_ran_it_twice() -> Result<()> { 13 | let mut command = SealedCommand::new(&mut File::open("/bin/sh")?)?; 14 | command.arg("-c").arg("/bin/true"); 15 | for _ in 0..2 { 16 | assert!(command.output()?.status.success()); 17 | } 18 | Ok(()) 19 | } 20 | 21 | // Test that we can execute a script with a shebang. 22 | #[test] 23 | fn shebang() { 24 | let mut command = SealedCommand::new(&mut &b"#!/bin/sh\necho 'it works'\n"[..]).unwrap(); 25 | let output = command.output().unwrap(); 26 | eprintln!("{}", String::from_utf8_lossy(&output.stderr)); 27 | assert!(output.status.success()); 28 | assert_eq!(output.stdout, b"it works\n"); 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) iliana destroyer of worlds 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches-ignore: 5 | - magick 6 | - 'gh-readonly-queue/**' 7 | pull_request: 8 | merge_group: 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | toolchain: [stable, 1.59] 14 | sysctl: ["vm.memfd_noexec=0", "vm.memfd_noexec=1"] 15 | include: 16 | - toolchain: 1.59 17 | # https://users.rust-lang.org/t/skip-doctest-from-command-line/57379/2 18 | test_args: --lib --bins --tests 19 | fail-fast: false 20 | runs-on: ubuntu-24.04 21 | steps: 22 | - uses: actions/checkout@v4 23 | - run: sudo sysctl -w ${{ matrix.sysctl }} 24 | - run: rustup default ${{ matrix.toolchain }} 25 | - run: cargo test --locked ${{ matrix.test_args }} 26 | - run: cargo test --locked --no-default-features ${{ matrix.test_args }} 27 | - run: cargo fmt -- --check 28 | if: ${{ matrix.toolchain == 'stable' }} 29 | - run: cargo clippy --locked --all-targets -- -D warnings 30 | if: ${{ matrix.toolchain == 'stable' }} 31 | - run: RUSTDOCFLAGS="-D warnings" cargo doc --locked 32 | if: ${{ matrix.toolchain == 'stable' }} 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.1.0] - 2024-10-03 10 | ### Added 11 | - `SealOptions`, an interface for directly creating and sealing anonymous files outside the context 12 | of executing them 13 | 14 | ### Fixed 15 | - Creating executable anonymous files works correctly since Linux 6.3 when the sysctl 16 | `vm.memfd_noexec = 1` is set 17 | - `is_sealed` correctly handles the presence of additional seals (e.g. `F_SEAL_FUTURE_WRITE` since 18 | Linux 5.1 or `F_SEAL_EXEC` since Linux 6.3) 19 | 20 | ### Changed 21 | - `SealedCommand` and `execute_sealed` set `F_SEAL_EXEC` on Linux 6.3 and newer 22 | - `log` is now an optional dependency (remains enabled by default) 23 | - Log messages use symbolic names for syscall values 24 | - Moved source repository to 25 | - Minimum supported Rust version (MSRV) now 1.59.0 26 | 27 | ## [1.0.0] - 2020-09-29 28 | ### Changed 29 | - Set `argv[0]` to the original `argv[0]` in `ensure_sealed` 30 | - Minimum supported Rust version (MSRV) now 1.45.0 31 | 32 | ## [0.2.0] - 2020-06-23 33 | ### Changed 34 | - No longer set `MFD_CLOEXEC` if `#!` is detected at the beginning of a program 35 | 36 | ## [0.1.1] - 2020-03-15 37 | ### Changed 38 | - Allow builds on Android platforms 39 | 40 | ## [0.1.0] - 2019-11-15 41 | ### Added 42 | - Everything! 43 | 44 | [Unreleased]: https://github.com/iliana/pentacle/compare/v1.1.0...HEAD 45 | [1.1.0]: https://github.com/iliana/pentacle/compare/v1.0.0...v1.1.0 46 | [1.0.0]: https://github.com/iliana/pentacle/compare/v0.2.0...v1.0.0 47 | [0.2.0]: https://github.com/iliana/pentacle/compare/v0.1.1...v0.2.0 48 | [0.1.1]: https://github.com/iliana/pentacle/compare/v0.1.0...v0.1.1 49 | [0.1.0]: https://github.com/iliana/pentacle/releases/tag/v0.1.0 50 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) iliana destroyer of worlds 2 | // SPDX-License-Identifier: MIT 3 | 4 | //! pentacle is a library for executing programs as sealed anonymous files on Linux, using 5 | //! `memfd_create(2)`. 6 | //! 7 | //! This is useful for executing programs that execute untrusted programs with root permissions, or 8 | //! ensuring a cryptographically-verified program is not tampered with after verification but 9 | //! before execution. 10 | //! 11 | //! The library provides [a wrapper around `Command`][`SealedCommand`] as well as two helper 12 | //! functions, [`ensure_sealed`] and [`is_sealed`], for programs that execute sealed versions of 13 | //! themselves. 14 | //! 15 | //! ``` 16 | //! fn main() { 17 | //! pentacle::ensure_sealed().unwrap(); 18 | //! 19 | //! // The rest of your code 20 | //! } 21 | //! ``` 22 | //! 23 | //! Lower-level control over the creation and sealing of anonymous files is available via 24 | //! [`SealOptions`]. 25 | 26 | #![cfg_attr(coverage_nightly, feature(coverage_attribute))] 27 | #![cfg_attr(not(coverage_nightly), deny(unstable_features))] 28 | #![deny( 29 | missing_copy_implementations, 30 | missing_debug_implementations, 31 | missing_docs, 32 | rust_2018_idioms, 33 | unsafe_op_in_unsafe_fn 34 | )] 35 | #![warn(clippy::pedantic)] 36 | #![allow(clippy::needless_doctest_main)] 37 | 38 | #[cfg(not(any(target_os = "linux", target_os = "android")))] 39 | compile_error!("pentacle only works on linux or android"); 40 | 41 | mod file; 42 | mod syscall; 43 | 44 | pub use crate::file::{MustSealError, SealOptions}; 45 | 46 | use std::fmt::{self, Debug}; 47 | use std::fs::File; 48 | use std::io::{self, Error, Read, Write}; 49 | use std::ops::{Deref, DerefMut}; 50 | use std::os::unix::io::AsRawFd; 51 | use std::os::unix::process::CommandExt; 52 | use std::process::Command; 53 | 54 | const OPTIONS: SealOptions<'static> = SealOptions::new().executable(true); 55 | 56 | /// Ensure the currently running program is a sealed anonymous file. 57 | /// 58 | /// If `/proc/self/exe` is not a sealed anonymous file, a new anonymous file is created, 59 | /// `/proc/self/exe` is copied to it, the file is sealed, and [`CommandExt::exec`] is called. When 60 | /// the program begins again, this function will detect `/proc/self/exe` as a sealed anonymous 61 | /// file and return `Ok(())`. 62 | /// 63 | /// You should call this function at the beginning of `main`. This function has the same 64 | /// implications as [`CommandExt::exec`]: no destructors on the current stack or any other thread’s 65 | /// stack will be run. 66 | /// 67 | /// # Errors 68 | /// 69 | /// An error is returned if `/proc/self/exe` fails to open, `memfd_create(2)` fails, the `fcntl(2)` 70 | /// `F_GET_SEALS` or `F_ADD_SEALS` commands fail, or copying from `/proc/self/exe` to the anonymous 71 | /// file fails. 72 | pub fn ensure_sealed() -> Result<(), Error> { 73 | let mut file = File::open("/proc/self/exe")?; 74 | if OPTIONS.is_sealed(&file) { 75 | Ok(()) 76 | } else { 77 | let mut command = SealedCommand::new(&mut file)?; 78 | let mut args = std::env::args_os().fuse(); 79 | if let Some(arg0) = args.next() { 80 | command.arg0(arg0); 81 | } 82 | command.args(args); 83 | Err(command.exec()) 84 | } 85 | } 86 | 87 | /// Verify whether the currently running program is a sealed anonymous file. 88 | /// 89 | /// This function returns `false` if opening `/proc/self/exe` fails. 90 | #[must_use] 91 | pub fn is_sealed() -> bool { 92 | File::open("/proc/self/exe") 93 | .map(|f| OPTIONS.is_sealed(&f)) 94 | .unwrap_or(false) 95 | } 96 | 97 | /// A [`Command`] wrapper that spawns sealed memory-backed programs. 98 | /// 99 | /// You can use the standard [`Command`] builder methods (such as [`spawn`][`Command::spawn`] and 100 | /// [`CommandExt::exec`]) via [`Deref` coercion][`DerefMut`]. 101 | pub struct SealedCommand { 102 | inner: Command, 103 | // we need to keep this memfd open for the lifetime of this struct 104 | _memfd: File, 105 | } 106 | 107 | impl SealedCommand { 108 | /// Constructs a new [`Command`] for launching the program data in `program` as a sealed 109 | /// memory-backed file, with the same default configuration as [`Command::new`]. 110 | /// 111 | /// The memory-backed file will close on `execve(2)` **unless** the program starts with `#!` 112 | /// (indicating that it is an interpreter script). 113 | /// 114 | /// `argv[0]` of the program will default to the file descriptor path in procfs (for example, 115 | /// `/proc/self/fd/3`). [`CommandExt::arg0`] can override this. 116 | /// 117 | /// # Errors 118 | /// 119 | /// An error is returned if `memfd_create(2)` fails, the `fcntl(2)` `F_GET_SEALS` or 120 | /// `F_ADD_SEALS` commands fail, or copying from `program` to the anonymous file fails. 121 | pub fn new(program: &mut R) -> Result { 122 | // If the program starts with `#!` (a shebang or hash-bang), the kernel will (almost 123 | // always; depends if `BINFMT_SCRIPT` is enabled) determine which interpreter to exec and 124 | // pass the script along as the first argument. In this case, the argument will be 125 | // `/proc/self/fd/{}`, which gets closed if MFD_CLOEXEC is set. We check for `#!` and only 126 | // set MFD_CLOEXEC if it's not there. 127 | let mut buf = [0; 8192]; 128 | let n = program.read(&mut buf)?; 129 | let options = OPTIONS.close_on_exec(buf.get(..2) != Some(b"#!")); 130 | 131 | let mut memfd = options.create()?; 132 | memfd.write_all(&buf[..n])?; 133 | io::copy(program, &mut memfd)?; 134 | options.seal(&mut memfd)?; 135 | 136 | Ok(Self { 137 | inner: Command::new(format!("/proc/self/fd/{}", memfd.as_raw_fd())), 138 | _memfd: memfd, 139 | }) 140 | } 141 | } 142 | 143 | impl Debug for SealedCommand { 144 | #[cfg_attr(coverage_nightly, coverage(off))] 145 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 146 | self.inner.fmt(f) 147 | } 148 | } 149 | 150 | impl Deref for SealedCommand { 151 | type Target = Command; 152 | 153 | #[cfg_attr(coverage_nightly, coverage(off))] 154 | fn deref(&self) -> &Command { 155 | &self.inner 156 | } 157 | } 158 | 159 | impl DerefMut for SealedCommand { 160 | #[cfg_attr(coverage_nightly, coverage(off))] 161 | fn deref_mut(&mut self) -> &mut Command { 162 | &mut self.inner 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/syscall.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) iliana destroyer of worlds 2 | // SPDX-License-Identifier: MIT 3 | 4 | use std::ffi::CStr; 5 | use std::fmt::{self, Debug}; 6 | use std::fs::File; 7 | use std::io::Error; 8 | use std::os::unix::io::{AsRawFd, FromRawFd}; 9 | 10 | use libc::{ 11 | c_char, c_int, c_long, c_uint, syscall, SYS_fcntl, SYS_memfd_create, F_ADD_SEALS, F_GET_SEALS, 12 | F_SEAL_FUTURE_WRITE, F_SEAL_GROW, F_SEAL_SEAL, F_SEAL_SHRINK, F_SEAL_WRITE, MFD_ALLOW_SEALING, 13 | MFD_CLOEXEC, MFD_EXEC, MFD_NOEXEC_SEAL, 14 | }; 15 | 16 | // not yet present in the libc crate 17 | // linux: include/uapi/linux/fcntl.h 18 | const F_SEAL_EXEC: c_int = 0x0020; 19 | 20 | pub(crate) fn memfd_create(name: &CStr, flags: MemfdFlags) -> Result { 21 | let name: *const c_char = name.as_ptr(); 22 | let result = SyscallResult::new(unsafe { syscall(SYS_memfd_create, name, flags.0) }); 23 | #[cfg(feature = "log")] 24 | log::trace!("memfd_create({name:?}, {flags:?}) = {result:?}"); 25 | result.0.map(|value| unsafe { File::from_raw_fd(value) }) 26 | } 27 | 28 | pub(crate) fn fcntl_get_seals(file: &File) -> Result { 29 | let fd: c_int = file.as_raw_fd(); 30 | let result = SyscallResult::new(unsafe { syscall(SYS_fcntl, fd, F_GET_SEALS) }); 31 | let result = SyscallResult(result.0.map(SealFlags)); 32 | #[cfg(feature = "log")] 33 | log::trace!("fcntl({fd}, F_GET_SEALS) = {result:?}"); 34 | result.0 35 | } 36 | 37 | pub(crate) fn fcntl_add_seals(file: &File, arg: SealFlags) -> Result<(), Error> { 38 | let fd: c_int = file.as_raw_fd(); 39 | let result = SyscallResult::new(unsafe { syscall(SYS_fcntl, fd, F_ADD_SEALS, arg.0) }); 40 | #[cfg(feature = "log")] 41 | log::trace!("fcntl({fd}, F_ADD_SEALS, {arg:?}) = {result:?}"); 42 | result.0.map(|_| ()) 43 | } 44 | 45 | #[repr(transparent)] 46 | struct SyscallResult(Result); 47 | 48 | impl SyscallResult { 49 | #[inline] 50 | fn new(value: c_long) -> Self { 51 | // The `syscall` function returns c_long regardless of the actual return value of the 52 | // syscall. In the case of memfd_create(2) and fcntl(2), both syscalls return c_int. 53 | // Truncation of the return value is correct behavior on Linux; see: 54 | // https://github.com/rust-lang/rust/blob/56e35a5dbb37898433a43133dff0398f46d577b8/library/std/src/sys/pal/unix/weak.rs#L160-L184 55 | #![allow(clippy::cast_possible_truncation)] 56 | let value = value as c_int; 57 | 58 | // memfd_create(2) and fcntl(2) both return -1 on error. 59 | if value == -1 { 60 | Self(Err(Error::last_os_error())) 61 | } else { 62 | Self(Ok(value)) 63 | } 64 | } 65 | } 66 | 67 | impl Debug for SyscallResult { 68 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 69 | match &self.0 { 70 | Ok(value) => write!(f, "{value:?}"), 71 | Err(err) => write!(f, "-1 {err}"), 72 | } 73 | } 74 | } 75 | 76 | /// "mom, can i have bitflags?" "we have bitflags at home" 77 | macro_rules! flags_impl { 78 | ($struct:ident, $type:ty, $($name:ident => $flag:ident),* $(,)?) => { 79 | #[derive(Copy, Clone, PartialEq)] 80 | #[repr(transparent)] 81 | #[must_use] 82 | pub(crate) struct $struct($type); 83 | 84 | impl $struct { 85 | $( 86 | pub(crate) const $name: Self = Self($flag); 87 | )* 88 | const NAMES: &'static [($type, &'static str)] = &[$(($flag, stringify!($flag))),*]; 89 | const KNOWN: $type = 0 $(| $flag)*; 90 | 91 | #[inline] 92 | #[allow(unused)] 93 | pub(crate) const fn all(self, other: Self) -> bool { 94 | self.0 & other.0 == other.0 95 | } 96 | 97 | #[inline] 98 | #[allow(unused)] 99 | pub(crate) const fn any(self, other: Self) -> bool { 100 | self.0 & other.0 != 0 101 | } 102 | 103 | #[inline] 104 | #[allow(unused)] 105 | pub(crate) const fn only(self, other: Self) -> Self { 106 | Self(self.0 & other.0) 107 | } 108 | 109 | #[inline] 110 | pub(crate) const fn set(self, other: Self, value: bool) -> Self { 111 | if value { 112 | Self(self.0 | other.0) 113 | } else { 114 | Self(self.0 & !(other.0 & Self::KNOWN)) 115 | } 116 | } 117 | } 118 | 119 | impl Debug for $struct { 120 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 121 | if self.0 == 0 { 122 | return write!(f, "0"); 123 | } 124 | let mut remaining = *self; 125 | let mut first = true; 126 | for (flag, name) in Self::NAMES { 127 | if remaining.0 & flag != 0 { 128 | write!(f, "{}{name}", if first { "" } else { "|" })?; 129 | remaining = remaining.set(Self(*flag), false); 130 | first = false; 131 | } 132 | } 133 | if remaining.0 != 0 { 134 | write!(f, "{}{:#x}", if first { "" } else { "|" }, remaining.0)?; 135 | } 136 | if !first { 137 | write!(f, " ({:#x})", self.0)?; 138 | } 139 | Ok(()) 140 | } 141 | } 142 | }; 143 | } 144 | 145 | flags_impl!( 146 | MemfdFlags, 147 | c_uint, 148 | CLOEXEC => MFD_CLOEXEC, 149 | ALLOW_SEALING => MFD_ALLOW_SEALING, 150 | NOEXEC_SEAL => MFD_NOEXEC_SEAL, 151 | EXEC => MFD_EXEC, 152 | ); 153 | 154 | flags_impl!( 155 | SealFlags, 156 | c_int, 157 | SEAL => F_SEAL_SEAL, 158 | SHRINK => F_SEAL_SHRINK, 159 | GROW => F_SEAL_GROW, 160 | WRITE => F_SEAL_WRITE, 161 | FUTURE_WRITE => F_SEAL_FUTURE_WRITE, 162 | EXEC => F_SEAL_EXEC, 163 | ); 164 | 165 | #[cfg(test)] 166 | mod test { 167 | use super::MemfdFlags; 168 | 169 | #[test] 170 | fn flags_debug() { 171 | assert_eq!(format!("{:?}", MemfdFlags(0)), "0"); 172 | assert_eq!(format!("{:?}", MemfdFlags(0x1)), "MFD_CLOEXEC (0x1)"); 173 | assert_eq!( 174 | format!("{:?}", MemfdFlags(0x3)), 175 | "MFD_CLOEXEC|MFD_ALLOW_SEALING (0x3)" 176 | ); 177 | assert_eq!(format!("{:?}", MemfdFlags(0x80)), "0x80"); 178 | assert_eq!(format!("{:?}", MemfdFlags(0x81)), "MFD_CLOEXEC|0x80 (0x81)"); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/file.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) iliana destroyer of worlds 2 | // SPDX-License-Identifier: MIT 3 | 4 | //! Lower-level functions for creating sealed anonymous files. 5 | 6 | use std::ffi::CStr; 7 | use std::fmt::{self, Debug, Display}; 8 | use std::fs::{File, Permissions}; 9 | use std::io::{self, Error, ErrorKind, Read}; 10 | use std::os::unix::fs::PermissionsExt as _; 11 | 12 | use crate::syscall::{MemfdFlags, SealFlags}; 13 | 14 | use libc::EINVAL; 15 | 16 | // SAFETY: The provided slice is nul-terminated and does not contain any interior nul bytes. On Rust 17 | // 1.64 and later (rust-lang/rust#99977), these required invariants are checked at compile time. 18 | // 19 | // The ideal alternative here is to use C-string literals, introduced in Rust 1.77, but that is a 20 | // steep MSRV bump to introduce at time of writing this comment. 21 | const DEFAULT_MEMFD_NAME: &CStr = 22 | unsafe { CStr::from_bytes_with_nul_unchecked(b"pentacle_sealed\0") }; 23 | 24 | macro_rules! seal { 25 | ( 26 | $seal_ident:ident 27 | $( { $( #[ $attr:meta ] )* } )? , 28 | $must_seal_ident:ident 29 | $( { $( #[ $must_attr:meta ] )* } )? , 30 | $( ? $preflight:ident : )? $flag:ident, 31 | $try_to:expr, 32 | $default:expr 33 | ) => { 34 | #[doc = concat!("If `true`, try to ", $try_to, ".")] 35 | #[doc = ""] 36 | #[doc = "If `false`, also set"] 37 | #[doc = concat!("[`SealOptions::", stringify!($must_seal_ident), "`]")] 38 | #[doc = "to `false`."] 39 | #[doc = ""] 40 | #[doc = concat!("This flag is `", $default, "` by default.")] 41 | $($( #[ $attr ] )*)? 42 | pub const fn $seal_ident(mut self, $seal_ident: bool) -> SealOptions<'a> { 43 | if true $( && self.$preflight() )? { 44 | self.seal_flags = self.seal_flags.set(SealFlags::$flag, $seal_ident); 45 | } 46 | if !$seal_ident { 47 | self.must_seal_flags = self.must_seal_flags.set(SealFlags::$flag, false); 48 | } 49 | self 50 | } 51 | 52 | #[doc = "If `true`, also set"] 53 | #[doc = concat!("[`SealOptions::", stringify!($seal_ident), "`] to `true`")] 54 | #[doc = "and ensure it is successful when [`SealOptions::seal`] is called."] 55 | #[doc = ""] 56 | #[doc = concat!("This flag is `", $default, "` by default.")] 57 | $($( #[ $must_attr ] )*)? 58 | pub const fn $must_seal_ident(mut self, $must_seal_ident: bool) -> SealOptions<'a> { 59 | if $must_seal_ident { 60 | self.seal_flags = self.seal_flags.set(SealFlags::$flag, true); 61 | } 62 | self.must_seal_flags = self.must_seal_flags.set(SealFlags::$flag, $must_seal_ident); 63 | self 64 | } 65 | }; 66 | } 67 | 68 | /// Options for creating a sealed anonymous file. 69 | #[derive(Debug, Clone, PartialEq)] 70 | #[must_use] 71 | pub struct SealOptions<'a> { 72 | memfd_name: &'a CStr, 73 | memfd_flags: MemfdFlags, 74 | seal_flags: SealFlags, 75 | must_seal_flags: SealFlags, 76 | } 77 | 78 | impl<'a> SealOptions<'a> { 79 | /// Create a default set of options ready for configuration. 80 | /// 81 | /// This is equivalent to: 82 | /// ``` 83 | /// # use pentacle::SealOptions; 84 | /// # let result = 85 | /// SealOptions::new() 86 | /// .close_on_exec(true) 87 | /// .memfd_name(c"pentacle_sealed") 88 | /// .must_seal_seals(true) 89 | /// .must_seal_shrinking(true) 90 | /// .must_seal_growing(true) 91 | /// .must_seal_writing(true) 92 | /// .seal_future_writing(false) 93 | /// .seal_executable(false); 94 | /// # assert_eq!(result, SealOptions::new()); 95 | /// ``` 96 | pub const fn new() -> SealOptions<'a> { 97 | const MEMFD_DEFAULT: MemfdFlags = MemfdFlags::CLOEXEC.set(MemfdFlags::ALLOW_SEALING, true); 98 | const SEAL_DEFAULT: SealFlags = SealFlags::SEAL 99 | .set(SealFlags::SHRINK, true) 100 | .set(SealFlags::GROW, true) 101 | .set(SealFlags::WRITE, true); 102 | 103 | SealOptions { 104 | memfd_name: DEFAULT_MEMFD_NAME, 105 | memfd_flags: MEMFD_DEFAULT, 106 | seal_flags: SEAL_DEFAULT, 107 | must_seal_flags: SEAL_DEFAULT, 108 | } 109 | } 110 | 111 | /// Sets the close-on-exec (`CLOEXEC`) flag for the new file. 112 | /// 113 | /// When a child process is created, the child normally inherits any open file descriptors. 114 | /// Setting the close-on-exec flag will cause this file descriptor to automatically be closed 115 | /// instead. 116 | /// 117 | /// This flag is `true` by default, matching the behavior of [`std::fs`]. 118 | pub const fn close_on_exec(mut self, close_on_exec: bool) -> SealOptions<'a> { 119 | self.memfd_flags = self.memfd_flags.set(MemfdFlags::CLOEXEC, close_on_exec); 120 | self 121 | } 122 | 123 | /// Sets whether the resulting file must have or not have execute permission set. 124 | /// 125 | /// If set, the OS is explicitly asked to set the execute permission when `exec` is 126 | /// `true`, or unset the execute permission when `exec` is `false`. If the OS refuses, 127 | /// [`SealOptions::create`] tries to set or unset the execute permission, and returns an error 128 | /// if it fails. 129 | /// 130 | /// Calling this function enables the equivalent of calling [`SealOptions::seal_executable`] 131 | /// with `true` for implementation reasons. 132 | /// 133 | /// This flag is neither `true` nor `false` by default; instead behavior is delegated to the 134 | /// OS's default behavior. 135 | /// 136 | /// # Context 137 | /// 138 | /// The original `memfd_create(2)` implementation on Linux creates anonymous files with the 139 | /// executable permission set. Later in Linux 6.3, programs and system administrators were 140 | /// given tools to control this (see also ): 141 | /// 142 | /// - Setting the sysctl `vm.memfd_noexec = 1` disables creating executable anonymous files 143 | /// unless the program requests it with `MFD_EXEC` (set by pentacle if `executable` is 144 | /// `true`). 145 | /// - Setting the sysctl `vm.memfd_noexec = 2` disables the ability to create executable 146 | /// anonymous files altogether, and `MFD_NOEXEC_SEAL` _must_ be used (set by pentacle if 147 | /// `executable` is `false`). 148 | /// - Calling `memfd_create(2)` with `MFD_NOEXEC_SEAL` enables the `F_SEAL_EXEC` seal. 149 | /// 150 | /// Linux prior to 6.3 is unaware of `MFD_EXEC` and `F_SEAL_EXEC`. If `memfd_create(2)` sets 151 | /// `errno` to `EINVAL`, this library retries the call without possibly-unknown flags, and the 152 | /// permission bits of the memfd are adjusted depending on this setting. 153 | pub const fn executable(mut self, executable: bool) -> SealOptions<'a> { 154 | self.memfd_flags = self 155 | .memfd_flags 156 | .set(MemfdFlags::EXEC, executable) 157 | .set(MemfdFlags::NOEXEC_SEAL, !executable); 158 | self.seal_flags = self.seal_flags.set(SealFlags::EXEC, true); 159 | self 160 | } 161 | 162 | const fn is_executable_set(&self) -> bool { 163 | const MASK: MemfdFlags = MemfdFlags::EXEC.set(MemfdFlags::NOEXEC_SEAL, true); 164 | 165 | self.memfd_flags.any(MASK) 166 | } 167 | 168 | /// Set a name for the file for debugging purposes. 169 | /// 170 | /// On Linux, this name is displayed as the target of the symlink in `/proc/self/fd/`. 171 | /// 172 | /// The default name is `pentacle_sealed`. 173 | pub const fn memfd_name(mut self, name: &'a CStr) -> SealOptions<'a> { 174 | self.memfd_name = name; 175 | self 176 | } 177 | 178 | seal!( 179 | seal_seals, 180 | must_seal_seals, 181 | SEAL, 182 | "prevent further seals from being set on this file", 183 | true 184 | ); 185 | seal!( 186 | seal_shrinking, 187 | must_seal_shrinking, 188 | SHRINK, 189 | "prevent shrinking this file", 190 | true 191 | ); 192 | seal!( 193 | seal_growing, 194 | must_seal_growing, 195 | GROW, 196 | "prevent growing this file", 197 | true 198 | ); 199 | seal!( 200 | seal_writing, 201 | must_seal_writing, 202 | WRITE, 203 | "prevent writing to this file", 204 | true 205 | ); 206 | seal!( 207 | seal_future_writing { 208 | #[doc = ""] 209 | #[doc = "This requires at least Linux 5.1."] 210 | }, 211 | must_seal_future_writing { 212 | #[doc = ""] 213 | #[doc = "This requires at least Linux 5.1."] 214 | }, 215 | FUTURE_WRITE, 216 | "prevent directly writing to this file or creating new writable mappings, \ 217 | but allow writes to existing writable mappings", 218 | false 219 | ); 220 | seal!( 221 | seal_executable { 222 | #[doc = ""] 223 | #[doc = "If [`SealOptions::executable`] has already been called,"] 224 | #[doc = "this function does nothing, apart from setting"] 225 | #[doc = "[`SealOptions::must_seal_executable`] to `false`"] 226 | #[doc = "if `seal_executable` is `false`."] 227 | #[doc = ""] 228 | #[doc = "This requires at least Linux 6.3."] 229 | }, 230 | must_seal_executable { 231 | #[doc = ""] 232 | #[doc = "This requires at least Linux 6.3."] 233 | }, 234 | ? seal_executable_preflight : EXEC, 235 | "prevent modifying the executable permission of the file", 236 | false 237 | ); 238 | 239 | const fn seal_executable_preflight(&self) -> bool { 240 | !self.is_executable_set() 241 | } 242 | 243 | /// Create an anonymous file, copy the contents of `reader` to it, and seal it. 244 | /// 245 | /// This is equivalent to: 246 | /// ``` 247 | /// # let options = pentacle::SealOptions::new(); 248 | /// # let reader: &mut &[u8] = &mut &[][..]; 249 | /// let mut file = options.create()?; 250 | /// std::io::copy(reader, &mut file)?; 251 | /// options.seal(&mut file)?; 252 | /// # Ok::<(), std::io::Error>(()) 253 | /// ``` 254 | /// 255 | /// # Errors 256 | /// 257 | /// This method returns an error when any of [`SealOptions::create`], [`std::io::copy`], or 258 | /// [`SealOptions::seal`] fail. 259 | pub fn copy_and_seal(&self, reader: &mut R) -> Result { 260 | let mut file = self.create()?; 261 | io::copy(reader, &mut file)?; 262 | self.seal(&mut file)?; 263 | Ok(file) 264 | } 265 | 266 | /// Create an unsealed anonymous file with these options. 267 | /// 268 | /// It is the caller's responsibility to seal this file after writing with 269 | /// [`SealOptions::seal`]. If possible, avoid using this function and prefer 270 | /// [`SealOptions::copy_and_seal`]. 271 | /// 272 | /// # Errors 273 | /// 274 | /// This method returns an error when: 275 | /// - `memfd_create(2)` fails 276 | /// - `SealOptions::executable` was set but permissions cannot be changed as required 277 | pub fn create(&self) -> Result { 278 | let file = match crate::syscall::memfd_create(self.memfd_name, self.memfd_flags) { 279 | Ok(file) => file, 280 | Err(err) if err.raw_os_error() == Some(EINVAL) && self.is_executable_set() => { 281 | // Linux prior to 6.3 will not know about `MFD_EXEC` or `MFD_NOEXEC_SEAL`, 282 | // and returns `EINVAL` when it gets unknown flag bits. Retry without the 283 | // possibly-unknown flag, and then attempt to set the appropriate permissions. 284 | // 285 | // (If `vm.memfd_noexec = 2`, we won't hit this branch because the OS returns 286 | // EACCES.) 287 | crate::syscall::memfd_create( 288 | self.memfd_name, 289 | self.memfd_flags 290 | .set(MemfdFlags::EXEC, false) 291 | .set(MemfdFlags::NOEXEC_SEAL, false), 292 | )? 293 | } 294 | Err(err) => return Err(err), 295 | }; 296 | 297 | if self.is_executable_set() { 298 | let permissions = file.metadata()?.permissions(); 299 | let new_permissions = 300 | Permissions::from_mode(if self.memfd_flags.all(MemfdFlags::NOEXEC_SEAL) { 301 | permissions.mode() & !0o111 302 | } else if self.memfd_flags.all(MemfdFlags::EXEC) { 303 | permissions.mode() | 0o111 304 | } else { 305 | return Ok(file); 306 | }); 307 | if permissions != new_permissions { 308 | file.set_permissions(new_permissions)?; 309 | } 310 | } 311 | 312 | Ok(file) 313 | } 314 | 315 | /// Seal an anonymous file with these options. 316 | /// 317 | /// This should be called on a file created with [`SealOptions::create`]. Attempting to use 318 | /// this method on other files will likely fail. 319 | /// 320 | /// # Errors 321 | /// 322 | /// This method returns an error when: 323 | /// - the `fcntl(2)` `F_ADD_SEALS` command fails (other than `EINVAL`) 324 | /// - the `fcntl(2)` `F_GET_SEALS` command fails 325 | /// - if any required seals are not present (in this case, 326 | /// [`Error::source`][`std::error::Error::source`] will be [`MustSealError`]) 327 | pub fn seal(&self, file: &mut File) -> Result<(), Error> { 328 | // Set seals in groups, based on how recently the seal was added to Linux. Ignore `EINVAL`; 329 | // we'll verify against `self.must_seal_flags`. 330 | const GROUPS: &[SealFlags] = &[ 331 | // Linux 6.3 332 | SealFlags::EXEC, 333 | // Linux 5.1 334 | SealFlags::FUTURE_WRITE, 335 | // Linux 3.17 336 | SealFlags::SEAL 337 | .set(SealFlags::SHRINK, true) 338 | .set(SealFlags::GROW, true) 339 | .set(SealFlags::WRITE, true), 340 | ]; 341 | 342 | for group in GROUPS { 343 | match crate::syscall::fcntl_add_seals(file, self.seal_flags.only(*group)) { 344 | Ok(()) => {} 345 | Err(err) if err.raw_os_error() == Some(EINVAL) => {} 346 | Err(err) => return Err(err), 347 | } 348 | } 349 | 350 | if self.is_sealed_inner(file)? { 351 | Ok(()) 352 | } else { 353 | Err(Error::new( 354 | ErrorKind::InvalidInput, 355 | MustSealError { _priv: () }, 356 | )) 357 | } 358 | } 359 | 360 | /// Check if `file` is sealed as required by these options. 361 | /// 362 | /// If the file doesn't support sealing (or `fcntl(2)` otherwise returns an error), this method 363 | /// returns `false`. 364 | #[must_use] 365 | pub fn is_sealed(&self, file: &File) -> bool { 366 | self.is_sealed_inner(file).unwrap_or(false) 367 | } 368 | 369 | fn is_sealed_inner(&self, file: &File) -> Result { 370 | Ok(crate::syscall::fcntl_get_seals(file)?.all(self.must_seal_flags)) 371 | } 372 | } 373 | 374 | impl<'a> Default for SealOptions<'a> { 375 | fn default() -> SealOptions<'a> { 376 | SealOptions::new() 377 | } 378 | } 379 | 380 | /// The [`Error::source`][`std::error::Error::source`] returned by [`SealOptions::seal`] if required 381 | /// seals are not present. 382 | #[allow(missing_copy_implementations)] 383 | pub struct MustSealError { 384 | _priv: (), 385 | } 386 | 387 | impl Debug for MustSealError { 388 | #[cfg_attr(coverage_nightly, coverage(off))] 389 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 390 | f.debug_struct("MustSealError").finish_non_exhaustive() 391 | } 392 | } 393 | 394 | impl Display for MustSealError { 395 | #[cfg_attr(coverage_nightly, coverage(off))] 396 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 397 | write!(f, "some required seals are not present") 398 | } 399 | } 400 | 401 | impl std::error::Error for MustSealError {} 402 | 403 | #[cfg(test)] 404 | mod test { 405 | use std::ffi::CString; 406 | use std::os::unix::fs::PermissionsExt as _; 407 | 408 | use super::{MemfdFlags, SealFlags, SealOptions, DEFAULT_MEMFD_NAME}; 409 | 410 | const ALL_SEALS: SealFlags = SealFlags::SEAL 411 | .set(SealFlags::SHRINK, true) 412 | .set(SealFlags::GROW, true) 413 | .set(SealFlags::WRITE, true) 414 | .set(SealFlags::FUTURE_WRITE, true) 415 | .set(SealFlags::EXEC, true); 416 | const NO_SEALS: SealFlags = SealFlags::SEAL.set(SealFlags::SEAL, false); 417 | 418 | #[cfg_attr(coverage_nightly, coverage(off))] 419 | #[test] 420 | fn new() { 421 | let options = SealOptions { 422 | memfd_name: &CString::new("asdf").unwrap(), 423 | memfd_flags: MemfdFlags::ALLOW_SEALING, 424 | seal_flags: NO_SEALS, 425 | must_seal_flags: ALL_SEALS, 426 | }; 427 | assert_eq!( 428 | options 429 | .close_on_exec(true) 430 | .memfd_name(DEFAULT_MEMFD_NAME) 431 | .must_seal_seals(true) 432 | .must_seal_shrinking(true) 433 | .must_seal_growing(true) 434 | .must_seal_writing(true) 435 | .seal_future_writing(false) 436 | .seal_executable(false), 437 | SealOptions::new() 438 | ); 439 | } 440 | 441 | #[cfg_attr(coverage_nightly, coverage(off))] 442 | #[test] 443 | fn flags() { 444 | let mut options = SealOptions::new(); 445 | assert!(options.memfd_flags.all(MemfdFlags::ALLOW_SEALING)); 446 | 447 | assert!(options.memfd_flags.all(MemfdFlags::CLOEXEC)); 448 | options = options.close_on_exec(false); 449 | assert!(!options.memfd_flags.any(MemfdFlags::CLOEXEC)); 450 | options = options.close_on_exec(true); 451 | assert!(options.memfd_flags.all(MemfdFlags::CLOEXEC)); 452 | 453 | assert_eq!( 454 | options.seal_flags, 455 | ALL_SEALS 456 | .set(SealFlags::FUTURE_WRITE, false) 457 | .set(SealFlags::EXEC, false) 458 | ); 459 | assert_eq!( 460 | options.must_seal_flags, 461 | ALL_SEALS 462 | .set(SealFlags::FUTURE_WRITE, false) 463 | .set(SealFlags::EXEC, false) 464 | ); 465 | options = options 466 | .must_seal_future_writing(true) 467 | .must_seal_executable(true); 468 | assert_eq!(options.seal_flags, ALL_SEALS); 469 | assert_eq!(options.must_seal_flags, ALL_SEALS); 470 | // `seal_*(false)` unsets `must_seal_*` 471 | options = options 472 | .seal_seals(false) 473 | .seal_shrinking(false) 474 | .seal_growing(false) 475 | .seal_writing(false) 476 | .seal_future_writing(false) 477 | .seal_executable(false); 478 | assert_eq!(options.seal_flags, NO_SEALS); 479 | assert_eq!(options.must_seal_flags, NO_SEALS); 480 | // `seal_*(true)` does not set `must_seal_*` 481 | options = options 482 | .seal_seals(true) 483 | .seal_shrinking(true) 484 | .seal_growing(true) 485 | .seal_writing(true) 486 | .seal_future_writing(true) 487 | .seal_executable(true); 488 | assert_eq!(options.seal_flags, ALL_SEALS); 489 | assert_eq!(options.must_seal_flags, NO_SEALS); 490 | // `must_seal_*(true)` sets `seal_*` 491 | options = options 492 | .seal_seals(false) 493 | .seal_shrinking(false) 494 | .seal_growing(false) 495 | .seal_writing(false) 496 | .seal_future_writing(false) 497 | .seal_executable(false); 498 | assert_eq!(options.seal_flags, NO_SEALS); 499 | assert_eq!(options.must_seal_flags, NO_SEALS); 500 | options = options 501 | .must_seal_seals(true) 502 | .must_seal_shrinking(true) 503 | .must_seal_growing(true) 504 | .must_seal_writing(true) 505 | .must_seal_future_writing(true) 506 | .must_seal_executable(true); 507 | assert_eq!(options.seal_flags, ALL_SEALS); 508 | assert_eq!(options.must_seal_flags, ALL_SEALS); 509 | // `must_seal_*(false)` does not unset `seal_*` 510 | options = options 511 | .must_seal_seals(false) 512 | .must_seal_shrinking(false) 513 | .must_seal_growing(false) 514 | .must_seal_writing(false) 515 | .must_seal_future_writing(false) 516 | .must_seal_executable(false); 517 | assert_eq!(options.seal_flags, ALL_SEALS); 518 | assert_eq!(options.must_seal_flags, NO_SEALS); 519 | } 520 | 521 | #[cfg_attr(coverage_nightly, coverage(off))] 522 | #[test] 523 | fn execute_flags() { 524 | let mut options = SealOptions::new(); 525 | assert!(!options.seal_flags.any(SealFlags::EXEC)); 526 | options = options.seal_executable(true); 527 | assert!(options.seal_flags.all(SealFlags::EXEC)); 528 | options = options.seal_executable(false); 529 | assert!(!options.seal_flags.any(SealFlags::EXEC)); 530 | 531 | for _ in 0..2 { 532 | options = options.executable(true); 533 | assert!(options.memfd_flags.all(MemfdFlags::EXEC)); 534 | assert!(!options.memfd_flags.any(MemfdFlags::NOEXEC_SEAL)); 535 | assert!(options.seal_flags.all(SealFlags::EXEC)); 536 | // no-op once `executable` is called 537 | options = options.seal_executable(false); 538 | assert!(options.seal_flags.all(SealFlags::EXEC)); 539 | 540 | options = options.executable(false); 541 | assert!(!options.memfd_flags.any(MemfdFlags::EXEC)); 542 | assert!(options.memfd_flags.all(MemfdFlags::NOEXEC_SEAL)); 543 | assert!(options.seal_flags.all(SealFlags::EXEC)); 544 | // no-op once `executable` is called 545 | options = options.seal_executable(false); 546 | assert!(options.seal_flags.all(SealFlags::EXEC)); 547 | } 548 | 549 | assert!(!options.must_seal_flags.any(SealFlags::EXEC)); 550 | options = options.must_seal_executable(true); 551 | assert!(options.seal_flags.all(SealFlags::EXEC)); 552 | assert!(options.must_seal_flags.all(SealFlags::EXEC)); 553 | options = options.seal_executable(false); 554 | assert!(options.seal_flags.all(SealFlags::EXEC)); 555 | assert!(!options.must_seal_flags.any(SealFlags::EXEC)); 556 | } 557 | 558 | #[cfg_attr(coverage_nightly, coverage(off))] 559 | #[test] 560 | fn executable() { 561 | let file = SealOptions::new() 562 | .executable(false) 563 | .copy_and_seal(&mut &[][..]) 564 | .unwrap(); 565 | assert_eq!(file.metadata().unwrap().permissions().mode() & 0o111, 0); 566 | 567 | let file = SealOptions::new() 568 | .executable(true) 569 | .copy_and_seal(&mut &[][..]) 570 | .unwrap(); 571 | assert_eq!(file.metadata().unwrap().permissions().mode() & 0o111, 0o111); 572 | } 573 | } 574 | --------------------------------------------------------------------------------