Require forced macOS preferences for managed configuration (#46032)

## Why

`CFPreferencesCopyAppValue` also searches user-writable domains. Ordinary user defaults must not supply trusted administrator configuration or override lower requirements layers.

## What changed

- Check `CFPreferencesAppValueIsForced` before reading `config_toml_base64` and `requirements_toml_base64`, and recheck after reading to ignore values that became unforced.
- Wrap returned property-list values as `CFType` and require a `CFString`, returning `InvalidData` for other types without exposing preference contents.

## Testing

Add tests for both preference keys covering unforced and missing values, loss of forced status during a read, preservation of string contents, and rejection of non-string values with diagnostics that omit their contents.

GitOrigin-RevId: 2eff08ba6b073b4c59ca757e054d112d2a5256d0
This commit is contained in:
joeflorencio-openai
2026-09-16 21:18:36 +00:00
committed by copyberry
parent 105fe8761c
commit 73bf181272
2 changed files with 215 additions and 13 deletions

View File

@@ -1,3 +1,8 @@
//! Loads administrator configuration only from forced macOS preferences.
//!
//! Ordinary user defaults must never become trusted managed configuration layers.
//! Forced values must be strings containing base64-encoded TOML.
use crate::RequirementsLayerEntry;
use crate::config_requirements::RequirementSource;
use crate::config_toml::ConfigToml;
@@ -8,6 +13,8 @@ use crate::strict_config::config_error_from_ignored_toml_value_fields_for_source
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use core_foundation::base::Boolean;
use core_foundation::base::CFType;
use core_foundation::base::TCFType;
use core_foundation::string::CFString;
use core_foundation::string::CFStringRef;
@@ -21,6 +28,12 @@ const MANAGED_PREFERENCES_APPLICATION_ID: &str = "com.openai.codex";
const MANAGED_PREFERENCES_CONFIG_KEY: &str = "config_toml_base64";
const MANAGED_PREFERENCES_REQUIREMENTS_KEY: &str = "requirements_toml_base64";
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFPreferencesCopyAppValue(key: CFStringRef, application_id: CFStringRef) -> *mut c_void;
fn CFPreferencesAppValueIsForced(key: CFStringRef, application_id: CFStringRef) -> Boolean;
}
#[derive(Debug, Clone)]
pub(super) struct ManagedAdminConfigLayer {
pub config: TomlValue,
@@ -121,27 +134,73 @@ fn load_managed_admin_requirements() -> io::Result<Option<String>> {
}
fn load_managed_preference(key_name: &str) -> io::Result<Option<String>> {
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFPreferencesCopyAppValue(key: CFStringRef, application_id: CFStringRef) -> *mut c_void;
let key = CFString::new(key_name);
let application = CFString::new(MANAGED_PREFERENCES_APPLICATION_ID);
load_managed_preference_with(
key_name,
|| preference_is_forced(&key, &application),
|| copy_preference_value(&key, &application),
)
}
fn preference_is_forced(key: &CFString, application: &CFString) -> bool {
unsafe {
CFPreferencesAppValueIsForced(key.as_concrete_TypeRef(), application.as_concrete_TypeRef())
!= 0
}
}
fn copy_preference_value(key: &CFString, application: &CFString) -> Option<CFType> {
let value_ref = unsafe {
CFPreferencesCopyAppValue(key.as_concrete_TypeRef(), application.as_concrete_TypeRef())
};
if value_ref.is_null() {
return None;
}
let value_ref = unsafe {
CFPreferencesCopyAppValue(
CFString::new(key_name).as_concrete_TypeRef(),
CFString::new(MANAGED_PREFERENCES_APPLICATION_ID).as_concrete_TypeRef(),
)
};
// CopyAppValue returns an owned property-list value, not necessarily a string.
Some(unsafe { CFType::wrap_under_create_rule(value_ref) })
}
if value_ref.is_null() {
fn load_managed_preference_with(
key_name: &str,
mut is_forced: impl FnMut() -> bool,
copy_value: impl FnOnce() -> Option<CFType>,
) -> io::Result<Option<String>> {
// CopyAppValue also searches user-writable domains. Only forced values may
// supply administrator configuration or override lower requirements layers.
if !is_forced() {
tracing::debug!(
"Managed preferences for {MANAGED_PREFERENCES_APPLICATION_ID} key {key_name} not found",
"No forced managed preference for {MANAGED_PREFERENCES_APPLICATION_ID} key {key_name}"
);
return Ok(None);
}
let value = unsafe { CFString::wrap_under_create_rule(value_ref as _) }.to_string();
Ok(Some(value))
let Some(value) = copy_value() else {
tracing::debug!(
"Managed preferences for {MANAGED_PREFERENCES_APPLICATION_ID} key {key_name} not found"
);
return Ok(None);
};
// Reject a user-default fallback if the preference stopped being forced
// during the read. These separate calls do not form an atomic snapshot.
if !is_forced() {
tracing::debug!(
"Managed preference {MANAGED_PREFERENCES_APPLICATION_ID}:{key_name} is no longer forced after reading"
);
return Ok(None);
}
let value = value.downcast::<CFString>().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Managed preference {MANAGED_PREFERENCES_APPLICATION_ID}:{key_name} must be a string"
),
)
})?;
Ok(Some(value.to_string()))
}
fn parse_managed_config_base64(
@@ -228,3 +287,7 @@ fn decode_managed_preferences_base64(encoded: &str) -> io::Result<String> {
io::Error::new(io::ErrorKind::InvalidData, err)
})
}
#[cfg(test)]
#[path = "macos_tests.rs"]
mod tests;

