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:
CodeWhale Bot committed 2026-11-09 15:44:05 -08:00
1 parent 1dd0039027
commit b3e874b8fb
5 files changed
+149 -29

No files matched your search

+5
View File
@@ -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
+134 -23
View File
@@ -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,
+2 -2
View File
@@ -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"
);
+3 -4
View File
@@ -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())
+5
View File
@@ -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