Skip to content

Possible fork_exec() using vfork() without setsid(), breaking child process waitpid() on macOS 26 (Tahoe) #807

Description

@obsidian33

On macOS (Tahoe), plugin scripts spawned by SketchyBar fail when they invoke commands that internally use waitpid() - most notably brew outdated which crashes with:

Error: undefined method 'success?' for nil
/opt/homebrew/Library/Homebrew/hardware.rb:106:in 'Hardware::CPU.cores'

The root cause appears to be that fork_exec() in src/misc/helpers.h uses vfork() and execvp() without calling setsid() in the child process. The spawned plugin processes end up with "Session ID 0" and no controlling terminal (TTY: ??). On macOS 26, this causes Ruby's IO.popen("-") (fork mode) to fail to properly waitpid() for its child, leaving $? ($CHILD_STATUS) as nil. Homebrew's Hardware::CPU.cores calls Utils.popen_read("getconf", "_NPROCESSORS_ONLN") and then checks $CHILD_STATUS.success?, which raises NoMethodError on nil.

This likely worked on earlier macOS versions (14, 15) where the kernel allowed waitpid() to succeed even with Session ID 0.

Reproduction

  1. Install SketchyBar v2.23.0 via Homebrew on macOS 26
  2. Create a plugin script that runs brew outdated
  3. Observe the error in /opt/homebrew/var/log/sketchybar/sketchybar.err.log when running sketchybar using brew services

Root cause analysis

The relevant code in src/misc/helpers.h (https://github.com/FelixKratz/SketchyBar/blob/master/src/misc/helpers.h):

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
static inline bool fork_exec(char *command, struct env_vars* env_vars) {
  int pid = vfork();
  if (pid == -1) return false;
  if (pid !=  0) return true;
  alarm(FORK_TIMEOUT);
  exit(sync_exec(command, env_vars));
}
#pragma clang diagnostic pop

Key observations:

  • vfork() is used (and the deprecation warning is explicitly suppressed). Apple has deprecated vfork() and recommends posix_spawn() instead.
  • No setsid() call is made in the child before exec, so spawned processes inherit Session ID 0 (no session) and have no controlling terminal.
  • On macOS 26, this breaks waitpid() behavior in grandchild processes (e.g., Ruby calling IO.popen("-") which forks internally).

Evidence:

  • Running ps -o pid,ppid,sess,tty,command on a plugin process shows SESS=0 and TTY=??
  • Wrapping the plugin script with setsid (e.g., perl -e 'use POSIX; POSIX::setsid(); exec @ARGV') before execution completely fixes the issue
  • The same Ruby binary and brew outdated command works perfectly from a normal terminal session
  • Both Ruby 3.4.8 and 4.0.1 are affected equally — this is not a Ruby version issue
  • SIGCHLD signal disposition is DEFAULT in both contexts — not a signal handler issue

Suggested fix

Option A (minimal): Add setsid() in the child before exec:

static inline bool fork_exec(char *command, struct env_vars* env_vars) {
  int pid = fork();
  if (pid == -1) return false;
  if (pid !=  0) return true;
  setsid();
  alarm(FORK_TIMEOUT);
  exit(sync_exec(command, env_vars));
}

Option B (recommended by Apple): Replace vfork() + execvp() with posix_spawn(), which is the modern API Apple recommends for spawning child processes and handles session/process group setup correctly.

Current workaround

Users can work around this in their plugin scripts by re-execing in a new session:

#!/bin/bash
if [ -z "$_SETSID_DONE" ]; then
  export _SETSID_DONE=1
  exec /usr/bin/perl -e 'use POSIX; POSIX::setsid(); exec @ARGV' -- "$0" "$@"
fi
# ... rest of plugin script

Environment

  • macOS: 26.3.1 (Build 25D2128)
  • Kernel: Darwin 25.3.0 (xnu-12377.91.3~2/RELEASE_ARM64_T6020)
  • Hardware: Mac14,10 (Apple M2 Pro)
  • SketchyBar: v2.23.0
  • Ruby: 3.4.8 and 4.0.1 (both tested, both affected)
  • Homebrew: installed via /opt/homebrew

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions