Intro

This post contains the original report as sent to Apple Security with slight redactions for clarity. It has been fixed in macOS Tahoe (and *OS 26), and behavior on macOS now matches expected one.

I have originally stumbled it because I was trying to run CoreDNS on macOS, and ended up removing bugged functionality from the CoreDNS while also submitting the issue in XNU to Apple.

I plan to look into how exactly this issue has been fixed once updated XNU sources are released.

Apple’s disclosure:

A logic issue was addressed with improved state management. A UDP server socket bound to a local interface may become bound to all interfaces.


Summary

The sendmsg system call accepts a msghdr which can have control messages in it, and one of supported control messages is IPPROTO_IP/IP_PKTINFO.

It allows the userspace program to specify what if_index and/or what source IP address to use when sending the UDP message, and is often used when running UDP servers to respond using same if_index/source address to match the “request” packet.

The UDP sockets can also be bound to a specific address and port to “listen” and accept packets on it, which is how UDP servers are usually made.

However, when a sendmsg with IP_PKTINFO is used on a bound UDP socket – the binding address on it is reset and forced to 0.0.0.0, making it accept packets for any address on the system on any interface.

For example, a “private” UDP server which was specifically bound to localhost gets implicitly exposed to any address after sending a packet from a localhost address to a localhost address.

This has been a known yet unfixed issue for some time, for example:

And it has already caused some security issues, for example:

Other interesting issue caused by this bug is allowing to bind multiple sockets to the same port on 0.0.0.0, without having SO_REUSEPORT on them. It should not be possible to bind multiple sockets from different programs to the same port on the same address without REUSEPORT, but binding them to different addresses and triggering this bug makes it possible, which might cause other issues – I haven’t tested this aspect.

The only other kernel/OS I know of which supports PKTINFO is Linux, and it is not affected by this issue – sending UDP packets doesn’t modify the socket itself, unlike on XNU.

I’ve attached a PoC UDP echo server to reproduce the issue, it can be built with simple cc udp-echo.c -o udp-echo-xnu.

I believe this issue might be caused by this code: XNU: bsd/netinet/udp_usrreq.c udp_output It’s not valid to just do udp_dodisconnect on IP_PKTINFO – the binding should be kept as it was, and not reset to 0.0.0.0.

Steps to reproduce

  1. Build provided udp-echo.c PoC file: cc udp-echo.c -o udp-echo-xnu
  2. Run ./udp-echo-xnu 127.0.0.1 9011
  3. Try to connect to it from a different host – it shouldn’t be possible and won’t be possible
  4. Connect to it from local machine, i.e. with netcat:
$ echo local | nc -w 1 -u 127.0.0.1 9011
local
  1. Try to connect to it from a different host – it will connect now, since the binding address is lost – the server was implicitly exposed to all addresses, even though it was explicitly bound to one.

Actual results

Here’s log from the server:

$ ./udp-echo-xnu 127.0.0.1 9011
[30480] Listening on 127.0.0.1:9011
[30480] [initial] Bound address: 127.0.0.1:9011

We can verify that the socket is indeed bound to 127.0.0.1:9011

$ lsof -nP -i 4UDP:9011
COMMAND     PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
udp-echo- 30480 stek    3u  IPv4 0x8e9d1c52ab54a041      0t0  UDP 127.0.0.1:9011

We trigger the bug by running:

$ echo local | nc -w 1 -u 127.0.0.1 9011
local

Log from the server:

[30480] [recvmsg] control buffer (24 bytes):
[30480] 18 00 00 00 00 00 00 00 1a 00 00 00 01 00 00 00
[30480] 00 00 00 00 7f 00 00 01
[30480] control: level=0, type=26, len=24
[30480] addr: 127.0.0.1, spec_dst: 0.0.0.0 (interface index: 1)
[30480] received 6 bytes from 127.0.0.1:55352
[30480] 6c 6f 63 61 6c 0a
[30480] [sendmsg] control buffer (24 bytes):
[30480] 18 00 00 00 00 00 00 00 1a 00 00 00 01 00 00 00
[30480] 7f 00 00 01 00 00 00 00
[30480] control: level=0, type=26, len=24
[30480] addr: 0.0.0.0, spec_dst: 127.0.0.1 (interface index: 1)
[30480] [before sendmsg] Bound address: 127.0.0.1:9011
[30480] sent 6 bytes to 127.0.0.1:55352
[30480] 6c 6f 63 61 6c 0a
[30480] [after sendmsg] Bound address: 0.0.0.0:9011

Notice how the Bound address has changed. We can actually see that in lsof too:

$ lsof -nP -i 4UDP:9011
COMMAND     PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
udp-echo- 30480 stek    3u  IPv4 0x8e9d1c52ab54a041      0t0  UDP *:9011

