Skip to content

Commit bbe9ef4

Browse files
committed
cuprated: private file permissions (Windows)
1 parent ef6da9d commit bbe9ef4

5 files changed

Lines changed: 202 additions & 55 deletions

File tree

binaries/cuprated/src/config.rs

Lines changed: 20 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ pub fn read_config_and_args() -> Config {
130130

131131
let config = args.apply_args(config);
132132

133+
#[cfg(target_os = "windows")]
134+
cuprate_helper::fs::set_private_directory_permissions(&config.writable_directories());
135+
133136
if args.dry_run {
134137
config.dry_run_check();
135138
}
@@ -247,6 +250,20 @@ impl Config {
247250
format!("{HEADER}{doc}")
248251
}
249252

253+
/// Returns the directories cuprate writes to.
254+
pub fn writable_directories(&self) -> Vec<&Path> {
255+
let mut paths = vec![
256+
self.fs.fast_data_directory.as_path(),
257+
self.fs.slow_data_directory.as_path(),
258+
self.fs.cache_directory.as_path(),
259+
];
260+
#[cfg(feature = "arti")]
261+
if matches!(self.tor.mode, TorMode::Arti | TorMode::Auto) {
262+
paths.push(self.tor.arti.directory_path.as_path());
263+
}
264+
paths
265+
}
266+
250267
/// Attempts to read a config file in [`toml`] format from the given [`Path`].
251268
///
252269
/// # Errors
@@ -503,46 +520,9 @@ impl Config {
503520
}
504521
}
505522

