Skip to content

Release FFI Core Foundation objects deterministically - #23607

Merged
MikeMcQuaid merged 4 commits into
Homebrew:mainfrom
dot-agi:ffi-deterministic-cf-release
Aug 22, 2026
Merged

Release FFI Core Foundation objects deterministically#23607
MikeMcQuaid merged 4 commits into
Homebrew:mainfrom
dot-agi:ffi-deterministic-cf-release

Conversation

@dot-agi

@dot-agi dot-agi commented Aug 21, 2026

Copy link
Copy Markdown
Contributor
  • Have you followed our Contributing guidelines?
  • Have you checked for other open Pull Requests for the same change?
  • Have you explained what your changes do? Performance claims (e.g. "this is faster") must include Hyperfine benchmarks.
  • Have you explained why you'd like these changes included, not just what they do?
  • For bug fixes, have you given step-by-step brew commands to reproduce the bug?
  • Have you written new tests (excluding integration tests)? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) locally?

  • I did not use AI/LLM to create this PR, or I disclosed the tool/model below and reviewed its output; I did not attribute commits to AI and will answer maintainer questions and review comments myself without AI/LLM.

AI disclosure: this PR was produced with Claude Code (model: Claude Fable 5), in the same session that diagnosed and filed #23606, as requested by @MikeMcQuaid in #23606 (comment). All changes were verified locally as described under "Verification" below.


Fixes #23606.

What this does

  • Removes CoreFoundation.autorelease, which handed CFRelease of Core Foundation objects to Ruby GC via Fiddle::Pointer#free=.
  • The FFI creator methods now follow the Core Foundation Create Rule: the caller owns the result.
  • Adds CoreFoundation::ReleasePool and CoreFoundation.with_release_pool. The pool releases every tracked object in reverse creation order when the block finishes, including when it raises.
  • Scopes all Core Foundation objects in FFI::Security, Cask::Quarantine.cask! and FFI::Foundation.trash_item to such a pool. Nothing Core Foundation-owned is left for GC.

Why

This implements the fix preferred in #23606 (comment).

The crash in #23606 happened because GC-owned SecStaticCode objects from cask signing checks survived until Ruby's lazy GC sweep ran — inside the child of IO.popen("-") in Utils.popen, between fork and exec. The GC-time CFRelease ran Security.framework destructors there. Those destructors log via os_log, which dereferenced stale shared memory in the forked child and segfaulted. The fully symbolized crash report in the issue shows every step of that chain.

With this change, no Core Foundation object can ever be released at GC time, so fork children can no longer run framework destructors. A missed release would now be a small bounded leak in a short-lived process instead of a fork-safety crash.

The issue also suggested eagerly resolving MacOS.languages before cask artifact installation ("could do this too"). I left that out to keep this diff focused; happy to do it here or as a follow-up if wanted.

Reproduction

brew update
brew upgrade --greedy
# Crashed while upgrading a quarantined cask, at the save_config_file step.
# Both `Utils.popen` fork children (lazy `MacOS.languages` resolution) hit
# `[BUG] Segmentation fault` in `_os_log_preferences_refresh`.
# The crash is timing-dependent (needs stale logd shared memory at fork time);
# the defective GC-owned release path is deterministic. Full analysis: #23606.

Verification

  • brew lgtm passes locally: brew typecheck clean, brew style --changed clean (6 files), brew tests --changed green (4 spec files).
  • New unit tests cover with_release_pool: reverse-order release, release on exception, NULL-pointer handling and block return value.
  • The real-FFI specs still pass on macOS 26.6.1: os/mac/ffi/security (code-signing checks against /bin/ls), cask/quarantine (real quarantine xattr on a temporary file) and os/mac/ffi/foundation (real trash round-trip).
  • Structural check via brew ruby, counting live Fiddle::Pointers whose free function is CFRelease after running Security.designated_requirement + Security.requirement_match:
    • before this change: 10
    • after this change: 0

The cask quarantine/signing FFI code handed `CFRelease` of Core
Foundation objects (including Security.framework `SecStaticCode`
objects) to Ruby GC via `Fiddle::Pointer#free=`. GC can run inside the
child of `IO.popen("-")` between `fork` and `exec` (`Utils.popen`),
where releasing these objects is not fork-safe: Security.framework
destructors log via `os_log`, which dereferences stale shared memory in
the forked child and segfaults.

Scope every Core Foundation object to a release pool that is drained
deterministically before returning to the caller, so nothing is left
for GC-time release.

Fixes Homebrew#23606.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Homebrew’s macOS FFI wrappers to deterministically release Core Foundation objects (instead of deferring CFRelease to Ruby GC), addressing fork-safety crashes described in #23606.

Changes:

  • Replaces GC-managed Core Foundation releasing with explicit CoreFoundation.release and a new CoreFoundation.with_release_pool API.
  • Scopes Core Foundation allocations in FFI::Security, FFI::Foundation.trash_item, and Cask::Quarantine.cask! to a release pool drained at block end (even on exceptions).
  • Adds/updates unit tests to cover with_release_pool semantics (reverse-order release, exception safety, NULL handling, return value).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Library/Homebrew/os/mac/ffi/core_foundation.rb Introduces ReleasePool/with_release_pool, removes autorelease, and adds explicit release.
Library/Homebrew/os/mac/ffi/security.rb Ensures Security.framework Core Foundation objects are released deterministically within a release pool.
Library/Homebrew/os/mac/ffi/foundation.rb Wraps trash_item Core Foundation string creation in a release pool.
Library/Homebrew/extend/os/mac/cask/quarantine.rb Scopes quarantine Core Foundation objects to a release pool to avoid GC-time releases.
Library/Homebrew/test/os/mac/ffi/core_foundation_spec.rb Updates existing CoreFoundation FFI spec and adds new tests for with_release_pool.
Library/Homebrew/test/cask/quarantine_spec.rb Stubs CoreFoundation.release and updates expectations to match the new release-pool-based flow.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread Library/Homebrew/extend/os/mac/cask/quarantine.rb
Addresses Copilot review feedback: `next if success` relied on nothing
following the release pool block in `cask!`. Guard the raise with
`unless success` so the block has no early-exit semantics at all.
- Restrict the Create Rule note in CoreFoundation's module docs to the
  creator methods; constant accessors return borrowed references that
  must not be released.
- Note in Security.retained_pointer that the out-parameter scratch
  buffer is plain malloc memory that is safe to leave to GC.
- Use dot-form for the class-method describe block in the
  CoreFoundation spec, matching sibling specs.

@MikeMcQuaid MikeMcQuaid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@MikeMcQuaid
MikeMcQuaid added this pull request to the merge queue Aug 22, 2026
Merged via the queue into Homebrew:main with commit 519f531 Aug 22, 2026
45 checks passed
@dot-agi
dot-agi deleted the ffi-deterministic-cf-release branch August 22, 2026 11:26
pull Bot pushed a commit to specialized806/brew that referenced this pull request Aug 22, 2026
`Cask::Config`'s `languages` default is a `LazyObject` that is only
forced while serialising the config at the end of artifact
installation. That late resolution forks the whole process
(`Utils.popen` running `defaults read`) after the installer has
already used fork-hostile frameworks such as Security.framework for
signing checks.

Memoise `MacOS.languages` at the start of
`Cask::Installer#install_artifacts`, which both installs and upgrades
go through, so the fork happens before any artifact or signing work
and the later lazy resolution becomes a memo lookup.

Follow-up to Homebrew#23607, requested in
Homebrew#23606 (comment).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cask upgrade segfault: Ruby GC releases Security.framework objects on the child side of fork in Utils.popen

3 participants