And the server is now reachable on any address the machine has, i.e. from a different machine on local network, or on VPN interfaces the machine has.

I’ve connected from a different machine in local network (192.168.55.5) as an example – such connection should not be possible for a socket which was previously explicitly bound to localhost:

[30480] [recvmsg] control buffer (24 bytes):
[30480] 18 00 00 00 00 00 00 00 1a 00 00 00 0b 00 00 00
[30480] 00 00 00 00 c0 a8 37 87
[30480] control: level=0, type=26, len=24
[30480] addr: 192.168.55.135, spec_dst: 0.0.0.0 (interface index: 11)
[30480] received 5 bytes from 192.168.55.5:55290
[30480] 70 77 6e 64 0a
[30480] [sendmsg] control buffer (24 bytes):
[30480] 18 00 00 00 00 00 00 00 1a 00 00 00 0b 00 00 00
[30480] c0 a8 37 87 00 00 00 00
[30480] control: level=0, type=26, len=24
[30480] addr: 0.0.0.0, spec_dst: 192.168.55.135 (interface index: 11)
[30480] [before sendmsg] Bound address: 0.0.0.0:9011
[30480] sent 5 bytes to 192.168.55.5:55290
[30480] 70 77 6e 64 0a
[30480] [after sendmsg] Bound address: 0.0.0.0:9011

multiple sockets with same address/port

a demonstration for the other issue caused by this bug:

# starting two servers on the same port, but different addresses
$ ./udp-echo-xnu 127.0.0.1 9011 & ./udp-echo-xnu 192.168.55.135 9011 &
[1] 31237
[2] 31238
[31237] Listening on 127.0.0.1:9011
[31237] [initial] Bound address: 127.0.0.1:9011
[31238] Listening on 192.168.55.135:9011
[31238] [initial] Bound address: 192.168.55.135:9011

$ lsof -nP -i 4UDP:9011
COMMAND     PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
udp-echo- 31132 stek    3u  IPv4 0xdf670b25b6cf9b5f      0t0  UDP 127.0.0.1:9011
udp-echo- 31133 stek    3u  IPv4 0x8e9d1c52ab54a041      0t0  UDP 192.168.55.135:9011

# trigger the issue for 127.0.0.1 server
[31237] [recvmsg] control buffer (24 bytes):
[31237] 18 00 00 00 00 00 00 00 1a 00 00 00 01 00 00 00
[31237] 00 00 00 00 7f 00 00 01
[31237] control: level=0, type=26, len=24
[31237] addr: 127.0.0.1, spec_dst: 0.0.0.0 (interface index: 1)
[31237] received 6 bytes from 127.0.0.1:60300
[31237] 6c 6f 63 61 6c 0a
[31237] [sendmsg] control buffer (24 bytes):
[31237] 18 00 00 00 00 00 00 00 1a 00 00 00 01 00 00 00
[31237] 7f 00 00 01 00 00 00 00
[31237] control: level=0, type=26, len=24
[31237] addr: 0.0.0.0, spec_dst: 127.0.0.1 (interface index: 1)
[31237] [before sendmsg] Bound address: 127.0.0.1:9011
[31237] sent 6 bytes to 127.0.0.1:60300
[31237] 6c 6f 63 61 6c 0a
[31237] [after sendmsg] Bound address: 0.0.0.0:9011

# checking the bindings -- first server is rebound
$ lsof -nP -i 4UDP:9011
COMMAND     PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
udp-echo- 31237 stek    3u  IPv4 0x6060d6573ac8b70f      0t0  UDP *:9011
udp-echo- 31238 stek    3u  IPv4 0x8e9d1c52ab54a041      0t0  UDP 192.168.55.135:9011

# trigger the issue for 192.168.55.135 server
$ echo local | nc -w 1 -u 192.168.55.135 9011
local

[31238] [recvmsg] control buffer (24 bytes):
[31238] 18 00 00 00 00 00 00 00 1a 00 00 00 0b 00 00 00
[31238] 00 00 00 00 c0 a8 37 87
[31238] control: level=0, type=26, len=24
[31238] addr: 192.168.55.135, spec_dst: 0.0.0.0 (interface index: 11)
[31238] received 6 bytes from 192.168.55.135:60612
[31238] 6c 6f 63 61 6c 0a
[31238] [sendmsg] control buffer (24 bytes):
[31238] 18 00 00 00 00 00 00 00 1a 00 00 00 0b 00 00 00
[31238] c0 a8 37 87 00 00 00 00
[31238] control: level=0, type=26, len=24
[31238] addr: 0.0.0.0, spec_dst: 192.168.55.135 (interface index: 11)
[31238] [before sendmsg] Bound address: 192.168.55.135:9011
[31238] sent 6 bytes to 192.168.55.135:60612
[31238] 6c 6f 63 61 6c 0a
[31238] [after sendmsg] Bound address: 0.0.0.0:9011

