Skip to content
Merged
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
20 changes: 20 additions & 0 deletions .github/workflows/weekly-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ jobs:
- name: Build compiler lane worker
shell: bash
run: cargo build --locked --release -p bamts-verification --bin ts_lane_worker
- name: Fetch pinned TypeScript compiler authority
shell: bash
run: >-
cargo run --locked -p bamts-verification -- source fetch
typescript-7-compiler --dest target/authority/typescript-7.0.2
- name: Fetch pinned TypeScript test authority
shell: bash
run: >-
cargo run --locked -p bamts-verification -- source fetch
typescript-primary-tests --dest target/authority/typescript-7.0.2-tests
- name: Run weekly-audit receipt shard
shell: bash
env:
Expand Down Expand Up @@ -191,6 +201,16 @@ jobs:
- name: Build compiler lane worker
shell: bash
run: cargo build --locked --release -p bamts-verification --bin ts_lane_worker
- name: Fetch pinned TypeScript compiler authority
shell: bash
run: >-
cargo run --locked -p bamts-verification -- source fetch
typescript-7-compiler --dest target/authority/typescript-7.0.2
- name: Fetch pinned TypeScript test authority
shell: bash
run: >-
cargo run --locked -p bamts-verification -- source fetch
typescript-primary-tests --dest target/authority/typescript-7.0.2-tests
- name: Merge complete compatible matrix
shell: bash
env:
Expand Down
72 changes: 64 additions & 8 deletions crates/bamts-native/src/cache_guard/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use windows::{
ACCESS_ALLOWED_ACE, ACE_HEADER, ACL,
Authorization::{
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
GetSecurityInfo, SDDL_REVISION_1, SE_FILE_OBJECT,
ConvertStringSidToSidW, GetSecurityInfo, SDDL_REVISION_1, SE_FILE_OBJECT,
},
CONTAINER_INHERIT_ACE, CopySid, CreateWellKnownSid, DACL_SECURITY_INFORMATION,
EqualSid, GENERIC_MAPPING, GetAce, GetLengthSid, GetSecurityDescriptorControl,
Expand Down Expand Up @@ -47,6 +47,10 @@ use super::{CacheGuardError, HeldArchive};
const MAX_CHAIN_DEPTH: usize = 32;
const MAX_NAME_ATTEMPTS: usize = 128;
const COMPARE_BUFFER_BYTES: usize = 64 * 1024;
// Windows Modules Installer owns protected system directories, including the
// system-drive root on current Windows images. It is not a private-cache owner.
const TRUSTED_INSTALLER_SID: &str =
"S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464";
static NEXT_INVOCATION_ID: AtomicU64 = AtomicU64::new(0);

#[derive(Clone, Copy)]
Expand Down Expand Up @@ -113,6 +117,7 @@ struct TrustedSids {
system: OwnedSid,
administrators: OwnedSid,
creator_owner: OwnedSid,
trusted_installer: OwnedSid,
}

#[derive(Debug)]
Expand Down Expand Up @@ -281,21 +286,25 @@ impl TrustedSids {
system: well_known_sid(WinLocalSystemSid)?,
administrators: well_known_sid(WinBuiltinAdministratorsSid)?,
creator_owner: well_known_sid(WinCreatorOwnerSid)?,
trusted_installer: parse_sid(TRUSTED_INSTALLER_SID)?,
})
}

