about summary refs log tree commit diff
diff options
context:
space:
mode:
authorLeón Orell Valerian Liehr <me@fmease.dev>2025-09-05 22:47:17 +0200
committerGitHub <noreply@github.com>2025-09-05 22:47:17 +0200
commit349fbba24f0158c1ed293658c31de5c8a0954e5a (patch)
tree5cac37fc5b5f62e4f92156d453c9f4efe2172ef6
parent99317ef14d0be42fa4039eea7c5ce50cb4e9aee7 (diff)
parent846d6a4466813f3ad12e2bf15db4071f22b8ae0b (diff)
downloadrust-349fbba24f0158c1ed293658c31de5c8a0954e5a.tar.gz
rust-349fbba24f0158c1ed293658c31de5c8a0954e5a.zip
Rollup merge of #138944 - madsmtm:apple_os_version_check, r=tgross35
Add `__isPlatformVersionAtLeast` and `__isOSVersionAtLeast` symbols

## Motivation

When Objective-C code uses ```@available(...)`,`` Clang inserts a call to [`__isPlatformVersionAtLeast`](https://github.com/llvm/llvm-project/blob/llvmorg-20.1.0/compiler-rt/lib/builtins/os_version_check.c#L276) (`__isOSVersionAtLeast` in older Clang versions). These symbols not being available sometimes ends up causing linker errors. See the new test `tests/run-make/apple-c-available-links` for a minimal reproducer.

The workaround is to link `libclang_rt.osx.a`, see e.g. https://github.com/alexcrichton/curl-rust/issues/279. But that's very difficult for users to figure out (and the backreferences to that issue indicates that people are still running into this in their own projects every so often).

For another recent example, this is preventing `rustc` from using LLVM assertions on macOS, see https://github.com/rust-lang/rust/pull/62592#issuecomment-510670657 and https://github.com/rust-lang/rust/pull/134275#issuecomment-2543067830.

It is also a blocker for [setting the correct minimum OS version in `cc-rs`](https://github.com/rust-lang/rust/issues/136113), since fixing this in `cc-rs` might end up introducing linker errors in places where we weren't before (by default, if using e.g. ```@available(macos`` 10.15, *)`, the symbol usually happens to be left out, since `clang` defaults to compiling for the host macOS version, and thus things _seem_ to work - but the availability check actually compiles down to nothing, which is a huge correctness footgun for running on older OSes).

(My super secret evil agenda is also to expose some variant of ```@available``` in Rust's `std` after https://github.com/rust-lang/rfcs/pull/3750 progresses further, will probably file an ACP for this later. But I believe this PR has value regardless of those future plans, since we'd be making C/Objective-C/Swift interop easier).

## Solution

Implement `__isPlatformVersionAtLeast` and `__isOSVersionAtLeast` as part of the "public ABI" that `std` exposes.

**This is insta-stable**, in the same sense that additions to `compiler-builtins` are insta-stable, though the availability of these symbols can probably be considered a "quality of implementation" detail rather than a stable promise.

I originally proposed to implement this in `compiler-builtins`, see https://github.com/rust-lang/compiler-builtins/pull/794, but we discussed moving it to `std` instead ([Zulip thread](https://rust-lang.zulipchat.com/#narrow/channel/219381-t-libs/topic/Provide.20.60__isPlatformVersionAtLeast.60.20in.20.60std.60.3F/with/507880717)), which makes the implementation substantially simpler, and we avoid gnarly issues with requiring the user to link `libSystem.dylib` (since `std` unconditionally does that).

Note that this does not solve the linker errors for (pure) `#![no_std]` users, but that's _probably_ fine, if you are using ```@available``` to test the OS version on Apple platforms, you're likely also using `std` (and it is still possible to work around by linking `libclang_rt.*.a`).

A thing to note about the implementation, I've choosen to stray a bit from LLVM's upstream implementation, and not use `_availability_version_check` since [it has problems when compiling with an older SDK](https://github.com/llvm/llvm-project/issues/64227). Instead, we use `sysctl kern.osproductversion` when available to still avoid the costly PList lookup in most cases, but still with a fall back to the PList lookup when that is not available (with the PList fallback being is similar to LLVM's implementation).

## Testing

Apple has a lot of different "modes" that they can run binaries in, which can be a bit difficult to find your bearings in, but I've tried to be as thorough as I could in testing them all.

Tested using roughly the equivalent of `./x test library/std -- platform_version` on the following configurations:
- macOS 14.7.3 on a Macbook Pro M2
    - `aarch64-apple-darwin`
    - `x86_64-apple-darwin` (under Rosetta)
    - `aarch64-apple-ios-macabi`
    - `x86_64-apple-ios-macabi` (under Rosetta)
    - `aarch64-apple-ios` (using Xcode's "Designed for iPad" setting)
    - `aarch64-apple-ios-sim` (in iOS Simulator, as iPhone with iOS 17.5)
    - `aarch64-apple-ios-sim` (in iOS Simulator, as iPad with iOS 18.2)
    - `aarch64-apple-tvos-sim` (in tvOS Simulator)
    - `aarch64-apple-watchos-sim` (in watchOS Simulator)
    - `aarch64-apple-ios-sim` (in visionOS simulator, using Xcode's "Designed for iPad" setting)
    - `aarch64-apple-visionos-sim` (in visionOS Simulator)
- macOS 15.3.1 VM
    - `aarch64-apple-darwin`
    - `aarch64-apple-ios-macabi`
- macOS 10.12.6 on an Intel Macbook from 2013
    - `x86_64-apple-darwin`
    - `i686-apple-darwin`
    - `x86_64-apple-ios` (in iOS Simulator)
- iOS 9.3.6 on a 1st generation iPad Mini
    - `armv7-apple-ios` with an older compiler

Along with manually inspecting the output of `version_from_sysctl()` and `version_from_plist()`, and verifying that they actually match what's expected.

I believe the only real omissions here would be:
- `aarch64-apple-ios` on a newer iPhone that has `sysctl` available (iOS 11.4 or above).
- `aarch64-apple-ios` on a Vision Pro using Xcode's "Designed for iPad" setting.

But I don't have the hardware available to test those.

``@rustbot`` label O-apple A-linkage -T-compiler -A-meta -A-run-make

try-job: aarch64-apple
-rw-r--r--compiler/rustc_symbol_mangling/src/v0.rs8
-rw-r--r--library/std/src/lib.rs2
-rw-r--r--library/std/src/sys/mod.rs1
-rw-r--r--library/std/src/sys/platform_version/darwin/core_foundation.rs180
-rw-r--r--library/std/src/sys/platform_version/darwin/mod.rs351
-rw-r--r--library/std/src/sys/platform_version/darwin/public_extern.rs151
-rw-r--r--library/std/src/sys/platform_version/darwin/tests.rs379
-rw-r--r--library/std/src/sys/platform_version/mod.rs13
-rw-r--r--tests/run-make/apple-c-available-links/foo.c22
-rw-r--r--tests/run-make/apple-c-available-links/main.rs7
-rw-r--r--tests/run-make/apple-c-available-links/rmake.rs14
-rw-r--r--triagebot.toml1
12 files changed, 1127 insertions, 2 deletions
diff --git a/compiler/rustc_symbol_mangling/src/v0.rs b/compiler/rustc_symbol_mangling/src/v0.rs
index 0cbd48ba08c..0655c2d5e81 100644
--- a/compiler/rustc_symbol_mangling/src/v0.rs
+++ b/compiler/rustc_symbol_mangling/src/v0.rs
@@ -82,9 +82,13 @@ pub(super) fn mangle<'tcx>(
 }
 
 pub fn mangle_internal_symbol<'tcx>(tcx: TyCtxt<'tcx>, item_name: &str) -> String {
-    if item_name == "rust_eh_personality" {
+    match item_name {
         // rust_eh_personality must not be renamed as LLVM hard-codes the name
-        return "rust_eh_personality".to_owned();
+        "rust_eh_personality" => return item_name.to_owned(),
+        // Apple availability symbols need to not be mangled to be usable by
+        // C/Objective-C code.
+        "__isPlatformVersionAtLeast" | "__isOSVersionAtLeast" => return item_name.to_owned(),
+        _ => {}
     }
 
     let prefix = "_R";
diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs
index 99b380c4793..97db0d6ab75 100644
--- a/library/std/src/lib.rs
+++ b/library/std/src/lib.rs
@@ -354,6 +354,7 @@
 #![feature(hasher_prefixfree_extras)]
 #![feature(hashmap_internals)]
 #![feature(hint_must_use)]
+#![feature(int_from_ascii)]
 #![feature(ip)]
 #![feature(lazy_get)]
 #![feature(maybe_uninit_slice)]
@@ -369,6 +370,7 @@
 #![feature(slice_internals)]
 #![feature(slice_ptr_get)]
 #![feature(slice_range)]
+#![feature(slice_split_once)]
 #![feature(std_internals)]
 #![feature(str_internals)]
 #![feature(sync_unsafe_cell)]
diff --git a/library/std/src/sys/mod.rs b/library/std/src/sys/mod.rs
index 6324c1a232a..8c115015580 100644
--- a/library/std/src/sys/mod.rs
+++ b/library/std/src/sys/mod.rs
@@ -26,6 +26,7 @@ pub mod io;
 pub mod net;
 pub mod os_str;
 pub mod path;
+pub mod platform_version;
 pub mod process;
 pub mod random;
 pub mod stdio;
diff --git a/library/std/src/sys/platform_version/darwin/core_foundation.rs b/library/std/src/sys/platform_version/darwin/core_foundation.rs
new file mode 100644
index 00000000000..1e0d15fcf66
--- /dev/null
+++ b/library/std/src/sys/platform_version/darwin/core_foundation.rs
@@ -0,0 +1,180 @@
+//! Minimal utilities for interfacing with a dynamically loaded CoreFoundation.
+#![allow(non_snake_case, non_upper_case_globals)]
+use super::root_relative;
+use crate::ffi::{CStr, c_char, c_void};
+use crate::ptr::null_mut;
+use crate::sys::common::small_c_string::run_path_with_cstr;
+
+// MacTypes.h
+pub(super) type Boolean = u8;
+// CoreFoundation/CFBase.h
+pub(super) type CFTypeID = usize;
+pub(super) type CFOptionFlags = usize;
+pub(super) type CFIndex = isize;
+pub(super) type CFTypeRef = *mut c_void;
+pub(super) type CFAllocatorRef = CFTypeRef;
+pub(super) const kCFAllocatorDefault: CFAllocatorRef = null_mut();
+// CoreFoundation/CFError.h
+pub(super) type CFErrorRef = CFTypeRef;
+// CoreFoundation/CFData.h
+pub(super) type CFDataRef = CFTypeRef;
+// CoreFoundation/CFPropertyList.h
+pub(super) const kCFPropertyListImmutable: CFOptionFlags = 0;
+pub(super) type CFPropertyListFormat = CFIndex;
+pub(super) type CFPropertyListRef = CFTypeRef;
+// CoreFoundation/CFString.h
+pub(super) type CFStringRef = CFTypeRef;
+pub(super) type CFStringEncoding = u32;
+pub(super) const kCFStringEncodingUTF8: CFStringEncoding = 0x08000100;
+// CoreFoundation/CFDictionary.h
+pub(super) type CFDictionaryRef = CFTypeRef;
+
+/// An open handle to the dynamically loaded CoreFoundation framework.
+///
+/// This is `dlopen`ed, and later `dlclose`d. This is done to try to avoid
+/// "leaking" the CoreFoundation symbols to the rest of the user's binary if
+/// they decided to not link CoreFoundation themselves.
+///
+/// It is also faster to look up symbols directly via this handle than with
+/// `RTLD_DEFAULT`.
+pub(super) struct CFHandle(*mut c_void);
+
+macro_rules! dlsym_fn {
+    (
+        unsafe fn $name:ident($($param:ident: $param_ty:ty),* $(,)?) $(-> $ret:ty)?;
+    ) => {
+        pub(super) unsafe fn $name(&self, $($param: $param_ty),*) $(-> $ret)? {
+            let ptr = unsafe {
+                libc::dlsym(
+                    self.0,
+                    concat!(stringify!($name), '\0').as_bytes().as_ptr().cast(),
+                )
+            };
+            if ptr.is_null() {
+                let err = unsafe { CStr::from_ptr(libc::dlerror()) };
+                panic!("could not find function {}: {err:?}", stringify!($name));
+            }
+
+            // SAFETY: Just checked that the symbol isn't NULL, and macro invoker verifies that
+            // the signature is correct.
+            let fnptr = unsafe {
+                crate::mem::transmute::<
+                    *mut c_void,
+                    unsafe extern "C" fn($($param_ty),*) $(-> $ret)?,
+                >(ptr)
+            };
+
+            // SAFETY: Upheld by caller.
+            unsafe { fnptr($($param),*) }
+        }
+    };
+}
+
+impl CFHandle {
+    /// Link to the CoreFoundation dylib, and look up symbols from that.
+    pub(super) fn new() -> Self {
+        // We explicitly use non-versioned path here, to allow this to work on older iOS devices.
+        let cf_path =
+            root_relative("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation");
+
+        let handle = run_path_with_cstr(&cf_path, &|path| unsafe {
+            Ok(libc::dlopen(path.as_ptr(), libc::RTLD_LAZY | libc::RTLD_LOCAL))
+        })
+        .expect("failed allocating string");
+
+        if handle.is_null() {
+            let err = unsafe { CStr::from_ptr(libc::dlerror()) };
+            panic!("could not open CoreFoundation.framework: {err:?}");
+        }
+
+        Self(handle)
+    }
+
+    pub(super) fn kCFAllocatorNull(&self) -> CFAllocatorRef {
+        // Available: in all CF versions.
+        let static_ptr = unsafe { libc::dlsym(self.0, c"kCFAllocatorNull".as_ptr()) };
+        if static_ptr.is_null() {
+            let err = unsafe { CStr::from_ptr(libc::dlerror()) };
+            panic!("could not find kCFAllocatorNull: {err:?}");
+        }
+        unsafe { *static_ptr.cast() }
+    }
+
+    // CoreFoundation/CFBase.h
+    dlsym_fn!(
+        // Available: in all CF versions.
+        unsafe fn CFRelease(cf: CFTypeRef);
+    );
+    dlsym_fn!(
+        // Available: in all CF versions.
+        unsafe fn CFGetTypeID(cf: CFTypeRef) -> CFTypeID;
+    );
+
+    // CoreFoundation/CFData.h
+    dlsym_fn!(
+        // Available: in all CF versions.
+        unsafe fn CFDataCreateWithBytesNoCopy(
+            allocator: CFAllocatorRef,
+            bytes: *const u8,
+            length: CFIndex,
+            bytes_deallocator: CFAllocatorRef,
+        ) -> CFDataRef;
+    );
+
+    // CoreFoundation/CFPropertyList.h
+    dlsym_fn!(
+        // Available: since macOS 10.6.
+        unsafe fn CFPropertyListCreateWithData(
+            allocator: CFAllocatorRef,
+            data: CFDataRef,
+            options: CFOptionFlags,
+            format: *mut CFPropertyListFormat,
+            error: *mut CFErrorRef,
+        ) -> CFPropertyListRef;
+    );
+
+    // CoreFoundation/CFString.h
+    dlsym_fn!(
+        // Available: in all CF versions.
+        unsafe fn CFStringGetTypeID() -> CFTypeID;
+    );
+    dlsym_fn!(
+        // Available: in all CF versions.
+        unsafe fn CFStringCreateWithCStringNoCopy(
+            alloc: CFAllocatorRef,
+            c_str: *const c_char,
+            encoding: CFStringEncoding,
+            contents_deallocator: CFAllocatorRef,
+        ) -> CFStringRef;
+    );
+    dlsym_fn!(
+        // Available: in all CF versions.
+        unsafe fn CFStringGetCString(
+            the_string: CFStringRef,
+            buffer: *mut c_char,
+            buffer_size: CFIndex,
+            encoding: CFStringEncoding,
+        ) -> Boolean;
+    );
+
+    // CoreFoundation/CFDictionary.h
+    dlsym_fn!(
+        // Available: in all CF versions.
+        unsafe fn CFDictionaryGetTypeID() -> CFTypeID;
+    );
+    dlsym_fn!(
+        // Available: in all CF versions.
+        unsafe fn CFDictionaryGetValue(
+            the_dict: CFDictionaryRef,
+            key: *const c_void,
+        ) -> *const c_void;
+    );
+}
+
+impl Drop for CFHandle {
+    fn drop(&mut self) {
+        // Ignore errors when closing. This is also what `libloading` does:
+        // https://docs.rs/libloading/0.8.6/src/libloading/os/unix/mod.rs.html#374
+        let _ = unsafe { libc::dlclose(self.0) };
+    }
+}
diff --git a/library/std/src/sys/platform_version/darwin/mod.rs b/library/std/src/sys/platform_version/darwin/mod.rs
new file mode 100644
index 00000000000..06b97fcdef4
--- /dev/null
+++ b/library/std/src/sys/platform_version/darwin/mod.rs
@@ -0,0 +1,351 @@
+use self::core_foundation::{
+    CFDictionaryRef, CFHandle, CFIndex, CFStringRef, CFTypeRef, kCFAllocatorDefault,
+    kCFPropertyListImmutable, kCFStringEncodingUTF8,
+};
+use crate::borrow::Cow;
+use crate::bstr::ByteStr;
+use crate::ffi::{CStr, c_char};
+use crate::num::{NonZero, ParseIntError};
+use crate::path::{Path, PathBuf};
+use crate::ptr::null_mut;
+use crate::sync::atomic::{AtomicU32, Ordering};
+use crate::{env, fs};
+
+mod core_foundation;
+mod public_extern;
+#[cfg(test)]
+mod tests;
+
+/// The version of the operating system.
+///
+/// We use a packed u32 here to allow for fast comparisons and to match Mach-O's `LC_BUILD_VERSION`.
+type OSVersion = u32;
+
+/// Combine parts of a version into an [`OSVersion`].
+///
+/// The size of the parts are inherently limited by Mach-O's `LC_BUILD_VERSION`.
+#[inline]
+const fn pack_os_version(major: u16, minor: u8, patch: u8) -> OSVersion {
+    let (major, minor, patch) = (major as u32, minor as u32, patch as u32);
+    (major << 16) | (minor << 8) | patch
+}
+
+/// [`pack_os_version`], but takes `i32` and saturates.
+///
+/// Instead of using e.g. `major as u16`, which truncates.
+#[inline]
+fn pack_i32_os_version(major: i32, minor: i32, patch: i32) -> OSVersion {
+    let major: u16 = major.try_into().unwrap_or(u16::MAX);
+    let minor: u8 = minor.try_into().unwrap_or(u8::MAX);
+    let patch: u8 = patch.try_into().unwrap_or(u8::MAX);
+    pack_os_version(major, minor, patch)
+}
+
+/// Get the current OS version, packed according to [`pack_os_version`].
+///
+/// # Semantics
+///
+/// The reported version on macOS might be 10.16 if the SDK version of the binary is less than 11.0.
+/// This is a workaround that Apple implemented to handle applications that assumed that macOS
+/// versions would always start with "10", see:
+/// <https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/libsyscall/wrappers/system-version-compat.c>
+///
+/// It _is_ possible to get the real version regardless of the SDK version of the binary, this is
+/// what Zig does:
+/// <https://github.com/ziglang/zig/blob/0.13.0/lib/std/zig/system/darwin/macos.zig>
+///
+/// We choose to not do that, and instead follow Apple's behaviour here, and return 10.16 when
+/// compiled with an older SDK; the user should instead upgrade their tooling.
+///
+/// NOTE: `rustc` currently doesn't set the right SDK version when linking with ld64, so this will
+/// have the wrong behaviour with `-Clinker=ld` on x86_64. But that's a `rustc` bug:
+/// <https://github.com/rust-lang/rust/issues/129432>
+#[inline]
+fn current_version() -> OSVersion {
+    // Cache the lookup for performance.
+    //
+    // 0.0.0 is never going to be a valid version ("vtool" reports "n/a" on 0 versions), so we use
+    // that as our sentinel value.
+    static CURRENT_VERSION: AtomicU32 = AtomicU32::new(0);
+
+    // We use relaxed atomics instead of e.g. a `Once`, it doesn't matter if multiple threads end up
+    // racing to read or write the version, `lookup_version` should be idempotent and always return
+    // the same value.
+    //
+    // `compiler-rt` uses `dispatch_once`, but that's overkill for the reasons above.
+    let version = CURRENT_VERSION.load(Ordering::Relaxed);
+    if version == 0 {
+        let version = lookup_version().get();
+        CURRENT_VERSION.store(version, Ordering::Relaxed);
+        version
+    } else {
+        version
+    }
+}
+
+/// Look up the os version.
+///
+/// # Aborts
+///
+/// Aborts if reading or parsing the version fails (or if the system was out of memory).
+///
+/// We deliberately choose to abort, as having this silently return an invalid OS version would be
+/// impossible for a user to debug.
+// The lookup is costly and should be on the cold path because of the cache in `current_version`.
+#[cold]
+// Micro-optimization: We use `extern "C"` to abort on panic, allowing `current_version` (inlined)
+// to be free of unwind handling. Aborting is required for `__isPlatformVersionAtLeast` anyhow.
+extern "C" fn lookup_version() -> NonZero<OSVersion> {
+    // Try to read from `sysctl` first (faster), but if that fails, fall back to reading the
+    // property list (this is roughly what `_availability_version_check` does internally).
+    let version = version_from_sysctl().unwrap_or_else(version_from_plist);
+
+    // Use `NonZero` to try to make it clearer to the optimizer that this will never return 0.
+    NonZero::new(version).expect("version cannot be 0.0.0")
+}
+
+/// Read the version from `kern.osproductversion` or `kern.iossupportversion`.
+///
+/// This is faster than `version_from_plist`, since it doesn't need to invoke `dlsym`.
+fn version_from_sysctl() -> Option<OSVersion> {
+    // This won't work in the simulator, as `kern.osproductversion` returns the host macOS version,
+    // and `kern.iossupportversion` returns the host macOS' iOSSupportVersion (while you can run
+    // simulators with many different iOS versions).
+    if cfg!(target_abi = "sim") {
+        // Fall back to `version_from_plist` on these targets.
+        return None;
+    }
+
+    let sysctl_version = |name: &CStr| {
+        let mut buf: [u8; 32] = [0; 32];
+        let mut size = buf.len();
+        let ptr = buf.as_mut_ptr().cast();
+        let ret = unsafe { libc::sysctlbyname(name.as_ptr(), ptr, &mut size, null_mut(), 0) };
+        if ret != 0 {
+            // This sysctl is not available.
+            return None;
+        }
+        let buf = &buf[..(size - 1)];
+
+        if buf.is_empty() {
+            // The buffer may be empty when using `kern.iossupportversion` on an actual iOS device,
+            // or on visionOS when running under "Designed for iPad".
+            //
+            // In that case, fall back to `kern.osproductversion`.
+            return None;
+        }
+
+        Some(parse_os_version(buf).unwrap_or_else(|err| {
+            panic!("failed parsing version from sysctl ({}): {err}", ByteStr::new(buf))
+        }))
+    };
+
+    // When `target_os = "ios"`, we may be in many different states:
+    // - Native iOS device.
+    // - iOS Simulator.
+    // - Mac Catalyst.
+    // - Mac + "Designed for iPad".
+    // - Native visionOS device + "Designed for iPad".
+    // - visionOS simulator + "Designed for iPad".
+    //
+    // Of these, only native, Mac Catalyst and simulators can be differentiated at compile-time
+    // (with `target_abi = ""`, `target_abi = "macabi"` and `target_abi = "sim"` respectively).
+    //
+    // That is, "Designed for iPad" will act as iOS at compile-time, but the `ProductVersion` will
+    // still be the host macOS or visionOS version.
+    //
+    // Furthermore, we can't even reliably differentiate between these at runtime, since
+    // `dyld_get_active_platform` isn't publicly available.
+    //
+    // Fortunately, we won't need to know any of that; we can simply attempt to get the
+    // `iOSSupportVersion` (which may be set on native iOS too, but then it will be set to the host
+    // iOS version), and if that fails, fall back to the `ProductVersion`.
+    if cfg!(target_os = "ios") {
+        // https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/bsd/kern/kern_sysctl.c#L2077-L2100
+        if let Some(ios_support_version) = sysctl_version(c"kern.iossupportversion") {
+            return Some(ios_support_version);
+        }
+
+        // On Mac Catalyst, if we failed looking up `iOSSupportVersion`, we don't want to
+        // accidentally fall back to `ProductVersion`.
+        if cfg!(target_abi = "macabi") {
+            return None;
+        }
+    }
+
+    // Introduced in macOS 10.13.4.
+    // https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/bsd/kern/kern_sysctl.c#L2015-L2051
+    sysctl_version(c"kern.osproductversion")
+}
+
+/// Look up the current OS version(s) from `/System/Library/CoreServices/SystemVersion.plist`.
+///
+/// More specifically, from the `ProductVersion` and `iOSSupportVersion` keys, and from
+/// `$IPHONE_SIMULATOR_ROOT/System/Library/CoreServices/SystemVersion.plist` on the simulator.
+///
+/// This file was introduced in macOS 10.3, which is well below the minimum supported version by
+/// `rustc`, which is (at the time of writing) macOS 10.12.
+///
+/// # Implementation
+///
+/// We do roughly the same thing in here as `compiler-rt`, and dynamically look up CoreFoundation
+/// utilities for parsing PLists (to avoid having to re-implement that in here, as pulling in a full
+/// PList parser into `std` seems costly).
+///
+/// If this is found to be undesirable, we _could_ possibly hack it by parsing the PList manually
+/// (it seems to use the plain-text "xml1" encoding/format in all versions), but that seems brittle.
+fn version_from_plist() -> OSVersion {
+    // Read `SystemVersion.plist`. Always present on Apple platforms, reading it cannot fail.
+    let path = root_relative("/System/Library/CoreServices/SystemVersion.plist");
+    let plist_buffer = fs::read(&path).unwrap_or_else(|e| panic!("failed reading {path:?}: {e}"));
+    let cf_handle = CFHandle::new();
+    parse_version_from_plist(&cf_handle, &plist_buffer)
+}
+
+/// Parse OS version from the given PList.
+///
+/// Split out from [`version_from_plist`] to allow for testing.
+fn parse_version_from_plist(cf_handle: &CFHandle, plist_buffer: &[u8]) -> OSVersion {
+    let plist_data = unsafe {
+        cf_handle.CFDataCreateWithBytesNoCopy(
+            kCFAllocatorDefault,
+            plist_buffer.as_ptr(),
+            plist_buffer.len() as CFIndex,
+            cf_handle.kCFAllocatorNull(),
+        )
+    };
+    assert!(!plist_data.is_null(), "failed creating CFData");
+    let _plist_data_release = Deferred(|| unsafe { cf_handle.CFRelease(plist_data) });
+
+    let plist = unsafe {
+        cf_handle.CFPropertyListCreateWithData(
+            kCFAllocatorDefault,
+            plist_data,
+            kCFPropertyListImmutable,
+            null_mut(), // Don't care about the format of the PList.
+            null_mut(), // Don't care about the error data.
+        )
+    };
+    assert!(!plist.is_null(), "failed reading PList in SystemVersion.plist");
+    let _plist_release = Deferred(|| unsafe { cf_handle.CFRelease(plist) });
+
+    assert_eq!(
+        unsafe { cf_handle.CFGetTypeID(plist) },
+        unsafe { cf_handle.CFDictionaryGetTypeID() },
+        "SystemVersion.plist did not contain a dictionary at the top level"
+    );
+    let plist: CFDictionaryRef = plist.cast();
+
+    // Same logic as in `version_from_sysctl`.
+    if cfg!(target_os = "ios") {
+        if let Some(ios_support_version) =
+            unsafe { string_version_key(cf_handle, plist, c"iOSSupportVersion") }
+        {
+            return ios_support_version;
+        }
+
+        // Force Mac Catalyst to use iOSSupportVersion (do not fall back to ProductVersion).
+        if cfg!(target_abi = "macabi") {
+            panic!("expected iOSSupportVersion in SystemVersion.plist");
+        }
+    }
+
+    // On all other platforms, we can find the OS version by simply looking at `ProductVersion`.
+    unsafe { string_version_key(cf_handle, plist, c"ProductVersion") }
+        .expect("expected ProductVersion in SystemVersion.plist")
+}
+
+/// Look up a string key in a CFDictionary, and convert it to an [`OSVersion`].
+unsafe fn string_version_key(
+    cf_handle: &CFHandle,
+    plist: CFDictionaryRef,
+    lookup_key: &CStr,
+) -> Option<OSVersion> {
+    let cf_lookup_key = unsafe {
+        cf_handle.CFStringCreateWithCStringNoCopy(
+            kCFAllocatorDefault,
+            lookup_key.as_ptr(),
+            kCFStringEncodingUTF8,
+            cf_handle.kCFAllocatorNull(),
+        )
+    };
+    assert!(!cf_lookup_key.is_null(), "failed creating CFString");
+    let _lookup_key_release = Deferred(|| unsafe { cf_handle.CFRelease(cf_lookup_key) });
+
+    let value: CFTypeRef =
+        unsafe { cf_handle.CFDictionaryGetValue(plist, cf_lookup_key) }.cast_mut();
+    // `CFDictionaryGetValue` is a "getter", so we should not release,
+    // the value is held alive internally by the CFDictionary, see:
+    // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmPractical.html#//apple_ref/doc/uid/TP40004447-SW12
+    if value.is_null() {
+        return None;
+    }
+
+    assert_eq!(
+        unsafe { cf_handle.CFGetTypeID(value) },
+        unsafe { cf_handle.CFStringGetTypeID() },
+        "key in SystemVersion.plist must be a string"
+    );
+    let value: CFStringRef = value.cast();
+
+    let mut version_str = [0u8; 32];
+    let ret = unsafe {
+        cf_handle.CFStringGetCString(
+            value,
+            version_str.as_mut_ptr().cast::<c_char>(),
+            version_str.len() as CFIndex,
+            kCFStringEncodingUTF8,
+        )
+    };
+    assert_ne!(ret, 0, "failed getting string from CFString");
+
+    let version_str =
+        CStr::from_bytes_until_nul(&version_str).expect("failed converting CFString to CStr");
+
+    Some(parse_os_version(version_str.to_bytes()).unwrap_or_else(|err| {
+        panic!(
+            "failed parsing version from PList ({}): {err}",
+            ByteStr::new(version_str.to_bytes())
+        )
+    }))
+}
+
+/// Parse an OS version from a bytestring like b"10.1" or b"14.3.7".
+fn parse_os_version(version: &[u8]) -> Result<OSVersion, ParseIntError> {
+    if let Some((major, minor)) = version.split_once(|&b| b == b'.') {
+        let major = u16::from_ascii(major)?;
+        if let Some((minor, patch)) = minor.split_once(|&b| b == b'.') {
+            let minor = u8::from_ascii(minor)?;
+            let patch = u8::from_ascii(patch)?;
+            Ok(pack_os_version(major, minor, patch))
+        } else {
+            let minor = u8::from_ascii(minor)?;
+            Ok(pack_os_version(major, minor, 0))
+        }
+    } else {
+        let major = u16::from_ascii(version)?;
+        Ok(pack_os_version(major, 0, 0))
+    }
+}
+
+/// Get a path relative to the root directory in which all files for the current env are located.
+fn root_relative(path: &str) -> Cow<'_, Path> {
+    if cfg!(target_abi = "sim") {
+        let mut root = PathBuf::from(env::var_os("IPHONE_SIMULATOR_ROOT").expect(
+            "environment variable `IPHONE_SIMULATOR_ROOT` must be set when executing under simulator",
+        ));
+        // Convert absolute path to relative path, to make the `.push` work as expected.
+        root.push(Path::new(path).strip_prefix("/").unwrap());
+        root.into()
+    } else {
+        Path::new(path).into()
+    }
+}
+
+struct Deferred<F: FnMut()>(F);
+
+impl<F: FnMut()> Drop for Deferred<F> {
+    fn drop(&mut self) {
+        (self.0)();
+    }
+}
diff --git a/library/std/src/sys/platform_version/darwin/public_extern.rs b/library/std/src/sys/platform_version/darwin/public_extern.rs
new file mode 100644
index 00000000000..967cdb4920f
--- /dev/null
+++ b/library/std/src/sys/platform_version/darwin/public_extern.rs
@@ -0,0 +1,151 @@
+//! # Runtime version checking ABI for other compilers.
+//!
+//! The symbols in this file are useful for us to expose to allow linking code written in the
+//! following languages when using their version checking functionality:
+//! - Clang's `__builtin_available` macro.
+//! - Objective-C's `@available`.
+//! - Swift's `#available`,
+//!
+//! Without Rust exposing these symbols, the user would encounter a linker error when linking to
+//! C/Objective-C/Swift libraries using these features.
+//!
+//! The presence of these symbols is mostly considered a quality-of-implementation detail, and
+//! should not be relied upon to be available. The intended effect is that linking with code built
+//! with Clang's `__builtin_available` (or similar) will continue to work. For example, we may
+//! decide to remove `__isOSVersionAtLeast` if support for Clang 11 (Xcode 11) is dropped.
+//!
+//! ## Background
+//!
+//! The original discussion of this feature can be found at:
+//! - <https://lists.llvm.org/pipermail/cfe-dev/2016-July/049851.html>
+//! - <https://reviews.llvm.org/D27827>
+//! - <https://reviews.llvm.org/D30136>
+//!
+//! And the upstream implementation of these can be found in `compiler-rt`:
+//! <https://github.com/llvm/llvm-project/blob/llvmorg-20.1.0/compiler-rt/lib/builtins/os_version_check.c>
+//!
+//! Ideally, these symbols should probably have been a part of Apple's `libSystem.dylib`, both
+//! because their implementation is quite complex, using allocation, environment variables, file
+//! access and dynamic library loading (and emitting all of this into every binary).
+//!
+//! The reason why Apple chose to not do that originally is lost to the sands of time, but a good
+//! reason would be that implementing it as part of `compiler-rt` allowed them to back-deploy this
+//! to older OSes immediately.
+//!
+//! In Rust's case, while we may provide a feature similar to `@available` in the future, we will
+//! probably do so as a macro exposed by `std` (and not as a compiler builtin). So implementing this
+//! in `std` makes sense, since then we can implement it using `std` utilities, and we can avoid
+//! having `compiler-builtins` depend on `libSystem.dylib`.
+//!
+//! This does mean that users that attempt to link C/Objective-C/Swift code _and_ use `#![no_std]`
+//! in all their crates may get a linker error because these symbols are missing. Using `no_std` is
+//! quite uncommon on Apple systems though, so it's probably fine to not support this use-case.
+//!
+//! The workaround would be to link `libclang_rt.osx.a` or otherwise use Clang's `compiler-rt`.
+//!
+//! See also discussion in <https://github.com/rust-lang/compiler-builtins/pull/794>.
+//!
+//! ## Implementation details
+//!
+//! NOTE: Since macOS 10.15, `libSystem.dylib` _has_ actually provided the undocumented
+//! `_availability_version_check` via `libxpc` for doing the version lookup (zippered, which is why
+//! it requires a platform parameter to differentiate between macOS and Mac Catalyst), though its
+//! usage may be a bit dangerous, see:
+//! - <https://reviews.llvm.org/D150397>
+//! - <https://github.com/llvm/llvm-project/issues/64227>
+//!
+//! Besides, we'd need to implement the version lookup via PList to support older versions anyhow,
+//! so we might as well use that everywhere (since it can also be optimized more after inlining).
+
+#![allow(non_snake_case)]
+
+use super::{current_version, pack_i32_os_version};
+
+/// Whether the current platform's OS version is higher than or equal to the given version.
+///
+/// The first argument is the _base_ Mach-O platform (i.e. `PLATFORM_MACOS`, `PLATFORM_IOS`, etc.,
+/// but not `PLATFORM_IOSSIMULATOR` or `PLATFORM_MACCATALYST`) of the invoking binary.
+///
+/// Arguments are specified statically by Clang. Inlining with LTO should allow the versions to be
+/// combined into a single `u32`, which should make comparisons faster, and should make the
+/// `BASE_TARGET_PLATFORM` check a no-op.
+//
+// SAFETY: The signature is the same as what Clang expects, and we export weakly to allow linking
+// both this and `libclang_rt.*.a`, similar to how `compiler-builtins` does it:
+// https://github.com/rust-lang/compiler-builtins/blob/0.1.113/src/macros.rs#L494
+//
+// NOTE: This symbol has a workaround in the compiler's symbol mangling to avoid mangling it, while
+// still not exposing it from non-cdylib (like `#[no_mangle]` would).
+#[rustc_std_internal_symbol]
+// extern "C" is correct, Clang assumes the function cannot unwind:
+// https://github.com/llvm/llvm-project/blob/llvmorg-20.1.0/clang/lib/CodeGen/CGObjC.cpp#L3980
+//
+// If an error happens in this, we instead abort the process.
+pub(super) extern "C" fn __isPlatformVersionAtLeast(
+    platform: i32,
+    major: i32,
+    minor: i32,
+    subminor: i32,
+) -> i32 {
+    let version = pack_i32_os_version(major, minor, subminor);
+
+    // Mac Catalyst is a technology that allows macOS to run in a different "mode" that closely
+    // resembles iOS (and has iOS libraries like UIKit available).
+    //
+    // (Apple has added a "Designed for iPad" mode later on that allows running iOS apps
+    // natively, but we don't need to think too much about those, since they link to
+    // iOS-specific system binaries as well).
+    //
+    // To support Mac Catalyst, Apple added the concept of a "zippered" binary, which is a single
+    // binary that can be run on both macOS and Mac Catalyst (has two `LC_BUILD_VERSION` Mach-O
+    // commands, one set to `PLATFORM_MACOS` and one to `PLATFORM_MACCATALYST`).
+    //
+    // Most system libraries are zippered, which allows re-use across macOS and Mac Catalyst.
+    // This includes the `libclang_rt.osx.a` shipped with Xcode! This means that `compiler-rt`
+    // can't statically know whether it's compiled for macOS or Mac Catalyst, and thus this new
+    // API (which replaces `__isOSVersionAtLeast`) is needed.
+    //
+    // In short:
+    //      normal  binary calls  normal  compiler-rt --> `__isOSVersionAtLeast` was enough
+    //      normal  binary calls zippered compiler-rt --> `__isPlatformVersionAtLeast` required
+    //     zippered binary calls zippered compiler-rt --> `__isPlatformOrVariantPlatformVersionAtLeast` called
+
+    // FIXME(madsmtm): `rustc` doesn't support zippered binaries yet, see rust-lang/rust#131216.
+    // But once it does, we need the pre-compiled `std` shipped with rustup to be zippered, and thus
+    // we also need to handle the `platform` difference here:
+    //
+    // if cfg!(target_os = "macos") && platform == 2 /* PLATFORM_IOS */ && cfg!(zippered) {
+    //     return (version.to_u32() <= current_ios_version()) as i32;
+    // }
+    //
+    // `__isPlatformOrVariantPlatformVersionAtLeast` would also need to be implemented.
+
+    // The base Mach-O platform for the current target.
+    const BASE_TARGET_PLATFORM: i32 = if cfg!(target_os = "macos") {
+        1 // PLATFORM_MACOS
+    } else if cfg!(target_os = "ios") {
+        2 // PLATFORM_IOS
+    } else if cfg!(target_os = "tvos") {
+        3 // PLATFORM_TVOS
+    } else if cfg!(target_os = "watchos") {
+        4 // PLATFORM_WATCHOS
+    } else if cfg!(target_os = "visionos") {
+        11 // PLATFORM_VISIONOS
+    } else {
+        0 // PLATFORM_UNKNOWN
+    };
+    debug_assert_eq!(
+        platform, BASE_TARGET_PLATFORM,
+        "invalid platform provided to __isPlatformVersionAtLeast",
+    );
+
+    (version <= current_version()) as i32
+}
+
+/// Old entry point for availability. Used when compiling with older Clang versions.
+// SAFETY: Same as for `__isPlatformVersionAtLeast`.
+#[rustc_std_internal_symbol]
+pub(super) extern "C" fn __isOSVersionAtLeast(major: i32, minor: i32, subminor: i32) -> i32 {
+    let version = pack_i32_os_version(major, minor, subminor);
+    (version <= current_version()) as i32
+}
diff --git a/library/std/src/sys/platform_version/darwin/tests.rs b/library/std/src/sys/platform_version/darwin/tests.rs
new file mode 100644
index 00000000000..76dc4482c98
--- /dev/null
+++ b/library/std/src/sys/platform_version/darwin/tests.rs
@@ -0,0 +1,379 @@
+use super::public_extern::*;
+use super::*;
+use crate::process::Command;
+
+#[test]
+fn test_general_available() {
+    // Lowest version always available.
+    assert_eq!(__isOSVersionAtLeast(0, 0, 0), 1);
+    // This high version never available.
+    assert_eq!(__isOSVersionAtLeast(9999, 99, 99), 0);
+}
+
+#[test]
+fn test_saturating() {
+    // Higher version than supported by OSVersion -> make sure we saturate.
+    assert_eq!(__isOSVersionAtLeast(0x10000, 0, 0), 0);
+}
+
+#[test]
+#[cfg_attr(not(target_os = "macos"), ignore = "`sw_vers` is only available on host macOS")]
+fn compare_against_sw_vers() {
+    let sw_vers = Command::new("sw_vers").arg("-productVersion").output().unwrap().stdout;
+    let sw_vers = String::from_utf8(sw_vers).unwrap();
+    let mut sw_vers = sw_vers.trim().split('.');
+
+    let major: i32 = sw_vers.next().unwrap().parse().unwrap();
+    let minor: i32 = sw_vers.next().unwrap_or("0").parse().unwrap();
+    let subminor: i32 = sw_vers.next().unwrap_or("0").parse().unwrap();
+    assert_eq!(sw_vers.count(), 0);
+
+    // Current version is available
+    assert_eq!(__isOSVersionAtLeast(major, minor, subminor), 1);
+
+    // One lower is available
+    assert_eq!(__isOSVersionAtLeast(major, minor, subminor.saturating_sub(1)), 1);
+    assert_eq!(__isOSVersionAtLeast(major, minor.saturating_sub(1), subminor), 1);
+    assert_eq!(__isOSVersionAtLeast(major.saturating_sub(1), minor, subminor), 1);
+
+    // One higher isn't available
+    assert_eq!(__isOSVersionAtLeast(major, minor, subminor + 1), 0);
+    assert_eq!(__isOSVersionAtLeast(major, minor + 1, subminor), 0);
+    assert_eq!(__isOSVersionAtLeast(major + 1, minor, subminor), 0);
+
+    // Test directly against the lookup
+    assert_eq!(lookup_version().get(), pack_os_version(major as _, minor as _, subminor as _));
+}
+
+#[test]
+fn sysctl_same_as_in_plist() {
+    if let Some(version) = version_from_sysctl() {
+        assert_eq!(version, version_from_plist());
+    }
+}
+
+#[test]
+fn lookup_idempotent() {
+    let version = lookup_version();
+    for _ in 0..10 {
+        assert_eq!(version, lookup_version());
+    }
+}
+
+/// Test parsing a bunch of different PLists found in the wild, to ensure that
+/// if we decide to parse it without CoreFoundation in the future, that it
+/// would continue to work, even on older platforms.
+#[test]
+fn parse_plist() {
+    #[track_caller]
+    fn check(
+        (major, minor, patch): (u16, u8, u8),
+        ios_version: Option<(u16, u8, u8)>,
+        plist: &str,
+    ) {
+        let expected = if cfg!(target_os = "ios") {
+            if let Some((ios_major, ios_minor, ios_patch)) = ios_version {
+                pack_os_version(ios_major, ios_minor, ios_patch)
+            } else if cfg!(target_abi = "macabi") {
+                // Skip checking iOS version on Mac Catalyst.
+                return;
+            } else {
+                // iOS version will be parsed from ProductVersion
+                pack_os_version(major, minor, patch)
+            }
+        } else {
+            pack_os_version(major, minor, patch)
+        };
+        let cf_handle = CFHandle::new();
+        assert_eq!(expected, parse_version_from_plist(&cf_handle, plist.as_bytes()));
+    }
+
+    // macOS 10.3.0
+    let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+        <plist version="1.0">
+        <dict>
+            <key>ProductBuildVersion</key>
+            <string>7B85</string>
+            <key>ProductCopyright</key>
+            <string>Apple Computer, Inc. 1983-2003</string>
+            <key>ProductName</key>
+            <string>Mac OS X</string>
+            <key>ProductUserVisibleVersion</key>
+            <string>10.3</string>
+            <key>ProductVersion</key>
+            <string>10.3</string>
+        </dict>
+        </plist>
+    "#;
+    check((10, 3, 0), None, plist);
+
+    // macOS 10.7.5
+    let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+        <plist version="1.0">
+        <dict>
+            <key>ProductBuildVersion</key>
+            <string>11G63</string>
+            <key>ProductCopyright</key>
+            <string>1983-2012 Apple Inc.</string>
+            <key>ProductName</key>
+            <string>Mac OS X</string>
+            <key>ProductUserVisibleVersion</key>
+            <string>10.7.5</string>
+            <key>ProductVersion</key>
+            <string>10.7.5</string>
+        </dict>
+        </plist>
+    "#;
+    check((10, 7, 5), None, plist);
+
+    // macOS 14.7.4
+    let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+        <plist version="1.0">
+        <dict>
+            <key>BuildID</key>
+            <string>6A558D8A-E2EA-11EF-A1D3-6222CAA672A8</string>
+            <key>ProductBuildVersion</key>
+            <string>23H420</string>
+            <key>ProductCopyright</key>
+            <string>1983-2025 Apple Inc.</string>
+            <key>ProductName</key>
+            <string>macOS</string>
+            <key>ProductUserVisibleVersion</key>
+            <string>14.7.4</string>
+            <key>ProductVersion</key>
+            <string>14.7.4</string>
+            <key>iOSSupportVersion</key>
+            <string>17.7</string>
+        </dict>
+        </plist>
+    "#;
+    check((14, 7, 4), Some((17, 7, 0)), plist);
+
+    // SystemVersionCompat.plist on macOS 14.7.4
+    let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+        <plist version="1.0">
+        <dict>
+            <key>BuildID</key>
+            <string>6A558D8A-E2EA-11EF-A1D3-6222CAA672A8</string>
+            <key>ProductBuildVersion</key>
+            <string>23H420</string>
+            <key>ProductCopyright</key>
+            <string>1983-2025 Apple Inc.</string>
+            <key>ProductName</key>
+            <string>Mac OS X</string>
+            <key>ProductUserVisibleVersion</key>
+            <string>10.16</string>
+            <key>ProductVersion</key>
+            <string>10.16</string>
+            <key>iOSSupportVersion</key>
+            <string>17.7</string>
+        </dict>
+        </plist>
+    "#;
+    check((10, 16, 0), Some((17, 7, 0)), plist);
+
+    // macOS 15.4 Beta 24E5238a
+    let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+        <plist version="1.0">
+        <dict>
+            <key>BuildID</key>
+            <string>67A50F62-00DA-11F0-BDB6-F99BB8310D2A</string>
+            <key>ProductBuildVersion</key>
+            <string>24E5238a</string>
+            <key>ProductCopyright</key>
+            <string>1983-2025 Apple Inc.</string>
+            <key>ProductName</key>
+            <string>macOS</string>
+            <key>ProductUserVisibleVersion</key>
+            <string>15.4</string>
+            <key>ProductVersion</key>
+            <string>15.4</string>
+            <key>iOSSupportVersion</key>
+            <string>18.4</string>
+        </dict>
+        </plist>
+    "#;
+    check((15, 4, 0), Some((18, 4, 0)), plist);
+
+    // iOS Simulator 17.5
+    let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+        <plist version="1.0">
+        <dict>
+            <key>BuildID</key>
+            <string>210B8A2C-09C3-11EF-9DB8-273A64AEFA1C</string>
+            <key>ProductBuildVersion</key>
+            <string>21F79</string>
+            <key>ProductCopyright</key>
+            <string>1983-2024 Apple Inc.</string>
+            <key>ProductName</key>
+            <string>iPhone OS</string>
+            <key>ProductVersion</key>
+            <string>17.5</string>
+        </dict>
+        </plist>
+    "#;
+    check((17, 5, 0), None, plist);
+
+    // visionOS Simulator 2.3
+    let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+        <plist version="1.0">
+        <dict>
+            <key>BuildID</key>
+            <string>57CEFDE6-D079-11EF-837C-8B8C7961D0AC</string>
+            <key>ProductBuildVersion</key>
+            <string>22N895</string>
+            <key>ProductCopyright</key>
+            <string>1983-2025 Apple Inc.</string>
+            <key>ProductName</key>
+            <string>xrOS</string>
+            <key>ProductVersion</key>
+            <string>2.3</string>
+            <key>SystemImageID</key>
+            <string>D332C7F1-08DF-4DD9-8122-94EF39A1FB92</string>
+            <key>iOSSupportVersion</key>
+            <string>18.3</string>
+        </dict>
+        </plist>
+    "#;
+    check((2, 3, 0), Some((18, 3, 0)), plist);
+
+    // tvOS Simulator 18.2
+    let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+        <plist version="1.0">
+        <dict>
+            <key>BuildID</key>
+            <string>617587B0-B059-11EF-BE70-4380EDE44645</string>
+            <key>ProductBuildVersion</key>
+            <string>22K154</string>
+            <key>ProductCopyright</key>
+            <string>1983-2024 Apple Inc.</string>
+            <key>ProductName</key>
+            <string>Apple TVOS</string>
+            <key>ProductVersion</key>
+            <string>18.2</string>
+            <key>SystemImageID</key>
+            <string>8BB5A425-33F0-4821-9F93-40E7ED92F4E0</string>
+        </dict>
+        </plist>
+    "#;
+    check((18, 2, 0), None, plist);
+
+    // watchOS Simulator 11.2
+    let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+        <plist version="1.0">
+        <dict>
+            <key>BuildID</key>
+            <string>BAAE2D54-B122-11EF-BF78-C6C6836B724A</string>
+            <key>ProductBuildVersion</key>
+            <string>22S99</string>
+            <key>ProductCopyright</key>
+            <string>1983-2024 Apple Inc.</string>
+            <key>ProductName</key>
+            <string>Watch OS</string>
+            <key>ProductVersion</key>
+            <string>11.2</string>
+            <key>SystemImageID</key>
+            <string>79F773E2-2041-43B4-98EE-FAE52402AE95</string>
+        </dict>
+        </plist>
+    "#;
+    check((11, 2, 0), None, plist);
+
+    // iOS 9.3.6
+    let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+        <plist version="1.0">
+        <dict>
+            <key>ProductBuildVersion</key>
+            <string>13G37</string>
+            <key>ProductCopyright</key>
+            <string>1983-2019 Apple Inc.</string>
+            <key>ProductName</key>
+            <string>iPhone OS</string>
+            <key>ProductVersion</key>
+            <string>9.3.6</string>
+        </dict>
+        </plist>
+    "#;
+    check((9, 3, 6), None, plist);
+}
+
+#[test]
+#[should_panic = "SystemVersion.plist did not contain a dictionary at the top level"]
+fn invalid_plist() {
+    let cf_handle = CFHandle::new();
+    let _ = parse_version_from_plist(&cf_handle, b"INVALID");
+}
+
+#[test]
+#[cfg_attr(
+    target_abi = "macabi",
+    should_panic = "expected iOSSupportVersion in SystemVersion.plist"
+)]
+#[cfg_attr(
+    not(target_abi = "macabi"),
+    should_panic = "expected ProductVersion in SystemVersion.plist"
+)]
+fn empty_plist() {
+    let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+        <plist version="1.0">
+        <dict>
+        </dict>
+        </plist>
+    "#;
+    let cf_handle = CFHandle::new();
+    let _ = parse_version_from_plist(&cf_handle, plist.as_bytes());
+}
+
+#[test]
+fn parse_version() {
+    #[track_caller]
+    fn check(major: u16, minor: u8, patch: u8, version: &str) {
+        assert_eq!(
+            pack_os_version(major, minor, patch),
+            parse_os_version(version.as_bytes()).unwrap()
+        )
+    }
+
+    check(0, 0, 0, "0");
+    check(0, 0, 0, "0.0.0");
+    check(1, 0, 0, "1");
+    check(1, 2, 0, "1.2");
+    check(1, 2, 3, "1.2.3");
+    check(9999, 99, 99, "9999.99.99");
+
+    // Check leading zeroes
+    check(10, 0, 0, "010");
+    check(10, 20, 0, "010.020");
+    check(10, 20, 30, "010.020.030");
+    check(10000, 100, 100, "000010000.00100.00100");
+
+    // Too many parts
+    assert!(parse_os_version(b"1.2.3.4").is_err());
+
+    // Empty
+    assert!(parse_os_version(b"").is_err());
+
+    // Invalid digit
+    assert!(parse_os_version(b"A.B").is_err());
+
+    // Missing digits
+    assert!(parse_os_version(b".").is_err());
+    assert!(parse_os_version(b".1").is_err());
+    assert!(parse_os_version(b"1.").is_err());
+
+    // Too large
+    assert!(parse_os_version(b"100000").is_err());
+    assert!(parse_os_version(b"1.1000").is_err());
+    assert!(parse_os_version(b"1.1.1000").is_err());
+}
diff --git a/library/std/src/sys/platform_version/mod.rs b/library/std/src/sys/platform_version/mod.rs
new file mode 100644
index 00000000000..88896c97ea3
--- /dev/null
+++ b/library/std/src/sys/platform_version/mod.rs
@@ -0,0 +1,13 @@
+//! Runtime lookup of operating system / platform version.
+//!
+//! Related to [RFC 3750](https://github.com/rust-lang/rfcs/pull/3750), which
+//! does version detection at compile-time.
+//!
+//! See also the `os_info` crate.
+
+#[cfg(target_vendor = "apple")]
+mod darwin;
+
+// In the future, we could expand this module with:
+// - `RtlGetVersion` on Windows.
+// - `__system_property_get` on Android.
diff --git a/tests/run-make/apple-c-available-links/foo.c b/tests/run-make/apple-c-available-links/foo.c
new file mode 100644
index 00000000000..eff99a8b12a
--- /dev/null
+++ b/tests/run-make/apple-c-available-links/foo.c
@@ -0,0 +1,22 @@
+int foo(void) {
+    // Act as if using some API that's a lot newer than the deployment target.
+    //
+    // This forces Clang to insert a call to __isPlatformVersionAtLeast,
+    // and linking will fail if that is not present.
+    if (__builtin_available(
+        macos 1000.0,
+        ios 1000.0,
+        tvos 1000.0,
+        watchos 1000.0,
+        // CI runs below Xcode 15, where `visionos` wasn't a valid key in
+        // `__builtin_available`.
+#ifdef TARGET_OS_VISION
+        visionos 1000.0,
+#endif
+        *
+    )) {
+        return 1;
+    } else {
+        return 0;
+    }
+}
diff --git a/tests/run-make/apple-c-available-links/main.rs b/tests/run-make/apple-c-available-links/main.rs
new file mode 100644
index 00000000000..4ffada43c1b
--- /dev/null
+++ b/tests/run-make/apple-c-available-links/main.rs
@@ -0,0 +1,7 @@
+unsafe extern "C" {
+    safe fn foo() -> core::ffi::c_int;
+}
+
+fn main() {
+    assert_eq!(foo(), 0);
+}
diff --git a/tests/run-make/apple-c-available-links/rmake.rs b/tests/run-make/apple-c-available-links/rmake.rs
new file mode 100644
index 00000000000..44a5ee94d57
--- /dev/null
+++ b/tests/run-make/apple-c-available-links/rmake.rs
@@ -0,0 +1,14 @@
+//! Test that using `__builtin_available` in C (`@available` in Objective-C)
+//! successfully links (because `std` provides the required symbols).
+
+//@ only-apple __builtin_available is (mostly) specific to Apple platforms.
+
+use run_make_support::{cc, rustc, target};
+
+fn main() {
+    // Invoke the C compiler to generate an object file.
+    cc().arg("-c").input("foo.c").output("foo.o").run();
+
+    // Link the object file together with a Rust program.
+    rustc().target(target()).input("main.rs").link_arg("foo.o").run();
+}
diff --git a/triagebot.toml b/triagebot.toml
index b957c6465e6..6924ed4a0d9 100644
--- a/triagebot.toml
+++ b/triagebot.toml
@@ -380,6 +380,7 @@ trigger_files = [
 [autolabel."O-apple"]
 trigger_files = [
     "library/std/src/os/darwin",
+    "library/std/src/sys/platform_version/darwin",
     "library/std/src/sys/sync/thread_parking/darwin.rs",
     "compiler/rustc_target/src/spec/base/apple",
 ]