# both servers now listening on 0.0.0.0:9011, but they haven't specified reuse port opt
$ lsof -nP -i 4UDP:9011
COMMAND     PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
udp-echo- 31237 stek    3u  IPv4 0x6060d6573ac8b70f      0t0  UDP *:9011
udp-echo- 31238 stek    3u  IPv4 0x8e9d1c52ab54a041      0t0  UDP *:9011

# and they're both getting packets randomly now

Expected results

Here’s the same code running on Linux – the only other OS I know of which supports the IP_PKTINFO for sendmsg. It’s pretty much what I am expecting from XNU too, thus it’s in the “expected results” section.

$ ./udp-echo-lnx 127.0.0.1 9011
[24] Listening on 127.0.0.1:9011
[24] [initial] Bound address: 127.0.0.1:9011
[24] [recvmsg] control buffer (32 bytes):
[24] 1c 00 00 00 00 00 00 00 00 00 00 00 08 00 00 00
[24] 01 00 00 00 7f 00 00 01 7f 00 00 01 00 00 00 00
[24] control: level=0, type=8, len=28
[24] addr: 127.0.0.1, spec_dst: 127.0.0.1 (interface index: 1)
[24] received 6 bytes from 127.0.0.1:48425
[24] 6c 6f 63 61 6c 0a
[24] [sendmsg] control buffer (32 bytes):
[24] 1c 00 00 00 00 00 00 00 00 00 00 00 08 00 00 00
[24] 01 00 00 00 7f 00 00 01 7f 00 00 01 00 00 00 00
[24] control: level=0, type=8, len=28
[24] addr: 127.0.0.1, spec_dst: 127.0.0.1 (interface index: 1)
[24] [before sendmsg] Bound address: 127.0.0.1:9011
[24] sent 6 bytes to 127.0.0.1:48425
[24] 6c 6f 63 61 6c 0a
[24] [after sendmsg] Bound address: 127.0.0.1:9011

The socket stays bound to 127.0.0.1 as expected, and sending a packet on a bound socket doesn’t make it exposed on all addresses like on XNU.

Attached code

Also available as GitHub Gist.

`udp-echo.c`
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#define BUFFER_SIZE 1024
#define CONTROL_BUFFER_SIZE 1024

// This is a PoC/reproducer for CVE-2025-43359
// See https://stek29.rocks/2025/10/13/xnu-udp-pktinfo-cve for the full report

void uslogf(const char *fmt, ...) {
  va_list args;
  va_start(args, fmt);
  fprintf(stdout, "[%d] ", getpid());
  vfprintf(stdout, fmt, args);
  va_end(args);
}

void print_hex(const unsigned char *data, size_t len) {
  for (size_t i = 0; i < len; ++i) {
    if (i % 16 == 0)
      fprintf(stdout, "[%d] ", getpid());
    printf("%02x ", data[i]);
    if ((i + 1) % 16 == 0)
      printf("\n");
  }
  if (len % 16 != 0)
    printf("\n");
}

void print_bound_address(int sockfd, const char *label) {
  struct sockaddr_in addr;
  socklen_t addrlen = sizeof(addr);
  if (getsockname(sockfd, (struct sockaddr *)&addr, &addrlen) == 0) {
    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &addr.sin_addr, ip, sizeof(ip));
    uslogf("[%s] Bound address: %s:%d\n", label, ip, ntohs(addr.sin_port));
  } else {
    perror("getsockname failed");
  }
}

void print_control_messages(struct msghdr *msg) {
  struct cmsghdr *cmsg;
  for (cmsg = CMSG_FIRSTHDR(msg); cmsg != NULL; cmsg = CMSG_NXTHDR(msg, cmsg)) {
    uslogf("control: level=%d, type=%d, len=%u\n", cmsg->cmsg_level,
           cmsg->cmsg_type, (unsigned)cmsg->cmsg_len);

    if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) {
      struct in_pktinfo *pktinfo = (struct in_pktinfo *)CMSG_DATA(cmsg);
      char dst[INET_ADDRSTRLEN], spec[INET_ADDRSTRLEN];
      inet_ntop(AF_INET, &pktinfo->ipi_addr, dst, sizeof(dst));
      inet_ntop(AF_INET, &pktinfo->ipi_spec_dst, spec, sizeof(spec));
      uslogf("addr: %s, spec_dst: %s (interface index: %d)\n", dst, spec,
             pktinfo->ipi_ifindex);
    }
  }
}

