aboutsummaryrefslogtreecommitdiff
path: root/src/platform/macos.rs
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-09-14 23:13:52 +0300
committerGitHub <noreply@github.com>2026-09-14 22:13:52 +0200
commit5e38e5fb5fc2cc5d30844df71432a60742bcb572 (patch)
tree31896855b510cf03c57e19e4e3ef454bef028a59 /src/platform/macos.rs
parentb0f21ed2cb85613050b794bf6c78a7c367a94e0d (diff)
fix: protect integration configs from failed writes (#3973)
* fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 * fix: atomically write integration configs refs #3970 --------- Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com> Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'src/platform/macos.rs')
-rw-r--r--src/platform/macos.rs143
1 files changed, 143 insertions, 0 deletions
diff --git a/src/platform/macos.rs b/src/platform/macos.rs
index 4e86deab..4b11a618 100644
--- a/src/platform/macos.rs
+++ b/src/platform/macos.rs
@@ -23,6 +23,149 @@ pub(crate) use super::unix_common::{
ClientStreamReader, StatusCommandGuard,
};
+#[cfg(test)]
+mod config_file_tests;
+
+pub(crate) fn config_file_link_count(path: &Path) -> std::io::Result<u64> {
+ use std::os::unix::fs::MetadataExt;
+ Ok(std::fs::metadata(path)?.nlink())
+}
+
+pub(crate) fn check_config_write_target(_target: &std::path::Path) -> std::io::Result<()> {
+ Ok(())
+}
+
+pub(crate) fn write_existing_config(
+ _target: &std::path::Path,
+ _contents: &[u8],
+) -> std::io::Result<bool> {
+ // Unix keeps atomic replacement for existing files too.
+ Ok(false)
+}
+
+pub(crate) fn create_config_temporary(
+ path: &Path,
+ private: bool,
+) -> std::io::Result<std::fs::File> {
+ if !private {
+ return std::fs::OpenOptions::new()
+ .write(true)
+ .create_new(true)
+ .open(path);
+ }
+ use std::os::fd::FromRawFd;
+ // Darwin's opaque ACL/filesec APIs (<sys/acl.h>, <sys/fcntl.h>) are not
+ // exposed by libc. Supply a non-inheriting empty ACL at creation: clearing
+ // inherited ACEs later cannot revoke descriptors opened in the meantime.
+ unsafe extern "C" {
+ fn acl_init(count: libc::c_int) -> *mut libc::c_void;
+ fn acl_free(acl: *mut libc::c_void) -> libc::c_int;
+ fn acl_get_flagset_np(acl: *mut libc::c_void, flags: *mut *mut libc::c_void)
+ -> libc::c_int;
+ fn acl_add_flag_np(flags: *mut libc::c_void, flag: libc::c_uint) -> libc::c_int;
+ fn filesec_init() -> *mut libc::c_void;
+ fn filesec_free(security: *mut libc::c_void);
+ fn filesec_set_property(
+ security: *mut libc::c_void,
+ property: libc::c_int,
+ value: *const libc::c_void,
+ ) -> libc::c_int;
+ fn openx_np(
+ path: *const libc::c_char,
+ flags: libc::c_int,
+ security: *mut libc::c_void,
+ ) -> libc::c_int;
+ }
+ const FILESEC_MODE: libc::c_int = 4;
+ const FILESEC_ACL: libc::c_int = 5;
+ const ACL_FLAG_NO_INHERIT: libc::c_uint = 1 << 17;
+ let path =
+ std::ffi::CString::new(path.as_os_str().as_bytes()).map_err(std::io::Error::other)?;
+ let acl = unsafe { acl_init(0) };
+ if acl.is_null() {
+ return Err(std::io::Error::last_os_error());
+ }
+ let security = unsafe { filesec_init() };
+ if security.is_null() {
+ let error = std::io::Error::last_os_error();
+ unsafe {
+ acl_free(acl);
+ }
+ return Err(error);
+ }
+ let result = (|| {
+ let mut flags = std::ptr::null_mut();
+ let mode: libc::mode_t = 0o600;
+ if unsafe { acl_get_flagset_np(acl, &mut flags) } != 0
+ || unsafe { acl_add_flag_np(flags, ACL_FLAG_NO_INHERIT) } != 0
+ || unsafe {
+ filesec_set_property(security, FILESEC_MODE, std::ptr::from_ref(&mode).cast())
+ } != 0
+ || unsafe {
+ filesec_set_property(security, FILESEC_ACL, std::ptr::from_ref(&acl).cast())
+ } != 0
+ {
+ return Err(std::io::Error::last_os_error());
+ }
+ let fd = unsafe {
+ openx_np(
+ path.as_ptr(),
+ libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC,
+ security,
+ )
+ };
+ if fd < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ // The successful exclusive create returns one owned descriptor.
+ Ok(unsafe { std::fs::File::from_raw_fd(fd) })
+ })();
+ unsafe {
+ filesec_free(security);
+ acl_free(acl);
+ }
+ result
+}
+
+pub(crate) fn write_config_temporary(
+ source: Option<&Path>,
+ temporary: &Path,
+ contents: &[u8],
+) -> std::io::Result<()> {
+ use std::os::{fd::AsRawFd, unix::fs::MetadataExt};
+ let mut output = std::fs::OpenOptions::new()
+ .write(true)
+ .truncate(true)
+ .open(temporary)?;
+ if let Some(source) = source {
+ let input = std::fs::File::open(source)?;
+ let metadata = input.metadata()?;
+ let current = output.metadata()?;
+ if (metadata.uid(), metadata.gid()) != (current.uid(), current.gid())
+ && unsafe { libc::fchown(output.as_raw_fd(), metadata.uid(), metadata.gid()) } != 0
+ {
+ return Err(std::io::Error::last_os_error());
+ }
+ // Prepare access controls while the temporary is still empty. Copy ACLs
+ // before mode bits so no inherited/default grant can expose the content.
+ // Do not copy data or old timestamps.
+ if unsafe {
+ libc::fcopyfile(
+ input.as_raw_fd(),
+ output.as_raw_fd(),
+ std::ptr::null_mut(),
+ libc::COPYFILE_ACL | libc::COPYFILE_XATTR,
+ )
+ } != 0
+ {
+ return Err(std::io::Error::last_os_error());
+ }
+ output.set_permissions(metadata.permissions())?;
+ }
+ output.write_all(contents)?;
+ output.sync_all()
+}
+
const PROC_PGRP_ONLY: u32 = 2;
const SERVER_NOFILE_LIMIT_TARGET: libc::rlim_t = 8192;