mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-10 17:10:18 -10:00
fix(windows): accept sandbox AppContainer ACEs on the finalized plugin root; share the Fleet profile lock
The plugin runtime root verifier required exactly one ACE, but the sandboxed plugin host adds its own read/execute AppContainer (S-1-15-2-*) grant, so every extension-host test failed on Windows. The new verifier still requires a protected DACL, the current user as owner with exactly read+execute, and no grant above read+execute; it tolerates only AppContainer SIDs besides the user. save_agent_profile_document re-opened its write lock with the deny-write bundle opener while holding a write handle, which is ERROR_SHARING_VIOLATION on Windows; use the read-only fully-shared opener for the identity check.
This commit is contained in:
1 parent
1dd0039027
commit
b3e874b8fb
5 files changed
+149
-29
No files matched your search
@@ -462,6 +462,11 @@ tag. Items moved to 0.10.4 by name are marked below.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Saving a Fleet role on Windows no longer fails with a sharing violation:**
|
||||
choosing a model for a saved project or personal agent profile wrote nothing
|
||||
and reported "The process cannot access the file because it is being used by
|
||||
another process". The write-lock identity check now reopens the lock without
|
||||
denying the writer that already holds it.
|
||||
- **A missing or unreachable state directory no longer breaks the turn:**
|
||||
when sub-agent state under `.codewhale/state` does not exist or is a
|
||||
symlink loop, the session warns once, naming the path, and keeps going
|
||||
|
||||
@@ -1727,22 +1727,145 @@ fn secure_windows_owner_only_handle(file: &File, inherit_to_children: bool) -> R
|
||||
pub(crate) fn verify_windows_owner_only_handle(file: &File) -> Result<()> {
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_ALL_ACCESS;
|
||||
|
||||
verify_windows_owner_only_handle_access(file, FILE_ALL_ACCESS, false)
|
||||
verify_windows_owner_only_handle_access(file, FILE_ALL_ACCESS)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn verify_windows_owner_only_read_execute_handle(file: &File) -> Result<()> {
|
||||
use windows_sys::Win32::Storage::FileSystem::{FILE_GENERIC_EXECUTE, FILE_GENERIC_READ};
|
||||
pub fn verify_windows_finalized_read_only_handle(file: &File) -> Result<()> {
|
||||
use std::os::windows::io::AsRawHandle as _;
|
||||
use windows_sys::Win32::Foundation::{
|
||||
ERROR_SUCCESS, GENERIC_ALL, GENERIC_EXECUTE, GENERIC_READ, GENERIC_WRITE,
|
||||
};
|
||||
use windows_sys::Win32::Security::Authorization::{GetSecurityInfo, SE_FILE_OBJECT};
|
||||
use windows_sys::Win32::Security::{
|
||||
ACCESS_ALLOWED_ACE, ACL, DACL_SECURITY_INFORMATION, EqualSid, GetAce,
|
||||
GetSecurityDescriptorControl, GetSidIdentifierAuthority, GetSidSubAuthority,
|
||||
GetSidSubAuthorityCount, IsValidSid, OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR,
|
||||
PSID, SE_DACL_PROTECTED,
|
||||
};
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
|
||||
};
|
||||
|
||||
verify_windows_owner_only_handle_access(file, FILE_GENERIC_READ | FILE_GENERIC_EXECUTE, true)
|
||||
const ACCESS_ALLOWED_ACE_TYPE: u8 = 0;
|
||||
const INHERIT_ONLY_ACE: u8 = 0x08;
|
||||
const APP_PACKAGE_AUTHORITY: [u8; 6] = [0, 0, 0, 0, 0, 15];
|
||||
const APP_PACKAGE_BASE_RID: u32 = 2;
|
||||
const READ_EXECUTE: u32 = FILE_GENERIC_READ | FILE_GENERIC_EXECUTE;
|
||||
|
||||
let user = CurrentWindowsUser::open()?;
|
||||
let mut owner: PSID = std::ptr::null_mut();
|
||||
let mut dacl: *mut ACL = std::ptr::null_mut();
|
||||
let mut descriptor: PSECURITY_DESCRIPTOR = std::ptr::null_mut();
|
||||
// SAFETY: the handle remains valid and all output pointers are writable.
|
||||
let result = unsafe {
|
||||
GetSecurityInfo(
|
||||
file.as_raw_handle(),
|
||||
SE_FILE_OBJECT,
|
||||
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
|
||||
&mut owner,
|
||||
std::ptr::null_mut(),
|
||||
&mut dacl,
|
||||
std::ptr::null_mut(),
|
||||
&mut descriptor,
|
||||
)
|
||||
};
|
||||
if result != ERROR_SUCCESS {
|
||||
return Err(std::io::Error::from_raw_os_error(result as i32))
|
||||
.context("reading Codewhale-owned read-only storage security descriptor");
|
||||
}
|
||||
let _descriptor = WindowsLocalAllocation(descriptor.cast());
|
||||
let mut control = 0;
|
||||
let mut revision = 0;
|
||||
// SAFETY: `descriptor` is the live descriptor returned above.
|
||||
if unsafe { GetSecurityDescriptorControl(descriptor, &mut control, &mut revision) } == 0 {
|
||||
return Err(std::io::Error::last_os_error())
|
||||
.context("reading Codewhale-owned storage DACL protection");
|
||||
}
|
||||
anyhow::ensure!(
|
||||
control & SE_DACL_PROTECTED != 0,
|
||||
"Codewhale-owned storage DACL is not protected"
|
||||
);
|
||||
// SAFETY: `owner` is non-null; `user.sid()` is owned by `user`.
|
||||
anyhow::ensure!(
|
||||
!owner.is_null() && unsafe { EqualSid(owner, user.sid()) } != 0,
|
||||
"Codewhale-owned read-only storage owner is not the current user"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
!dacl.is_null(),
|
||||
"Codewhale-owned read-only storage must have a DACL"
|
||||
);
|
||||
let normalized_access = |access: u32| {
|
||||
let mut normalized =
|
||||
access & !(GENERIC_ALL | GENERIC_EXECUTE | GENERIC_READ | GENERIC_WRITE);
|
||||
for (generic, specific) in [
|
||||
(GENERIC_ALL, FILE_ALL_ACCESS),
|
||||
(GENERIC_EXECUTE, FILE_GENERIC_EXECUTE),
|
||||
(GENERIC_READ, FILE_GENERIC_READ),
|
||||
(GENERIC_WRITE, FILE_GENERIC_WRITE),
|
||||
] {
|
||||
if access & generic != 0 {
|
||||
normalized |= specific;
|
||||
}
|
||||
}
|
||||
normalized
|
||||
};
|
||||
// SAFETY: `dacl` belongs to the live descriptor.
|
||||
let ace_count = u32::from(unsafe { (*dacl).AceCount });
|
||||
let mut user_access = 0;
|
||||
for index in 0..ace_count {
|
||||
let mut raw = std::ptr::null_mut();
|
||||
// SAFETY: `index` is below the ACL's own ACE count.
|
||||
if unsafe { GetAce(dacl, index, &mut raw) } == 0 || raw.is_null() {
|
||||
return Err(std::io::Error::last_os_error())
|
||||
.context("reading Codewhale-owned read-only storage DACL entry");
|
||||
}
|
||||
// SAFETY: every ACE begins with ACE_HEADER, which ACCESS_ALLOWED_ACE
|
||||
// starts with; the type is checked before the SID is read.
|
||||
let ace = unsafe { &*raw.cast::<ACCESS_ALLOWED_ACE>() };
|
||||
anyhow::ensure!(
|
||||
ace.Header.AceType == ACCESS_ALLOWED_ACE_TYPE,
|
||||
"Codewhale-owned read-only storage DACL holds an unexpected entry type"
|
||||
);
|
||||
let sid: PSID = std::ptr::addr_of!(ace.SidStart).cast_mut().cast();
|
||||
// SAFETY: an access-allowed ACE stores its SID at `SidStart`.
|
||||
anyhow::ensure!(
|
||||
unsafe { IsValidSid(sid) } != 0,
|
||||
"Codewhale-owned read-only storage DACL holds an invalid SID"
|
||||
);
|
||||
let access = normalized_access(ace.Mask);
|
||||
anyhow::ensure!(
|
||||
access & !READ_EXECUTE == 0,
|
||||
"Codewhale-owned read-only storage DACL grants more than read and execute"
|
||||
);
|
||||
// SAFETY: `sid` was validated above; `user.sid()` is owned by `user`.
|
||||
if unsafe { EqualSid(sid, user.sid()) } != 0 {
|
||||
if ace.Header.AceFlags & INHERIT_ONLY_ACE == 0 {
|
||||
user_access |= access;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// SAFETY: `sid` was validated above, so its authority, count and
|
||||
// first sub-authority are readable.
|
||||
let is_app_container = unsafe {
|
||||
(*GetSidIdentifierAuthority(sid)).Value == APP_PACKAGE_AUTHORITY
|
||||
&& *GetSidSubAuthorityCount(sid) > 0
|
||||
&& *GetSidSubAuthority(sid, 0) == APP_PACKAGE_BASE_RID
|
||||
};
|
||||
anyhow::ensure!(
|
||||
is_app_container,
|
||||
"Codewhale-owned read-only storage DACL grants another user"
|
||||
);
|
||||
}
|
||||
anyhow::ensure!(
|
||||
user_access == READ_EXECUTE,
|
||||
"Codewhale-owned read-only storage DACL does not grant the current user read and execute"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn verify_windows_owner_only_handle_access(
|
||||
file: &File,
|
||||
expected_access: u32,
|
||||
require_protected: bool,
|
||||
) -> Result<()> {
|
||||
fn verify_windows_owner_only_handle_access(file: &File, expected_access: u32) -> Result<()> {
|
||||
use std::os::windows::io::AsRawHandle as _;
|
||||
use windows_sys::Win32::Foundation::{
|
||||
ERROR_SUCCESS, GENERIC_ALL, GENERIC_EXECUTE, GENERIC_READ, GENERIC_WRITE,
|
||||
@@ -1752,8 +1875,8 @@ fn verify_windows_owner_only_handle_access(
|
||||
SE_FILE_OBJECT, SET_ACCESS, TRUSTEE_IS_SID,
|
||||
};
|
||||
use windows_sys::Win32::Security::{
|
||||
ACL, DACL_SECURITY_INFORMATION, EqualSid, GetSecurityDescriptorControl,
|
||||
OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, SE_DACL_PROTECTED,
|
||||
ACL, DACL_SECURITY_INFORMATION, EqualSid, OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR,
|
||||
PSID,
|
||||
};
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
|
||||
@@ -1781,18 +1904,6 @@ fn verify_windows_owner_only_handle_access(
|
||||
.context("reading Codewhale-owned xAI OAuth security descriptor");
|
||||
}
|
||||
let _descriptor = WindowsLocalAllocation(descriptor.cast());
|
||||
if require_protected {
|
||||
let mut control = 0;
|
||||
let mut revision = 0;
|
||||
if unsafe { GetSecurityDescriptorControl(descriptor, &mut control, &mut revision) } == 0 {
|
||||
return Err(std::io::Error::last_os_error())
|
||||
.context("reading Codewhale-owned storage DACL protection");
|
||||
}
|
||||
anyhow::ensure!(
|
||||
control & SE_DACL_PROTECTED != 0,
|
||||
"Codewhale-owned storage DACL is not protected"
|
||||
);
|
||||
}
|
||||
// SAFETY: `owner` is non-null; `user.sid()` is owned by `user`.
|
||||
anyhow::ensure!(
|
||||
!owner.is_null() && unsafe { EqualSid(owner, user.sid()) } != 0,
|
||||
|
||||
@@ -234,7 +234,7 @@ pub fn save_agent_profile_document(
|
||||
codewhale_util::files::same_file(
|
||||
&guard,
|
||||
&codewhale_util::files::WorkspaceFile::open(&root, &lock_relative, false)?
|
||||
.open_file()?,
|
||||
.open_file_shared()?,
|
||||
)?,
|
||||
"Profile write lock changed; reopen the source"
|
||||
);
|
||||
@@ -270,7 +270,7 @@ pub fn save_agent_profile_document(
|
||||
codewhale_util::files::same_file(
|
||||
&guard,
|
||||
&codewhale_util::files::WorkspaceFile::open(&root, &lock_relative, false)?
|
||||
.open_file()?,
|
||||
.open_file_shared()?,
|
||||
)?,
|
||||
"Profile write lock changed; reopen the source"
|
||||
);
|
||||
|
||||
@@ -1746,10 +1746,9 @@ pub(super) fn verify_finalized_stage_root(
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
codewhale_config::private_directory::verify_windows_owner_only_read_execute_handle(&held)
|
||||
.map_err(|error| {
|
||||
format!("Plugin runtime root has not been finalized read-only: {error:#}")
|
||||
})?;
|
||||
codewhale_config::private_directory::verify_windows_finalized_read_only_handle(&held).map_err(
|
||||
|error| format!("Plugin runtime root has not been finalized read-only: {error:#}"),
|
||||
)?;
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
Err("Plugin runtime root finalization cannot be verified on this platform".to_string())
|
||||
|
||||
@@ -462,6 +462,11 @@ tag. Items moved to 0.10.4 by name are marked below.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Saving a Fleet role on Windows no longer fails with a sharing violation:**
|
||||
choosing a model for a saved project or personal agent profile wrote nothing
|
||||
and reported "The process cannot access the file because it is being used by
|
||||
another process". The write-lock identity check now reopens the lock without
|
||||
denying the writer that already holds it.
|
||||
- **A missing or unreachable state directory no longer breaks the turn:**
|
||||
when sub-agent state under `.codewhale/state` does not exist or is a
|
||||
symlink loop, the session warns once, naming the path, and keeps going
|
||||
|
||||
Reference in new issue
Block a user