fix: allow safe workdir cleanup of FIFOs and Unix sockets
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
//! identity only and report CleanupPending/unknown, not current cleanliness or removal permission.
|
||||
//!
|
||||
//! The cleanup walker never reads ordinary file bodies or computes content digests. It records
|
||||
//! device/inode, type, regular-file executable bits, and size/nanosecond mtime for files and symlinks.
|
||||
//! device/inode, type, regular-file executable bits, and size/nanosecond mtime for non-directory
|
||||
//! entries, including FIFOs and Unix sockets. IPC endpoints are never opened for I/O or connected.
|
||||
//! Directories retain identity/type only because child unlink changes their timestamps. Symlink
|
||||
//! targets are neither read nor followed. This is metadata protection, not byte equality:
|
||||
//! same-size writes restoring mtime are not guaranteed to be detected. Normal Git inspection may
|
||||
@@ -735,7 +736,7 @@ mod linux {
|
||||
use std::ffi::{CString, OsStr};
|
||||
use std::os::fd::{AsRawFd, FromRawFd};
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
||||
|
||||
#[repr(C)]
|
||||
struct OpenHow {
|
||||
@@ -916,7 +917,16 @@ mod linux {
|
||||
io_observation::record(io_observation::Event::EntryInspection);
|
||||
let file = open(dir, name, libc::O_PATH)?;
|
||||
let metadata = file.metadata()?;
|
||||
if !metadata.is_file() && !metadata.is_dir() && !metadata.file_type().is_symlink() {
|
||||
let kind = metadata.file_type();
|
||||
// O_PATH inspects IPC entries without opening a FIFO stream or connecting
|
||||
// to a socket. They are unlinked, never traversed, under the same identity
|
||||
// and mount checks as regular entries; their type alone is not occupancy.
|
||||
if !kind.is_file()
|
||||
&& !kind.is_dir()
|
||||
&& !kind.is_symlink()
|
||||
&& !kind.is_fifo()
|
||||
&& !kind.is_socket()
|
||||
{
|
||||
return Err(io::Error::from_raw_os_error(libc::EBUSY));
|
||||
}
|
||||
Ok(Entry {
|
||||
@@ -1163,6 +1173,91 @@ pub(super) mod tests {
|
||||
(runtime, materializer, binding)
|
||||
}
|
||||
|
||||
fn create_fifo(path: &Path) {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let name = std::ffi::CString::new(path.as_os_str().as_bytes()).unwrap();
|
||||
// SAFETY: NUL-terminated path inside the isolated test directory.
|
||||
assert_eq!(unsafe { libc::mkfifo(name.as_ptr(), 0o600) }, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_unlinks_fifo_and_socket_without_opening_or_connecting() {
|
||||
use std::os::unix::net::UnixListener;
|
||||
for retry in [false, true] {
|
||||
let (runtime, materializer, binding) = fixture();
|
||||
fs::write(binding.root.join(".git/info/exclude"), "target/\n").unwrap();
|
||||
let output = binding.root.join("target");
|
||||
fs::create_dir(&output).unwrap();
|
||||
let fifo = output.join("ready.fifo");
|
||||
let socket = output.join("service.sock");
|
||||
create_fifo(&fifo);
|
||||
let listener = UnixListener::bind(&socket).unwrap();
|
||||
listener.set_nonblocking(true).unwrap();
|
||||
let mut watch = BodyWatch::new(&[fifo, socket]);
|
||||
let id = &binding.working_directory.id;
|
||||
if retry {
|
||||
let (result, audit) =
|
||||
io_observation::observe(Some(1), || materializer.cleanup_working_directory(id));
|
||||
assert_eq!(
|
||||
result.unwrap_err().code,
|
||||
"working_directory_cleanup_permission_denied"
|
||||
);
|
||||
assert_eq!(audit.unlinks, 1);
|
||||
assert_eq!(audit.entry_readable_opens, 0);
|
||||
}
|
||||
drop(materializer);
|
||||
let materializer = RuntimeGitMaterializer::new(runtime.path());
|
||||
let (result, audit) =
|
||||
io_observation::observe(None, || materializer.cleanup_working_directory(id));
|
||||
assert_eq!(
|
||||
result.unwrap().summary.status,
|
||||
WorkingDirectoryStatusKind::NotFound
|
||||
);
|
||||
assert!(!binding.working_directory_root.exists());
|
||||
assert_eq!(audit.entry_readable_opens, 0);
|
||||
watch.assert_no_body_access();
|
||||
assert_eq!(
|
||||
listener.accept().unwrap_err().kind(),
|
||||
io::ErrorKind::WouldBlock
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_preserves_replaced_fifo_and_socket_before_unlink() {
|
||||
use std::os::unix::net::UnixListener;
|
||||
for socket in [false, true] {
|
||||
let (runtime, materializer, binding) = fixture();
|
||||
fs::write(binding.root.join(".git/info/exclude"), "target/\n").unwrap();
|
||||
let output = binding.root.join("target");
|
||||
fs::create_dir(&output).unwrap();
|
||||
let path = output.join("endpoint");
|
||||
let _original = if socket {
|
||||
Some(UnixListener::bind(&path).unwrap())
|
||||
} else {
|
||||
create_fifo(&path);
|
||||
None
|
||||
};
|
||||
let error = remove_with_eraser(
|
||||
&materializer,
|
||||
&binding.working_directory.id,
|
||||
|tree, relative, entries| {
|
||||
fs::rename(&path, runtime.path().join("original-endpoint")).unwrap();
|
||||
let _replacement = if socket {
|
||||
Some(UnixListener::bind(&path).unwrap())
|
||||
} else {
|
||||
create_fifo(&path);
|
||||
None
|
||||
};
|
||||
erase(tree, relative, entries)
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code, "working_directory_cleanup_changes_present");
|
||||
assert!(path.symlink_metadata().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fail_once(materializer: &RuntimeGitMaterializer, id: &str) {
|
||||
let error = remove_with_eraser(materializer, id, |_, _, _| {
|
||||
Err(EraseError::Io(io::Error::from_raw_os_error(libc::EACCES)))
|
||||
|
||||
Reference in New Issue
Block a user