int main(int argc, char *argv[]) {
  if (argc != 3) {
    fprintf(stderr, "Usage: %s <listen_addr> <port>\n", argv[0]);
    return EXIT_FAILURE;
  }

  const char *listen_addr = argv[1];
  int port = atoi(argv[2]);

  int sockfd;
  struct sockaddr_in server_addr, client_addr;
  char buffer[BUFFER_SIZE];
  char control_buffer[CONTROL_BUFFER_SIZE];
  struct iovec iov[1];
  struct msghdr msg;
  struct cmsghdr *cmsg;
  struct in_pktinfo *pktinfo;
  socklen_t client_len = sizeof(client_addr);

  if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
    perror("socket failed");
    exit(EXIT_FAILURE);
  }

  int opt = 1;
  if (setsockopt(sockfd, IPPROTO_IP, IP_PKTINFO, &opt, sizeof(opt)) < 0) {
    perror("setsockopt IP_PKTINFO failed");
    close(sockfd);
    exit(EXIT_FAILURE);
  }

  memset(&server_addr, 0, sizeof(server_addr));
  server_addr.sin_family = AF_INET;
  inet_pton(AF_INET, listen_addr, &server_addr.sin_addr);
  server_addr.sin_port = htons(port);

  if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
    perror("bind failed");
    close(sockfd);
    exit(EXIT_FAILURE);
  }

  uslogf("Listening on %s:%d\n", listen_addr, port);
  print_bound_address(sockfd, "initial");

  while (1) {
    memset(&msg, 0, sizeof(msg));
    memset(&client_addr, 0, sizeof(client_addr));
    memset(control_buffer, 0, CONTROL_BUFFER_SIZE);

    iov[0].iov_base = buffer;
    iov[0].iov_len = sizeof(buffer);

    msg.msg_name = &client_addr;
    msg.msg_namelen = sizeof(client_addr);
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    msg.msg_control = control_buffer;
    msg.msg_controllen = CONTROL_BUFFER_SIZE;

    ssize_t n = recvmsg(sockfd, &msg, 0);
    if (n < 0) {
      perror("recvmsg failed");
      continue;
    }

    uslogf("[recvmsg] control buffer (%u bytes):\n", msg.msg_controllen);
    print_hex((unsigned char *)msg.msg_control, msg.msg_controllen);
    print_control_messages(&msg);

    pktinfo = NULL;
    for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL;
         cmsg = CMSG_NXTHDR(&msg, cmsg)) {
      if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) {
        pktinfo = (struct in_pktinfo *)CMSG_DATA(cmsg);
        break;
      }
    }

    char client_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
    uslogf("received %zd bytes from %s:%d\n", n, client_ip,
           ntohs(client_addr.sin_port));
    print_hex((unsigned char *)buffer, n);

    struct msghdr reply_msg;
    struct iovec reply_iov[1];
    char reply_ctrl[CONTROL_BUFFER_SIZE];
    memset(&reply_msg, 0, sizeof(reply_msg));
    memset(reply_ctrl, 0, sizeof(reply_ctrl));

    reply_iov[0].iov_base = buffer;
    reply_iov[0].iov_len = n;

    reply_msg.msg_name = &client_addr;
    reply_msg.msg_namelen = sizeof(client_addr);
    reply_msg.msg_iov = reply_iov;
    reply_msg.msg_iovlen = 1;
    reply_msg.msg_control = reply_ctrl;
    reply_msg.msg_controllen = CMSG_SPACE(sizeof(struct in_pktinfo));

    struct cmsghdr *reply_cmsg = CMSG_FIRSTHDR(&reply_msg);
    reply_cmsg->cmsg_level = IPPROTO_IP;
    reply_cmsg->cmsg_type = IP_PKTINFO;
    reply_cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo));

    struct in_pktinfo *reply_pktinfo =
        (struct in_pktinfo *)CMSG_DATA(reply_cmsg);
    memset(reply_pktinfo, 0, sizeof(struct in_pktinfo));

    if (pktinfo) {
      reply_pktinfo->ipi_ifindex = pktinfo->ipi_ifindex;
      reply_pktinfo->ipi_spec_dst = pktinfo->ipi_addr;
      reply_pktinfo->ipi_addr = pktinfo->ipi_spec_dst;
    }

    uslogf("[sendmsg] control buffer (%u bytes):\n", reply_msg.msg_controllen);
    print_hex((unsigned char *)reply_msg.msg_control, reply_msg.msg_controllen);
    print_control_messages(&reply_msg);

    print_bound_address(sockfd, "before sendmsg");
    if (sendmsg(sockfd, &reply_msg, 0) < 0) {
      perror("sendmsg failed");
    } else {
      uslogf("sent %zd bytes to %s:%d\n", n, client_ip,
             ntohs(client_addr.sin_port));
      print_hex((unsigned char *)buffer, n);
    }
    print_bound_address(sockfd, "after sendmsg");
  }

  close(sockfd);
  return 0;
}