Skip to content

Commit 984ce03

Browse files
committed
ioctls: Extract unsafe ioctl code into composefs-ioctls crate
Move all ioctl-related unsafe code (fs-verity ioctls, loop device ioctls) into a dedicated crate. This allows the main composefs crate and all other crates to use `#![forbid(unsafe_code)]`. Relates to: #123 Assisted-by: OpenCode (Claude claude-opus-4-5@20251101) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent fc1d4e2 commit 984ce03

16 files changed

Lines changed: 628 additions & 274 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ unsafe_code = "deny" # https://github.com/containers/composefs-rs/issues/123
1717

1818
[workspace.dependencies]
1919
composefs = { version = "0.3.0", path = "crates/composefs", default-features = false }
20+
composefs-ioctls = { version = "0.3.0", path = "crates/composefs-ioctls", default-features = false }
2021
composefs-oci = { version = "0.3.0", path = "crates/composefs-oci", default-features = false }
2122
composefs-boot = { version = "0.3.0", path = "crates/composefs-boot", default-features = false }
2223
composefs-http = { version = "0.3.0", path = "crates/composefs-http", default-features = false }

crates/composefs-boot/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! bootloader entries. It supports both Boot Loader Specification (Type 1) entries
66
//! and Unified Kernel Images (Type 2) for UEFI boot.
77
8+
#![forbid(unsafe_code)]
89
#![deny(missing_debug_implementations)]
910

1011
pub mod bootloader;

