Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix misuse of fork in sandbox causing crashes #18183

Merged
merged 1 commit into from
Aug 28, 2024
Merged

Fix misuse of fork in sandbox causing crashes #18183

merged 1 commit into from
Aug 28, 2024

Conversation

Bo98
Copy link
Member

@Bo98 Bo98 commented Aug 28, 2024

First of all: I recommend viewing the diff with "hide whitespace" enabled - the changes aren't actually that drastic.


We've recently received reports of the sandbox hard crashing Ruby in macOS 15 beta for some users. We've also seen related reports on OS X 10.11 after a recent Tempfile crypto RNG change in Ruby 3.3.

The sandbox used to be a fork + exec but due to security tightening over time this has increased in complexity to the point we no longer actually have an exec inside the top-level fork and we are now breaking the golden rule of fork on macOS, as stated by man fork:

CAVEATS
     There are limits to what you can do in the child process.  To be totally
     safe you should restrict yourself to only executing async-signal safe
     operations until such time as one of the exec functions is called.  All
     APIs, including global data symbols, in any framework or library should
     be assumed to be unsafe after a fork() unless explicitly documented to be
     safe or async-signal safe.  If you need to use these frameworks in the
     child process, you must exec.  In this situation it is reasonable to exec
     yourself.

The flow was:

safe_fork ->
  configures sandbox (calls `Pathname` code which use CoreFoundation unicode normalisation - macOS 15 crasher)
  write sandbox file (uses crypto RNG for `Tempfile` - OS X 10.11 crasher)
  get current time
  create PTY
  create a second fork (inside `PTY.spawn`) ->
    configure child-side of PTY
    exec
  configure parent-side of PTY
  create IO threads
  read syslog
  write sandbox log

Instead, let's Util.safe_fork inside sandbox.rb closely around the minimum set of code required to be run in the child process before a final exec. This significantly reduces the surface area of external function calls we are doing in the child process pre-exec.

New flow:

configures sandbox
write sandbox file
get current time
create PTY
safe_fork ->
    configure child-side of PTY
    exec
configure parent-side of PTY
create IO threads
read syslog
write sandbox log

So only a small part is now under a fork.

@Bo98 Bo98 force-pushed the sandbox-fork-fix branch 2 times, most recently from 2c7d7da to b5ca2d9 Compare August 28, 2024 03:29
@Bo98 Bo98 force-pushed the sandbox-fork-fix branch 2 times, most recently from ce129ee to 39aeb72 Compare August 28, 2024 03:59
@carlocab
Copy link
Member

we are now breaking the golden rule of fork on macOS

I think we should at least document needing to exec ASAP after calling Utils.safe_fork in a comment above it.

Ideally we should also find some way to enforce it (with a Rubocop maybe?), but it's not very clear to me how to do that.

Copy link
Member

@MikeMcQuaid MikeMcQuaid left a comment

Choose a reason for hiding this comment

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

Makes sense to me, thanks @Bo98! One readability comment but can self-merge before or after addressing without a re-review.

Library/Homebrew/sandbox.rb Show resolved Hide resolved
@Bo98
Copy link
Member Author

Bo98 commented Aug 28, 2024

Ideally we should also find some way to enforce it (with a Rubocop maybe?), but it's not very clear to me how to do that.

Yeah this seems very difficult to do, particularly since you'd ideally follow function calls to check it properly. A little bit of setup before exec is OK as long as it's minimal.

@Bo98 Bo98 force-pushed the sandbox-fork-fix branch from 39aeb72 to 6a0db50 Compare August 28, 2024 12:53
@carlocab
Copy link
Member

Yeah this seems very difficult to do, particularly since you'd ideally follow function calls to check it properly. A little bit of setup before exec is OK as long as it's minimal.

I guess at minimum we can look at the AST to make sure exec is called at some point? Obviously this doesn't stop you from doing stuff you're not allowed to do before exec is called, but it would probably have prevented this bug. (Right?)

@Bo98
Copy link
Member Author

Bo98 commented Aug 28, 2024

If it doesn't get tricked by:

Utils.safe_fork do
  if Sandbox.available?
    sandbox = Sandbox.new
    sandbox.exec(*args)
  else
    exec(*args)
  end
end

then yeah it may have helped.

sandbox.exec technically did do an exec itself but that changed at some point. I renamed it to run which makes that clearer.

@Bo98 Bo98 merged commit f5d39c8 into master Aug 28, 2024
27 checks passed
@Bo98 Bo98 deleted the sandbox-fork-fix branch August 28, 2024 14:15
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.

3 participants