fn owner_is_trusted(&self, candidate: PSID) -> bool {
fn owner_is_trusted(&self, candidate: PSID, policy: DirectoryPolicy) -> bool {
[
self.user.as_sid(),
self.system.as_sid(),
self.administrators.as_sid(),
]
.into_iter()
.any(|trusted| equal_sid(candidate, trusted))
|| (matches!(policy, DirectoryPolicy::Ancestor)
&& equal_sid(candidate, self.trusted_installer.as_sid()))
}

fn mutation_is_trusted(&self, candidate: PSID) -> bool {
self.owner_is_trusted(candidate) || equal_sid(candidate, self.creator_owner.as_sid())
fn mutation_is_trusted(&self, candidate: PSID, policy: DirectoryPolicy) -> bool {
self.owner_is_trusted(candidate, policy)
|| equal_sid(candidate, self.creator_owner.as_sid())
}
}

Expand Down Expand Up @@ -409,7 +418,7 @@ fn inspect_security_descriptor(
policy: DirectoryPolicy,
trusted: &TrustedSids,
) -> Result<(), CacheGuardError> {
if owner.is_invalid() || !trusted.owner_is_trusted(owner) {
if owner.is_invalid() || !trusted.owner_is_trusted(owner, policy) {
return Err(CacheGuardError::UntrustedOwner {
path: path.to_owned(),
owner: sid_to_string(owner, path).unwrap_or_else(|_| "<invalid-sid>".to_owned()),
Expand Down Expand Up @@ -461,7 +470,7 @@ fn inspect_security_descriptor(
continue;
}
let trustee = PSID(ptr::from_ref(&ace.SidStart).cast_mut().cast());
if !trusted.mutation_is_trusted(trustee) {
if !trusted.mutation_is_trusted(trustee, policy) {
return Err(CacheGuardError::UntrustedWriteAce {
path: path.to_owned(),
trustee: sid_to_string(trustee, path)
Expand Down Expand Up @@ -637,8 +646,23 @@ fn well_known_sid(
Ok(buffer)
}

fn parse_sid(value: &str) -> Result<OwnedSid, CacheGuardError> {
let path = Path::new("<sid>");
let value = wide(OsStr::new(value));
let mut sid = PSID(ptr::null_mut());
// SAFETY: value is NUL terminated and sid is a valid output pointer.
unsafe { ConvertStringSidToSidW(PCWSTR(value.as_ptr()), &mut sid) }
.map_err(|error| CacheGuardError::io(path, io::Error::from_raw_os_error(error.code().0)))?;
let result = copy_sid(sid, path);
// SAFETY: ConvertStringSidToSidW allocated sid with LocalAlloc on success.
unsafe {
LocalFree(Some(HLOCAL(sid.0)));
}
result
}

fn copy_sid(source: PSID, path: &Path) -> Result<OwnedSid, CacheGuardError> {
// SAFETY: source comes from a successful token/security query.
// SAFETY: source comes from a successful Windows SID query or conversion.
let size = unsafe { GetLengthSid(source) };
let mut output = OwnedSid::with_byte_capacity(size as usize);
// SAFETY: the pointer-aligned output has at least the size reported for source.
Expand Down Expand Up @@ -682,7 +706,8 @@ fn wide(value: &OsStr) -> Vec<u16> {
mod tests {
use super::{
CONTAINER_INHERIT_ACE, DELETE, DirectoryPolicy, FILE_DELETE_CHILD, FILE_WRITE_DATA,
INHERIT_ONLY_ACE, OBJECT_INHERIT_ACE, PrivateCacheRoot, WRITE_DAC, ace_grants_mutation,
INHERIT_ONLY_ACE, OBJECT_INHERIT_ACE, PrivateCacheRoot, TrustedSids, WRITE_DAC,
ace_grants_mutation, parse_sid, well_known_sid,
};
use std::{
fs,
Expand All @@ -700,6 +725,37 @@ mod tests {
))
}

#[test]
fn trusted_installer_is_trusted_only_for_ancestors() {
let trusted = TrustedSids::current().expect("trusted SIDs");
let installer = trusted.trusted_installer.as_sid();
assert!(trusted.owner_is_trusted(installer, DirectoryPolicy::Ancestor));
assert!(trusted.mutation_is_trusted(installer, DirectoryPolicy::Ancestor));
assert!(!trusted.owner_is_trusted(installer, DirectoryPolicy::Cache));
assert!(!trusted.mutation_is_trusted(installer, DirectoryPolicy::Cache));
}

#[test]
fn system_ancestor_exception_preserves_the_exact_trust_boundary() {
let trusted = TrustedSids::current().expect("trusted SIDs");
let other_service =
parse_sid("S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478465")
.expect("distinct service SID");
let everyone = well_known_sid(windows::Win32::Security::WinWorldSid).expect("Everyone SID");
for policy in [DirectoryPolicy::Ancestor, DirectoryPolicy::Cache] {
for owner in [&trusted.user, &trusted.system, &trusted.administrators] {
assert!(trusted.owner_is_trusted(owner.as_sid(), policy));
assert!(trusted.mutation_is_trusted(owner.as_sid(), policy));
}
for untrusted in [&other_service, &everyone] {
assert!(!trusted.owner_is_trusted(untrusted.as_sid(), policy));
assert!(!trusted.mutation_is_trusted(untrusted.as_sid(), policy));
}
assert!(!trusted.owner_is_trusted(trusted.creator_owner.as_sid(), policy));
assert!(trusted.mutation_is_trusted(trusted.creator_owner.as_sid(), policy));
}
}

#[test]
fn inherit_only_cache_ace_is_checked_for_descendant_mutation() {
let inherit_only = INHERIT_ONLY_ACE.0 as u8;
Expand Down
49 changes: 48 additions & 1 deletion crates/bamts-verification/tests/workflow_receipts.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{fs, path::PathBuf};
use std::{collections::BTreeMap, fs, path::PathBuf};

fn repository_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
Expand All @@ -13,6 +13,53 @@ fn workflow(name: &str) -> String {
.unwrap_or_else(|error| panic!("cannot read {name}: {error}"))
}

#[test]
fn compiler_receipt_jobs_provision_authority_before_execution() {
#[derive(serde::Deserialize)]
struct Workflow {
jobs: BTreeMap<String, Job>,
}

#[derive(serde::Deserialize)]
struct Job {
#[serde(default)]
steps: Vec<Step>,
}

#[derive(serde::Deserialize)]
struct Step {
#[serde(default)]
run: String,
}

for name in ["ci.yml", "nightly.yml", "weekly-audit.yml"] {
let parsed: Workflow = serde_saphyr::from_str(&workflow(name))
.unwrap_or_else(|error| panic!("cannot parse {name}: {error}"));
let mut checked_jobs = 0;
for (job_name, job) in parsed.jobs {
let Some(execution) = job.steps.iter().position(|step| {
step.run.contains("--catalog typescript-7.0.2")
&& (step.run.contains("suite run") || step.run.contains("suite merge"))
}) else {
continue;
};
checked_jobs += 1;
for required in [
"source fetch typescript-7-compiler --dest target/authority/typescript-7.0.2",
"source fetch typescript-primary-tests --dest target/authority/typescript-7.0.2-tests",
] {
assert!(
job.steps[..execution]
.iter()
.any(|step| step.run.contains(required)),
"{name}:{job_name} must provision `{required}` before receipt execution"
);
}
}
assert!(checked_jobs > 0, "{name} contains no compiler receipt jobs");
}
}

#[test]
fn receipt_workflows_bind_attempt_and_merge_complete_matrices() {
for (name, raw_root, merged_root, retention) in [
Expand Down
Loading