Skip to main content

std/sys/platform_version/darwin/
mod.rs

1use self::core_foundation::{
2    CFDictionaryRef, CFHandle, CFIndex, CFStringRef, CFTypeRef, kCFAllocatorDefault,
3    kCFPropertyListImmutable, kCFStringEncodingUTF8,
4};
5use crate::borrow::Cow;
6use crate::bstr::ByteStr;
7use crate::ffi::{CStr, c_char};
8use crate::num::{NonZero, ParseIntError};
9use crate::path::{Path, PathBuf};
10use crate::ptr::null_mut;
11use crate::sync::atomic::{AtomicU32, Ordering};
12use crate::{env, fs};
13
14mod core_foundation;
15mod public_extern;
16#[cfg(test)]
17mod tests;
18
19/// The version of the operating system.
20///
21/// We use a packed u32 here to allow for fast comparisons and to match Mach-O's `LC_BUILD_VERSION`.
22type OSVersion = u32;
23
24/// Combine parts of a version into an [`OSVersion`].
25///
26/// The size of the parts are inherently limited by Mach-O's `LC_BUILD_VERSION`.
27#[inline]
28const fn pack_os_version(major: u16, minor: u8, patch: u8) -> OSVersion {
29    let (major, minor, patch) = (major as u32, minor as u32, patch as u32);
30    (major << 16) | (minor << 8) | patch
31}
32
33/// [`pack_os_version`], but takes `i32` and saturates.
34///
35/// Instead of using e.g. `major as u16`, which truncates.
36#[inline]
37fn pack_i32_os_version(major: i32, minor: i32, patch: i32) -> OSVersion {
38    let major: u16 = major.try_into().unwrap_or(u16::MAX);
39    let minor: u8 = minor.try_into().unwrap_or(u8::MAX);
40    let patch: u8 = patch.try_into().unwrap_or(u8::MAX);
41    pack_os_version(major, minor, patch)
42}
43
44/// Get the current OS version, packed according to [`pack_os_version`].
45///
46/// # Semantics
47///
48/// The reported version on macOS might be 10.16 if the SDK version of the binary is less than 11.0.
49/// This is a workaround that Apple implemented to handle applications that assumed that macOS
50/// versions would always start with "10", see:
51/// <https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/libsyscall/wrappers/system-version-compat.c>
52///
53/// It _is_ possible to get the real version regardless of the SDK version of the binary, this is
54/// what Zig does:
55/// <https://github.com/ziglang/zig/blob/0.13.0/lib/std/zig/system/darwin/macos.zig>
56///
57/// We choose to not do that, and instead follow Apple's behaviour here, and return 10.16 when
58/// compiled with an older SDK; the user should instead upgrade their tooling.
59///
60/// NOTE: `rustc` currently doesn't set the right SDK version when linking with ld64, so this will
61/// have the wrong behaviour with `-Clinker=ld` on x86_64. But that's a `rustc` bug:
62/// <https://github.com/rust-lang/rust/issues/129432>
63#[inline]
64fn current_version() -> OSVersion {
65    // Cache the lookup for performance.
66    //
67    // 0.0.0 is never going to be a valid version ("vtool" reports "n/a" on 0 versions), so we use
68    // that as our sentinel value.
69    static CURRENT_VERSION: AtomicU32 = AtomicU32::new(0);
70
71    // We use relaxed atomics instead of e.g. a `Once`, it doesn't matter if multiple threads end up
72    // racing to read or write the version, `lookup_version` should be idempotent and always return
73    // the same value.
74    //
75    // `compiler-rt` uses `dispatch_once`, but that's overkill for the reasons above.
76    let version = CURRENT_VERSION.load(Ordering::Relaxed);
77    if version == 0 {
78        let version = lookup_version().get();
79        CURRENT_VERSION.store(version, Ordering::Relaxed);
80        version
81    } else {
82        version
83    }
84}
85
86/// Look up the os version.
87///
88/// # Aborts
89///
90/// Aborts if reading or parsing the version fails (or if the system was out of memory).
91///
92/// We deliberately choose to abort, as having this silently return an invalid OS version would be
93/// impossible for a user to debug.
94// The lookup is costly and should be on the cold path because of the cache in `current_version`.
95#[cold]
96// Micro-optimization: We use `extern "C"` to abort on panic, allowing `current_version` (inlined)
97// to be free of unwind handling. Aborting is required for `__isPlatformVersionAtLeast` anyhow.
98extern "C" fn lookup_version() -> NonZero<OSVersion> {
99    // Try to read from `sysctl` first (faster), but if that fails, fall back to reading the
100    // property list (this is roughly what `_availability_version_check` does internally).
101    let version = version_from_sysctl().unwrap_or_else(version_from_plist);
102
103    // Use `NonZero` to try to make it clearer to the optimizer that this will never return 0.
104    NonZero::new(version).expect("version cannot be 0.0.0")
105}
106
107/// Read the version from `kern.osproductversion` or `kern.iossupportversion`.
108///
109/// This is faster than `version_from_plist`, since it doesn't need to invoke `dlsym`.
110fn version_from_sysctl() -> Option<OSVersion> {
111    // This won't work in the simulator, as `kern.osproductversion` returns the host macOS version,
112    // and `kern.iossupportversion` returns the host macOS' iOSSupportVersion (while you can run
113    // simulators with many different iOS versions).
114    if cfg!(target_abi = "sim") {
115        // Fall back to `version_from_plist` on these targets.
116        return None;
117    }
118
119    let sysctl_version = |name: &CStr| {
120        let mut buf: [u8; 32] = [0; 32];
121        let mut size = buf.len();
122        let ptr = buf.as_mut_ptr().cast();
123        let ret = unsafe { libc::sysctlbyname(name.as_ptr(), ptr, &mut size, null_mut(), 0) };
124        if ret != 0 {
125            // This sysctl is not available.
126            return None;
127        }
128        let buf = &buf[..(size - 1)];
129
130        if buf.is_empty() {
131            // The buffer may be empty when using `kern.iossupportversion` on an actual iOS device,
132            // or on visionOS when running under "Designed for iPad".
133            //
134            // In that case, fall back to `kern.osproductversion`.
135            return None;
136        }
137
138        Some(parse_os_version(buf).unwrap_or_else(|err| {
139            panic!("failed parsing version from sysctl ({}): {err}", ByteStr::new(buf))
140        }))
141    };
142
143    // When `target_os = "ios"`, we may be in many different states:
144    // - Native iOS device.
145    // - iOS Simulator.
146    // - Mac Catalyst.
147    // - Mac + "Designed for iPad".
148    // - Native visionOS device + "Designed for iPad".
149    // - visionOS simulator + "Designed for iPad".
150    //
151    // Of these, only native, Mac Catalyst and simulators can be differentiated at compile-time
152    // (with `target_abi = ""`, `target_abi = "macabi"` and `target_abi = "sim"` respectively).
153    //
154    // That is, "Designed for iPad" will act as iOS at compile-time, but the `ProductVersion` will
155    // still be the host macOS or visionOS version.
156    //
157    // Furthermore, we can't even reliably differentiate between these at runtime, since
158    // `dyld_get_active_platform` isn't publicly available.
159    //
160    // Fortunately, we won't need to know any of that; we can simply attempt to get the
161    // `iOSSupportVersion` (which may be set on native iOS too, but then it will be set to the host
162    // iOS version), and if that fails, fall back to the `ProductVersion`.
163    if cfg!(target_os = "ios") {
164        // https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/bsd/kern/kern_sysctl.c#L2077-L2100
165        if let Some(ios_support_version) = sysctl_version(c"kern.iossupportversion") {
166            return Some(ios_support_version);
167        }
168
169        // On Mac Catalyst, if we failed looking up `iOSSupportVersion`, we don't want to
170        // accidentally fall back to `ProductVersion`.
171        if cfg!(target_abi = "macabi") {
172            return None;
173        }
174    }
175
176    // Introduced in macOS 10.13.4.
177    // https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/bsd/kern/kern_sysctl.c#L2015-L2051
178    sysctl_version(c"kern.osproductversion")
179}
180
181/// Look up the current OS version(s) from `/System/Library/CoreServices/SystemVersion.plist`.
182///
183/// More specifically, from the `ProductVersion` and `iOSSupportVersion` keys, and from
184/// `$IPHONE_SIMULATOR_ROOT/System/Library/CoreServices/SystemVersion.plist` on the simulator.
185///
186/// This file was introduced in macOS 10.3, which is well below the minimum supported version by
187/// `rustc`, which is (at the time of writing) macOS 10.12.
188///
189/// # Implementation
190///
191/// We do roughly the same thing in here as `compiler-rt`, and dynamically look up CoreFoundation
192/// utilities for parsing PLists (to avoid having to re-implement that in here, as pulling in a full
193/// PList parser into `std` seems costly).
194///
195/// If this is found to be undesirable, we _could_ possibly hack it by parsing the PList manually
196/// (it seems to use the plain-text "xml1" encoding/format in all versions), but that seems brittle.
197fn version_from_plist() -> OSVersion {
198    // Read `SystemVersion.plist`. Always present on Apple platforms, reading it cannot fail.
199    let path = root_relative("/System/Library/CoreServices/SystemVersion.plist");
200    let plist_buffer = fs::read(&path).unwrap_or_else(|e| panic!("failed reading {path:?}: {e}"));
201    let cf_handle = CFHandle::new();
202    parse_version_from_plist(&cf_handle, &plist_buffer)
203}
204
205/// Parse OS version from the given PList.
206///
207/// Split out from [`version_from_plist`] to allow for testing.
208fn parse_version_from_plist(cf_handle: &CFHandle, plist_buffer: &[u8]) -> OSVersion {
209    let plist_data = unsafe {
210        cf_handle.CFDataCreateWithBytesNoCopy(
211            kCFAllocatorDefault,
212            plist_buffer.as_ptr(),
213            plist_buffer.len() as CFIndex,
214            cf_handle.kCFAllocatorNull(),
215        )
216    };
217    assert!(!plist_data.is_null(), "failed creating CFData");
218    let _plist_data_release = Deferred(|| unsafe { cf_handle.CFRelease(plist_data) });
219
220    let plist = unsafe {
221        cf_handle.CFPropertyListCreateWithData(
222            kCFAllocatorDefault,
223            plist_data,
224            kCFPropertyListImmutable,
225            null_mut(), // Don't care about the format of the PList.
226            null_mut(), // Don't care about the error data.
227        )
228    };
229    assert!(!plist.is_null(), "failed reading PList in SystemVersion.plist");
230    let _plist_release = Deferred(|| unsafe { cf_handle.CFRelease(plist) });
231
232    assert_eq!(
233        unsafe { cf_handle.CFGetTypeID(plist) },
234        unsafe { cf_handle.CFDictionaryGetTypeID() },
235        "SystemVersion.plist did not contain a dictionary at the top level"
236    );
237    let plist: CFDictionaryRef = plist.cast();
238
239    // Same logic as in `version_from_sysctl`.
240    if cfg!(target_os = "ios") {
241        if let Some(ios_support_version) =
242            unsafe { string_version_key(cf_handle, plist, c"iOSSupportVersion") }
243        {
244            return ios_support_version;
245        }
246
247        // Force Mac Catalyst to use iOSSupportVersion (do not fall back to ProductVersion).
248        if cfg!(target_abi = "macabi") {
249            panic!("expected iOSSupportVersion in SystemVersion.plist");
250        }
251    }
252
253    // On all other platforms, we can find the OS version by simply looking at `ProductVersion`.
254    unsafe { string_version_key(cf_handle, plist, c"ProductVersion") }
255        .expect("expected ProductVersion in SystemVersion.plist")
256}
257
258/// Look up a string key in a CFDictionary, and convert it to an [`OSVersion`].
259unsafe fn string_version_key(
260    cf_handle: &CFHandle,
261    plist: CFDictionaryRef,
262    lookup_key: &CStr,
263) -> Option<OSVersion> {
264    let cf_lookup_key = unsafe {
265        cf_handle.CFStringCreateWithCStringNoCopy(
266            kCFAllocatorDefault,
267            lookup_key.as_ptr(),
268            kCFStringEncodingUTF8,
269            cf_handle.kCFAllocatorNull(),
270        )
271    };
272    assert!(!cf_lookup_key.is_null(), "failed creating CFString");
273    let _lookup_key_release = Deferred(|| unsafe { cf_handle.CFRelease(cf_lookup_key) });
274
275    let value: CFTypeRef =
276        unsafe { cf_handle.CFDictionaryGetValue(plist, cf_lookup_key) }.cast_mut();
277    // `CFDictionaryGetValue` is a "getter", so we should not release,
278    // the value is held alive internally by the CFDictionary, see:
279    // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmPractical.html#//apple_ref/doc/uid/TP40004447-SW12
280    if value.is_null() {
281        return None;
282    }
283
284    assert_eq!(
285        unsafe { cf_handle.CFGetTypeID(value) },
286        unsafe { cf_handle.CFStringGetTypeID() },
287        "key in SystemVersion.plist must be a string"
288    );
289    let value: CFStringRef = value.cast();
290
291    let mut version_str = [0u8; 32];
292    let ret = unsafe {
293        cf_handle.CFStringGetCString(
294            value,
295            version_str.as_mut_ptr().cast::<c_char>(),
296            version_str.len() as CFIndex,
297            kCFStringEncodingUTF8,
298        )
299    };
300    assert_ne!(ret, 0, "failed getting string from CFString");
301
302    let version_str =
303        CStr::from_bytes_until_nul(&version_str).expect("failed converting CFString to CStr");
304
305    Some(parse_os_version(version_str.to_bytes()).unwrap_or_else(|err| {
306        panic!(
307            "failed parsing version from PList ({}): {err}",
308            ByteStr::new(version_str.to_bytes())
309        )
310    }))
311}
312
313/// Parse an OS version from a bytestring like b"10.1" or b"14.3.7".
314fn parse_os_version(version: &[u8]) -> Result<OSVersion, ParseIntError> {
315    if let Some((major, minor)) = version.split_once(|&b| b == b'.') {
316        let major = u16::from_ascii(major)?;
317        if let Some((minor, patch)) = minor.split_once(|&b| b == b'.') {
318            let minor = u8::from_ascii(minor)?;
319            let patch = u8::from_ascii(patch)?;
320            Ok(pack_os_version(major, minor, patch))
321        } else {
322            let minor = u8::from_ascii(minor)?;
323            Ok(pack_os_version(major, minor, 0))
324        }
325    } else {
326        let major = u16::from_ascii(version)?;
327        Ok(pack_os_version(major, 0, 0))
328    }
329}
330
331/// Get a path relative to the root directory in which all files for the current env are located.
332fn root_relative(path: &str) -> Cow<'_, Path> {
333    if cfg!(target_abi = "sim") {
334        let mut root = PathBuf::from(env::var_os("IPHONE_SIMULATOR_ROOT").expect(
335            "environment variable `IPHONE_SIMULATOR_ROOT` must be set when executing under simulator",
336        ));
337        // Convert absolute path to relative path, to make the `.push` work as expected.
338        root.push(Path::new(path).strip_prefix("/").unwrap());
339        root.into()
340    } else {
341        Path::new(path).into()
342    }
343}
344
345struct Deferred<F: FnMut()>(F);
346
347impl<F: FnMut()> Drop for Deferred<F> {
348    fn drop(&mut self) {
349        (self.0)();
350    }
351}