mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
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:
committed by
copyberry
parent
105fe8761c
commit
73bf181272
@@ -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;
|
||||
|
||||
139
codex-rs/config/src/loader/macos_tests.rs
Normal file
139
codex-rs/config/src/loader/macos_tests.rs
Normal 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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user