Skip to content

macOS: notification overlay leaks empty ~/ffiXXXXXX libffi temp files (one per display, per notification) #529

@hsancarlos

Description

@hsancarlos

Summary

On macOS, every desktop-notification banner leaves empty ffiXXXXXX files in $HOME. The overlay is rendered by osascript -l JavaScript scripts/mac-overlay.js, launched once per display. JavaScript-for-Automation calls AppKit through the Objective-C bridge, which uses libffi. Under osascript's hardened runtime (no allow-jit / allow-unsigned-executable-memory entitlement), libffi can't get anonymous executable memory and falls back to mkstemp("$HOME/ffiXXXXXX") + mmap(PROT_EXEC), then never unlinks it. With notification_all_screens: true and N displays, each notification leaks ~N empty files.

Environment

  • peon-ping 2.29.0
  • macOS 15.7.7 (x86_64), 2 displays
  • System /usr/bin/osascript (hardened runtime)

Reproduction

  1. Defaults: desktop_notifications: true, notification_all_screens: true.
  2. Trigger any notification (e.g. a Stop/Notification hook event).
  3. ~1–2s later (the overlay is backgrounded via nohup … &), new 0-byte ~/ffiXXXXXX files appear — one per display.

Evidence

  • Files match libffi's open_temp_exec_file template (ffi + 6 mkstemp chars), mode 0600, size 0.
  • Caught live: one notification spawned two osascript … mac-overlay.js processes (slots 0 and 1 for 2 screens) and created ~/ffiXXXXXX ~1.9s later.
  • Setting desktop_notifications: false stops all new files; sounds (afplay/peon-play, no libffi) are unaffected.
  • The system libffi used by python3 ctypes and Ruby Fiddle uses static trampolines and does not leak — the issue is specific to the JXA/AppKit overlay under osascript.
  • Real-world accumulation: 48 files over ~3 days on one machine.

Impact

Cosmetic but unbounded — $HOME steadily fills with empty files, and the ffi prefix gives users no hint of the origin (mistaken for compiler/AWS/Homebrew leftovers).

Suggested fixes (any one)

  1. Reap after render: snapshot ffiXXXXXX before/after launching the overlay and remove the new ones once osascript exits.
  2. Redirect the leak, then sweep: launch the overlay subprocess with HOME pointed at a peon-owned scratch dir (libffi's last-resort dir is $HOME), and clean that dir. Note: TMPDIR/LIBFFI_TMPDIR alone won't help — those dirs fail osascript's PROT_EXEC test on hardened macOS, which is why it lands in $HOME.
  3. Avoid the libffi bridge: render via a compiled Swift helper (like peon-play) or UNUserNotification/terminal-notifier instead of JXA→AppKit.

Debugged, drafted, and raised by Claude (Anthropic's Claude Code) on behalf of a peon-ping user.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions