ES_EVENT_TYPE_AUTH_SIGNAL DENY causes Dock icon to disappear and LaunchServices to lose track of the process
Platform: macOS 11.0 (Big Sur) – macOS 15 (Sequoia) Xcode: 16.4 (16F6) Language: Swift, EndpointSecurity framework Testing OS: macOS 15.5 (primary), reproduced on macOS 11.0+
[1]Description
I'm developing a System Extension using the EndpointSecurity framework for a security product. My extension subscribes to ES_EVENT_TYPE_AUTH_SIGNAL to block unauthorized signals sent to protected GUI applications (self-protection feature).
When I respond with ES_AUTH_RESULT_DENY to an AUTH_SIGNAL event targeting a GUI application, the system enters an inconsistent state:
- The Dock icon disappears —
loginwindowremoves the app's UI via itsapplicationQuitevent, even though the process is still running - LaunchServices loses track of the application's PID — it can no longer determine the PID from the LSASN
- Activity Monitor's subsequent Force Quit attempts fail silently — no
kill()syscall is issued because LaunchServices cannot resolve the PID
The issue only resolves after:
- Restarting Activity Monitor (clears its internal cache), or
- Relaunching the protected application (re-registers with LaunchServices)
Expected: Signal is denied, the process keeps running, Dock icon remains visible, and Activity Monitor can still force-quit the process normally.
Actual: Dock icon disappears after the first blocked signal. Subsequent Force Quit attempts do nothing — no kill() syscall is issued. The process remains alive but is invisible to the system.
[2]Minimal Reproducible Code
Requires System Extension entitlement:
com.apple.developer.endpoint-security.client
entitlements.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.endpoint-security.client</key>
<true/>
</dict>
</plist>
SignalBlockingDemo.swift
import EndpointSecurity
import Foundation
var client: OpaquePointer?
es_new_client(&client) { _, message in
guard message.pointee.event_type == ES_EVENT_TYPE_AUTH_SIGNAL else { return }
let sig = message.pointee.event.signal.sig
let target = message.pointee.event.signal.target.pointee
let targetPid = audit_token_to_pid(target.audit_token)
// es_string_token_t does not guarantee null-termination — read via buffer
let esToken = target.executable.pointee.path
let targetPath: String
let count = Int(esToken.length)
if count > 0, let rawPtr = esToken.data {
let buf = UnsafeBufferPointer(
start: UnsafeRawPointer(rawPtr).assumingMemoryBound(to: UInt8.self),
count: count)
targetPath = String(decoding: buf, as: UTF8.self)
} else {
targetPath = ""
}
// Protect a specific GUI app — replace with your target path
let protectedPath = "/Applications/Numbers.app/Contents/MacOS/Numbers"
guard targetPath == protectedPath else {
es_respond_auth_result(client!, message, ES_AUTH_RESULT_ALLOW, false)
return
}
print("[ES] Blocking signal \(sig) -> pid \(targetPid) (\(targetPath))")
// After this DENY: Dock icon disappears, LaunchServices loses the PID
es_respond_auth_result(client!, message, ES_AUTH_RESULT_DENY, false)
}
let events: [es_event_type_t] = [ES_EVENT_TYPE_AUTH_SIGNAL]
es_subscribe(client!, events, UInt32(events.count))
print("Signal blocking active. Press Enter to stop.")
_ = readLine()
es_unsubscribe_all(client!)
es_delete_client(client!)
Build & run:
swiftc -o SignalBlockingDemo SignalBlockingDemo.swift
codesign --force --sign - --entitlements entitlements.plist SignalBlockingDemo
sudo ./SignalBlockingDemo
[3]Steps to Reproduce
- Build and run
SignalBlockingDemoas above (targets Numbers.app) - Launch Numbers.app — note its PID
- Open Activity Monitor
- In Activity Monitor, select Numbers → click Force Quit (⊗)
- Observe: ES extension logs
"Blocking signal 15"— signal is denied - Bug: Numbers.app Dock icon disappears, even though the process is alive
- Press Enter in the demo terminal to stop signal blocking
- In Activity Monitor, click Force Quit again on the Numbers process
- Bug: No error shown in Activity Monitor UI, but the process is NOT terminated
- In Console.app (filter:
LaunchServices), observe:"Unable to determine pid of LSASN:{hi=0x1;lo=0x...}" - Confirm: No
kill()syscall is issued — verify with DTrace script below
DTrace verification (trace_kill.d):
syscall::kill:entry
/execname == "Activity Monitor"/
{
printf("%Y: Activity Monitor calling kill(%d, %d)\n", walltimestamp, arg0, arg1);
}
sudo dtrace -s trace_kill.d
During the broken Force Quit: no output (no kill() issued).
After restarting Activity Monitor and retrying: kill() appears and process terminates.
[4 What We've Tried
- Allowing ALL signals → Dock icon never disappears, behavior is normal
- Subscribing to
AUTH_SIGNALbut always returningALLOW→ no issue - Denying signals only on headless daemon processes → no issue observed
- Always allowing signals from
launchd(PID 1) → does not prevent the Dock issue - Always allowing
SIGCHLD,SIGWINCH,SIGCONT→ does not prevent the Dock issue
Hypothesis: loginwindow observes the AUTH_SIGNAL event (or a related notification) and proactively removes the Dock UI entry when a termination signal targets a GUI app — regardless of whether the signal was ultimately denied. This seems like a coordination gap between EndpointSecurity's signal interception and loginwindow/LaunchServices' app lifecycle management.
[5] Specific Questions
- Is it expected that
loginwindowremoves the Dock UI entry for a GUI app whenAUTH_SIGNALis received, even if the signal is ultimately denied (ES_AUTH_RESULT_DENY)? - Is there a known coordination mechanism between EndpointSecurity's
AUTH_SIGNALandloginwindow/LaunchServicesthat we should be aware of when implementing self-protection for GUI apps? - Is there a recommended pattern or API for protecting a GUI app from termination signals via
AUTH_SIGNALwithout disrupting its Dock presence and LaunchServices registration? - Should we notify
loginwindoworLaunchServicesto re-register the application after denying a signal, and if so, how?
[6] Additional Context
- The issue reproduces on macOS 11.0 through macOS 15.5
- Tested with Numbers.app and other GUI applications — all reproduce the same behavior
- The issue is NOT reproducible when the protected process is a headless daemon (no Dock presence)
launchd(PID 1) senders are always allowed in our policySIGCHLD,SIGWINCH,SIGCONTare excluded from our deny list- DTS Case ID: 19226051
- Feedback ID :FB22338746