git_policy_cli/subvolume_sync_attrs.rs
1//! What:
2//! Copying the attributes of one directory entry onto its copy: owner, permission bits, access and modification
3//! times to the nanosecond, and extended attributes (user attributes and POSIX ACLs included); and recreating a
4//! named pipe.
5//! Why:
6//! `cp --archive` and `rsync --archive --xattrs` did this in the migration script. The index stat rewrite trusts
7//! that sizes and modification times survive the copy, and Git compares the mode and owner of each entry, so the
8//! copy must carry them. Failures that an unprivileged user must expect are tolerated and counted, never silent: an
9//! owner other than the caller's cannot be set (`EPERM`), and a security label (`security.selinux`) is set by the
10//! policy for a new file, not by the caller. A failure that no unprivileged user should meet (a permission change or
11//! a time change refused for lack of space, say) is an error, because the copy would be recorded as faithful when it
12//! is not. The attributes are applied through the descriptor of the copy, so a path swapped for a link while the
13//! pass runs cannot redirect them; only a symbolic link, which cannot be opened, is addressed by its path.
14//!
15//! In TS you'd write (pseudocode):
16//! ```ts
17//! await chown(copy, st.uid, st.gid); await chmod(copy, st.mode & 0o7777); await utimes(copy, st.atime, st.mtime);
18//! ```
19
20use super::fs_primitive_error::c_path;
21use std::collections::BTreeMap;
22use std::ffi::{CStr, CString};
23use std::fs::{File, Metadata};
24use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
25use std::os::unix::io::AsRawFd;
26use std::path::Path;
27
28/// What:
29/// How many attribute operations could not be applied, by kind.
30/// Why:
31/// The report says so, so a person knows what the copy does not carry.
32///
33/// In TS you'd write (pseudocode):
34/// ```ts
35/// type AttributeLosses = { owner: number; extendedAttributes: number; mode: number; times: number };
36/// ```
37#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
38pub struct AttributeLosses {
39 /// Entries whose owner or group could not be set.
40 pub owner: u64,
41 /// Extended attributes that could not be copied.
42 pub extended_attributes: u64,
43 /// Entries whose permission bits the operating system refused to change for lack of ownership.
44 pub mode: u64,
45 /// Entries whose times the operating system refused to change for lack of ownership.
46 pub times: u64,
47}
48
49/// What:
50/// An attribute operation that failed in a way the copy cannot tolerate.
51/// Why:
52/// The caller names the path and turns it into the migration's error.
53///
54/// In TS you'd write (pseudocode):
55/// ```ts
56/// type AttributeFailure = { action: string; error: Error };
57/// ```
58#[derive(Debug)]
59pub struct AttributeFailure {
60 /// What was being set, in words for `cannot <action> <path>`.
61 pub action: &'static str,
62 /// The operating system's refusal.
63 pub error: std::io::Error,
64}
65
66/// What:
67/// Where an attribute operation lands: an open file or directory, or a symbolic link addressed by its path.
68/// Why:
69/// The `f` calls work on a descriptor and cannot be redirected; a symbolic link cannot be opened for them, so the
70/// `l` calls take its path and never follow it.
71///
72/// In TS you'd write (pseudocode):
73/// ```ts
74/// type Handle = { kind: 'descriptor'; file: FileHandle } | { kind: 'link'; path: string };
75/// ```
76#[derive(Clone, Copy)]
77enum Handle<'a> {
78 /// An open file, directory or named pipe.
79 Descriptor(&'a File),
80 /// A symbolic link.
81 Link(&'a CStr),
82}
83
84/// Whether an attribute is one the copy carries and the equality check compares: user attributes and POSIX ACLs.
85/// The security label and the other namespaces belong to the policy and to privileged users.
86fn is_carried(name: &[u8]) -> bool {
87 return name.starts_with(b"user.") || name == b"system.posix_acl_access" || name == b"system.posix_acl_default";
88}
89
90/// The names of the extended attributes of a handle, or nothing when it has none or the filesystem refuses.
91fn attribute_names(handle: Handle<'_>) -> Vec<Vec<u8>> {
92 // SAFETY: a null buffer with size 0 asks for the length of the list.
93 let length: isize = unsafe {
94 match handle {
95 Handle::Descriptor(file) => libc::flistxattr(file.as_raw_fd(), std::ptr::null_mut(), 0),
96 Handle::Link(path) => libc::llistxattr(path.as_ptr(), std::ptr::null_mut(), 0),
97 }
98 };
99 if length <= 0 {
100 return Vec::new();
101 }
102 let mut names: Vec<u8> = vec![0; length as usize];
103 // SAFETY: `names` holds `length` writable bytes.
104 let filled: isize = unsafe {
105 match handle {
106 Handle::Descriptor(file) => libc::flistxattr(file.as_raw_fd(), names.as_mut_ptr().cast(), names.len()),
107 Handle::Link(path) => libc::llistxattr(path.as_ptr(), names.as_mut_ptr().cast(), names.len()),
108 }
109 };
110 if filled <= 0 {
111 return Vec::new();
112 }
113 names.truncate(filled as usize);
114 return names
115 .split(|byte: &u8| { return *byte == 0; })
116 .filter(|name: &&[u8]| { return !name.is_empty(); })
117 .map(<[u8]>::to_vec)
118 .collect();
119}
120
121/// The value of one extended attribute, or nothing when it cannot be read.
122fn attribute_value(handle: Handle<'_>, name: &CStr) -> Option<Vec<u8>> {
123 // SAFETY: a null buffer with size 0 asks for the length of the value.
124 let size: isize = unsafe {
125 match handle {
126 Handle::Descriptor(file) => libc::fgetxattr(file.as_raw_fd(), name.as_ptr(), std::ptr::null_mut(), 0),
127 Handle::Link(path) => libc::lgetxattr(path.as_ptr(), name.as_ptr(), std::ptr::null_mut(), 0),
128 }
129 };
130 if size < 0 {
131 return None;
132 }
133 let mut value: Vec<u8> = vec![0; size as usize];
134 // SAFETY: `value` holds `size` writable bytes.
135 let read: isize = unsafe {
136 match handle {
137 Handle::Descriptor(file) => libc::fgetxattr(file.as_raw_fd(), name.as_ptr(), value.as_mut_ptr().cast(), value.len()),
138 Handle::Link(path) => libc::lgetxattr(path.as_ptr(), name.as_ptr(), value.as_mut_ptr().cast(), value.len()),
139 }
140 };
141 if read < 0 {
142 return None;
143 }
144 value.truncate(read as usize);
145 return Some(value);
146}
147
148/// Set one extended attribute; true when the operating system accepted it.
149fn set_attribute(handle: Handle<'_>, name: &CStr, value: &[u8]) -> bool {
150 // SAFETY: `value` holds `value.len()` initialised bytes; the names are NUL-terminated strings.
151 let result: libc::c_int = unsafe {
152 match handle {
153 Handle::Descriptor(file) => libc::fsetxattr(file.as_raw_fd(), name.as_ptr(), value.as_ptr().cast(), value.len(), 0),
154 Handle::Link(path) => libc::lsetxattr(path.as_ptr(), name.as_ptr(), value.as_ptr().cast(), value.len(), 0),
155 }
156 };
157 return result == 0;
158}
159
160/// Remove one extended attribute; true when it is gone.
161fn remove_attribute(handle: Handle<'_>, name: &CStr) -> bool {
162 // SAFETY: the names are NUL-terminated strings.
163 let result: libc::c_int = unsafe {
164 match handle {
165 Handle::Descriptor(file) => libc::fremovexattr(file.as_raw_fd(), name.as_ptr()),
166 Handle::Link(path) => libc::lremovexattr(path.as_ptr(), name.as_ptr()),
167 }
168 };
169 return result == 0;
170}
171
172/// What:
173/// Copy every extended attribute of `source` onto the handle, then remove the carried ones the source lacks.
174/// Why:
175/// User attributes and ACLs belong to the file. An attribute the source no longer has must not survive on the copy
176/// (a removed ACL entry would keep granting access); only the carried namespaces are removed, because the security
177/// label is the policy's. A refused attribute is counted, because the caller is not the privileged user that may set
178/// every namespace.
179///
180/// In TS you'd write (pseudocode):
181/// ```ts
182/// for (const name of await listxattr(source)) await setxattr(copy, name, await getxattr(source, name));
183/// ```
184fn copy_extended_attributes(source: &CStr, destination: Handle<'_>, remove_extras: bool, losses: &mut AttributeLosses) {
185 let wanted: Vec<Vec<u8>> = attribute_names(Handle::Link(source));
186 for name in &wanted {
187 let Ok(attribute) = CString::new(name.as_slice()) else { continue };
188 let Some(value) = attribute_value(Handle::Link(source), attribute.as_c_str()) else {
189 losses.extended_attributes += 1;
190 continue;
191 };
192 if !set_attribute(destination, attribute.as_c_str(), value.as_slice()) {
193 losses.extended_attributes += 1;
194 }
195 }
196 if !remove_extras {
197 return;
198 }
199 for name in attribute_names(destination) {
200 if !is_carried(name.as_slice()) || wanted.contains(&name) {
201 continue;
202 }
203 let Ok(attribute) = CString::new(name.as_slice()) else { continue };
204 if !remove_attribute(destination, attribute.as_c_str()) {
205 losses.extended_attributes += 1;
206 }
207 }
208}
209
210/// The carried extended attributes of a path (a link's own, not its target's), by name.
211fn carried_attributes(path: &CStr) -> BTreeMap<Vec<u8>, Vec<u8>> {
212 let mut found: BTreeMap<Vec<u8>, Vec<u8>> = BTreeMap::new();
213 for name in attribute_names(Handle::Link(path)) {
214 if !is_carried(name.as_slice()) {
215 continue;
216 }
217 let Ok(attribute) = CString::new(name.as_slice()) else { continue };
218 if let Some(value) = attribute_value(Handle::Link(path), attribute.as_c_str()) {
219 found.insert(name, value);
220 }
221 }
222 return found;
223}
224
225/// What:
226/// Whether two paths carry the same user attributes and ACLs.
227/// Why:
228/// Size, time and mode do not change when only an attribute does, so the equality of a copy and its original has to
229/// look at them too, or a later pass would call a file with an outdated ACL unchanged.
230///
231/// In TS you'd write (pseudocode):
232/// ```ts
233/// const same = await carriedAttributesEqual(source, destination);
234/// ```
235pub fn carried_attributes_equal(source: &Path, destination: &Path) -> bool {
236 let (Ok(from), Ok(to)) = (c_path("llistxattr", source), c_path("llistxattr", destination)) else { return false };
237 return carried_attributes(from.as_c_str()) == carried_attributes(to.as_c_str());
238}
239
240/// A failed owner, permission or time change that only a missing privilege explains.
241fn is_missing_privilege(error: &std::io::Error) -> bool {
242 return matches!(error.raw_os_error(), Some(libc::EPERM | libc::EACCES));
243}
244
245/// What:
246/// Decide what a failed permission or time change means: a missing privilege is counted, anything else is an error.
247/// Why:
248/// An entry owned by someone else cannot be changed by this user, and the report says how many; a refusal for another
249/// reason (no space for the metadata, a read-only filesystem) leaves a copy that is not what it is recorded to be.
250///
251/// In TS you'd write (pseudocode):
252/// ```ts
253/// if (error?.code === 'EPERM') refused += 1; else if (error) throw error;
254/// ```
255fn settle(outcome: std::io::Result<()>, action: &'static str, refused: &mut u64) -> Result<(), AttributeFailure> {
256 let Err(error) = outcome else { return Ok(()) };
257 if !is_missing_privilege(&error) {
258 return Err(AttributeFailure { action, error });
259 }
260 *refused += 1;
261 return Ok(());
262}
263
264/// What:
265/// Set the owner and group of the copy to those of the original when they differ from what the copy has now.
266/// Why:
267/// The copy's owner comes from its creator and its group from the creator's group or the parent's setgid
268/// directory, so the destination, not the caller, is what must be compared with the original.
269///
270/// In TS you'd write (pseudocode):
271/// ```ts
272/// if (copyStat.uid !== st.uid || copyStat.gid !== st.gid) await lchown(copy, st.uid, st.gid);
273/// ```
274fn set_owner(destination: Handle<'_>, current: &Metadata, source: &Metadata, losses: &mut AttributeLosses) {
275 if source.uid() == current.uid() && source.gid() == current.gid() {
276 return;
277 }
278 // SAFETY: the descriptor or the NUL-terminated path is valid for the call.
279 let result: libc::c_int = unsafe {
280 match destination {
281 Handle::Descriptor(file) => libc::fchown(file.as_raw_fd(), source.uid(), source.gid()),
282 Handle::Link(path) => libc::lchown(path.as_ptr(), source.uid(), source.gid()),
283 }
284 };
285 if result != 0 {
286 losses.owner += 1;
287 }
288}
289
290/// What:
291/// Set the access and modification times of the copy, to the nanosecond.
292/// `futimens` acts on the descriptor; `utimensat` with `AT_SYMLINK_NOFOLLOW` acts on a symbolic link, not its target.
293/// Why:
294/// The index stat rewrite and Git's own stat check compare modification times.
295///
296/// In TS you'd write (pseudocode):
297/// ```ts
298/// await lutimes(path, atime, mtime);
299/// ```
300fn set_times(destination: Handle<'_>, source: &Metadata) -> std::io::Result<()> {
301 let times: [libc::timespec; 2] = [
302 libc::timespec { tv_sec: source.atime(), tv_nsec: source.atime_nsec() },
303 libc::timespec { tv_sec: source.mtime(), tv_nsec: source.mtime_nsec() },
304 ];
305 // SAFETY: `times` is two `timespec` values alive for the call, and the descriptor or path is valid.
306 let result: libc::c_int = unsafe {
307 match destination {
308 Handle::Descriptor(file) => libc::futimens(file.as_raw_fd(), times.as_ptr()),
309 Handle::Link(path) => libc::utimensat(libc::AT_FDCWD, path.as_ptr(), times.as_ptr(), libc::AT_SYMLINK_NOFOLLOW),
310 }
311 };
312 if result != 0 {
313 return Err(std::io::Error::last_os_error());
314 }
315 return Ok(());
316}
317
318/// The shared body of both public functions.
319fn apply(
320 source: &Path,
321 destination: Handle<'_>,
322 current: &Metadata,
323 metadata: &Metadata,
324 remove_extras: bool,
325 losses: &mut AttributeLosses,
326) -> Result<(), AttributeFailure> {
327 if let Ok(from) = c_path("llistxattr", source) {
328 copy_extended_attributes(from.as_c_str(), destination, remove_extras, losses);
329 }
330 set_owner(destination, current, metadata, losses);
331 if let Handle::Descriptor(file) = destination {
332 // SAFETY: the descriptor is open for the call.
333 let result: libc::c_int = unsafe { libc::fchmod(file.as_raw_fd(), metadata.mode() & 0o7777) };
334 let outcome: std::io::Result<()> = if result == 0 { Ok(()) } else { Err(std::io::Error::last_os_error()) };
335 settle(outcome, "set the permission bits of", &mut losses.mode)?;
336 }
337 settle(set_times(destination, metadata), "set the times of", &mut losses.times)?;
338 return Ok(());
339}
340
341/// What:
342/// Make the open copy carry the attributes of `source`: extended attributes, owner, permission bits and times, in this
343/// order.
344/// `remove_extras` also removes the carried attributes the source lacks.
345/// Why:
346/// Setting the owner clears the set-user-ID bits, so the permission bits come after it; the times come last,
347/// because the earlier calls change the change time but not the modification time, and writing the file's content
348/// must already be finished. The descriptor may be a regular file, a directory or a named pipe.
349///
350/// In TS you'd write (pseudocode):
351/// ```ts
352/// await preserveAttributes(source, copyHandle, metadata);
353/// ```
354pub fn preserve_on_descriptor(
355 source: &Path,
356 destination: &File,
357 metadata: &Metadata,
358 remove_extras: bool,
359 losses: &mut AttributeLosses,
360) -> Result<(), AttributeFailure> {
361 let current: Metadata = destination.metadata().map_err(|error: std::io::Error| { return AttributeFailure { action: "examine", error }; })?;
362 return apply(source, Handle::Descriptor(destination), ¤t, metadata, remove_extras, losses);
363}
364
365/// What:
366/// Make the symbolic link at `destination` carry the owner and times of the original link.
367/// Why:
368/// A link has no permission bits of its own and cannot be opened, so only the `l` calls on its path apply.
369///
370/// In TS you'd write (pseudocode):
371/// ```ts
372/// await lchown(link, st.uid, st.gid); await lutimes(link, st.atime, st.mtime);
373/// ```
374pub fn preserve_on_link(source: &Path, destination: &Path, metadata: &Metadata, losses: &mut AttributeLosses) -> Result<(), AttributeFailure> {
375 let name: CString = c_path("lchown", destination).map_err(|error| { return AttributeFailure { action: "address", error: std::io::Error::from_raw_os_error(error.errno) }; })?;
376 let current: Metadata = std::fs::symlink_metadata(destination).map_err(|error: std::io::Error| { return AttributeFailure { action: "examine", error }; })?;
377 return apply(source, Handle::Link(name.as_c_str()), ¤t, metadata, false, losses);
378}
379
380/// What:
381/// Open a named pipe without blocking, so attribute calls can use its descriptor.
382/// Why:
383/// Opening a pipe read-only with `O_NONBLOCK` succeeds at once whether or not a writer exists, and `O_NOFOLLOW`
384/// refuses a link put in its place.
385///
386/// In TS you'd write (pseudocode):
387/// ```ts
388/// const handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
389/// ```
390pub fn open_fifo(path: &Path) -> std::io::Result<File> {
391 return std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NONBLOCK | libc::O_NOFOLLOW | libc::O_CLOEXEC).open(path);
392}
393
394/// What:
395/// Create a named pipe (FIFO) with the permission bits of the original.
396/// `mkfifo(3)` is the call; the umask applies, and `preserve_on_descriptor` sets the exact bits afterwards.
397/// Why:
398/// A FIFO is part of the tree; sockets and device nodes are not recreated (the report lists them).
399///
400/// In TS you'd write (pseudocode):
401/// ```ts
402/// await mkfifo(path, mode);
403/// ```
404pub fn make_fifo(path: &Path, mode: u32) -> std::io::Result<()> {
405 let name = c_path("mkfifo", path).map_err(|error| { return std::io::Error::from_raw_os_error(error.errno); })?;
406 // SAFETY: `name` is a NUL-terminated string that outlives the call.
407 let result: libc::c_int = unsafe { libc::mkfifo(name.as_ptr(), mode & 0o7777) };
408 if result != 0 {
409 return Err(std::io::Error::last_os_error());
410 }
411 return Ok(());
412}
413
414/// Attribute controls stay out of the release executable.
415#[cfg(test)]
416#[path = "subvolume_sync_attrs_tests.rs"]
417mod tests;