View File

@@ -0,0 +1,139 @@
//! Exercise managed preference trust checks with injected reads and in-memory values.
use super::MANAGED_PREFERENCES_APPLICATION_ID;
use super::MANAGED_PREFERENCES_CONFIG_KEY;
use super::MANAGED_PREFERENCES_REQUIREMENTS_KEY;
use super::load_managed_preference_with;
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use core_foundation::array::CFArray;
use core_foundation::base::TCFType;
use core_foundation::boolean::CFBoolean;
use core_foundation::data::CFData;
use core_foundation::dictionary::CFDictionary;
use core_foundation::number::CFNumber;
use core_foundation::string::CFString;
use pretty_assertions::assert_eq;
use std::cell::Cell;
use std::io;
#[test]
fn unforced_preferences_are_ignored_without_reading() {
for key in [
MANAGED_PREFERENCES_CONFIG_KEY,
MANAGED_PREFERENCES_REQUIREMENTS_KEY,
] {
assert_eq!(
load_managed_preference_with(
key,
|| false,
|| panic!("unforced preference must not be read"),
)
.expect("ignore unforced preference"),
None
);
}
}
#[test]
fn forced_preferences_without_a_value_are_absent() {
for key in [
MANAGED_PREFERENCES_CONFIG_KEY,
MANAGED_PREFERENCES_REQUIREMENTS_KEY,
] {
assert_eq!(
load_managed_preference_with(key, || true, || None)
.expect("load absent forced preference"),
None
);
}
}
#[test]
fn preferences_that_become_unforced_during_read_are_ignored() {
for key in [
MANAGED_PREFERENCES_CONFIG_KEY,
MANAGED_PREFERENCES_REQUIREMENTS_KEY,
] {
for value in [
CFString::new("ordinary user default").as_CFType(),
CFBoolean::true_value().as_CFType(),
] {
let forced = Cell::new(/*value*/ true);
assert_eq!(
load_managed_preference_with(
key,
|| forced.get(),
|| {
forced.set(/*val*/ false);
Some(value)
},
)
.expect("ignore preference that became unforced during the read"),
None
);
}
}
}
#[test]
fn forced_string_preferences_preserve_contents() {
let encoded = BASE64_STANDARD.encode("sandbox_mode = 'read-only'");
for key in [
MANAGED_PREFERENCES_CONFIG_KEY,
MANAGED_PREFERENCES_REQUIREMENTS_KEY,
] {
for contents in ["", "Préférence gérée ✓", &encoded] {
assert_eq!(
load_managed_preference_with(
key,
|| true,
|| Some(CFString::new(contents).as_CFType()),
)
.expect("load string preference"),
Some(contents.to_owned())
);
}
}
}
#[test]
fn forced_non_string_preferences_return_redacted_errors() {
let private_contents = CFString::new("private preference contents");
let values = [
("boolean", CFBoolean::true_value().as_CFType()),
("number", CFNumber::from(/*value*/ 42).as_CFType()),
(
"data",
CFData::from_buffer(b"private preference contents").as_CFType(),
),
(
"array",
CFArray::from_CFTypes(std::slice::from_ref(&private_contents)).as_CFType(),
),
(
"dictionary",
CFDictionary::from_CFType_pairs(&[(CFString::new("secret"), private_contents)])
.as_CFType(),
),
];
for key in [
MANAGED_PREFERENCES_CONFIG_KEY,
MANAGED_PREFERENCES_REQUIREMENTS_KEY,
] {
for (type_name, value) in &values {
let error = load_managed_preference_with(key, || true, || Some(value.clone()))
.expect_err("reject non-string preference");
assert_eq!(
(error.kind(), error.to_string()),
(
io::ErrorKind::InvalidData,
format!(
"Managed preference {MANAGED_PREFERENCES_APPLICATION_ID}:{key} must be a string"
),
),
"unexpected diagnostic for {type_name} preference {key}"
);
}
}
}