sendto() system call doesn't return an error even when there is one

Please consider this very trivial C code, which was run on 15.3.1 of macos:

#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "sys/socket.h"
#include <string.h>
#include <unistd.h>
#include <ifaddrs.h>
#include <net/if.h>

// prints out the sockaddr_in6
void print_addr(const char *msg_prefix, struct sockaddr_in6 sa6) {
    char addr_text[INET6_ADDRSTRLEN] = {0};
    printf("%s%s:%d, addr family=%u\n",
           msg_prefix,
           inet_ntop(AF_INET6, &sa6.sin6_addr, (char *) &addr_text, INET6_ADDRSTRLEN),
           sa6.sin6_port,
           sa6.sin6_family);
}

// creates a datagram socket
int create_dgram_socket() {
    const int fd = socket(AF_INET6, SOCK_DGRAM, 0);
    if (fd < 0) {
        perror("Socket creation failed");
        return -1;
    }
    return fd;
}

int main() {
    printf("current process id:%ld parent process id: %ld\n", (long) getpid(), (long) getppid());
    //
    // hardcode a link-local IPv6 address of a interface which is down
    // ifconfig:
    // ,,,
    //  awdl0: flags=8822<BROADCAST,SMART,SIMPLEX,MULTICAST> mtu 1500
    //    options=6460<TSO4,TSO6,CHANNEL_IO,PARTIAL_CSUM,ZEROINVERT_CSUM>
    //    ...
    //    inet6 fe80::34be:50ff:fe14:ecd7%awdl0 prefixlen 64 scopeid 0x10
    //    nd6 options=201<PERFORMNUD,DAD>
    //    media: autoselect (<unknown type>)
    //    status: inactive
    //
    const char *ip6_addr_str = "fe80::34be:50ff:fe14:ecd7"; // link-local ipv6 address from above ifconfig output
    // parse the string literal to in6_addr
    struct in6_addr ip6_addr;
    int rv = inet_pton(AF_INET6, ip6_addr_str, &ip6_addr);
    if (rv != 1) {
        fprintf(stderr, "failed to parse ipv6 addr %s\n", ip6_addr_str);
        exit(EXIT_FAILURE);
    }

    // create a AF_INET6 SOCK_DGRAM socket
    const int sock_fd = create_dgram_socket();
    if (sock_fd < 0) {
        exit(EXIT_FAILURE);
    }
    printf("created a socket, descriptor=%d\n", sock_fd);
    // create a destination sockaddr which points to the above
    // ipv6 link-local address and an arbitrary port
    const int dest_port = 12345;
    struct sockaddr_in6 dest_sock_addr;
    memset((char *) &dest_sock_addr, 0, sizeof(struct sockaddr_in6));
    dest_sock_addr.sin6_addr = ip6_addr;
    dest_sock_addr.sin6_port = htons(dest_port);
    dest_sock_addr.sin6_family = AF_INET6;
    dest_sock_addr.sin6_scope_id = 0x10; // scopeid from the above ifconfig output

    // now sendto() to that address, whose network interface is down.
    // we expect sendto() to return an error
    print_addr("sendto() to ", dest_sock_addr);
    const char *msg = "hello";
    const size_t msg_len = strlen(msg) + 1;
    rv = sendto(sock_fd, msg, msg_len, 0, (struct sockaddr *) &dest_sock_addr, sizeof(dest_sock_addr));
    if (rv == -1) {
        perror("sendto() expectedly failed");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }
    printf("sendto() unexpectedly succeeded\n"); // should not reach here, we expect sendto() to return an error
    return 0;
}


It creates a SOCK_DGRAM socket and attempts to sendto() to a link-local IPv6 address of a local network interface which is not UP. The sendto() is expected to fail with a "network is down" (or at least fail with some error). Let's see how it behaves.

Copy that code to a file called netdown.c and compile it as follows:

clang netdown.c

Now run the program:

./a.out

That results in the following output:

current process id:29290 parent process id: 21614
created a socket, descriptor=3
sendto() to fe80::34be:50ff:fe14:ecd7:14640, addr family=30
sendto() unexpectedly succeeded

(To reproduce this locally, replace the IPv6 address in that code with a link-local IPv6 address of an interface that is not UP on your system)

Notice how the sendto() returned successfully without any error giving an impression to the application code that the message has been sent. In reality, the message isn't really sent. Here's the system logs from that run:

