mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Summary - add elevated-only token constructors that include the current token user SID in the restricted SID list - switch the elevated Windows command runner to use those constructors - leave the unelevated restricted-token path unchanged ## Why Windows named pipes created by tools like Ninja use the platform's default named-pipe ACL when no explicit security descriptor is provided. In the elevated sandbox, the pipe owner has access, but the write-restricted token can still fail its restricted-SID access check because the sandbox user SID was not in the restricting SID set. That causes child processes to exit successfully while Ninja never receives the expected pipe completion/close behavior and hangs. Including the elevated sandbox user's SID in the restricting SID list lets the restricted check succeed for these owner-scoped pipe objects without broadening the unelevated sandbox to the real signed-in user. ## Impact - fixes the minimal Ninja hang repro in the elevated Windows sandbox - preserves the existing unelevated sandbox behavior and write protections - keeps the change scoped to the elevated runner rather than changing shared token semantics - this does not affect file-writes for the sandbox because the sandbox users themselves do not receive any additional permissions over what the capability SIDs already have. In fact we don't even explicitly grant the sandbox user ACLs anywhere. ## Validation - `cargo build -p codex-windows-sandbox --quiet` - verified the stock `ninja.exe` minimal repro exits normally on host and in the elevated sandbox - verified the same repro still hangs in the unelevated sandbox, which is the intended scope of this change
458 lines
16 KiB
Rust
458 lines
16 KiB
Rust
use crate::winutil::to_wide;
|
|
use anyhow::anyhow;
|
|
use anyhow::Result;
|
|
use std::ffi::c_void;
|
|
use windows_sys::Win32::Foundation::CloseHandle;
|
|
use windows_sys::Win32::Foundation::GetLastError;
|
|
use windows_sys::Win32::Foundation::LocalFree;
|
|
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
|
|
use windows_sys::Win32::Foundation::HANDLE;
|
|
use windows_sys::Win32::Foundation::HLOCAL;
|
|
use windows_sys::Win32::Foundation::LUID;
|
|
use windows_sys::Win32::Security::AdjustTokenPrivileges;
|
|
use windows_sys::Win32::Security::Authorization::SetEntriesInAclW;
|
|
use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W;
|
|
use windows_sys::Win32::Security::Authorization::GRANT_ACCESS;
|
|
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_SID;
|
|
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_UNKNOWN;
|
|
use windows_sys::Win32::Security::Authorization::TRUSTEE_W;
|
|
use windows_sys::Win32::Security::CopySid;
|
|
use windows_sys::Win32::Security::CreateRestrictedToken;
|
|
use windows_sys::Win32::Security::CreateWellKnownSid;
|
|
use windows_sys::Win32::Security::GetLengthSid;
|
|
use windows_sys::Win32::Security::GetTokenInformation;
|
|
use windows_sys::Win32::Security::LookupPrivilegeValueW;
|
|
use windows_sys::Win32::Security::SetTokenInformation;
|
|
|
|
use windows_sys::Win32::Security::TokenDefaultDacl;
|
|
use windows_sys::Win32::Security::TokenGroups;
|
|
use windows_sys::Win32::Security::TokenUser;
|
|
use windows_sys::Win32::Security::ACL;
|
|
use windows_sys::Win32::Security::SID_AND_ATTRIBUTES;
|
|
use windows_sys::Win32::Security::TOKEN_ADJUST_DEFAULT;
|
|
use windows_sys::Win32::Security::TOKEN_ADJUST_PRIVILEGES;
|
|
use windows_sys::Win32::Security::TOKEN_ADJUST_SESSIONID;
|
|
use windows_sys::Win32::Security::TOKEN_ASSIGN_PRIMARY;
|
|
use windows_sys::Win32::Security::TOKEN_DUPLICATE;
|
|
use windows_sys::Win32::Security::TOKEN_PRIVILEGES;
|
|
use windows_sys::Win32::Security::TOKEN_QUERY;
|
|
use windows_sys::Win32::Security::TOKEN_USER;
|
|
use windows_sys::Win32::System::Threading::GetCurrentProcess;
|
|
|
|
const DISABLE_MAX_PRIVILEGE: u32 = 0x01;
|
|
const LUA_TOKEN: u32 = 0x04;
|
|
const WRITE_RESTRICTED: u32 = 0x08;
|
|
const GENERIC_ALL: u32 = 0x1000_0000;
|
|
const WIN_WORLD_SID: i32 = 1;
|
|
const SE_GROUP_LOGON_ID: u32 = 0xC0000000;
|
|
|
|
#[repr(C)]
|
|
struct TokenDefaultDaclInfo {
|
|
default_dacl: *mut ACL,
|
|
}
|
|
|
|
/// Sets a permissive default DACL so sandboxed processes can create pipes/IPC objects
|
|
/// without hitting ACCESS_DENIED when PowerShell builds pipelines.
|
|
unsafe fn set_default_dacl(h_token: HANDLE, sids: &[*mut c_void]) -> Result<()> {
|
|
if sids.is_empty() {
|
|
return Ok(());
|
|
}
|
|
let entries: Vec<EXPLICIT_ACCESS_W> = sids
|
|
.iter()
|
|
.map(|sid| EXPLICIT_ACCESS_W {
|
|
grfAccessPermissions: GENERIC_ALL,
|
|
grfAccessMode: GRANT_ACCESS,
|
|
grfInheritance: 0,
|
|
Trustee: TRUSTEE_W {
|
|
pMultipleTrustee: std::ptr::null_mut(),
|
|
MultipleTrusteeOperation: 0,
|
|
TrusteeForm: TRUSTEE_IS_SID,
|
|
TrusteeType: TRUSTEE_IS_UNKNOWN,
|
|
ptstrName: *sid as *mut u16,
|
|
},
|
|
})
|
|
.collect();
|
|
let mut p_new_dacl: *mut ACL = std::ptr::null_mut();
|
|
let res = SetEntriesInAclW(
|
|
entries.len() as u32,
|
|
entries.as_ptr(),
|
|
std::ptr::null_mut(),
|
|
&mut p_new_dacl,
|
|
);
|
|
if res != ERROR_SUCCESS {
|
|
return Err(anyhow!("SetEntriesInAclW failed: {res}"));
|
|
}
|
|
let mut info = TokenDefaultDaclInfo {
|
|
default_dacl: p_new_dacl,
|
|
};
|
|
let ok = SetTokenInformation(
|
|
h_token,
|
|
TokenDefaultDacl,
|
|
&mut info as *mut _ as *mut c_void,
|
|
std::mem::size_of::<TokenDefaultDaclInfo>() as u32,
|
|
);
|
|
if ok == 0 {
|
|
let err = GetLastError();
|
|
if !p_new_dacl.is_null() {
|
|
LocalFree(p_new_dacl as HLOCAL);
|
|
}
|
|
return Err(anyhow!(
|
|
"SetTokenInformation(TokenDefaultDacl) failed: {err}",
|
|
));
|
|
}
|
|
if !p_new_dacl.is_null() {
|
|
LocalFree(p_new_dacl as HLOCAL);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub unsafe fn world_sid() -> Result<Vec<u8>> {
|
|
let mut size: u32 = 0;
|
|
CreateWellKnownSid(
|
|
WIN_WORLD_SID,
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
&mut size,
|
|
);
|
|
let mut buf: Vec<u8> = vec![0u8; size as usize];
|
|
let ok = CreateWellKnownSid(
|
|
WIN_WORLD_SID,
|
|
std::ptr::null_mut(),
|
|
buf.as_mut_ptr() as *mut c_void,
|
|
&mut size,
|
|
);
|
|
if ok == 0 {
|
|
return Err(anyhow!("CreateWellKnownSid failed: {}", GetLastError()));
|
|
}
|
|
Ok(buf)
|
|
}
|
|
|
|
/// # Safety
|
|
/// Caller is responsible for freeing the returned SID with `LocalFree`.
|
|
pub unsafe fn convert_string_sid_to_sid(s: &str) -> Option<*mut c_void> {
|
|
#[link(name = "advapi32")]
|
|
unsafe extern "system" {
|
|
fn ConvertStringSidToSidW(StringSid: *const u16, Sid: *mut *mut c_void) -> i32;
|
|
}
|
|
let mut psid: *mut c_void = std::ptr::null_mut();
|
|
let ok = unsafe { ConvertStringSidToSidW(to_wide(s).as_ptr(), &mut psid) };
|
|
if ok != 0 {
|
|
Some(psid)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// # Safety
|
|
/// Caller must close the returned token handle.
|
|
pub unsafe fn get_current_token_for_restriction() -> Result<HANDLE> {
|
|
let desired = TOKEN_DUPLICATE
|
|
| TOKEN_QUERY
|
|
| TOKEN_ASSIGN_PRIMARY
|
|
| TOKEN_ADJUST_DEFAULT
|
|
| TOKEN_ADJUST_SESSIONID
|
|
| TOKEN_ADJUST_PRIVILEGES;
|
|
let mut h: HANDLE = 0;
|
|
#[link(name = "advapi32")]
|
|
unsafe extern "system" {
|
|
fn OpenProcessToken(
|
|
ProcessHandle: HANDLE,
|
|
DesiredAccess: u32,
|
|
TokenHandle: *mut HANDLE,
|
|
) -> i32;
|
|
}
|
|
let ok = unsafe { OpenProcessToken(GetCurrentProcess(), desired, &mut h) };
|
|
if ok == 0 {
|
|
return Err(anyhow!("OpenProcessToken failed: {}", GetLastError()));
|
|
}
|
|
Ok(h)
|
|
}
|
|
|
|
pub unsafe fn get_logon_sid_bytes(h_token: HANDLE) -> Result<Vec<u8>> {
|
|
unsafe fn scan_token_groups_for_logon(h: HANDLE) -> Option<Vec<u8>> {
|
|
let mut needed: u32 = 0;
|
|
GetTokenInformation(h, TokenGroups, std::ptr::null_mut(), 0, &mut needed);
|
|
if needed == 0 {
|
|
return None;
|
|
}
|
|
let mut buf: Vec<u8> = vec![0u8; needed as usize];
|
|
let ok = GetTokenInformation(
|
|
h,
|
|
TokenGroups,
|
|
buf.as_mut_ptr() as *mut c_void,
|
|
needed,
|
|
&mut needed,
|
|
);
|
|
if ok == 0 || (needed as usize) < std::mem::size_of::<u32>() {
|
|
return None;
|
|
}
|
|
let group_count = std::ptr::read_unaligned(buf.as_ptr() as *const u32) as usize;
|
|
// TOKEN_GROUPS layout is: DWORD GroupCount; SID_AND_ATTRIBUTES Groups[];
|
|
// On 64-bit, Groups is aligned to pointer alignment after 4-byte GroupCount.
|
|
let after_count = unsafe { buf.as_ptr().add(std::mem::size_of::<u32>()) } as usize;
|
|
let align = std::mem::align_of::<SID_AND_ATTRIBUTES>();
|
|
let aligned = (after_count + (align - 1)) & !(align - 1);
|
|
let groups_ptr = aligned as *const SID_AND_ATTRIBUTES;
|
|
for i in 0..group_count {
|
|
let entry: SID_AND_ATTRIBUTES = std::ptr::read_unaligned(groups_ptr.add(i));
|
|
if (entry.Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID {
|
|
let sid = entry.Sid;
|
|
let sid_len = GetLengthSid(sid);
|
|
if sid_len == 0 {
|
|
return None;
|
|
}
|
|
let mut out = vec![0u8; sid_len as usize];
|
|
if CopySid(sid_len, out.as_mut_ptr() as *mut c_void, sid) == 0 {
|
|
return None;
|
|
}
|
|
return Some(out);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
if let Some(v) = scan_token_groups_for_logon(h_token) {
|
|
return Ok(v);
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct TOKEN_LINKED_TOKEN {
|
|
linked_token: HANDLE,
|
|
}
|
|
const TOKEN_LINKED_TOKEN_CLASS: i32 = 19; // TokenLinkedToken
|
|
let mut ln_needed: u32 = 0;
|
|
GetTokenInformation(
|
|
h_token,
|
|
TOKEN_LINKED_TOKEN_CLASS,
|
|
std::ptr::null_mut(),
|
|
0,
|
|
&mut ln_needed,
|
|
);
|
|
if ln_needed >= std::mem::size_of::<TOKEN_LINKED_TOKEN>() as u32 {
|
|
let mut ln_buf: Vec<u8> = vec![0u8; ln_needed as usize];
|
|
let ok = GetTokenInformation(
|
|
h_token,
|
|
TOKEN_LINKED_TOKEN_CLASS,
|
|
ln_buf.as_mut_ptr() as *mut c_void,
|
|
ln_needed,
|
|
&mut ln_needed,
|
|
);
|
|
if ok != 0 {
|
|
let lt: TOKEN_LINKED_TOKEN =
|
|
std::ptr::read_unaligned(ln_buf.as_ptr() as *const TOKEN_LINKED_TOKEN);
|
|
if lt.linked_token != 0 {
|
|
let res = scan_token_groups_for_logon(lt.linked_token);
|
|
CloseHandle(lt.linked_token);
|
|
if let Some(v) = res {
|
|
return Ok(v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(anyhow!("Logon SID not present on token"))
|
|
}
|
|
|
|
unsafe fn get_user_sid_bytes(h_token: HANDLE) -> Result<Vec<u8>> {
|
|
let mut needed: u32 = 0;
|
|
GetTokenInformation(h_token, TokenUser, std::ptr::null_mut(), 0, &mut needed);
|
|
if needed == 0 {
|
|
return Err(anyhow!("TokenUser size query returned 0"));
|
|
}
|
|
let mut user_buf: Vec<u8> = vec![0u8; needed as usize];
|
|
let ok = GetTokenInformation(
|
|
h_token,
|
|
TokenUser,
|
|
user_buf.as_mut_ptr() as *mut c_void,
|
|
needed,
|
|
&mut needed,
|
|
);
|
|
if ok == 0 || (needed as usize) < std::mem::size_of::<TOKEN_USER>() {
|
|
return Err(anyhow!(
|
|
"GetTokenInformation(TokenUser) failed: {}",
|
|
GetLastError()
|
|
));
|
|
}
|
|
let token_user: TOKEN_USER = std::ptr::read_unaligned(user_buf.as_ptr() as *const TOKEN_USER);
|
|
let sid_len = GetLengthSid(token_user.User.Sid);
|
|
if sid_len == 0 {
|
|
return Err(anyhow!("GetLengthSid(TokenUser) failed: {}", GetLastError()));
|
|
}
|
|
let mut user_sid_bytes = vec![0u8; sid_len as usize];
|
|
if CopySid(
|
|
sid_len,
|
|
user_sid_bytes.as_mut_ptr() as *mut c_void,
|
|
token_user.User.Sid,
|
|
) == 0
|
|
{
|
|
return Err(anyhow!("CopySid(TokenUser) failed: {}", GetLastError()));
|
|
}
|
|
Ok(user_sid_bytes)
|
|
}
|
|
|
|
unsafe fn enable_single_privilege(h_token: HANDLE, name: &str) -> Result<()> {
|
|
let mut luid = LUID {
|
|
LowPart: 0,
|
|
HighPart: 0,
|
|
};
|
|
let ok = LookupPrivilegeValueW(std::ptr::null(), to_wide(name).as_ptr(), &mut luid);
|
|
if ok == 0 {
|
|
return Err(anyhow!("LookupPrivilegeValueW failed: {}", GetLastError()));
|
|
}
|
|
let mut tp: TOKEN_PRIVILEGES = std::mem::zeroed();
|
|
tp.PrivilegeCount = 1;
|
|
tp.Privileges[0].Luid = luid;
|
|
tp.Privileges[0].Attributes = 0x00000002; // SE_PRIVILEGE_ENABLED
|
|
let ok2 = AdjustTokenPrivileges(
|
|
h_token,
|
|
0,
|
|
&tp,
|
|
0,
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
);
|
|
if ok2 == 0 {
|
|
return Err(anyhow!("AdjustTokenPrivileges failed: {}", GetLastError()));
|
|
}
|
|
let err = GetLastError();
|
|
if err != 0 {
|
|
return Err(anyhow!("AdjustTokenPrivileges error {err}"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// # Safety
|
|
/// Caller must close the returned token handle.
|
|
pub unsafe fn create_readonly_token_with_cap(
|
|
psid_capability: *mut c_void,
|
|
) -> Result<(HANDLE, *mut c_void)> {
|
|
let base = get_current_token_for_restriction()?;
|
|
let res = create_readonly_token_with_cap_from(base, psid_capability);
|
|
CloseHandle(base);
|
|
res
|
|
}
|
|
|
|
/// # Safety
|
|
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
|
/// # Safety
|
|
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
|
pub unsafe fn create_readonly_token_with_cap_from(
|
|
base_token: HANDLE,
|
|
psid_capability: *mut c_void,
|
|
) -> Result<(HANDLE, *mut c_void)> {
|
|
let new_token = create_token_with_caps_from(base_token, &[psid_capability], &[])?;
|
|
Ok((new_token, psid_capability))
|
|
}
|
|
|
|
/// Create a restricted token that includes all provided capability SIDs.
|
|
///
|
|
/// # Safety
|
|
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
|
pub unsafe fn create_workspace_write_token_with_caps_from(
|
|
base_token: HANDLE,
|
|
psid_capabilities: &[*mut c_void],
|
|
) -> Result<HANDLE> {
|
|
create_token_with_caps_from(base_token, psid_capabilities, &[])
|
|
}
|
|
|
|
/// Create a restricted token that includes all provided capability SIDs plus the token user SID.
|
|
///
|
|
/// This is intended for the elevated sandbox backend, where the token user is the dedicated
|
|
/// sandbox account rather than the real signed-in user.
|
|
///
|
|
/// # Safety
|
|
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
|
pub unsafe fn create_workspace_write_token_with_caps_and_user_from(
|
|
base_token: HANDLE,
|
|
psid_capabilities: &[*mut c_void],
|
|
) -> Result<HANDLE> {
|
|
let mut user_sid_bytes = get_user_sid_bytes(base_token)?;
|
|
let psid_user = user_sid_bytes.as_mut_ptr() as *mut c_void;
|
|
create_token_with_caps_from(base_token, psid_capabilities, &[psid_user])
|
|
}
|
|
|
|
/// Create a restricted token that includes all provided capability SIDs.
|
|
///
|
|
/// # Safety
|
|
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
|
pub unsafe fn create_readonly_token_with_caps_from(
|
|
base_token: HANDLE,
|
|
psid_capabilities: &[*mut c_void],
|
|
) -> Result<HANDLE> {
|
|
create_token_with_caps_from(base_token, psid_capabilities, &[])
|
|
}
|
|
|
|
/// Create a restricted token that includes all provided capability SIDs plus the token user SID.
|
|
///
|
|
/// This is intended for the elevated sandbox backend, where the token user is the dedicated
|
|
/// sandbox account rather than the real signed-in user.
|
|
///
|
|
/// # Safety
|
|
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
|
pub unsafe fn create_readonly_token_with_caps_and_user_from(
|
|
base_token: HANDLE,
|
|
psid_capabilities: &[*mut c_void],
|
|
) -> Result<HANDLE> {
|
|
let mut user_sid_bytes = get_user_sid_bytes(base_token)?;
|
|
let psid_user = user_sid_bytes.as_mut_ptr() as *mut c_void;
|
|
create_token_with_caps_from(base_token, psid_capabilities, &[psid_user])
|
|
}
|
|
|
|
unsafe fn create_token_with_caps_from(
|
|
base_token: HANDLE,
|
|
psid_capabilities: &[*mut c_void],
|
|
extra_restricting_sids: &[*mut c_void],
|
|
) -> Result<HANDLE> {
|
|
if psid_capabilities.is_empty() {
|
|
return Err(anyhow!("no capability SIDs provided"));
|
|
}
|
|
let mut logon_sid_bytes = get_logon_sid_bytes(base_token)?;
|
|
let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void;
|
|
let mut everyone = world_sid()?;
|
|
let psid_everyone = everyone.as_mut_ptr() as *mut c_void;
|
|
|
|
// Exact order: Capabilities..., ExtraRestricting..., Logon, Everyone
|
|
let mut entries: Vec<SID_AND_ATTRIBUTES> =
|
|
vec![std::mem::zeroed(); psid_capabilities.len() + extra_restricting_sids.len() + 2];
|
|
for (i, psid) in psid_capabilities.iter().enumerate() {
|
|
entries[i].Sid = *psid;
|
|
entries[i].Attributes = 0;
|
|
}
|
|
let extras_idx = psid_capabilities.len();
|
|
for (i, psid) in extra_restricting_sids.iter().enumerate() {
|
|
entries[extras_idx + i].Sid = *psid;
|
|
entries[extras_idx + i].Attributes = 0;
|
|
}
|
|
let logon_idx = extras_idx + extra_restricting_sids.len();
|
|
entries[logon_idx].Sid = psid_logon;
|
|
entries[logon_idx].Attributes = 0;
|
|
entries[logon_idx + 1].Sid = psid_everyone;
|
|
entries[logon_idx + 1].Attributes = 0;
|
|
|
|
let mut new_token: HANDLE = 0;
|
|
let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED;
|
|
let ok = CreateRestrictedToken(
|
|
base_token,
|
|
flags,
|
|
0,
|
|
std::ptr::null(),
|
|
0,
|
|
std::ptr::null(),
|
|
entries.len() as u32,
|
|
entries.as_mut_ptr(),
|
|
&mut new_token,
|
|
);
|
|
if ok == 0 {
|
|
return Err(anyhow!("CreateRestrictedToken failed: {}", GetLastError()));
|
|
}
|
|
|
|
let mut dacl_sids: Vec<*mut c_void> = Vec::with_capacity(psid_capabilities.len() + 2);
|
|
dacl_sids.push(psid_logon);
|
|
dacl_sids.push(psid_everyone);
|
|
dacl_sids.extend_from_slice(psid_capabilities);
|
|
set_default_dacl(new_token, &dacl_sids)?;
|
|
|
|
enable_single_privilege(new_token, "SeChangeNotifyPrivilege")?;
|
|
Ok(new_token)
|
|
}
|