Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion src/uu/test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ clap = { workspace = true }
fluent = { workspace = true }
libc = { workspace = true }
thiserror = { workspace = true }
uucore = { workspace = true, features = ["process"] }
uucore = { workspace = true, features = ["process", "wide"] }

[target.'cfg(windows)'.dependencies]
windows-sys = { workspace = true, features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Security_Authorization",
"Win32_System_Threading",
] }

[dev-dependencies]
tempfile = { workspace = true }
Expand Down
10 changes: 10 additions & 0 deletions src/uu/test/src/platform/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

#[cfg(windows)]
pub use self::windows::owned_by_current_token;

#[cfg(windows)]
mod windows;
124 changes: 124 additions & 0 deletions src/uu/test/src/platform/windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore (vars) PSECURITY PSID

use std::ffi::OsStr;
use std::ptr;
use uucore::wide::ToWide;
use windows_sys::Win32::Foundation::{CloseHandle, ERROR_SUCCESS, HANDLE, LocalFree};
use windows_sys::Win32::Security::Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT};
use windows_sys::Win32::Security::{
EqualSid, GROUP_SECURITY_INFORMATION, GetTokenInformation, OWNER_SECURITY_INFORMATION,
PSECURITY_DESCRIPTOR, PSID, TOKEN_QUERY, TokenOwner, TokenPrimaryGroup,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};

/// A security descriptor allocated by the security API, freed on drop.
struct Descriptor(PSECURITY_DESCRIPTOR);

impl Drop for Descriptor {
fn drop(&mut self) {
// SAFETY: the pointer comes from GetNamedSecurityInfoW; freeing a null
// pointer is a no-op.
unsafe { LocalFree(self.0) };
}
}

/// A handle on the token of the current process, closed on drop.
struct Token(HANDLE);

impl Drop for Token {
fn drop(&mut self) {
// SAFETY: the handle comes from a successful OpenProcessToken.
unsafe { CloseHandle(self.0) };
}
}

/// Whether `sid` is the SID the current process token stamps onto the objects it
/// creates: its owner or, with `group`, its primary group.
///
/// Those two are the counterparts of the effective UID and GID. They are not
/// always the token user: a process running elevated has the Administrators
/// group as its token owner, and that is what lands in the descriptor of a file
/// it creates, so comparing against the token user would report that you do not
/// own a file you just created. Group membership is not a substitute — it would
/// also match every group you happen to belong to, such as `Everyone`, and
/// report ownership of files that are not yours.
fn matches_token_sid(sid: PSID, group: bool) -> bool {
let mut handle: HANDLE = ptr::null_mut();
// SAFETY: GetCurrentProcess returns a pseudo handle that needs no closing,
// and `handle` is a valid out pointer.
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut handle) } == 0 {
return false;
}
let token = Token(handle);
let class = if group { TokenPrimaryGroup } else { TokenOwner };

let mut size = 0;
// SAFETY: a null buffer of length zero only asks for the size to allocate.
unsafe { GetTokenInformation(token.0, class, ptr::null_mut(), 0, &raw mut size) };
if size == 0 {
return false;
}

// TOKEN_OWNER and TOKEN_PRIMARY_GROUP are both a lone SID pointer followed
// by the SID it points at, so the buffer has to be pointer aligned.
let mut buffer = vec![0usize; (size as usize).div_ceil(size_of::<usize>())];
// SAFETY: `buffer` is at least `size` bytes long, as reported above.
let ok = unsafe {
GetTokenInformation(
token.0,
class,
buffer.as_mut_ptr().cast(),
size,
&raw mut size,
)
};
if ok == 0 {
return false;
}

// SAFETY: the call above wrote one of those two structures into `buffer`,
// and the SID pointer is its first field.
let token_sid = unsafe { *buffer.as_ptr().cast::<PSID>() };

// SAFETY: `sid` is valid while its descriptor lives, `token_sid` while
// `buffer` does.
!token_sid.is_null() && unsafe { EqualSid(sid, token_sid) } != 0
}

/// Whether `path` is owned by the current process token, comparing its owner
/// (or, with `group`, its primary group) SID — the Windows analogue of matching
/// `st_uid`/`st_gid` against the effective UID/GID for `-O` and `-G`.
pub fn owned_by_current_token(path: &OsStr, group: bool) -> bool {
let wide_path = path.to_wide_null();
let mut sid: PSID = ptr::null_mut();
// Owns the memory `sid` points into, so it has to live until the comparison
// below is done.
let mut descriptor = Descriptor(ptr::null_mut());
let (info, owner_out, group_out) = if group {
(GROUP_SECURITY_INFORMATION, ptr::null_mut(), &raw mut sid)
} else {
(OWNER_SECURITY_INFORMATION, &raw mut sid, ptr::null_mut())
};

// SAFETY: `wide_path` is NUL-terminated and outlives the call, and the out
// pointers are valid. On success `sid` points into the descriptor.
let status = unsafe {
GetNamedSecurityInfoW(
wide_path.as_ptr(),
SE_FILE_OBJECT,
info,
owner_out,
group_out,
ptr::null_mut(),
ptr::null_mut(),
&raw mut descriptor.0,
)
};

status == ERROR_SUCCESS && !sid.is_null() && matches_token_sid(sid, group)
}
23 changes: 17 additions & 6 deletions src/uu/test/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