PID Type    Date & Time                         Process Message
	debug	2025-03-13 23:36:36.830147 +0530	kernel	Process (a.out) allowed via dev tool environment (/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal)
	debug	2025-03-13 23:36:36.833054 +0530	kernel	[SPI][HIDSPI] 
TX: 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
RX: 20 02 00 00 00 00 38 00 10 02 00 17 00 00 2E 00
26700	error	2025-03-13 23:36:36.838607 +0530	nehelper	Failed to get the signing identifier for 29290: No such process
26700	error	2025-03-13 23:36:36.838608 +0530	nehelper	Failed to get the code directory hash for 29290: No such process
	default	2025-03-13 23:36:36.840070 +0530	kernel	cfil_dispatch_attach_event:3507 CFIL: Failed to get effective audit token for <sockID 22289651233205710 <4f3051d7ec2dce>>
26700	error	2025-03-13 23:36:36.840678 +0530	nehelper	Failed to get the signing identifier for 29290: No such process
26700	error	2025-03-13 23:36:36.840679 +0530	nehelper	Failed to get the code directory hash for 29290: No such process
	default	2025-03-13 23:36:36.841742 +0530	kernel	cfil_hash_entry_log:6082 <CFIL: Error: sosend_reinject() failed>: [29290 ] <UDP(17) out so 891be95f39bd0385 22289651233205710 22289651233205710 age 0> lport 60244 fport 12345 laddr fe80::34be:50ff:fe14:ecd7 faddr fe80::34be:50ff:fe14:ecd7 hash D7EC2DCE
	default	2025-03-13 23:36:36.841756 +0530	kernel	cfil_service_inject_queue:4466 CFIL: sosend() failed 50

Notice the last line where it states the sosend() (and internal impl detail of macos) failed with error code 50, which corresponds to ENETDOWN ("Network is down"). However, like I noted, this error was never propagated back to the application from the sendto() system call.

The documentation of sendto() system call states:

man sendto
...
Locally detected errors are indicated by a return value of -1.
...
RETURN VALUES
     Upon successful completion, the number of bytes which were sent is returned.  Otherwise, -1 is returned and the global variable errno is set to indicate the error.

So I would expect sendto() to return -1, which it isn't.

The 15.3.1 source of xnu hasn't yet been published but there is the 15.3 version here https://github.com/apple-oss-distributions/xnu/tree/xnu-11215.81.4 and looking at the corresponding function cfil_service_inject_queue, line 4466 (the one which is reported in the logs) https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/bsd/net/content_filter.c#L4466, the code there logs this error and the cfil_service_inject_queue function then returns back the error. However, looking at the call sites of the call to cfil_service_inject_queue(...), there are several places within that file which don't track the return value (representing an error value) and just ignore it. Is that intentional and does that explain this issue?

Does this deserve to be reported as a bug through feedback assistant?

Does this deserve to be reported as a bug through feedback assistant?

Tricky.

This is one of those places where things have moved on to the point where it’s hard to maintain traditional BSD Sockets invariants. Specifically, sendto is simultaneously not allowed to block (in non-blocking mode) and required to correctly return local errors. But what happens if the system can’t check for local errors without blocking?

Whether that’s a bug or not depends on your context. And speaking of context, what’s the context here?

I suspect that this is coming out of a test suite. Is that right? Or do you have real world code that’s running into problems because of this behaviour?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Whether that’s a bug or not depends on your context. And speaking of context, what’s the context here?

I suspect that this is coming out of a test suite. Is that right? Or do you have real world code that’s running into problems because of this behaviour?

The above native C code was hand crafted from a Java program which runs as part of our large testsuite. Although that program is part of a testsuite, the actual program is very trivial and matches what one would expect out of a normal Java real world application.

This current thread uses ENETDOWN as an example of demonstrating an issue where the sendto() doesn't report back errors promptly. After I opened this thread, I noticed a variant of this issue where the error starts getting reported on subsequent calls to sendto() if the program adds a small delay between the 2 calls. I explain that in a separate thread here https://developer.apple.com/forums/thread/776630. I will add a Java program to that thread, which demonstrates a real world usage where this current behaviour is problematic.

I'm sorry about these 2 duplicate threads, but I only realized later that these 2 issues are just a variation of each other. I think it would be easier to just continue this discussion in that other thread.

sendto() system call doesn't return an error even when there is one
 
 
Q