Endpoint Security

RSS for tag

Develop system extensions that enhance user security using Endpoint Security.

Posts under Endpoint Security tag

43 Posts

Post

Replies

Boosts

Views

Activity

Endpoint Security Framework Bug: setuid Event Incorrectly Attributed to Parent Process During posix_spawn
Feedback ticket ID: FB21797397 Summary When using posix_spawn() with posix_spawnattr_set_uid_np() to spawn a child process with a different UID, the eslogger incorrectly reports a setuid event as an event originating from the parent process instead of the child process. Steps to Reproduce Create a binary that do the following: Configure posix_spawnattr_t that set the process UIDs to some other user ID (I'll use 501 in this example). Uses posix_spawn() to spawn a child process Run eslogger with the event types setuid, fork, exec Execute the binary as root process using sudo or from root owned shell Terminate the launched eslogger Observe the process field in the setuid event Expected behavior The eslogger will report events indicating a process launch and uid changes so the child process is set to 501. i.e.: fork setuid - Done by child process exec Actual behavior The process field in the setuid event is reported as the parent process (that called posix_spawn) - indicating UID change to the parent process. Attachments I'm attaching source code for a small project with a 2 binaries: I'll add the source code for the project at the end of the file + attach filtered eslogger JSONs One that runs the descirbed posix_spawn flow One that produces the exact same sequence of events by doing different operation and reaching a different process state: Parent calls fork() Parent process calls setuid(501) Child process calls exec() Why this is problematic Both binaries in my attachment do different operations, achieving different process state (1 is parent with UID=0 and child with UID=501 while the other is parent UID=501 and child UID=0), but report the same sequence of events. Code #include <cstdio> #include <spawn.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <string.h> // environ contains the current environment variables extern char **environ; extern "C" { int posix_spawnattr_set_uid_np(posix_spawnattr_t *attr, uid_t uid); int posix_spawnattr_set_gid_np(posix_spawnattr_t *attr, gid_t gid); } int main() { pid_t pid; int status; posix_spawnattr_t attr; // 1. Define the executable path and arguments const char *path = "/bin/sleep"; char *const argv[] = {(char *)"sleep", (char *)"1", NULL}; // 2. Initialize spawn attributes if ((status = posix_spawnattr_init(&attr)) != 0) { fprintf(stderr, "posix_spawnattr_init: %s\n", strerror(status)); return EXIT_FAILURE; } // 3. Set the UID for the child process (e.g., UID 501) // Note: Parent must be root to change to a different user uid_t target_uid = 501; if ((status = posix_spawnattr_set_uid_np(&attr, target_uid)) != 0) { fprintf(stderr, "posix_spawnattr_set_uid_np: %s\n", strerror(status)); posix_spawnattr_destroy(&attr); return EXIT_FAILURE; } // 4. Spawn the process printf("Spawning /bin/sleep 1 as UID %d...\n", target_uid); status = posix_spawn(&pid, path, NULL, &attr, argv, environ); if (status == 0) { printf("Successfully spawned child with PID: %d\n", pid); // Wait for the child to finish (will take 63 seconds) if (waitpid(pid, &status, 0) != -1) { printf("Child process exited with status %d\n", WEXITSTATUS(status)); } else { perror("waitpid"); } } else { fprintf(stderr, "posix_spawn: %s\n", strerror(status)); } // 5. Clean up posix_spawnattr_destroy(&attr); return (status == 0) ? EXIT_SUCCESS : EXIT_FAILURE; } #include <cstdio> #include <cstdlib> #include <unistd.h> #include <sys/wait.h> #include <errno.h> #include <string.h> // This program demonstrates fork + setuid + exec behavior for ES framework bug report // 1. Parent forks // 2. Parent does setuid(501) // 3. Child waits with sleep syscall // 4. Child performs exec int main() { printf("Parent PID: %d, UID: %d, EUID: %d\n", getpid(), getuid(), geteuid()); pid_t pid = fork(); if (pid < 0) { // Fork failed perror("fork"); return EXIT_FAILURE; } if (pid == 0) { // Child process printf("Child PID: %d, UID: %d, EUID: %d\n", getpid(), getuid(), geteuid()); // Child waits for a bit with sleep syscall printf("Child sleeping for 2 seconds...\n"); sleep(2); // Child performs exec printf("Child executing child_exec...\n"); // Get the path to child_exec (same directory as this executable) char *const argv[] = {(char *)"/bin/sleep", (char *)"2", NULL}; // Try to exec child_exec from current directory first execv("/bin/sleep", argv); // If exec fails perror("execv"); return EXIT_FAILURE; } else { // Parent process printf("Parent forked child with PID: %d\n", pid); // Parent does setuid(501) printf("Parent calling setuid(501)...\n"); if (setuid(501) != 0) { perror("setuid"); // Continue anyway to observe behavior } printf("Parent after setuid - UID: %d, EUID: %d\n", getuid(), geteuid()); // Wait for child to finish int status; if (waitpid(pid, &status, 0) != -1) { if (WIFEXITED(status)) { printf("Child exited with status %d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("Child killed by signal %d\n", WTERMSIG(status)); } } else { perror("waitpid"); } } return EXIT_SUCCESS; } posix_spawn.json fork_exec.json
3
0
663
1w
My EndpointSecurity Client process is kicked by OS on Mac sleep/wake cycle
Hi, I develop an ES client applying rule-engine evaluating ES events (mostly File-system events). It is a bit non-standard not being deployed as a System-Extension, but rather as a global daemon. On some Macs, I sometimes see "crash reports" for the ES process, all sharing Termination Reason: Namespace ENDPOINTSECURITY, Code 2 EndpointSecurity client terminated because it failed to respond to a message before its deadline All of these happen not while normal Mac usage, but rather right at Mac wakeup time after sleep. My guess is, some ES_AUTH events (with deadline) arrive when Mac goes to sleep, and somehow my high-priority dispatch_queue handling them is "put to sleep" mid processing them, so when the Mac wakes up - event handling continues long after the deadline passed, and MacOS decides to kick the process. Questions: What is the recommended behavior with ES vs Sleep/Wake cycles? (we're not an antivirus, and we don't care much to clear events or go "blind" for such time) Can I specify somewhere in the info.plist of my bundle (this is built like an App) that my process should't be put to sleep, or that the OS should sleep it only when it becomes idle, or some other way tells the OS it is "ready for sleep" ? If not -- How do I observe the scenario so I can suspend my event handling IN TIME and resume on wake? Thanks!
4
0
99
1w
Error 500 when requesting endpoint security entitlement
Hello, I am attempting to request the endpoint-security.client entitlement for my app using the following form: https://developer.apple.com/contact/request/system-extension/ After submitting the form, I consistently receive an HTTP 500 error from Apple’s servers. Could you please provide guidance on whether this is a known issue or if there is something I may be doing incorrectly? I appreciate your assistance.
1
0
113
1w
bluetooth control
I am learning about endpoint security and other system extensions, while I was handling ES_EVENT_TYPE_AUTH_IOKIT_OPEN event I realized that I cannot auth deny any bluetooth events. I tried to deny any open or execute events related to com.apple.bluetoothd but it did not work. I searched google and found out that I can use CoreBluetooth to control bluetooth. But when I get connected to bluetooth keyboard or mouse, didConnectPeripheral dose not get called or when I call [central cancelPeripheralConnection:peripheral] disconnection never happens. Is there any recommendation for handling or controlling events related to bluetooth connection?
3
0
1k
3w
When is the kTCCServiceEndpointSecurityClient permission set by macOS?
[Q] When is the kTCCServiceEndpointSecurityClient set by macOS and in which conditions? From what I'm gathering, the kTCCServiceEndpointSecurityClient can not be set by a configuration profile and the end user can only grant full disk access. I searched for documentation on Apple's develop website (with the "kTCCServiceEndpointSecurityClient" search) and did not get any useful result. Using a more complete search engine, or the forum search engine, only points to the old annoying big bug in macOS Ventura. The problem I'm investigating is showing a process being listed as getting granted kTCCServiceEndpointSecurityClient permissions in the TCC database when: it's not an Endpoint Security client. it does not have the ES Client entitlement. the bundle of the process includes another process that is an ES Client and is spawn-ed by this process but I don't see why this should have an impact. This process is supposed to have been granted kTCCServiceSystemPolicyAllFiles via end user interaction or configuration profile. AFAIK, the kTCCServiceEndpointSecurityClient permission can only be set by macOS itself. So this looks like to be either a bug in macOS, an undocumented behavior or I'm missing something. Hence the initial question. macOS 15.7.3 / Apple Silicon
2
0
124
3w
macOS 15 (Sequoia): Endpoint Security client runs by hand, but LaunchDaemon fails with TCC “Full Disk Access” denial on unmanaged Macs
Platforms: macOS 15.x (Sequoia), Intel-Based App type: Endpoint Security (ES) client, notarized Developer ID app + LaunchDaemon Goal: Boot-time ES client that runs on any Mac (managed or unmanaged) Summary Our ES client launches and functions when started manually (terminal), but when loaded as a LaunchDaemon it fails to initialize the ES connection with: (libEndpointSecurity.dylib) Failed to open service: 0xe00002d8: Caller lacks TCC authorization for Full Disk Access We can’t find a supported way to grant Full Disk Access (SystemPolicyAllFiles) to a system daemon on unmanaged Macs (no MDM). Local installation of a PPPC (TCC) profile is rejected as “must originate from a user-approved MDM server.” We’re seeking confirmation: Is MDM now the only supported path for a boot-time ES daemon that requires FDA? If so, what’s Apple’s recommended approach for unmanaged Macs? Environment & Artifacts Binary (path placeholder): /Library/Application Support///App/.app/Contents/MacOS/ Universal (x86_64 + arm64) Notarized, hardened runtime; Developer ID Team <TEAM_ID> Entitlements include: com.apple.developer.endpoint-security.client (present) Daemon plist (simplified; placeholders used): <?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>Label</key> <string>com.example.esd</string> <key>Program</key> <string>/Library/Application Support/<VENDOR>/<PRODUCT>/Platform/<daemon-exec></string> <key>WorkingDirectory</key> <string>/Library/Application Support/<VENDOR>/<PRODUCT>/Platform</string> <key>RunAtLoad</key><true/> <key>KeepAlive</key><true/> </dict></plist> Designated requirement (abridged & masked): identifier "<BUNDLE_ID>" and anchor apple generic and certificate 1[...] and certificate leaf[...] and certificate leaf[subject.OU] = "<TEAM_ID>" What works Launching the ES client manually (interactive shell) succeeds; ES events flow. Signature, notarization, entitlements, Gatekeeper: all OK. What fails (daemon) launchctl print system/ shows it starts, but Console logs: (libEndpointSecurity.dylib) Failed to open service: 0xe00002d8:Caller lacks TCC authorization for Full Disk Access System TCC DB shows ES consent rows but no allow for TCCServiceSystemPolicyAllFiles for the daemon binary. Installing a PPPC mobileconfig locally (system scope) is blocked as “must originate from a user-approved MDM server.” Repro (minimal) Install app bundle + LaunchDaemon plist above (placeholders). Verify entitlements & notarization: codesign -dvvv --entitlements :- "" spctl --assess --type execute -vv "" Start daemon & watch logs: sudo launchctl bootstrap system "/Library/LaunchDaemons/.plist" log stream --style compact --predicate 'process == "" OR subsystem == "com.apple.TCC"' --info Observe FDA denial message only in daemon context. Attempt to add FDA via PPPC profile (system scope) → rejected unless installed by user-approved MDM. Questions for Apple On macOS 14/15, is Full Disk Access for system daemons strictly MDM-only via PPPC (i.e., not installable locally)? Under what conditions would libEndpointSecurity report a Full Disk Access denial at client initialization, given ES consent is distinct from FDA? For unmanaged Macs needing boot-time ES processing, does Apple recommend a split: root LaunchDaemon (ES subscription; no protected file I/O) + per-user LaunchAgent (user-granted FDA) via XPC for on-demand disk access? Would moving ES connection code into a System Extension change FDA requirements for unmanaged devices, or is FDA still governed by PPPC/MDM? If behavior changed across releases, can Apple confirm the intended policy so vendors can document MDM requirements vs. unmanaged install paths? What we’ve tried Verified signature, notarization, hardened runtime, ES entitlement present. Confirmed context difference: manual run OK; daemon fails. Inspected system TCC: ES consent rows present; no FDA allow for daemon. Tried installing system-scoped PPPC locally → blocked as “must originate from a user-approved MDM server.” Considered LaunchAgent-only, but ES needs root; evaluating daemon+agent split to keep ES in root and put FDA-gated work in user space. What we need A definitive statement on the supported way to grant FDA to a system daemon on macOS 14/15. If MDM PPPC is required, we’ll ship “daemon mode requires MDM” and provide a daemon+agent fallback for unmanaged devices. If a compliant non-MDM path exists for daemon FDA on unmanaged Macs, please share exact steps. Thanks! Happy to provide additional logs privately if helpful.
12
0
1.8k
Jan ’26
DesktopServicesHelper appears to delete or unlink the source file before the ESF auth event deadline is reached, rather than waiting for the full deadline window.
On macOS Tahoe, our application using the Endpoint Security Framework (ESF) observes that during file copies through finder application, DesktopServicesHelper unlinks the source file if the ESF authorization response is delayed for ~5 seconds, even though the authorization event deadline remains 15 seconds, indicating that the process does not wait for the full ESF deadline before deleting the file. Before Tahoe, we didnt see this behaviour.
7
1
301
Jan ’26
Bug Exists in the ES_EVENT_TYPE_AUTH_UNLINK Event of System Extensions
System Version: 26.1 Device: M1 Mac Mini Steps to Reproduce: Create a UNIX socket file at /tmp/a.sock Execute the command: ln /tmp/a.sock /tmp/b.sock Execute the command: rm /tmp/b.sock Bug Description: At this point, a bug occurs in the ES_EVENT_TYPE_AUTH_UNLINK event of the system extension. The value returned for message->event.unlink.target->path is /tmp/a.sock, while it is expected to be /tmp/b.sock Reproducibility: 100%
2
0
132
Jan ’26
Endpoint Security Extension Crashing Frequently When Handling ES_EVENT_TYPE_AUTH_OPEN
I am facing a persistent issue with an Endpoint Security (ES) extension that is crashing only when processing the ES_EVENT_TYPE_AUTH_OPEN event. Other event types, including ES_EVENT_TYPE_NOTIFY_OPEN and ES_EVENT_TYPE_NOTIFY_MMAP, work without any problems. func startMonitoring() { guard !isMonitoring else { return } let result = es_new_client(&gClient) { (client, message) in guard message.pointee.action_type == ES_ACTION_TYPE_AUTH else { return } let pid = audit_token_to_pid(message.pointee.process.pointee.audit_token) if pid == gSelfPID { es_respond_flags_result(client, message, 0x7FFFFFFF, true) var token = message.pointee.process.pointee.audit_token es_mute_process(client, &token) return } guard message.pointee.event_type == ES_EVENT_TYPE_AUTH_OPEN else { es_respond_auth_result(client, message, ES_AUTH_RESULT_ALLOW, true) return } let pathData = message.pointee.event.open.file.pointee.path guard let pathPtr = pathData.data else { es_respond_flags_result(client, message, 0x7FFFFFFF, true) return } if shouldSkipPath(pathPtr, length: pathData.length) { es_respond_flags_result(client, message, 0x7FFFFFFF, true) return } if !hasBlockedExtension(pathPtr, length: pathData.length) { es_respond_flags_result(client, message, 0x7FFFFFFF, true) return } guard let execPathPtr = message.pointee.process.pointee.executable.pointee.path.data else { es_respond_flags_result(client, message, 0x7FFFFFFF, true) return } if isGoogleChrome(execPathPtr) { es_respond_auth_result(client, message, ES_AUTH_RESULT_DENY, true) } else { es_respond_flags_result(client, message, 0x7FFFFFFF, true) var token = message.pointee.process.pointee.audit_token es_mute_process(client, &token) } } guard result == ES_NEW_CLIENT_RESULT_SUCCESS, let client = gClient else { return } es_clear_cache(client) muteNoisyPaths(client: client) let events: [es_event_type_t] = [ES_EVENT_TYPE_AUTH_OPEN] let subResult = es_subscribe(client, events, UInt32(events.count)) guard subResult == ES_RETURN_SUCCESS else { es_delete_client(client) gClient = nil return } isMonitoring = true }
1
0
98
Dec ’25
UI-Less Host App for Endpoint Security Extension Installation
According to Apple's development documentation, if I want to install an Endpoint Security system extension, I need to develop a host app that must be installed in the Applications directory. Now, I want to create an ES extension to protect users from accessing certain folders. However, I don't want a custom app to pop up asking the user to allow the installation of the ES extension. (To clarify, it's fine if the system authorization request dialog pops up, but I don't want the host app's UI to appear.) Is there any way to do this?
1
0
87
Oct ’25
Something odd with Endpoint Security & was_mapped_writable
I'm seeing some odd behavior which may be a bug. I've broken it down to a least common denominator to reproduce it. But maybe I'm doing something wrong. I am opening a file read-write. I'm then mapping the file read-only and private: void* pointer = mmap(NULL, 17, PROT_READ, MAP_FILE | MAP_PRIVATE, fd, 0); I then unmap the memory and close the file. After the close, eslogger shows me this: {"close":{"modified":false,[...],"was_mapped_writable":false}} Which makes sense. I then change the mmap statement to: void* pointer = mmap(NULL, 17, PROT_READ, MAP_FILE | MAP_SHARED, fd, 0); I run the new code and and the close looks like: {"close":{"modified":false, [....], "was_mapped_writable":true}} Which also makes sense. I then run the original again (ie, with MAP_PRIVATE vs. MAP_SHARED) and the close looks like: {"close":{"modified":false,"was_mapped_writable":true,[...]} Which doesn't appear to be correct. Now if I just open and close the file (again, read-write) and don't mmap anything the close still shows: {"close":{ [...], "was_mapped_writable":true,"modified":false}} And the same is true if I open the file read-only. It will remain that way until I delete the file. If I recreate the file and try again, everything is good until I map it MAP_SHARED. I tried this with macOS 13.6.7 and macOS 15.0.1.
3
0
767
Oct ’25
How to update Endpoint Security Extension?
Hi, I’m expecting the demo from Apple, but I think I’m seeing different behavior on my end. Here are the steps I followed: Run the SampleEndpointApp example. Copy SampleEndpointApp to the /Applications folder. Run and enable the Extension and Full Disk Access → Everything works properly. I modify something in the example and start again from step #1, but nothing gets updated as shown in the demo video. The only way I can make it work is by going into Settings, manually removing the ES extension, and then running the app again. My concern is: if I deliver the application to clients, how can I properly update the extension without requiring them to manually remove it first? Thanks,
1
0
196
Sep ’25
New TCC Modify syestem extension event doesn't send svent when tcc db changed with sqlite
Hey, we also opened a bug regarding this behavior on April, back when you introduce the new event on MacOs 15.4 The bug ticket is: FB17139326 Starting macOs 15.4 you added a new event for the system extension framework named: tcc_modify The event should be triggered every-time there is a change regarding the tcc db (granted / revoked using various ways). One of the ways you can grant / revoke tcc db permission is by changing the user sqlite with root permissions. You can change various permissions regarding the user for example the apps that allowed to use microphone permissions. It is expected that when granted / revoked permissions using sqlite for microphone we will get notify from the system extension for tcc modify event. but the actual result is that the permission is added without any tcc modify event. We wanted to know if this is intentional that changing the user tcc db with root permissions, using sqlite and not conventional methods (user popup / settings), suppose to not initiate an event, and we should monitor them using other methods. Thank you, Idan
8
0
233
Sep ’25
Capturing file read events in Endpoint Security client
Hi everyone! I'd like to create an application for system monitoring using the Endpoint Security framework. I already have a working prototype and now I am trying to expand its capabilities to capture more event types. Started looking at filesystem-related events as one of the most important ones for my use case. These seem to be supported fairly well by the framework (ES_EVENT_TYPE_NOTIFY_OPEN/CLOSE/CREATE/WRITE etc.) However, the "READ FILE" event seems to be absent… Am I missing something here, or Endpoint Security framework does not provide this kind of information? If it doesn't, what is the reason behind this? Capturing this type of events seems quite relevant for security-related software. Thanks & Best regards, Roman
3
0
133
Aug ’25
Validating Signature Of XPC Process
Quinn, you've often suggested that to validate the other side of an XPC connection, we should use the audit token. But that's not available from the XPC object, whereas the PID is. So everyone uses the PID. While looking for something completely unrelated, I found this in the SecCode.h file OSStatus SecCodeCreateWithXPCMessage(xpc_object_t message, SecCSFlags flags, SecCodeRef * __nonnull CF_RETURNS_RETAINED target); Would this be the preferred way to do this now? At least from 11.0 and up. Like I said, I was looking for something completely unrelated and found this and don't have the cycles right now to try it. But it looks promising from the description and I wanted to check in with you about it in case you can say yes or no before I get a chance to test it. Thanks
8
0
8.2k
Aug ’25
es_event_open_t fflag showing O_SEARCH instead of FREAD/FWRITE on macOS 26 beta 4
Hello, Starting with macOS 26 beta 4, I’ve noticed that the fflag field in es_event_open_t sometimes contains O_SEARCH instead of the expected FREAD or FWRITE values. According to the documentation, fflag should represent the kernel-applied flags (e.g., FREAD, FWRITE), not the open(2) oflag values. However, in my tests, when intercepting ES_EVENT_TYPE_AUTH_OPEN events, the value appears to match O_SEARCH in certain cases. Is this an intentional change in macOS 26, or could it be a bug in the current beta? If this is expected behavior, could you clarify under what conditions O_SEARCH or some oflag are returned? Environment: macOS 26 beta 4 Endpoint Security Framework Thanks in advance for any clarification!
3
0
111
Aug ’25