1use super::{
5 clone_tree, create_dst, fs_create_dir, fs_remove_dir, fs_rename_dir, fs_type_name, io_error,
6 unsupported, walk_files, CloneBackend,
7};
8use crate::error::{ErrorCode, MitosisError, Result};
9use std::fs::{File, Metadata, OpenOptions};
10use std::io::Write;
11use std::os::fd::AsRawFd;
12use std::os::unix::fs::OpenOptionsExt;
13use std::path::Path;
14use std::sync::atomic::{AtomicU64, Ordering};
15
16const REFLINK_HINT: &str =
17 "put MITOSIS_HOME on Btrfs or XFS (mkfs.xfs -m reflink=1), or set backend = \"copy\"";
18
19const FS_IOC_FIEMAP: u32 = 0xC020_660B;
21const FIEMAP_EXTENT_LAST: u32 = 0x0001;
22const FIEMAP_EXTENT_SHARED: u32 = 0x2000;
23const FIEMAP_BATCH: usize = 64;
24
25#[repr(C)]
26#[derive(Clone, Copy)]
27#[allow(dead_code)] struct FiemapExtent {
29 fe_logical: u64,
30 fe_physical: u64,
31 fe_length: u64,
32 fe_reserved64: [u64; 2],
33 fe_flags: u32,
34 fe_reserved: [u32; 3],
35}
36
37#[repr(C)]
39#[allow(dead_code)] struct Fiemap {
41 fm_start: u64,
42 fm_length: u64,
43 fm_flags: u32,
44 fm_mapped_extents: u32,
45 fm_extent_count: u32,
46 fm_reserved: u32,
47 fm_extents: [FiemapExtent; FIEMAP_BATCH],
48}
49
50pub struct ReflinkBackend;
51
52impl CloneBackend for ReflinkBackend {
53 fn name(&self) -> &'static str {
54 "reflink"
55 }
56
57 fn check_supported(&self, path: &Path) -> Result<()> {
58 static PROBE: AtomicU64 = AtomicU64::new(0);
60 let tag = format!(
61 ".mitosis-reflink-probe-{}-{}",
62 std::process::id(),
63 PROBE.fetch_add(1, Ordering::Relaxed)
64 );
65 let (a, b) = (path.join(format!("{tag}-a")), path.join(format!("{tag}-b")));
66 let probed = probe(&a, &b);
67 let _ = std::fs::remove_file(&a);
68 let _ = std::fs::remove_file(&b);
69 probed.map_err(|e| {
70 unsupported(
71 format!(
72 "reflink (FICLONE) does not work on the '{}' file system at {}: {e}",
73 fs_type_name(path),
74 path.display()
75 ),
76 REFLINK_HINT,
77 )
78 })
79 }
80
81 fn clone_dir(&self, src: &Path, dst: &Path) -> Result<()> {
82 clone_tree(src, dst, &clone_file)
83 }
84
85 fn create_dir(&self, path: &Path) -> Result<()> {
86 fs_create_dir(path)
87 }
88
89 fn remove_dir(&self, path: &Path) -> Result<()> {
90 fs_remove_dir(path)
91 }
92
93 fn rename_dir(&self, from: &Path, to: &Path) -> Result<()> {
94 fs_rename_dir(from, to)
95 }
96
97 fn private_size(&self, path: &Path) -> Option<u64> {
100 let mut total = 0u64;
101 let done = walk_files(path, &mut |p, _| {
102 total += file_private_size(p)?;
103 Some(())
104 });
105 matches!(done, Ok(Some(()))).then_some(total)
106 }
107}
108
109fn ficlone(src: &File, dst: &File) -> std::io::Result<()> {
110 if unsafe { libc::ioctl(dst.as_raw_fd(), libc::FICLONE, src.as_raw_fd()) } == 0 {
111 Ok(())
112 } else {
113 Err(std::io::Error::last_os_error())
114 }
115}
116
117fn probe(a: &Path, b: &Path) -> std::io::Result<()> {
119 let mut fa = OpenOptions::new()
120 .read(true)
121 .write(true)
122 .create_new(true)
123 .mode(0o600)
124 .open(a)?;
125 fa.write_all(&[0u8; 4096])?;
126 let fb = OpenOptions::new()
127 .write(true)
128 .create_new(true)
129 .mode(0o600)
130 .open(b)?;
131 ficlone(&fa, &fb)
132}
133
134fn clone_file(src: &Path, dst: &Path, md: &Metadata) -> Result<()> {
135 let s = File::open(src).map_err(|e| io_error("open", src, e))?;
136 let d = create_dst(dst, md)?;
137 ficlone(&s, &d).map_err(|e| {
138 let msg = format!("FICLONE {} -> {}: {e}", src.display(), dst.display());
139 match e.raw_os_error() {
140 Some(libc::EXDEV) => MitosisError::new(ErrorCode::CrossVolume, msg)
141 .with_hint("templates/ and clones/ must be on the same filesystem"),
142 Some(libc::EOPNOTSUPP | libc::EINVAL) => unsupported(msg, REFLINK_HINT),
143 _ => MitosisError::new(ErrorCode::Internal, msg),
144 }
145 })
146}
147
148fn file_private_size(path: &Path) -> Option<u64> {
150 let f = File::open(path).ok()?;
151 let mut fm: Fiemap = unsafe { std::mem::zeroed() };
152 let (mut start, mut total) = (0u64, 0u64);
153 loop {
154 fm.fm_start = start;
155 fm.fm_length = u64::MAX;
156 fm.fm_flags = 0;
157 fm.fm_mapped_extents = 0;
158 fm.fm_extent_count = FIEMAP_BATCH as u32;
159 if unsafe { libc::ioctl(f.as_raw_fd(), FS_IOC_FIEMAP as _, &mut fm as *mut Fiemap) } != 0 {
160 return None;
161 }
162 let n = (fm.fm_mapped_extents as usize).min(FIEMAP_BATCH);
163 if n == 0 {
164 return Some(total);
165 }
166 let extents = &fm.fm_extents[..n];
167 for e in extents {
168 if e.fe_flags & FIEMAP_EXTENT_SHARED == 0 {
169 total += e.fe_length;
170 }
171 }
172 let last = extents[n - 1];
173 let next = last.fe_logical.saturating_add(last.fe_length);
174 if last.fe_flags & FIEMAP_EXTENT_LAST != 0 || next <= start {
175 return Some(total);
176 }
177 start = next;
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::clone_backend::same_volume;
185 use std::fs::Permissions;
186 use std::fs::{create_dir, metadata, read, read_link, read_to_string, symlink_metadata, write};
187 use std::os::unix::fs::symlink;
188 use std::os::unix::fs::{MetadataExt, PermissionsExt};
189 use std::path::PathBuf;
190
191 fn reflink_root() -> Option<PathBuf> {
193 if std::env::var("MITOSIS_BACKEND").as_deref() != Ok("reflink") {
194 eprintln!("skipped: MITOSIS_BACKEND is not reflink");
195 return None;
196 }
197 Some(
198 std::env::var("MITOSIS_TEST_ROOT")
199 .unwrap_or_else(|_| "/tmp".into())
200 .into(),
201 )
202 }
203
204 #[test]
205 fn reflink_clone_is_isolated_and_shares_blocks() {
206 let Some(root) = reflink_root() else { return };
207 let tmp = tempfile::tempdir_in(root).unwrap();
208 ReflinkBackend.check_supported(tmp.path()).unwrap();
209 let (src, dst) = (tmp.path().join("src"), tmp.path().join("dst"));
210 create_dir(&src).unwrap();
211 let big = src.join("big");
212 write(&big, vec![7u8; 8 << 20]).unwrap();
213 std::fs::set_permissions(&big, Permissions::from_mode(0o640)).unwrap();
214 create_dir(src.join("sub")).unwrap();
215 write(src.join("sub/a.txt"), "hello").unwrap();
216
217 ReflinkBackend.clone_dir(&src, &dst).unwrap();
218 let private = ReflinkBackend.private_size(&dst).unwrap();
219 assert!(private < 64 * 1024, "clone has {private} private bytes");
220 let (smd, dmd) = (metadata(&big).unwrap(), metadata(dst.join("big")).unwrap());
221 assert_eq!(dmd.mode() & 0o7777, 0o640);
222 assert_eq!(dmd.modified().unwrap(), smd.modified().unwrap());
223
224 write(dst.join("sub/a.txt"), "changed").unwrap();
225 let mut f = OpenOptions::new()
226 .write(true)
227 .open(dst.join("big"))
228 .unwrap();
229 f.write_all(b"xxxx").unwrap();
230 drop(f);
231 assert_eq!(read_to_string(src.join("sub/a.txt")).unwrap(), "hello");
232 assert_eq!(read(&big).unwrap()[..4], [7u8; 4]);
233 assert!(ReflinkBackend.clone_dir(&src, &dst).is_err());
234 }
235
236 #[test]
237 fn reflink_keeps_symlinks_unfollowed() {
238 let Some(root) = reflink_root() else { return };
239 let tmp = tempfile::tempdir_in(root).unwrap();
240 let (src, dst) = (tmp.path().join("src"), tmp.path().join("dst"));
241 let wal_real = tmp.path().join("wal_real");
242 create_dir(&wal_real).unwrap();
243 write(wal_real.join("000000010000000000000001"), vec![1u8; 4096]).unwrap();
244 create_dir(&src).unwrap();
245 create_dir(src.join("pg_tblspc")).unwrap();
246 symlink("/elsewhere", src.join("pg_tblspc/16384")).unwrap();
247 symlink(&wal_real, src.join("pg_wal")).unwrap();
248
249 ReflinkBackend.clone_dir(&src, &dst).unwrap();
250 for link in ["pg_tblspc/16384", "pg_wal"] {
251 assert!(symlink_metadata(dst.join(link))
252 .unwrap()
253 .file_type()
254 .is_symlink());
255 assert_eq!(
256 read_link(dst.join(link)).unwrap(),
257 read_link(src.join(link)).unwrap()
258 );
259 }
260 assert_eq!(std::fs::read_dir(&wal_real).unwrap().count(), 1);
261 }
262
263 #[test]
264 fn reflink_cross_filesystem_is_cross_volume() {
265 let Some(root) = reflink_root() else { return };
266 let tmp = tempfile::tempdir_in(root).unwrap();
267 let other = tempfile::tempdir_in("/tmp").unwrap();
268 if same_volume(tmp.path(), other.path()).unwrap() {
269 eprintln!("skipped: /tmp is on the test file system");
270 return;
271 }
272 let src = tmp.path().join("src");
273 create_dir(&src).unwrap();
274 write(src.join("a"), "hello").unwrap();
275 let dst = other.path().join("dst");
276
277 let e = ReflinkBackend.clone_dir(&src, &dst).err().unwrap();
278 assert_eq!(e.code, ErrorCode::CrossVolume, "{e}");
279 assert!(!dst.exists());
280 }
281
282 #[test]
283 fn probe_rejects_non_reflink_fs() {
284 if std::env::var("MITOSIS_TEST_ROOT").is_err() {
285 eprintln!("skipped: not in the Linux test container");
286 return;
287 }
288 let tmp = tempfile::tempdir_in("/tmp").unwrap();
289 let e = ReflinkBackend.check_supported(tmp.path()).err().unwrap();
290 assert_eq!(e.code, ErrorCode::BackendUnsupported, "{e}");
291 assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
292 }
293}