git_policy_cli/fs_btrfs_ioctl.rs
1//! What:
2//! The three btrfs ioctls the wrapper needs, called in process: create an empty subvolume, snapshot a subvolume,
3//! and destroy a subvolume.
4//! Why:
5//! `libc` has `FICLONE` and the filesystem magic numbers but no `BTRFS_IOC_*` constants, and the wrapper never starts
6//! the `btrfs` program (the repository's rule: embed instead of spawning). The request numbers and the 4096-byte
7//! argument structures are taken from the kernel header `include/uapi/linux/btrfs.h` (the host copy is
8//! `/usr/include/linux/btrfs.h`): `BTRFS_IOCTL_MAGIC` is 0x94; `BTRFS_IOC_SUBVOL_CREATE` is `_IOW(0x94, 14,
9//! struct btrfs_ioctl_vol_args)`; `BTRFS_IOC_SNAP_DESTROY` is `_IOW(0x94, 15, struct btrfs_ioctl_vol_args)`;
10//! `BTRFS_IOC_SNAP_CREATE_V2` is `_IOW(0x94, 23, struct btrfs_ioctl_vol_args_v2)`. Both structures are 4096 bytes:
11//! `vol_args` is an `s64` fd and 4088 name bytes; `vol_args_v2` is an `s64` fd, `u64` transid, `u64` flags, a
12//! 32-byte union and 4040 name bytes. `strace` of `btrfs subvolume snapshot` and `btrfs subvolume delete` shows the
13//! same shapes (`doc/planning/cli-git-btrfs-worktree.md`, "Facts"). Each call is proven by one real call in a test on
14//! a btrfs fixture, and the encoding of the request numbers is proven against `libc::FICLONE`.
15//! Permissions (kernel `fs/btrfs/ioctl.c`): creating a subvolume is unrestricted; a snapshot needs ownership of the
16//! source; destroying one needs the `user_subvol_rm_allowed` mount option for an unprivileged user.
17//!
18//! In TS you'd write (pseudocode):
19//! ```ts
20//! await btrfsIoctl(parentFd, 'SNAP_CREATE_V2', { fd: sourceFd, flags: 0, name });
21//! ```
22
23use super::fs_primitive_error::PrimitiveError;
24use std::ffi::OsStr;
25use std::fs::File;
26use std::os::unix::ffi::OsStrExt;
27use std::os::unix::io::AsRawFd;
28use std::path::Path;
29
30/// `BTRFS_IOCTL_MAGIC`.
31const BTRFS_IOCTL_MAGIC: u32 = 0x94;
32
33/// Bits the request number gives to the argument size, after the type and number fields (`_IOC_SIZESHIFT`).
34const IOC_SIZE_SHIFT: u32 = 16;
35
36/// Bits to the type field (`_IOC_TYPESHIFT`).
37const IOC_TYPE_SHIFT: u32 = 8;
38
39/// What:
40/// How an architecture encodes the direction of an ioctl request: the value of `_IOC_WRITE` and where the field sits.
41/// Why:
42/// Linux has three encodings, not one. `include/uapi/asm-generic/ioctl.h` (x86, arm, arm64, riscv, s390, loongarch)
43/// gives the direction 2 bits at shift 30 with `_IOC_WRITE` 1; `arch/powerpc`, `arch/mips`, `arch/sparc` and
44/// `arch/alpha` define `_IOC_SIZEBITS` 13, `_IOC_DIRBITS` 3 and `_IOC_WRITE` 4, which puts the field at shift 29;
45/// `arch/parisc` keeps 14 size bits and defines `_IOC_WRITE` 2. A request number built with the wrong one reaches the
46/// kernel as a different request and fails.
47///
48/// In TS you'd write (pseudocode):
49/// ```ts
50/// type IocLayout = { write: number; directionShift: number };
51/// ```
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub struct IocLayout {
54 /// `_IOC_WRITE`: the direction value for an argument user space writes.
55 pub write: u32,
56 /// `_IOC_DIRSHIFT`: where the direction field starts.
57 pub direction_shift: u32,
58}
59
60/// The asm-generic encoding.
61pub const GENERIC_IOC_LAYOUT: IocLayout = IocLayout { write: 1, direction_shift: 30 };
62
63/// The encoding of powerpc, mips, sparc and alpha: a 3-bit direction field with `_IOC_WRITE` 4.
64pub const WIDE_DIRECTION_IOC_LAYOUT: IocLayout = IocLayout { write: 4, direction_shift: 29 };
65
66/// The encoding of parisc: `_IOC_WRITE` 2 with the generic field width.
67pub const PARISC_IOC_LAYOUT: IocLayout = IocLayout { write: 2, direction_shift: 30 };
68
69/// The encoding of the architecture this code is built for.
70#[cfg(any(target_arch = "powerpc", target_arch = "powerpc64", target_arch = "mips", target_arch = "mips32r6", target_arch = "mips64", target_arch = "mips64r6", target_arch = "sparc", target_arch = "sparc64"))]
71pub const HOST_IOC_LAYOUT: IocLayout = WIDE_DIRECTION_IOC_LAYOUT;
72
73/// The encoding of the architecture this code is built for.
74#[cfg(not(any(target_arch = "powerpc", target_arch = "powerpc64", target_arch = "mips", target_arch = "mips32r6", target_arch = "mips64", target_arch = "mips64r6", target_arch = "sparc", target_arch = "sparc64")))]
75pub const HOST_IOC_LAYOUT: IocLayout = GENERIC_IOC_LAYOUT;
76
77/// The size of `struct btrfs_ioctl_vol_args` and `struct btrfs_ioctl_vol_args_v2`, both 4096 bytes.
78pub const VOLUME_ARGUMENT_BYTES: usize = 4096;
79
80/// The longest name `BTRFS_IOC_SNAP_CREATE_V2` takes: `BTRFS_SUBVOL_NAME_MAX` (4039) plus its NUL, in the header.
81const V2_NAME_CAPACITY: usize = 4040;
82
83/// The longest name the version 1 argument takes: `BTRFS_PATH_NAME_MAX + 1`, in the header.
84const V1_NAME_CAPACITY: usize = 4088;
85
86/// The longest file name any Linux directory entry has (`NAME_MAX`); btrfs accepts nothing longer for a subvolume.
87const NAME_MAX: usize = 255;
88
89/// What:
90/// The request number `_IOW(magic, number, size)` the C macro computes under a given architecture's encoding.
91/// `u32` is an unsigned 32-bit integer (siblings `u64` and `i32`); the macro's result fits 32 bits.
92/// Why:
93/// The numbers are computed from the header's definition instead of typed in as magic constants, so a test can check
94/// the encoding against `libc::FICLONE`, which `libc` already defines as `_IOW(0x94, 9, int)` for each architecture.
95///
96/// In TS you'd write (pseudocode):
97/// ```ts
98/// const iow = (layout: IocLayout, nr: number, size: number) => (layout.write << layout.directionShift) | (size << 16) | (0x94 << 8) | nr;
99/// ```
100pub const fn ioc_write_with(layout: IocLayout, number: u32, size: usize) -> u32 {
101 return (layout.write << layout.direction_shift) | ((size as u32) << IOC_SIZE_SHIFT) | (BTRFS_IOCTL_MAGIC << IOC_TYPE_SHIFT) | number;
102}
103
104/// What:
105/// The request number `_IOW(magic, number, size)` for the architecture this code is built for.
106/// `libc::Ioctl` is the width the platform's `ioctl` takes.
107/// Why:
108/// See `ioc_write_with`.
109///
110/// In TS you'd write (pseudocode):
111/// ```ts
112/// const iow = (nr: number, size: number) => iowFor(hostLayout, nr, size);
113/// ```
114pub const fn ioc_write(number: u32, size: usize) -> libc::Ioctl {
115 return ioc_write_with(HOST_IOC_LAYOUT, number, size) as libc::Ioctl;
116}
117
118/// `BTRFS_IOC_SUBVOL_CREATE`: `_IOW(0x94, 14, struct btrfs_ioctl_vol_args)`.
119pub const BTRFS_IOC_SUBVOL_CREATE: libc::Ioctl = ioc_write(14, VOLUME_ARGUMENT_BYTES);
120
121/// `BTRFS_IOC_SNAP_DESTROY`: `_IOW(0x94, 15, struct btrfs_ioctl_vol_args)`.
122pub const BTRFS_IOC_SNAP_DESTROY: libc::Ioctl = ioc_write(15, VOLUME_ARGUMENT_BYTES);
123
124/// `BTRFS_IOC_SNAP_CREATE_V2`: `_IOW(0x94, 23, struct btrfs_ioctl_vol_args_v2)`.
125pub const BTRFS_IOC_SNAP_CREATE_V2: libc::Ioctl = ioc_write(23, VOLUME_ARGUMENT_BYTES);
126
127/// What:
128/// `struct btrfs_ioctl_vol_args`: a source descriptor and a name.
129/// `#[repr(C)]` keeps the C field order and padding, which the kernel reads.
130/// Why:
131/// The subvolume create and destroy ioctls take only a name; the kernel ignores `fd` for them.
132///
133/// In TS you'd write (pseudocode):
134/// ```ts
135/// type VolArgs = { fd: bigint; name: Uint8Array /* 4088 bytes */ };
136/// ```
137#[repr(C)]
138pub struct VolArgs {
139 /// `__s64 fd`.
140 pub fd: i64,
141 /// `char name[BTRFS_PATH_NAME_MAX + 1]`, NUL terminated.
142 pub name: [u8; V1_NAME_CAPACITY],
143}
144
145/// What:
146/// `struct btrfs_ioctl_vol_args_v2`: a source descriptor, flags and a name.
147/// Why:
148/// The snapshot ioctl takes the source subvolume as `fd` and the new name in the parent directory.
149///
150/// In TS you'd write (pseudocode):
151/// ```ts
152/// type VolArgsV2 = { fd: bigint; transid: bigint; flags: bigint; union: Uint8Array /* 32 bytes */; name: Uint8Array /* 4040 bytes */ };
153/// ```
154#[repr(C)]
155pub struct VolArgsV2 {
156 /// `__s64 fd`: the source subvolume's descriptor for a snapshot.
157 pub fd: i64,
158 /// `__u64 transid`: output, the transaction that made the snapshot.
159 pub transid: u64,
160 /// `__u64 flags`: 0 for a writable snapshot (`BTRFS_SUBVOL_RDONLY` makes a read-only one).
161 pub flags: u64,
162 /// The 32-byte union (`size` and `qgroup_inherit`, or `unused[4]`), zero for no qgroup inheritance.
163 pub union_bytes: [u8; 32],
164 /// `char name[BTRFS_SUBVOL_NAME_MAX + 1]`, NUL terminated.
165 pub name: [u8; V2_NAME_CAPACITY],
166}
167
168/// What:
169/// Check one name for a subvolume entry and copy it, NUL terminated, into a fixed buffer.
170/// `const N: usize` is the buffer length (a TS tuple length is not a type parameter, so this is a generic over a
171/// number).
172/// Why:
173/// The kernel takes one path component: a slash, an empty name, `.` or `..` would be a different request than the
174/// caller meant, so they fail with `EINVAL` here before any call.
175///
176/// In TS you'd write (pseudocode):
177/// ```ts
178/// function fillName(buffer: Uint8Array, name: Uint8Array): void { if (name.length === 0 || name.includes(0x2f) || name.length > 255) throw ...; buffer.set(name); }
179/// ```
180fn fill_name<const N: usize>(buffer: &mut [u8; N], name: &OsStr, operation: &'static str, parent: &Path) -> Result<(), PrimitiveError> {
181 let bytes: &[u8] = name.as_bytes();
182 let refused: bool = bytes.is_empty()
183 || bytes.len() > NAME_MAX
184 || bytes.len() >= N
185 || bytes.contains(&b'/')
186 || bytes.contains(&0)
187 || bytes == b"."
188 || bytes == b"..";
189 if refused {
190 return Err(PrimitiveError { operation, path: parent.join(name), errno: libc::EINVAL });
191 }
192 buffer[..bytes.len()].copy_from_slice(bytes);
193 return Ok(());
194}
195
196/// What:
197/// Open a directory for an ioctl on it.
198/// Why:
199/// The ioctls act on the directory descriptor; the kernel checks write and search permission on it itself.
200///
201/// In TS you'd write (pseudocode):
202/// ```ts
203/// const fd = await open(directory, 'r');
204/// ```
205fn open_directory(directory: &Path, operation: &'static str) -> Result<File, PrimitiveError> {
206 return File::open(directory)
207 .map_err(|error| { return PrimitiveError { operation, path: directory.to_path_buf(), errno: error.raw_os_error().unwrap_or(0) }; });
208}
209
210/// What:
211/// Create an empty subvolume named `name` inside `parent` (`BTRFS_IOC_SUBVOL_CREATE`).
212/// Why:
213/// Creation is not restricted to privileged users. An empty subvolume is an empty directory to Git, so
214/// `git worktree add` accepts it as a destination.
215///
216/// In TS you'd write (pseudocode):
217/// ```ts
218/// await btrfsIoctl(await open(parent), 'SUBVOL_CREATE', { name });
219/// ```
220pub fn subvolume_create(parent: &Path, name: &OsStr) -> Result<(), PrimitiveError> {
221 let operation: &'static str = "BTRFS_IOC_SUBVOL_CREATE";
222 let directory: File = open_directory(parent, operation)?;
223 // SAFETY: `VolArgs` is plain integers and bytes, so the all-zero value is a valid value.
224 let mut arguments: VolArgs = unsafe { std::mem::zeroed() };
225 fill_name(&mut arguments.name, name, operation, parent)?;
226 // SAFETY: `directory` is open for the call; `arguments` is a 4096-byte structure as the request number states.
227 let result: libc::c_int = unsafe { libc::ioctl(directory.as_raw_fd(), BTRFS_IOC_SUBVOL_CREATE, &arguments) };
228 if result != 0 {
229 return Err(PrimitiveError::last_os_error(operation, parent.join(name).as_path()));
230 }
231 return Ok(());
232}
233
234/// What:
235/// Snapshot the subvolume at `source` as a new subvolume `name` inside `parent` (`BTRFS_IOC_SNAP_CREATE_V2`, flags 0,
236/// so the snapshot is writable).
237/// Why:
238/// The kernel requires the source to be a subvolume root on the same filesystem and owned by the caller
239/// (`EINVAL`, `EXDEV` and `EPERM` otherwise), and shares every data block with the source.
240///
241/// In TS you'd write (pseudocode):
242/// ```ts
243/// await btrfsIoctl(await open(parent), 'SNAP_CREATE_V2', { fd: await open(source), flags: 0, name });
244/// ```
245pub fn snapshot_create(source: &Path, parent: &Path, name: &OsStr) -> Result<(), PrimitiveError> {
246 let operation: &'static str = "BTRFS_IOC_SNAP_CREATE_V2";
247 let directory: File = open_directory(parent, operation)?;
248 let origin: File = open_directory(source, operation)?;
249 // SAFETY: `VolArgsV2` is plain integers and bytes, so the all-zero value is a valid value.
250 let mut arguments: VolArgsV2 = unsafe { std::mem::zeroed() };
251 arguments.fd = i64::from(origin.as_raw_fd());
252 fill_name(&mut arguments.name, name, operation, parent)?;
253 // SAFETY: both descriptors are open for the call; `arguments` is a 4096-byte structure as the request number states.
254 let result: libc::c_int = unsafe { libc::ioctl(directory.as_raw_fd(), BTRFS_IOC_SNAP_CREATE_V2, &arguments) };
255 if result != 0 {
256 return Err(PrimitiveError::last_os_error(operation, parent.join(name).as_path()));
257 }
258 return Ok(());
259}
260
261/// What:
262/// Destroy the subvolume `name` inside `parent` (`BTRFS_IOC_SNAP_DESTROY`).
263/// Why:
264/// For an unprivileged user the kernel needs the `user_subvol_rm_allowed` mount option (`EPERM` otherwise), write
265/// and search permission on the subvolume root, and no nested subvolume inside (`ENOTEMPTY`). Without the option an
266/// empty subvolume is removed by `rmdir`, which `btrfs_rmdir` allows for an empty subvolume root.
267///
268/// In TS you'd write (pseudocode):
269/// ```ts
270/// await btrfsIoctl(await open(parent), 'SNAP_DESTROY', { name });
271/// ```
272pub fn snapshot_destroy(parent: &Path, name: &OsStr) -> Result<(), PrimitiveError> {
273 let operation: &'static str = "BTRFS_IOC_SNAP_DESTROY";
274 let directory: File = open_directory(parent, operation)?;
275 // SAFETY: `VolArgs` is plain integers and bytes, so the all-zero value is a valid value.
276 let mut arguments: VolArgs = unsafe { std::mem::zeroed() };
277 fill_name(&mut arguments.name, name, operation, parent)?;
278 // SAFETY: `directory` is open for the call; `arguments` is a 4096-byte structure as the request number states.
279 let result: libc::c_int = unsafe { libc::ioctl(directory.as_raw_fd(), BTRFS_IOC_SNAP_DESTROY, &arguments) };
280 if result != 0 {
281 return Err(PrimitiveError::last_os_error(operation, parent.join(name).as_path()));
282 }
283 return Ok(());
284}
285
286/// What:
287/// Split a path into its parent directory and last component.
288/// Why:
289/// The ioctls take the parent and a one-component name; callers hold whole paths. A path with no parent or no last
290/// component (`/`, `..`) fails with `EINVAL`.
291///
292/// In TS you'd write (pseudocode):
293/// ```ts
294/// const [parent, name] = [dirname(path), basename(path)];
295/// ```
296pub fn split_parent(path: &Path, operation: &'static str) -> Result<(std::path::PathBuf, std::ffi::OsString), PrimitiveError> {
297 let refused = || { return PrimitiveError { operation, path: path.to_path_buf(), errno: libc::EINVAL }; };
298 let name: &OsStr = path.file_name().ok_or_else(refused)?;
299 let parent: &Path = path.parent().ok_or_else(refused)?;
300 let parent_path: std::path::PathBuf = if parent.as_os_str().is_empty() { std::path::PathBuf::from(".") } else { parent.to_path_buf() };
301 return Ok((parent_path, name.to_os_string()));
302}
303
304/// What:
305/// Create an empty subvolume at a whole path.
306/// Why:
307/// The convenience over `subvolume_create` for callers that hold the destination path.
308///
309/// In TS you'd write (pseudocode):
310/// ```ts
311/// await subvolumeCreate(dirname(path), basename(path));
312/// ```
313pub fn subvolume_create_at(path: &Path) -> Result<(), PrimitiveError> {
314 let (parent, name) = split_parent(path, "BTRFS_IOC_SUBVOL_CREATE")?;
315 return subvolume_create(parent.as_path(), name.as_os_str());
316}
317
318/// What:
319/// Destroy the subvolume at a whole path.
320/// Why:
321/// The convenience over `snapshot_destroy` for callers that hold the path.
322///
323/// In TS you'd write (pseudocode):
324/// ```ts
325/// await snapshotDestroy(dirname(path), basename(path));
326/// ```
327pub fn subvolume_destroy_at(path: &Path) -> Result<(), PrimitiveError> {
328 let (parent, name) = split_parent(path, "BTRFS_IOC_SNAP_DESTROY")?;
329 return snapshot_destroy(parent.as_path(), name.as_os_str());
330}
331
332/// What:
333/// Snapshot the subvolume at `source` to the whole path `destination`.
334/// Why:
335/// The convenience over `snapshot_create` for callers that hold the destination path.
336///
337/// In TS you'd write (pseudocode):
338/// ```ts
339/// await snapshotCreate(source, dirname(destination), basename(destination));
340/// ```
341pub fn snapshot_create_at(source: &Path, destination: &Path) -> Result<(), PrimitiveError> {
342 let (parent, name) = split_parent(destination, "BTRFS_IOC_SNAP_CREATE_V2")?;
343 return snapshot_create(source, parent.as_path(), name.as_os_str());
344}
345
346/// Btrfs ioctl controls stay out of the release executable.
347#[cfg(test)]
348#[path = "fs_btrfs_ioctl_tests.rs"]
349mod tests;