506-
match Self::check_dir_permissions(&self.fs.fast_data_directory) {
507-
Ok(()) => println!(
508-
"Permissions are ok at {}",
509-
self.fs.fast_data_directory.display()
510-
),
511-
Err(e) => {
512-
eprintln_red(&format!("Error: {e}"));
513-
error = true;
514-
}
515-
}
516-
517-
match Self::check_dir_permissions(&self.fs.slow_data_directory) {
518-
Ok(()) => println!(
519-
"Permissions are ok at {}",
520-
self.fs.slow_data_directory.display()
521-
),
522-
Err(e) => {
523-
eprintln_red(&format!("Error: {e}"));
524-
error = true;
525-
}
526-
}
527-
528-
match Self::check_dir_permissions(&self.fs.cache_directory) {
529-
Ok(()) => println!(
530-
"Permissions are ok at {}",
531-
self.fs.cache_directory.display()
532-
),
533-
Err(e) => {
534-
eprintln_red(&format!("Error {e}"));
535-
error = true;
536-
}
537-
}
538-
539-
#[cfg(feature = "arti")]
540-
if matches!(self.tor.mode, TorMode::Arti | TorMode::Auto) {
541-
match Self::check_dir_permissions(&self.tor.arti.directory_path) {
542-
Ok(()) => println!(
543-
"Permissions are ok at {}",
544-
self.tor.arti.directory_path.display()
545-
),
523+
for path in self.writable_directories() {
524+
match Self::check_dir_permissions(path) {
525+
Ok(()) => println!("Permissions are ok at {}", path.display()),
546526
Err(e) => {
547527
eprintln_red(&format!("Error: {e}"));
548528
error = true;

binaries/cuprated/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ mod version;
5454

5555
fn main() {
5656
// Set global private permissions for created files.
57+
#[cfg(target_family = "unix")]
5758
cuprate_helper::fs::set_private_global_file_permissions();
5859

5960
// Initialize global static `LazyLock` data.

helper/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ serde = { workspace = true, optional = true, features = ["derive"] }
4343
# [thread] needs to activate one of these libs (windows|libc)
4444
# although it depends on what target we're building for.
4545
[target.'cfg(windows)'.dependencies]
46-
target_os_lib = { package = "windows", version = ">=0.51", features = ["Win32_System_Threading", "Win32_Foundation"], optional = true }
46+
target_os_lib = { package = "windows", version = ">=0.51", features = [
47+
"Win32_Foundation",
48+
"Win32_Security",
49+
"Win32_Security_Authorization",
50+
"Win32_Storage_FileSystem",
51+
"Win32_System_Threading",
52+
], optional = true }
4753
[target.'cfg(unix)'.dependencies]
4854
target_os_lib = { package = "libc", version = "0.2", optional = true }
4955

helper/src/fs.rs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -236,26 +236,27 @@ pub fn address_book_path(cache_dir: &Path, network: Network) -> PathBuf {
236236
// `rwxr-x---`
237237
//
238238
// # Windows
239-
// TODO: does nothing.
240-
#[cfg_attr(
241-
target_os = "windows",
242-
expect(
243-
clippy::missing_const_for_fn,
244-
reason = "remove when Windows is implemented"
245-
)
246-
)]
239+
// Not available. Permissions are set on a per-directory basis in `set_private_directory_permissions`.
240+
#[cfg(target_family = "unix")]
247241
pub fn set_private_global_file_permissions() {
248-
#[cfg(target_family = "unix")]
249242
// SAFETY: calling C.
250243
unsafe {
251244
target_os_lib::umask(0o027);
252245
}
246+
}
253247

254-
#[cfg(target_os = "windows")]
255-
// TODO: impl for Windows.
256-
{
257-
use target_os_lib as _;
258-
}
248+
// Apply private permissions to `roots`.
249+
//
250+
// # Windows
251+
// Restricts each newly-created directory to the current user, SYSTEM, and Administrators.
252+
//
253+
// # Unix
254+
// Not available. Permissions are set globally by `set_private_global_file_permissions`.
255+
#[cfg(target_os = "windows")]
256+
mod windows_perms;
257+
#[cfg(target_os = "windows")]
258+
pub fn set_private_directory_permissions(roots: &[&Path]) {
259+
windows_perms::apply(roots);
259260
}
260261

261262
//---------------------------------------------------------------------------------------------------- Tests

helper/src/fs/windows_perms.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
//! Windows ACL implementation for [`super::set_private_directory_permissions`].
2+
3+
//---------------------------------------------------------------------------------------------------- Use
4+
use std::ffi::OsStr;
5+
use std::os::windows::ffi::OsStrExt;
6+
use std::path::Path;
7+
use std::ptr;
8+
9+
use target_os_lib::core::{Error, Owned, Result, PCWSTR, PWSTR};
10+
use target_os_lib::Win32::Foundation::{
11+
ERROR_ALREADY_EXISTS, ERROR_INSUFFICIENT_BUFFER, E_UNEXPECTED, HANDLE, HLOCAL,
12+
};
13+
use target_os_lib::Win32::Security::Authorization::{
14+
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
15+
};
16+
use target_os_lib::Win32::Security::{
17+
GetTokenInformation, TokenUser, PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, TOKEN_ACCESS_MASK,
18+
TOKEN_QUERY, TOKEN_USER,
19+
};
20+
use target_os_lib::Win32::Storage::FileSystem::CreateDirectoryW;
21+
use target_os_lib::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
22+
23+
//---------------------------------------------------------------------------------------------------- SecurityDescriptor
24+
/// Security descriptor parsed from SDDL, with its `LocalAlloc` buffer freed on drop.
25+
struct SecurityDescriptor {
26+
psd: PSECURITY_DESCRIPTOR,
27+
_buf: Owned<HLOCAL>,
28+
}
29+
30+
impl SecurityDescriptor {
31+
fn from_sddl(sddl: &str) -> Result<Self> {
32+
let sddl_w = to_wide_nul(OsStr::new(sddl));
33+
let mut psd = PSECURITY_DESCRIPTOR::default();
34+
// SAFETY: `sddl_w` is owned and null-terminated; `psd` is a valid out-pointer.
35+
unsafe {
36+
ConvertStringSecurityDescriptorToSecurityDescriptorW(
37+
PCWSTR(sddl_w.as_ptr()),
38+
SDDL_REVISION_1,
39+
&raw mut psd,
40+
None,
41+
)
42+
}?;
43+
Ok(Self {
44+
psd,
45+
// SAFETY: `psd.0` is a `LocalAlloc` buffer per the function contract.
46+
_buf: unsafe { Owned::new(HLOCAL(psd.0)) },
47+
})
48+
}
49+
}
50+
51+
//---------------------------------------------------------------------------------------------------- Apply
52+
/// Apply a private ACL to each path in `roots`.
53+
///
54+
/// SYSTEM and Administrators are granted access alongside the user so
55+
/// Windows backup, antivirus, and indexer services keep working.
56+
pub(super) fn apply(roots: &[&Path]) {
57+
let user = match current_user_sid_string() {
58+
Ok(u) => u,
59+
Err(e) => {
60+
eprintln!("warning: could not retrieve user SID: {e}");
61+
return;
62+
}
63+
};
64+
let sddl = format!("O:{user}D:P(A;OICI;FA;;;{user})(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)");
65+
let sd = match SecurityDescriptor::from_sddl(&sddl) {
66+
Ok(sd) => sd,
67+
Err(e) => {
68+
eprintln!("warning: could not parse Windows security descriptor: {e}");
69+
return;
70+
}
71+
};
72+
73+
let sa = SECURITY_ATTRIBUTES {
74+
nLength: size_of::<SECURITY_ATTRIBUTES>() as u32,
75+
lpSecurityDescriptor: sd.psd.0,
76+
bInheritHandle: false.into(),
77+
};
78+
for root in roots {
79+
if let Err(e) = create_private_directory(root, &sa) {
80+
eprintln!(
81+
"warning: could not create private directory {}: {e}",
82+
root.display()
83+
);
84+
}
85+
}
86+
}
87+
88+
//---------------------------------------------------------------------------------------------------- Helpers
89+
fn current_user_sid_string() -> Result<String> {
90+
let token = open_process_token(TOKEN_QUERY)?;
91+
92+
let mut len: u32 = 0;
93+
// SAFETY: probe call with null buffer; `len` is a valid out-pointer.
94+
match unsafe { GetTokenInformation(*token, TokenUser, None, 0, &raw mut len) } {
95+
Err(e) if e.code() == ERROR_INSUFFICIENT_BUFFER.to_hresult() => {}
96+
Err(e) => return Err(e),
97+
Ok(()) => return Err(Error::from_hresult(E_UNEXPECTED)),
98+
}
99+
100+
// `u64` elements satisfy `TOKEN_USER`'s 8-byte alignment.
101+
let mut buf: Vec<u64> = vec![0; (len as usize).div_ceil(size_of::<u64>())];
102+
// SAFETY: `buf` is sized per the probe and aligned for `TOKEN_USER`.
103+
unsafe {
104+
GetTokenInformation(
105+
*token,
106+
TokenUser,
107+
Some(buf.as_mut_ptr().cast()),
108+
len,
109+
&raw mut len,
110+
)
111+
}?;
112+
113+
// SAFETY: `buf` was just populated as a `TOKEN_USER` by the call above.
114+
let token_user = unsafe { &*buf.as_ptr().cast::<TOKEN_USER>() };
115+
let mut sid_pwstr = PWSTR::null();
116+
// SAFETY: `token_user.User.Sid` is valid; out-pointer is owned.
117+
unsafe { ConvertSidToStringSidW(token_user.User.Sid, &raw mut sid_pwstr) }?;
118+
// SAFETY: `sid_pwstr.0` is a `LocalAlloc` buffer per `ConvertSidToStringSidW`.
119+
let _sid_guard = unsafe { Owned::new(HLOCAL(sid_pwstr.0.cast())) };
120+
121+
// SAFETY: `sid_pwstr` was just populated above. The `to_string` failure
122+
// case is unreachable for a Windows-emitted SID and maps to `E_UNEXPECTED`.
123+
unsafe { sid_pwstr.to_string() }.map_err(|_| Error::from_hresult(E_UNEXPECTED))
124+
}
125+
126+
fn create_private_directory(path: &Path, sa: &SECURITY_ATTRIBUTES) -> Result<()> {
127+
if path.is_dir() {
128+
return Ok(());
129+
}
130+
131+
if let Some(parent) = path.parent() {
132+
if !parent.as_os_str().is_empty() {
133+
create_private_directory(parent, sa)?;
134+
}
135+
}
136+
137+
let path_w = to_wide_nul(path.as_os_str());
138+
// SAFETY: `path_w` is owned; `sa` outlives this call.
139+
unsafe { CreateDirectoryW(PCWSTR(path_w.as_ptr()), Some(ptr::from_ref(sa))) }.or_else(|e| {
140+
if e.code() == ERROR_ALREADY_EXISTS.to_hresult() {
141+
Ok(())
142+
} else {
143+
Err(e)
144+
}
145+
})
146+
}
147+
148+
fn open_process_token(access: TOKEN_ACCESS_MASK) -> Result<Owned<HANDLE>> {
149+
let mut token = HANDLE::default();
150+
// SAFETY: `token` is a valid out-pointer; the returned handle is owned
151+
// by the resulting `Owned<HANDLE>`.
152+
unsafe { OpenProcessToken(GetCurrentProcess(), access, &raw mut token) }?;
153+
// SAFETY: `OpenProcessToken` returned Ok; `token` is now owned.
154+
Ok(unsafe { Owned::new(token) })
155+
}
156+
157+
fn to_wide_nul(s: &OsStr) -> Vec<u16> {
158+
s.encode_wide().chain(std::iter::once(0)).collect()
159+
}

0 commit comments

Comments
 (0)