crates/composefs-fuse/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
//! directory trees through FUSE. It supports read-only access to files, directories,
55
//! symlinks, and extended attributes, with data served from a composefs repository.
66
7+
#![forbid(unsafe_code)]
8+
79
use std::{
810
collections::HashMap,
911
ffi::OsStr,

crates/composefs-http/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
//! referenced objects from HTTP servers. It handles recursive fetching of nested splitstream
55
//! references and verifies content integrity using fsverity checksums.
66
7+
#![forbid(unsafe_code)]
8+
79
use std::{
810
collections::{HashMap, HashSet},
911
fs::File,

crates/composefs-ioctls/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "composefs-ioctls"
3+
description = "Low-level ioctl wrappers for composefs (fs-verity, loop devices)"
4+
keywords = ["composefs", "fsverity", "ioctl"]
5+
6+
edition.workspace = true
7+
license.workspace = true
8+
readme.workspace = true
9+
repository.workspace = true
10+
rust-version.workspace = true
11+
version.workspace = true
12+
13+
[features]
14+
default = []
15+
loop-device = []
16+
17+
[dependencies]
18+
rustix = { version = "1.0.0", features = ["fs"] }
19+
thiserror = "2"
20+
21+
[dev-dependencies]
22+
tempfile = "3.8.0"
23+
test-with = { version = "0.14", default-features = false }
24+
25+
[lints]
26+
workspace = true
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
//! Low-level ioctl interfaces for fs-verity kernel operations.
2+
//!
3+
//! This module provides safe wrappers around the Linux fs-verity ioctls
4+
//! for enabling and measuring fs-verity on files.
5+
6+
#![allow(unsafe_code)]
7+
8+
use std::{io::Error, os::fd::AsFd};
9+
10+
use rustix::{
11+
io::Errno,
12+
ioctl::{ioctl, opcode, Opcode, Setter, Updater},
13+
};
14+
use thiserror::Error;
15+
16+
/// Enabling fsverity failed.
17+
#[derive(Error, Debug)]
18+
pub enum EnableVerityError {
19+
/// I/O operation failed.
20+
#[error("{0}")]
21+
Io(#[from] Error),
22+
/// The filesystem does not support fs-verity.
23+
#[error("Filesystem does not support fs-verity")]
24+
FilesystemNotSupported,
25+
/// fs-verity is already enabled on the file.
26+
#[error("fs-verity is already enabled on file")]
27+
AlreadyEnabled,
28+
/// The file has an open writable file descriptor.
29+
#[error("File is opened for writing")]
30+
FileOpenedForWrite,
31+
/// Signature verification failed (when using kernel signatures).
32+
#[error("Signature verification failed")]
33+
SignatureVerificationFailed,
34+
}
35+
36+
/// Measuring fsverity failed.
37+
#[derive(Error, Debug)]
38+
pub enum MeasureVerityError {
39+
/// I/O operation failed.
40+
#[error("{0}")]
41+
Io(#[from] Error),
42+
/// fs-verity is not enabled on the file.
43+
#[error("fs-verity is not enabled on file")]
44+
VerityMissing,
45+
/// The filesystem does not support fs-verity.
46+
#[error("fs-verity is not supported by filesystem")]
47+
FilesystemNotSupported,
48+
/// The hash algorithm does not match the expected algorithm.
49+
#[error("Expected algorithm {expected}, found {found}")]
50+
InvalidDigestAlgorithm {
51+
/// The expected algorithm identifier.
52+
expected: u16,
53+
/// The actual algorithm identifier found.
54+
found: u16,
55+
},
56+
/// The digest size does not match the expected size.
57+
#[error("Expected digest size {expected}")]
58+
InvalidDigestSize {
59+
/// The expected digest size in bytes.
60+
expected: u16,
61+
},
62+
}
63+
64+
// See /usr/include/linux/fsverity.h
65+
#[repr(C)]
66+
#[derive(Debug)]
67+
struct FsVerityEnableArg {
68+
version: u32,
69+
hash_algorithm: u32,
70+
block_size: u32,
71+
salt_size: u32,
72+
salt_ptr: u64,
73+
sig_size: u32,
74+
__reserved1: u32,
75+
sig_ptr: u64,
76+
__reserved2: [u64; 11],
77+
}
78+
79+
// #define FS_IOC_ENABLE_VERITY _IOW('f', 133, struct fsverity_enable_arg)
80+
const FS_IOC_ENABLE_VERITY: Opcode = opcode::write::<FsVerityEnableArg>(b'f', 133);
81+
82+
/// Enable fs-verity on the target file without a signature.
83+
///
84+
/// This is a thin safe wrapper for the `FS_IOC_ENABLE_VERITY` ioctl.
85+
/// The file descriptor must be opened `O_RDONLY` and there must be no
86+
/// other writable file descriptors or mappings for the file.
87+
///
88+
/// # Arguments
89+
/// * `fd` - File descriptor opened O_RDONLY
90+
/// * `hash_algorithm` - Algorithm ID (1 = SHA-256, 2 = SHA-512)
91+
/// * `block_size` - Block size (typically 4096)
92+
pub fn fs_ioc_enable_verity(
93+
fd: impl AsFd,
94+
hash_algorithm: u8,
95+
block_size: u32,
96+
) -> Result<(), EnableVerityError> {
97+
fs_ioc_enable_verity_with_sig(fd, hash_algorithm, block_size, None)
98+
}
99+
100+
/// Enable fs-verity on the target file with an optional PKCS#7 signature.
101+
///
102+
/// When a signature is provided, the kernel will verify it against keys
103+
/// in the `.fs-verity` keyring before enabling verity.
104+
///
105+
/// # Arguments
106+
/// * `fd` - File descriptor opened O_RDONLY
107+
/// * `hash_algorithm` - Algorithm ID (1 = SHA-256, 2 = SHA-512)
108+
/// * `block_size` - Block size (typically 4096)
109+
/// * `signature` - Optional PKCS#7 DER-encoded signature
110+
pub fn fs_ioc_enable_verity_with_sig(
111+
fd: impl AsFd,
112+
hash_algorithm: u8,
113+
block_size: u32,
114+
signature: Option<&[u8]>,
115+
) -> Result<(), EnableVerityError> {
116+
let (sig_size, sig_ptr) = match signature {
117+
Some(sig) => (sig.len() as u32, sig.as_ptr() as u64),
118+
None => (0, 0),
119+
};
120+
121+
unsafe {
122+
match ioctl(
123+
fd,
124+
Setter::<{ FS_IOC_ENABLE_VERITY }, FsVerityEnableArg>::new(FsVerityEnableArg {
125+
version: 1,
126+
hash_algorithm: hash_algorithm as u32,
127+
block_size,
128+
salt_size: 0,
129+
salt_ptr: 0,
130+
sig_size,
131+
__reserved1: 0,
132+
sig_ptr,
133+
__reserved2: [0; 11],
134+
}),
135+
) {
136+
Err(Errno::NOTTY) | Err(Errno::OPNOTSUPP) => {
137+
Err(EnableVerityError::FilesystemNotSupported)
138+
}
139+
Err(Errno::EXIST) => Err(EnableVerityError::AlreadyEnabled),
140+
Err(Errno::TXTBSY) => Err(EnableVerityError::FileOpenedForWrite),
141+
Err(Errno::KEYREJECTED) => Err(EnableVerityError::SignatureVerificationFailed),
142+
Err(e) => Err(Error::from(e).into()),
143+
Ok(_) => Ok(()),
144+
}
145+
}
146+
}
147+
148+
/// Core definition of a fsverity digest returned by the kernel.
149+
#[repr(C)]
150+
#[derive(Debug)]
151+
struct FsVerityDigest<const N: usize> {
152+
digest_algorithm: u16,
153+
digest_size: u16,
154+
digest: [u8; N],
155+
}
156+
157+
// #define FS_IOC_MEASURE_VERITY _IORW('f', 134, struct fsverity_digest)
158+
const FS_IOC_MEASURE_VERITY: Opcode = opcode::read_write::<FsVerityDigest<0>>(b'f', 134);
159+
160+
/// Measure the fs-verity digest of a file.
161+
///
162+
/// Returns the raw digest bytes if successful. The generic parameter `N`
163+
/// specifies the expected digest size (32 for SHA-256, 64 for SHA-512).
164+
///
165+
/// # Arguments
166+
/// * `fd` - File descriptor to measure
167+
/// * `expected_algorithm` - Expected algorithm ID (1 = SHA-256, 2 = SHA-512)
168+
///
169+
/// # Returns
170+
/// The digest bytes on success.
171+
pub fn fs_ioc_measure_verity<const N: usize>(
172+
fd: impl AsFd,
173+
expected_algorithm: u8,
174+
) -> Result<[u8; N], MeasureVerityError> {
175+
let digest_size = N as u16;
176+
let digest_algorithm = expected_algorithm as u16;
177+
178+
let mut digest = FsVerityDigest::<N> {
179+
digest_algorithm,
180+
digest_size,
181+
digest: [0u8; N],
182+
};
183+
184+
let r = unsafe {
185+
ioctl(
186+
fd,
187+
Updater::<{ FS_IOC_MEASURE_VERITY }, FsVerityDigest<N>>::new(&mut digest),
188+
)
189+
};
190+
191+
match r {
192+
Ok(()) => {
193+
if digest.digest_algorithm != digest_algorithm {
194+
return Err(MeasureVerityError::InvalidDigestAlgorithm {
195+
expected: digest_algorithm,
196+
found: digest.digest_algorithm,
197+
});
198+
}
199+
if digest.digest_size != digest_size {
200+
return Err(MeasureVerityError::InvalidDigestSize {
201+
expected: digest_size,
202+
});
203+
}
204+
Ok(digest.digest)
205+
}
206+
Err(Errno::NODATA) => Err(MeasureVerityError::VerityMissing),
207+
Err(Errno::NOTTY | Errno::OPNOTSUPP) => Err(MeasureVerityError::FilesystemNotSupported),
208+
Err(Errno::OVERFLOW) => Err(MeasureVerityError::InvalidDigestSize {
209+
expected: digest.digest_size,
210+
}),
211+
Err(e) => Err(Error::from(e).into()),
212+
}
213+
}
214+
215+
#[cfg(test)]
216+
mod tests {
217+
use std::io::Write;
218+
219+
use tempfile::tempfile_in;
220+
221+
use super::*;
222+
223+
fn get_test_tmpdir() -> std::ffi::OsString {
224+
if let Some(path) = std::env::var_os("CFS_TEST_TMPDIR") {
225+
path
226+
} else {
227+
let home = std::env::var("HOME").expect("$HOME must be set when running tests");
228+
let tmp = std::path::PathBuf::from(home).join(".var/tmp");
229+
std::fs::create_dir_all(&tmp).expect("can't create ~/.var/tmp");
230+
tmp.into()
231+
}
232+
}
233+
234+
fn test_tempfile() -> std::fs::File {
235+
tempfile_in(get_test_tmpdir()).unwrap()
236+
}
237+
238+
#[test]
239+
fn test_measure_verity_missing() {
240+
let mut tf = test_tempfile();
241+
tf.write_all(b"test").unwrap();
242+
tf.sync_all().unwrap();
243+
244+
// Re-open read-only
245+
let path = format!("/proc/self/fd/{}", std::os::fd::AsRawFd::as_raw_fd(&tf));
246+
let ro_fd =
247+
rustix::fs::open(&path, rustix::fs::OFlags::RDONLY, rustix::fs::Mode::empty()).unwrap();
248+
249+
assert!(matches!(
250+
fs_ioc_measure_verity::<32>(&ro_fd, 1),
251+
Err(MeasureVerityError::VerityMissing)
252+
));
253+
}
254+
255+
#[test_with::path(/dev/shm)]
256+
#[test]
257+
fn test_measure_verity_not_supported() {
258+
let tf = tempfile_in("/dev/shm").unwrap();
259+
assert!(matches!(
260+
fs_ioc_measure_verity::<32>(&tf, 1),
261+
Err(MeasureVerityError::FilesystemNotSupported)
262+
));
263+
}
264+
265+
#[test_with::path(/dev/shm)]
266+
#[test]
267+
fn test_enable_verity_wrong_fs() {
268+
let file = tempfile_in("/dev/shm").unwrap();
269+
let err = fs_ioc_enable_verity(&file, 1, 4096).unwrap_err();
270+
assert!(matches!(err, EnableVerityError::FilesystemNotSupported));
271+
}
272+
}

crates/composefs-ioctls/src/lib.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! Low-level ioctl wrappers for composefs operations.
2+
//!
3+
//! This crate provides safe Rust wrappers around Linux ioctls used by composefs:
4+
//!
5+
//! - **fs-verity ioctls**: Enable and measure fs-verity on files
6+
//! - **Loop device ioctls**: Create loop devices (behind `loop-device` feature)
7+
//!
8+
//! # Safety
9+
//!
10+
//! All unsafe ioctl code is contained within this crate, allowing dependent
11+
//! crates to use `#![forbid(unsafe_code)]`.
12+
//!
13+
//! # Example
14+
//!
15+
//! ```ignore
16+
//! use composefs_ioctls::fsverity::{fs_ioc_enable_verity, fs_ioc_measure_verity};
17+
//!
18+
//! // Enable verity on a file
19+
//! fs_ioc_enable_verity(&file, 1, 4096)?; // SHA-256, 4K blocks
20+
//!
21+
//! // Measure the verity digest
22+
//! let digest: [u8; 32] = fs_ioc_measure_verity(&file, 1)?;
23+
//! ```
24+
25+
#![deny(unsafe_code)]
26+
27+
pub mod fsverity;
28+
29+
#[cfg(feature = "loop-device")]
30+
pub mod loop_device;
31+
32+
#[cfg(test)]
33+
mod test_utils;
34+
35+
// Re-export test utilities for use in other crates' tests
36+
#[doc(hidden)]
37+
pub mod test_utils_pub;

0 commit comments

Comments
 (0)