🎉 Phorion ranked #1 in independent EDR telemetry evaluations. Learn more

Reverse Engineering macOS 26.4's Undocumented Socket Bind Events

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 TCP1060
IPv6 TCP1060
IPv4 UDP11170
IPv6 UDP11170

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.

Let's Talk

See how Phorion protects your macOS fleet

Purpose-built by macOS security researchers. One lightweight agent delivering detection, prevention, and visibility.

Ready to see it in action? Book a demo and we'll show you how Phorion can protect your fleet.

Book a Demo

Error

Expect a personal email from our team.

Pricing About Us Blog