pub(crate) mod error;
mod parser;
#[cfg(windows)]
mod platform;

use clap::Command;
use error::{ParseError, ParseResult};
Expand Down Expand Up @@ -253,6 +255,16 @@ enum PathCondition {
Executable,
}

/// Whether the file was modified more recently than it was last read, the
/// condition behind `-N`. A timestamp the platform cannot report counts as
/// "not modified since read" rather than aborting.
fn modified_since_read(metadata: &fs::Metadata) -> bool {
matches!(
(metadata.accessed(), metadata.modified()),
(Ok(read), Ok(modified)) if read < modified
)
}

#[cfg(not(windows))]
fn path(path: &OsStr, condition: &PathCondition) -> bool {
use std::fs::Metadata;
Expand Down Expand Up @@ -295,9 +307,7 @@ fn path(path: &OsStr, condition: &PathCondition) -> bool {
PathCondition::CharacterSpecial => file_type.is_char_device(),
PathCondition::Directory => file_type.is_dir(),
PathCondition::Exists => true,
PathCondition::ExistsModifiedLastRead => {
metadata.accessed().unwrap() < metadata.modified().unwrap()
}
PathCondition::ExistsModifiedLastRead => modified_since_read(&metadata),
PathCondition::Regular => file_type.is_file(),
PathCondition::GroupIdFlag => metadata.mode() & S_ISGID != 0,
PathCondition::GroupOwns => metadata.gid() == getegid(),
Expand All @@ -316,6 +326,7 @@ fn path(path: &OsStr, condition: &PathCondition) -> bool {

#[cfg(windows)]
fn path(path: &OsStr, condition: &PathCondition) -> bool {
use crate::platform::owned_by_current_token;
use std::fs::metadata;

let Ok(stat) = metadata(path) else {
Expand All @@ -325,9 +336,9 @@ fn path(path: &OsStr, condition: &PathCondition) -> bool {
match condition {
PathCondition::Directory => stat.is_dir(),
PathCondition::Exists | PathCondition::Readable => true,
PathCondition::ExistsModifiedLastRead
| PathCondition::GroupOwns
| PathCondition::UserOwns => unimplemented!(),
PathCondition::ExistsModifiedLastRead => modified_since_read(&stat),
PathCondition::GroupOwns => owned_by_current_token(path, true),
PathCondition::UserOwns => owned_by_current_token(path, false),
PathCondition::Regular => stat.is_file(),
PathCondition::NonEmpty => stat.len() > 0,
PathCondition::Writable => !stat.permissions().readonly(),
Expand Down
24 changes: 20 additions & 4 deletions tests/by-util/test_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -703,13 +703,11 @@ fn test_parenthesized_right_parenthesis_as_literal() {
}

#[test]
#[cfg(not(windows))]
fn test_file_owned_by_euid() {
new_ucmd!().args(&["-O", "regular_file"]).succeeds();
}

#[test]
#[cfg(not(windows))]
fn test_nonexistent_file_not_owned_by_euid() {
new_ucmd!()
.args(&["-O", "nonexistent_file"])
Expand Down Expand Up @@ -749,7 +747,6 @@ fn test_file_owned_by_egid() {
}

#[test]
#[cfg(not(windows))]
fn test_nonexistent_file_not_owned_by_egid() {
new_ucmd!()
.args(&["-G", "nonexistent_file"])
Expand All @@ -773,6 +770,24 @@ fn test_file_not_owned_by_egid() {
.succeeds();
}

#[test]
#[cfg(windows)]
fn test_file_owned_by_current_group_windows() {
new_ucmd!().args(&["-G", "regular_file"]).succeeds();
}

#[test]
#[cfg(windows)]
fn test_file_not_owned_by_current_token_windows() {
// The system directory belongs to TrustedInstaller, so it is owned neither
// by the user nor by the Administrators group an elevated shell runs as.
let system_root = std::env::var("SystemRoot").expect("SystemRoot is not set");

new_ucmd!()
.args(&["-d", &system_root, "-a", "!", "-O", &system_root])
.succeeds();
}

#[test]
fn test_op_precedence_and_or_1() {
new_ucmd!().args(&[" ", "-o", "", "-a", ""]).succeeds();
Expand Down Expand Up @@ -964,7 +979,6 @@ fn test_bracket_syntax_version() {

#[test]
#[allow(non_snake_case)]
#[cfg(unix)]
fn test_file_N() {
use std::{fs::FileTimes, time::Duration};

Expand All @@ -980,6 +994,7 @@ fn test_file_N() {
.set_modified(std::time::UNIX_EPOCH);
f.set_times(times).unwrap();
// TODO: stat call for debugging #7570, remove?
#[cfg(unix)]
println!("{}", scene.cmd_shell("stat file").succeeds().stdout_str());
scene.ucmd().args(&["-N", "file"]).fails();

Expand All @@ -990,6 +1005,7 @@ fn test_file_N() {
.set_modified(std::time::UNIX_EPOCH + Duration::from_secs(123));
f.set_times(times).unwrap();
// TODO: stat call for debugging #7570, remove?
#[cfg(unix)]
println!("{}", scene.cmd_shell("stat file").succeeds().stdout_str());
scene.ucmd().args(&["-N", "file"]).succeeds();
}
Expand Down
Loading