Skip to content

Commit 2189462

Browse files
committed
Add ownership validation to cargo_util.
1 parent fb68ae4 commit 2189462

File tree

3 files changed

+323
-2
lines changed

3 files changed

+323
-2
lines changed

crates/cargo-util/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cargo-util"
3-
version = "0.1.3"
3+
version = "0.1.4"
44
edition = "2021"
55
license = "MIT OR Apache-2.0"
66
homepage = "https://github.com/rust-lang/cargo"
@@ -25,4 +25,4 @@ core-foundation = { version = "0.9.0", features = ["mac_os_10_7_support"] }
2525

2626
[target.'cfg(windows)'.dependencies]
2727
miow = "0.3.6"
28-
winapi = { version = "0.3.9", features = ["consoleapi", "minwindef"] }
28+
winapi = { version = "0.3.9", features = ["consoleapi", "minwindef", "sddl", "accctrl", "aclapi", "securitybaseapi"] }

crates/cargo-util/src/paths.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
//! Various utilities for working with files and paths.
22
3+
mod ownership;
4+
5+
pub use self::ownership::{validate_ownership, OwnershipError};
36
use anyhow::{Context, Result};
47
use filetime::FileTime;
58
use std::env;
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
use anyhow::Result;
2+
use std::collections::HashSet;
3+
use std::fmt;
4+
use std::path::{Path, PathBuf};
5+
6+
/// Checks the ownership of the given path matches the current user.
7+
///
8+
/// The `safe_directories` is a set of paths to allow, usually loaded from config.
9+
pub fn validate_ownership(path: &Path, safe_directories: &HashSet<PathBuf>) -> Result<()> {
10+
if safe_directories.get(Path::new("*")).is_some() {
11+
return Ok(());
12+
}
13+
for safe_dir in safe_directories {
14+
if path.starts_with(safe_dir) {
15+
return Ok(());
16+
}
17+
}
18+
_validate_ownership(path)
19+
}
20+
21+
#[cfg(unix)]
22+
fn _validate_ownership(path: &Path) -> Result<()> {
23+
use super::symlink_metadata;
24+
use std::env;
25+
use std::os::unix::fs::MetadataExt;
26+
let meta = symlink_metadata(path)?;
27+
let current_user = unsafe { libc::geteuid() };
28+
fn get_username(uid: u32) -> String {
29+
unsafe {
30+
let amt = match libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) {
31+
n if n < 0 => 512 as usize,
32+
n => n as usize,
33+
};
34+
let mut buf = Vec::with_capacity(amt);
35+
let mut passwd: libc::passwd = std::mem::zeroed();
36+
let mut result = std::ptr::null_mut();
37+
match libc::getpwuid_r(
38+
uid,
39+
&mut passwd,
40+
buf.as_mut_ptr(),
41+
buf.capacity(),
42+
&mut result,
43+
) {
44+
0 if !result.is_null() => {
45+
let ptr = passwd.pw_name as *const _;
46+
let bytes = std::ffi::CStr::from_ptr(ptr).to_bytes().to_vec();
47+
String::from_utf8_lossy(&bytes).into_owned()
48+
}
49+
_ => String::from("Unknown"),
50+
}
51+
}
52+
}
53+
// This is used for testing to simulate a failure.
54+
let simulate = match env::var_os("__CARGO_TEST_OWNERSHIP") {
55+
Some(p) if path == p => true,
56+
_ => false,
57+
};
58+
if current_user != meta.uid() || simulate {
59+
return Err(OwnershipError {
60+
owner: get_username(meta.uid()),
61+
current_user: get_username(current_user),
62+
path: path.to_owned(),
63+
}
64+
.into());
65+
}
66+
Ok(())
67+
}
68+
69+
#[cfg(windows)]
70+
fn _validate_ownership(path: &Path) -> Result<()> {
71+
unsafe { windows::_validate_ownership(path) }
72+
}
73+
74+
/// An error representing a file that is owned by a different user.
75+
#[allow(dead_code)] // Debug is required by std Error
76+
#[derive(Debug)]
77+
pub struct OwnershipError {
78+
pub owner: String,
79+
pub current_user: String,
80+
pub path: PathBuf,
81+
}
82+
83+
impl std::error::Error for OwnershipError {}
84+
85+
impl fmt::Display for OwnershipError {
86+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87+
write!(f, "{self:?}")
88+
}
89+
}
90+
91+
#[cfg(windows)]
92+
mod windows {
93+
use anyhow::{bail, Error, Result};
94+
use std::env;
95+
use std::ffi::OsString;
96+
use std::io;
97+
use std::os::windows::ffi::{OsStrExt, OsStringExt};
98+
use std::path::Path;
99+
use std::ptr::null_mut;
100+
use winapi::shared::minwindef::{DWORD, FALSE, HLOCAL, TRUE};
101+
use winapi::shared::sddl::ConvertSidToStringSidW;
102+
use winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER;
103+
use winapi::um::accctrl::SE_FILE_OBJECT;
104+
use winapi::um::aclapi::GetNamedSecurityInfoW;
105+
use winapi::um::errhandlingapi::GetLastError;
106+
use winapi::um::handleapi::CloseHandle;
107+
use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
108+
use winapi::um::securitybaseapi::{
109+
CheckTokenMembership, EqualSid, GetTokenInformation, IsValidSid, IsWellKnownSid,
110+
};
111+
use winapi::um::winbase::{LocalFree, LookupAccountSidW};
112+
use winapi::um::winnt::{
113+
TokenUser, WinBuiltinAdministratorsSid, DACL_SECURITY_INFORMATION, HANDLE,
114+
OWNER_SECURITY_INFORMATION, PSID, TOKEN_QUERY, TOKEN_USER,
115+
};
116+
117+
pub(super) unsafe fn _validate_ownership(path: &Path) -> Result<()> {
118+
let me = GetCurrentProcess();
119+
let mut token = null_mut();
120+
if OpenProcessToken(me, TOKEN_QUERY, &mut token) == 0 {
121+
return Err(
122+
Error::new(io::Error::last_os_error()).context("failed to get process token")
123+
);
124+
}
125+
let token = Handle { inner: token };
126+
let mut len: DWORD = 0;
127+
// Get the size of the token buffer.
128+
if GetTokenInformation(token.inner, TokenUser, null_mut(), 0, &mut len) != 0
129+
|| GetLastError() != ERROR_INSUFFICIENT_BUFFER
130+
{
131+
return Err(Error::new(io::Error::last_os_error())
132+
.context("failed to get token information size"));
133+
}
134+
// Get the SID of the current user.
135+
let mut token_info = Vec::<u8>::with_capacity(len as usize);
136+
if GetTokenInformation(
137+
token.inner,
138+
TokenUser,
139+
token_info.as_mut_ptr() as *mut _,
140+
len,
141+
&mut len,
142+
) == 0
143+
{
144+
return Err(
145+
Error::new(io::Error::last_os_error()).context("failed to get token information")
146+
);
147+
}
148+
let token_user = token_info.as_ptr() as *const TOKEN_USER;
149+
let user_sid = (*token_user).User.Sid;
150+
151+
// Get the SID of the owner of the path.
152+
let path_w = wide_path(path);
153+
let mut owner_sid = null_mut();
154+
let mut descriptor = LocalFreeWrapper { inner: null_mut() };
155+
let result = GetNamedSecurityInfoW(
156+
path_w.as_ptr(),
157+
SE_FILE_OBJECT,
158+
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
159+
&mut owner_sid,
160+
null_mut(),
161+
null_mut(),
162+
null_mut(),
163+
&mut descriptor.inner,
164+
);
165+
if result != 0 {
166+
let io_err = io::Error::from_raw_os_error(result as i32);
167+
return Err(Error::new(io_err).context(format!(
168+
"failed to get security descriptor for path {}",
169+
path.display()
170+
)));
171+
}
172+
if IsValidSid(owner_sid) == 0 {
173+
bail!(
174+
"unexpected invalid file owner sid for path {}",
175+
path.display()
176+
);
177+
}
178+
let simulate = match env::var_os("__CARGO_TEST_OWNERSHIP") {
179+
Some(p) if path == p => true,
180+
_ => false,
181+
};
182+
if !simulate && EqualSid(user_sid, owner_sid) != 0 {
183+
return Ok(());
184+
}
185+
// Allow paths that are owned by the Administrators Group if the user is
186+
// also a member of the group. This is added for convenience. Files
187+
// created when run with "Run as Administrator" are owned by the group.
188+
if !simulate && IsWellKnownSid(owner_sid, WinBuiltinAdministratorsSid) == TRUE {
189+
let mut is_member = FALSE;
190+
if CheckTokenMembership(null_mut(), user_sid, &mut is_member) == 0 {
191+
log::info!(
192+
"failed to check if member of administrators: {}",
193+
io::Error::last_os_error()
194+
);
195+
// Fall through
196+
} else {
197+
if is_member == TRUE {
198+
return Ok(());
199+
}
200+
}
201+
}
202+
203+
let owner = sid_to_name(owner_sid).unwrap_or_else(|| sid_to_string(owner_sid));
204+
let current_user = sid_to_name(user_sid).unwrap_or_else(|| sid_to_string(user_sid));
205+
return Err(super::OwnershipError {
206+
owner,
207+
current_user,
208+
path: path.to_owned(),
209+
}
210+
.into());
211+
}
212+
213+
unsafe fn sid_to_string(sid: PSID) -> String {
214+
let mut s_ptr = null_mut();
215+
if ConvertSidToStringSidW(sid, &mut s_ptr) == 0 {
216+
log::info!(
217+
"failed to convert sid to string: {}",
218+
io::Error::last_os_error()
219+
);
220+
return "Unknown".to_string();
221+
}
222+
let len = (0..).take_while(|&i| *s_ptr.offset(i) != 0).count();
223+
let slice: &[u16] = std::slice::from_raw_parts(s_ptr, len);
224+
let s = OsString::from_wide(slice);
225+
LocalFree(s_ptr as *mut _);
226+
s.into_string().unwrap_or_else(|_| "Unknown".to_string())
227+
}
228+
229+
unsafe fn sid_to_name(sid: PSID) -> Option<String> {
230+
// Note: This operation may be very expensive and slow.
231+
let mut name_size = 0;
232+
let mut domain_size = 0;
233+
let mut pe_use = 0;
234+
// Get the length of the name.
235+
if LookupAccountSidW(
236+
null_mut(), // lpSystemName (where to search)
237+
sid,
238+
null_mut(), // Name
239+
&mut name_size,
240+
null_mut(), // ReferencedDomainName
241+
&mut domain_size,
242+
&mut pe_use,
243+
) != 0
244+
|| GetLastError() != ERROR_INSUFFICIENT_BUFFER
245+
{
246+
log::debug!(
247+
"failed to determine sid name length: {}",
248+
io::Error::last_os_error()
249+
);
250+
return None;
251+
}
252+
let mut name: Vec<u16> = vec![0; name_size as usize];
253+
let mut domain: Vec<u16> = vec![0; domain_size as usize];
254+
if LookupAccountSidW(
255+
null_mut(),
256+
sid,
257+
name.as_mut_ptr(),
258+
&mut name_size,
259+
domain.as_mut_ptr(),
260+
&mut domain_size,
261+
&mut pe_use,
262+
) == 0
263+
{
264+
log::debug!(
265+
"failed to fetch name ({}): {}",
266+
name_size,
267+
io::Error::last_os_error()
268+
);
269+
return None;
270+
}
271+
let name = str_from_wide(&name);
272+
let domain = str_from_wide(&domain);
273+
274+
return Some(format!("{domain}\\{name}"));
275+
}
276+
277+
struct Handle {
278+
inner: HANDLE,
279+
}
280+
impl Drop for Handle {
281+
fn drop(&mut self) {
282+
unsafe {
283+
CloseHandle(self.inner);
284+
}
285+
}
286+
}
287+
288+
struct LocalFreeWrapper {
289+
inner: HLOCAL,
290+
}
291+
impl Drop for LocalFreeWrapper {
292+
fn drop(&mut self) {
293+
unsafe {
294+
if !self.inner.is_null() {
295+
LocalFree(self.inner);
296+
self.inner = null_mut();
297+
}
298+
}
299+
}
300+
}
301+
302+
fn str_from_wide(wide: &[u16]) -> String {
303+
let len = wide.iter().position(|i| *i == 0).unwrap_or(wide.len());
304+
let os_str = OsString::from_wide(&wide[..len]);
305+
os_str
306+
.into_string()
307+
.unwrap_or_else(|_| "Invalid UTF-8".to_string())
308+
}
309+
310+
fn wide_path(path: &Path) -> Vec<u16> {
311+
let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
312+
if wide.iter().any(|b| *b == 0) {
313+
panic!("nul byte in wide string");
314+
}
315+
wide.push(0);
316+
wide
317+
}
318+
}

0 commit comments

Comments
 (0)