Release FFI Core Foundation objects deterministically - #23607
Merged
Conversation
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.
4 tasks
Contributor
There was a problem hiding this comment.
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.releaseand a newCoreFoundation.with_release_poolAPI. - Scopes Core Foundation allocations in
FFI::Security,FFI::Foundation.trash_item, andCask::Quarantine.cask!to a release pool drained at block end (even on exceptions). - Adds/updates unit tests to cover
with_release_poolsemantics (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.
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.
8 tasks
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
brewcommands to reproduce the bug?brew lgtm(style, typechecking and tests) locally?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
CoreFoundation.autorelease, which handedCFReleaseof Core Foundation objects to Ruby GC viaFiddle::Pointer#free=.CoreFoundation::ReleasePoolandCoreFoundation.with_release_pool. The pool releases every tracked object in reverse creation order when the block finishes, including when it raises.FFI::Security,Cask::Quarantine.cask!andFFI::Foundation.trash_itemto 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
SecStaticCodeobjects from cask signing checks survived until Ruby's lazy GC sweep ran — inside the child ofIO.popen("-")inUtils.popen, betweenforkandexec. The GC-timeCFReleaseran Security.framework destructors there. Those destructors log viaos_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.languagesbefore 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
Verification
brew lgtmpasses locally:brew typecheckclean,brew style --changedclean (6 files),brew tests --changedgreen (4 spec files).with_release_pool: reverse-order release, release on exception, NULL-pointer handling and block return value.os/mac/ffi/security(code-signing checks against/bin/ls),cask/quarantine(real quarantine xattr on a temporary file) andos/mac/ffi/foundation(real trash round-trip).brew ruby, counting liveFiddle::Pointers whose free function isCFReleaseafter runningSecurity.designated_requirement+Security.requirement_match: