Skip to content

Commit c1b323d

Browse files
committed
🐛 Preserve Windows broken symlink metadata
Use handle-based Windows security APIs (GetSecurityInfo/SetSecurityInfo) with FILE_FLAG_OPEN_REPARSE_POINT to read and write metadata on symlinks without following them. This fixes broken symlinks losing their mode, ownership, and ACL information during archival. Changes: - Add FileHandle RAII wrapper and open_path with follow_symlink control - Add SecurityDescriptor::try_from_handle and apply_by_handle - Switch lchown and chmod to handle-based APIs for symlink correctness - Add set_facl_nofollow for symlink-aware ACL writes - Extract build_acl_buffer to share between set_d_acl and set_d_acl_by_handle - Use nofollow metadata collection in core.rs for symlink entries
1 parent 4b031b2 commit c1b323d

6 files changed

Lines changed: 471 additions & 51 deletions

File tree

cli/src/command/core.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -972,11 +972,15 @@ pub(crate) fn apply_metadata(
972972
}
973973
#[cfg(windows)]
974974
if let OwnerStrategy::Preserve { options } = &keep_options.owner_strategy {
975-
use crate::utils::os::windows::{fs::stat, security::SecurityDescriptor};
975+
use crate::utils::os::windows::{
976+
fs::{file_information, mode_from_file_information, open_read_metadata},
977+
security::SecurityDescriptor,
978+
};
976979

977-
let sd = SecurityDescriptor::try_from(path)?;
978-
let stat = stat(sd.path.as_ptr() as _)?;
979-
let mode = stat.st_mode;
980+
let handle = open_read_metadata(path, !meta.file_type().is_symlink())?;
981+
let info = file_information(handle.raw())?;
982+
let sd = SecurityDescriptor::try_from_handle(handle.raw(), path)?;
983+
let mode = mode_from_file_information(path, &info, meta.file_type().is_symlink());
980984
let user = sd.owner_sid()?;
981985
let group = sd.group_sid()?;
982986
// Get owner info: use overrides from OwnerStrategy
@@ -1007,7 +1011,15 @@ pub(crate) fn apply_metadata(
10071011
if let AclStrategy::Always = keep_options.acl_strategy {
10081012
use crate::chunk;
10091013
use pna::RawChunk;
1010-
match utils::acl::get_facl(path) {
1014+
#[cfg(windows)]
1015+
let acl_result = if meta.file_type().is_symlink() {
1016+
utils::os::windows::acl::get_facl_nofollow(path)
1017+
} else {
1018+
utils::acl::get_facl(path)
1019+
};
1020+
#[cfg(not(windows))]
1021+
let acl_result = utils::acl::get_facl(path);
1022+
match acl_result {
10111023
Ok(acl) => {
10121024
entry
10131025
.add_extra_chunk(RawChunk::from_data(chunk::faCl, acl.platform.to_bytes()));

cli/src/utils/os/windows/acl.rs

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
use crate::{
22
chunk::{self, AcePlatform, Identifier, OwnerType},
3-
utils::os::windows::security::{SecurityDescriptor, Sid, SidType},
3+
utils::os::windows::{
4+
fs::{open_read_metadata, open_write_dacl},
5+
security::{SecurityDescriptor, Sid, SidType},
6+
},
47
};
58
use field_offset::offset_of;
69
use std::{io, mem, path::Path, ptr::null_mut};
10+
use windows::Win32::Foundation::HANDLE;
711
use windows::Win32::Security::{
812
ACCESS_ALLOWED_ACE, ACCESS_DENIED_ACE, ACE_FLAGS, ACE_HEADER, ACL as Win32ACL, ACL_REVISION_DS,
913
AddAccessAllowedAceEx, AddAccessDeniedAceEx, CONTAINER_INHERIT_ACE, GetAce, INHERIT_ONLY_ACE,
@@ -38,6 +42,29 @@ pub fn get_facl<P: AsRef<Path>>(path: P) -> io::Result<chunk::Acl> {
3842
})
3943
}
4044

45+
pub fn set_facl_nofollow<P: AsRef<Path>>(path: P, ace_list: chunk::Acl) -> io::Result<()> {
46+
let path = path.as_ref();
47+
let handle = open_write_dacl(path, false)?;
48+
let acl = ACL::try_from_handle(handle.raw(), path)?;
49+
let group_sid = acl.security_descriptor.group_sid()?;
50+
let owner_sid = acl.security_descriptor.owner_sid()?;
51+
let acl_entries = ace_list
52+
.entries
53+
.into_iter()
54+
.map(|it| it.into_acl_entry_with(&owner_sid, &group_sid))
55+
.collect::<Vec<_>>();
56+
acl.set_d_acl_by_handle(handle.raw(), &acl_entries)
57+
}
58+
59+
pub fn get_facl_nofollow<P: AsRef<Path>>(path: P) -> io::Result<chunk::Acl> {
60+
let acl = ACL::try_from_nofollow(path.as_ref())?;
61+
let ace_list = acl.get_d_acl()?;
62+
Ok(chunk::Acl {
63+
platform: AcePlatform::Windows,
64+
entries: ace_list.into_iter().map(Into::into).collect(),
65+
})
66+
}
67+
4168
#[allow(non_camel_case_types)]
4269
type PACE_HEADER = *mut ACE_HEADER;
4370

@@ -52,9 +79,25 @@ impl ACL {
5279
})
5380
}
5481

82+
pub fn try_from_nofollow(path: &Path) -> io::Result<Self> {
83+
let handle = open_read_metadata(path, false)?;
84+
Ok(Self {
85+
security_descriptor: SecurityDescriptor::try_from_handle(handle.raw(), path)?,
86+
})
87+
}
88+
89+
pub fn try_from_handle(handle: HANDLE, path: &Path) -> io::Result<Self> {
90+
Ok(Self {
91+
security_descriptor: SecurityDescriptor::try_from_handle(handle, path)?,
92+
})
93+
}
94+
5595
pub fn get_d_acl(&self) -> io::Result<Vec<ACLEntry>> {
5696
let mut result = Vec::new();
5797
let p_acl = self.security_descriptor.p_dacl;
98+
if p_acl.is_null() {
99+
return Ok(result);
100+
}
58101
let count = unsafe { *p_acl }.AceCount as u32;
59102
for i in 0..count {
60103
let mut header: PACE_HEADER = null_mut();
@@ -106,37 +149,47 @@ impl ACL {
106149
}
107150

108151
pub fn set_d_acl(&self, acl_entries: &[ACLEntry]) -> io::Result<()> {
109-
let acl_size = acl_entries.iter().map(|it| it.size as usize).sum::<usize>()
110-
+ mem::size_of::<Win32ACL>();
111-
let mut new_acl_buffer = Vec::<u8>::with_capacity(acl_size);
112-
let new_acl = new_acl_buffer.as_mut_ptr();
113-
unsafe { InitializeAcl(new_acl as _, acl_size as u32, ACL_REVISION_DS) }?;
114-
for ace in acl_entries {
115-
match ace.ace_type {
116-
AceType::AccessAllow => unsafe {
117-
AddAccessAllowedAceEx(
118-
new_acl as _,
119-
ACL_REVISION_DS,
120-
ACE_FLAGS(ace.flags as u32),
121-
ace.mask,
122-
ace.sid.as_psid(),
123-
)
124-
},
125-
AceType::AccessDeny => unsafe {
126-
AddAccessDeniedAceEx(
127-
new_acl as _,
128-
ACL_REVISION_DS,
129-
ACE_FLAGS(ace.flags as u32),
130-
ace.mask,
131-
ace.sid.as_psid(),
132-
)
133-
},
134-
AceType::Unknown(n) => return Err(io::Error::other(format!("{}", n))),
135-
}?;
136-
}
152+
let buffer = build_acl_buffer(acl_entries)?;
137153
self.security_descriptor
138-
.apply(None, None, Some(new_acl as _))
154+
.apply(None, None, Some(buffer.as_ptr() as _))
155+
}
156+
157+
pub fn set_d_acl_by_handle(&self, handle: HANDLE, acl_entries: &[ACLEntry]) -> io::Result<()> {
158+
let buffer = build_acl_buffer(acl_entries)?;
159+
SecurityDescriptor::apply_by_handle(handle, None, None, Some(buffer.as_ptr() as _))
160+
}
161+
}
162+
163+
fn build_acl_buffer(acl_entries: &[ACLEntry]) -> io::Result<Vec<u8>> {
164+
let acl_size =
165+
acl_entries.iter().map(|it| it.size as usize).sum::<usize>() + mem::size_of::<Win32ACL>();
166+
let mut buffer = Vec::<u8>::with_capacity(acl_size);
167+
let ptr = buffer.as_mut_ptr();
168+
unsafe { InitializeAcl(ptr as _, acl_size as u32, ACL_REVISION_DS) }?;
169+
for ace in acl_entries {
170+
match ace.ace_type {
171+
AceType::AccessAllow => unsafe {
172+
AddAccessAllowedAceEx(
173+
ptr as _,
174+
ACL_REVISION_DS,
175+
ACE_FLAGS(ace.flags as u32),
176+
ace.mask,
177+
ace.sid.as_psid(),
178+
)
179+
},
180+
AceType::AccessDeny => unsafe {
181+
AddAccessDeniedAceEx(
182+
ptr as _,
183+
ACL_REVISION_DS,
184+
ACE_FLAGS(ace.flags as u32),
185+
ace.mask,
186+
ace.sid.as_psid(),
187+
)
188+
},
189+
AceType::Unknown(n) => return Err(io::Error::other(format!("{}", n))),
190+
}?;
139191
}
192+
Ok(buffer)
140193
}
141194

142195
#[derive(Clone, Copy, Debug, PartialEq)]

cli/src/utils/os/windows/fs.rs

Lines changed: 137 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,41 @@ pub(crate) mod owner;
33
use super::security::{SecurityDescriptor, Sid};
44
use crate::utils::str::encode_wide;
55
use std::io;
6+
use std::mem::size_of;
67
use std::path::Path;
8+
use windows::Win32::Foundation::{CloseHandle, HANDLE};
79
use windows::Win32::Storage::FileSystem::{
8-
MOVEFILE_COPY_ALLOWED, MOVEFILE_REPLACE_EXISTING, MoveFileExW,
10+
BY_HANDLE_FILE_INFORMATION, CreateFileW, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_READONLY,
11+
FILE_BASIC_INFO, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
12+
FILE_READ_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
13+
FILE_WRITE_ATTRIBUTES, FileBasicInfo, GetFileInformationByHandle, GetFileInformationByHandleEx,
14+
MOVEFILE_COPY_ALLOWED, MOVEFILE_REPLACE_EXISTING, MoveFileExW, OPEN_EXISTING, READ_CONTROL,
15+
SetFileInformationByHandle, WRITE_DAC, WRITE_OWNER,
916
};
1017
use windows::core::PCWSTR;
1118

19+
const MODE_SYMLINK: u16 = 0o120000;
20+
const MODE_DIR: u16 = 0o040000;
21+
const MODE_FILE: u16 = 0o100000;
22+
const MODE_READ_BITS: u16 = 0o444;
23+
const MODE_WRITE_BITS: u16 = 0o222;
24+
const MODE_EXEC_BITS: u16 = 0o111;
25+
26+
pub(crate) struct FileHandle(HANDLE);
27+
28+
impl FileHandle {
29+
#[inline]
30+
pub(crate) const fn raw(&self) -> HANDLE {
31+
self.0
32+
}
33+
}
34+
35+
impl Drop for FileHandle {
36+
fn drop(&mut self) {
37+
let _ = unsafe { CloseHandle(self.0) };
38+
}
39+
}
40+
1241
#[inline]
1342
pub(crate) fn move_file(src: &std::ffi::OsStr, dist: &std::ffi::OsStr) -> io::Result<()> {
1443
unsafe {
@@ -21,14 +50,88 @@ pub(crate) fn move_file(src: &std::ffi::OsStr, dist: &std::ffi::OsStr) -> io::Re
2150
.map_err(Into::into)
2251
}
2352

53+
#[inline]
54+
fn open_path(path: &Path, desired_access: u32, follow_symlink: bool) -> io::Result<FileHandle> {
55+
let path = encode_wide(path.as_os_str())?;
56+
let mut flags = FILE_FLAG_BACKUP_SEMANTICS;
57+
if !follow_symlink {
58+
flags |= FILE_FLAG_OPEN_REPARSE_POINT;
59+
}
60+
let handle = unsafe {
61+
CreateFileW(
62+
PCWSTR::from_raw(path.as_ptr()),
63+
desired_access,
64+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
65+
None,
66+
OPEN_EXISTING,
67+
flags,
68+
None,
69+
)
70+
}?;
71+
Ok(FileHandle(handle))
72+
}
73+
74+
#[inline]
75+
pub(crate) fn open_read_metadata(path: &Path, follow_symlink: bool) -> io::Result<FileHandle> {
76+
open_path(
77+
path,
78+
(READ_CONTROL | FILE_READ_ATTRIBUTES).0,
79+
follow_symlink,
80+
)
81+
}
82+
83+
#[inline]
84+
pub(crate) fn open_write_dacl(path: &Path, follow_symlink: bool) -> io::Result<FileHandle> {
85+
open_path(path, (READ_CONTROL | WRITE_DAC).0, follow_symlink)
86+
}
87+
88+
#[inline]
89+
pub(crate) fn file_information(handle: HANDLE) -> io::Result<BY_HANDLE_FILE_INFORMATION> {
90+
let mut info = BY_HANDLE_FILE_INFORMATION::default();
91+
unsafe { GetFileInformationByHandle(handle, &mut info) }?;
92+
Ok(info)
93+
}
94+
95+
#[inline]
96+
pub(crate) fn mode_from_file_information(
97+
path: &Path,
98+
info: &BY_HANDLE_FILE_INFORMATION,
99+
is_symlink: bool,
100+
) -> u16 {
101+
let mut mode = MODE_READ_BITS;
102+
if info.dwFileAttributes & FILE_ATTRIBUTE_READONLY.0 == 0 {
103+
mode |= MODE_WRITE_BITS;
104+
}
105+
if is_symlink {
106+
mode |= MODE_SYMLINK;
107+
return mode;
108+
}
109+
if info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY.0 != 0 {
110+
mode |= MODE_DIR | MODE_EXEC_BITS;
111+
return mode;
112+
}
113+
114+
mode |= MODE_FILE;
115+
if path
116+
.extension()
117+
.and_then(|it| it.to_str())
118+
.map(str::to_ascii_lowercase)
119+
.is_some_and(|ext| matches!(ext.as_str(), "bat" | "cmd" | "exe"))
120+
{
121+
mode |= MODE_EXEC_BITS;
122+
}
123+
mode
124+
}
125+
24126
#[inline]
25127
pub(crate) fn lchown<U: Into<Sid>, G: Into<Sid>>(
26128
path: &Path,
27129
owner: Option<U>,
28130
group: Option<G>,
29131
) -> io::Result<()> {
30-
let sd = SecurityDescriptor::try_from(path)?;
31-
sd.apply(
132+
let handle = open_path(path, (READ_CONTROL | WRITE_OWNER).0, false)?;
133+
SecurityDescriptor::apply_by_handle(
134+
handle.raw(),
32135
owner.and_then(|it| it.into().to_psid().ok()),
33136
group.and_then(|it| it.into().to_psid().ok()),
34137
None,
@@ -37,6 +140,37 @@ pub(crate) fn lchown<U: Into<Sid>, G: Into<Sid>>(
37140

38141
#[inline]
39142
pub(crate) fn chmod(path: &Path, mode: u16) -> io::Result<()> {
143+
if std::fs::symlink_metadata(path)?.file_type().is_symlink() {
144+
let handle = open_path(
145+
path,
146+
(FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES).0,
147+
false,
148+
)?;
149+
let mut info = FILE_BASIC_INFO::default();
150+
unsafe {
151+
GetFileInformationByHandleEx(
152+
handle.raw(),
153+
FileBasicInfo,
154+
&mut info as *mut _ as _,
155+
size_of::<FILE_BASIC_INFO>() as u32,
156+
)?;
157+
}
158+
if mode & MODE_WRITE_BITS == 0 {
159+
info.FileAttributes |= FILE_ATTRIBUTE_READONLY.0;
160+
} else {
161+
info.FileAttributes &= !FILE_ATTRIBUTE_READONLY.0;
162+
}
163+
unsafe {
164+
SetFileInformationByHandle(
165+
handle.raw(),
166+
FileBasicInfo,
167+
&info as *const _ as _,
168+
size_of::<FILE_BASIC_INFO>() as u32,
169+
)?;
170+
}
171+
return Ok(());
172+
}
173+
40174
let s = encode_wide(path.as_os_str())?;
41175
let code = unsafe { libc::wchmod(s.as_ptr() as _, mode as _) };
42176
if code == 0 {
@@ -46,16 +180,6 @@ pub(crate) fn chmod(path: &Path, mode: u16) -> io::Result<()> {
46180
}
47181
}
48182

49-
pub(crate) fn stat(path: *const libc::wchar_t) -> io::Result<libc::stat> {
50-
let mut stat = unsafe { std::mem::zeroed::<libc::stat>() };
51-
let code = unsafe { libc::wstat(path, &mut stat) };
52-
if code == 0 {
53-
Ok(stat)
54-
} else {
55-
Err(io::Error::last_os_error())
56-
}
57-
}
58-
59183
#[cfg(test)]
60184
mod tests {
61185
use super::*;

0 commit comments

Comments
 (0)