Before diving in, go read Patrick Wardle’s blog post on these new events. Chatting with Patrick while poking at the raw bytes ultimately led to the content in this post, so as Patrick would say, Mahalo! His post covers RESERVED_5 and RESERVED_6 (network connection AUTH and NOTIFY events) in detail, plus the general approach to subscribing and dumping these undocumented events. This post picks up the remaining two functional events, focusing on RESERVED_3 and RESERVED_4.
Background
Each macOS release brings new Endpoint Security Framework (ESF) events that extend the capabilities of security tools. With macOS 26.4, Apple added seven new events, but for the first time, they arrived completely undocumented. They show up in ESTypes.h labeled only as ES_EVENT_TYPE_RESERVED_0 through _6:
// The following events are available beginning in macOS 26.3
ES_EVENT_TYPE_RESERVED_0, // Unable to subscribe
ES_EVENT_TYPE_RESERVED_1, // Unable to subscribe
ES_EVENT_TYPE_RESERVED_2, // Unable to subscribe
// The following events are available beginning in macOS 26.4.0
ES_EVENT_TYPE_RESERVED_3,
ES_EVENT_TYPE_RESERVED_4,
ES_EVENT_TYPE_RESERVED_5, // Covered by Patrick's blog
ES_EVENT_TYPE_RESERVED_6, // Covered by Patrick's blog
Subscribing to RESERVED_3 and RESERVED_4 confirmed they follow the established ESF pattern: RESERVED_3 fires as an AUTH event (requiring a response), while RESERVED_4 fires as the corresponding NOTIFY event. Initial observation showed these events triggering when processes opened listening sockets, suggesting they hook into the bind() syscall. The challenge was reconstructing the undocumented event struct to extract useful data.
Exploring the Event Structure
Test Harness
To generate controlled events with known values, a simple Python one-liner serves as a test harness:
import socket;s=socket.socket();s.bind(('192.168.1.101',0x1234));s.listen();print('listening');s.accept()
This binds to IP 192.168.1.101 (hex: c0 a8 01 65) on port 0x1234 (hex: 34 12 in little-endian). With known values to look for, identifying fields in the raw event data becomes much easier.
Following the Pointers
Dumping the raw bytes of msg->event shows an 8-byte value that looks like a pointer, followed by zeros:
msg->event:
0000: 80 a3 e5 04 01 00 00 00 00 00 00 00 00 00 00 00 |................|
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
That first value (0x0104e5a380) is a pointer to an inner struct. Following it reveals another pointer at the start, then more data:
inner struct @ 0x0104e5a380:
0000: 98 a3 e5 04 01 00 00 00 01 00 00 00 00 00 00 00 |................|
0010: 06 00 00 00 00 00 00 00 02 00 00 00 c0 a8 01 65 |...............e|
0020: 00 00 00 00 00 00 00 00 00 00 00 00 34 12 00 00 |............4...|
The two pointers are pretty close to each other. The inner struct is at 0x0104e5a380, and the pointer it contains is 0x0104e5a398. That’s 0x18 (24 bytes) ahead. We can diagram the layout as follows:
inner struct layout:
+0x00: points to +0x18
+0x08: [unknown 16 bytes]
+0x18: <-- ptr points here
Following that pointer at +0x00: the value 0x0104e5a398, to +0x18. We get a couple of familiar patterns.
*inner->ptr @ 0x0104e5a398:
0000: 02 00 00 00 c0 a8 01 65 00 00 00 00 00 00 00 00 |.......e........|
0010: 00 00 00 00 34 12 00 00 00 00 00 00 00 00 00 00 |....4...........|
There’s the bind address from the test harness. At +0x04 we see c0 a8 01 65 (192.168.1.101), and at +0x14 we see 34 12 (port 0x1234 in little-endian). The first four bytes 02 00 00 00 we interpret as AF_INET (address family 2).
The Address Structure
To confirm this analysis, testing with IPv6 validates the address family field:
python3 -c "import socket;s=socket.socket(socket.AF_INET6,socket.SOCK_STREAM);s.bind(('::1',4660));s.listen();input('bound');s.close()"
With IPv6, the address family bytes change to 1e 00 00 00 (0x1e = 30 = AF_INET6).
The Unknown 16 Bytes
Going back to the 16 bytes between the initial pointer and the address struct (offsets +0x08 through +0x17), these likely contain socket metadata. Running different socket configurations and comparing the results reveals a pattern:
| Test | +0x08 | +0x0C | +0x10 | +0x14 |
|---|---|---|---|---|
| IPv4 TCP | 1 | 0 | 6 | 0 |
| IPv6 TCP | 1 | 0 | 6 | 0 |
| IPv4 UDP | 1 | 1 | 17 | 0 |
| IPv6 UDP | 1 | 1 | 17 | 0 |
The values at +0x10 stand out: 6 for TCP and 17 for UDP. These match the IANA protocol numbers from RFC 1700:
#define IPPROTO_TCP 6 /* tcp */
#define IPPROTO_UDP 17 /* udp */
The field at +0x0C appears to indicate socket type (0 for SOCK_STREAM, 1 for SOCK_DGRAM).
To understand the remaining fields, further testing explored edge cases. Setting various socket options like SO_REUSEADDR and SO_REUSEPORT before binding had no effect on any of these values. Binding to different interfaces (loopback versus all interfaces) also produced no change in the metadata fields.
Raw sockets do not trigger the event at all. Unix domain sockets also do not generate these events, which makes sense given ESF already has ES_EVENT_TYPE_NOTIFY_UIPC_BIND for that purpose.
Across all tests, the fields at +0x08 and +0x14 remained constant at 1 and 0 respectively. Without additional test cases that cause these values to change, their exact meaning remains unclear.
Reconstructed Structure
Putting it all together with the information we have:
typedef struct {
uint32_t family; // +0x00: AF_INET (2), AF_INET6 (30)
uint8_t addr[16]; // +0x04: IPv4 in first 4 bytes; IPv6 uses all 16
uint32_t port; // +0x14: port in host byte order
} es_socket_bind_addr_t;
#define ES_SOCK_TYPE_STREAM 0
#define ES_SOCK_TYPE_DGRAM 1
typedef struct {
es_socket_bind_addr_t *addr; // +0x00: pointer to address
uint32_t unknown_08; // +0x08: always 1
uint32_t socket_type; // +0x0C: STREAM(0) or DGRAM(1)
uint32_t protocol; // +0x10: IPPROTO_TCP(6), IPPROTO_UDP(17)
uint32_t unknown_14; // +0x14: always 0
} es_socket_bind_inner_t;
typedef struct {
es_socket_bind_inner_t *inner; // +0x00
} es_event_socket_bind_t;
Conclusion
Apple’s undocumented ES_EVENT_TYPE_RESERVED_3 and _4 events provide socket bind AUTH and NOTIFY capabilities respectively. By reverse engineering the event struct, these events expose:
- Address family (IPv4/IPv6)
- Bound IP address
- Port number
- Socket type and protocol
…and perhaps a little bit more we don’t quite understand yet. Combined with the network connection events Patrick documented, macOS 26.4 delivers substantial new network visibility through ESF. While these events remain undocumented and could change in future releases, they represent a meaningful expansion of what’s possible for macOS security tools, without the complexity of Network Extensions.
We look forward to Apple formally documenting these events and providing stable struct definitions in a future SDK release. New events means new detection opportunities, and we’re excited to further explore how to leverage this enhanced visibility.
