Skip to main content

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), &current, 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()), &current, 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;