macosioctlifconfig

IPv6 Address Assignment on en0 Throws 'Cannot Allocate Memory' Error After Multiple Additions


I was running a service that assigned a Unique Local Address (ULA) IPv6 to my en0 interface. Initially, I was able to add multiple IPv6 addresses without any issues. However, after adding 18 IPv6 addresses, I encountered the following error:

ifconfig: ioctl (SIOCAIFADDR): Cannot allocate memory

Even when I manually tried to assign another IPv6 address using:

sudo ifconfig en0 inet6 fc00:45bd::81aa:15c6 prefixlen 64 alias

I continued to receive the same error. This error persisted until I removed some of the previously assigned IPv6 addresses. Essentially, the 17th IPv6 address was added without any issues, but the error started appearing when attempting to add the 18th one.

At first, I suspected that Duplicate Address Detection (DAD) might be causing the issue. I checked my DAD settings when 18 IPv6 addresses were already assigned using:

sysctl -a | grep dad

The initial values were:

net.inet6.ip6.dad_count: 1
net.inet6.ip6.dad_enhanced: 1
net.inet6.ip6.nd6_dad_nonce_max_count: 3
net.inet6.icmp6.nd6_optimistic_dad: 63

To investigate further, I ran the following command:

/usr/bin/sudo ifconfig en0 inet6 add fc00:2703::53eb:6d0a; while sleep 0.001; do ifconfig en0 | grep net6 | cat -n | grep fc00:2703::53eb:6d0a; done

Initially, it returned:

ifconfig: ioctl (SIOCAIFADDR): Cannot allocate memory
  1302      inet6 fc00:2703::53eb:6d0a prefixlen 64 tentative 
  1302      inet6 fc00:2703::53eb:6d0a prefixlen 64 tentative 
.....
  1302      inet6 fc00:2703::53eb:6d0a prefixlen 64 tentative 
  1302      inet6 fc00:2703::53eb:6d0a prefixlen 64 
  1302      inet6 fc00:2703::53eb:6d0a prefixlen 64 
.....
  1302      inet6 fc00:2703::53eb:6d0a prefixlen 64 
  1302      inet6 fc00:2703::53eb:6d0a prefixlen 64 

After some time, the address became fully assigned without the "tentative" status.

I then disabled DAD by setting:

net.inet6.ip6.dad_count: 0
net.inet6.ip6.dad_enhanced: 0
net.inet6.ip6.nd6_dad_nonce_max_count: 0
net.inet6.icmp6.nd6_optimistic_dad: 0

Despite disabling DAD, I still encountered the "Cannot allocate memory" error when attempting to add another IPv6 address:

/usr/bin/sudo ifconfig en0 inet6 add fc00:3940::7013:336d; while sleep 0.001; do ifconfig en0 | grep net6 | cat -n | grep fc00:3940::7013:336d; done

The output was:

ifconfig: ioctl (SIOCAIFADDR): Cannot allocate memory
  1303      inet6 fc00:3940::7013:336d prefixlen 64 
  1303      inet6 fc00:3940::7013:336d prefixlen 64 
....
  1303      inet6 fc00:3940::7013:336d prefixlen 64 

The address was listed in the interface configuration but still resulted in the same error.

To further test this behavior, I ran the following minimal Go program that attempts to add multiple IPv6 addresses to my en0 interface:

package main

import (
    "crypto/sha256"
    "fmt"
    "math/big"
    "math/rand"
    "net/netip"
    "os/exec"
)

func main() {
    i := 0
    for {
        i++
        //command := exec.Command("sudo", "ifconfig", "en0", "inet6", GenerateIPv6Address().String(), "prefixlen", "64", "alias")
        command := exec.Command("sudo", "ifconfig", "en0", "inet6", "add", GenerateIPv6Address().String())
        out, err := command.CombinedOutput()

        if err != nil {
            fmt.Println("\u001B[33m", i, command.String(), "\u001B[0m")
            fmt.Println(fmt.Errorf(err.Error()))
            fmt.Println("\u001B[31m" + string(out) + "\u001B[0m")
            break
        } else {
            fmt.Println("\u001B[32m", i, command.String()+"\u001B[0m")
            fmt.Println(string(out))
        }
    }
}

func GenerateIPv6Address() netip.Addr {
    seedBytes := make([]byte, 8)
    _, err := rand.Read(seedBytes)
    if err != nil {
        return netip.Addr{}
    }

    hash := sha256.Sum256(seedBytes)
    seedValue := new(big.Int).SetBytes(hash[:8])

    randInt := seedValue.Uint64()
    group2 := randInt % 65534
    group3 := (randInt >> 16) % 65534
    group4 := (randInt >> 32) % 65534

    ipStr := fmt.Sprintf("fc00:%x::%x:%x", group2, group3, group4)

    if ip, err := netip.ParseAddr(ipStr); err == nil {
        return ip
    }

    return netip.Addr{}
}

Directly calling the ioctl system call with the SIOCAIFADDR operation also results in the same error.

#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/in_var.h>
#include <netinet6/nd6.h>
#include <sys/errno.h>
#include <stdint.h>

char* generateIPv6() {
    unsigned short group[8];
    char *buffer = malloc(40); // IPv6 max length is 39 characters + null terminator

    if (!buffer) {
        perror("Failed to allocate memory");
        exit(EXIT_FAILURE);
    }

    group[0] = 0xfc00; // Unique local address prefix
    for (int i = 1; i < 8; i++) {
        group[i] = arc4random() % 65536;
    }

    // Format the IPv6 address into the buffer
    snprintf(buffer, 40, "%x:%x:%x:%x:%x:%x:%x:%x",
           group[0], group[1], group[2], group[3],
           group[4], group[5], group[6], group[7]);

    return buffer; // Return the dynamically allocated address
}

int main() {
    int sockfd;
    struct in6_aliasreq ifr6;
    const char *interface = "en0";
    int prefix_len = 64;

    // Create socket
    sockfd = socket(AF_INET6, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        return 1;
    }

    // Infinite loop to generate and add IPv6 addresses
    while (1) {
        char *ip_address = generateIPv6();

        // Prepare interface request
        memset(&ifr6, 0, sizeof(ifr6));
        strncpy(ifr6.ifra_name, interface, IFNAMSIZ);

        if (inet_pton(AF_INET6, ip_address, &ifr6.ifra_addr.sin6_addr) != 1) {
            perror("Invalid IP address");
            free(ip_address);
            close(sockfd);
            return 1;
        }

        ifr6.ifra_addr.sin6_len = sizeof ifr6.ifra_addr;
        ifr6.ifra_addr.sin6_family = AF_INET6;
        ifr6.ifra_prefixmask.sin6_family = AF_INET6;
        ifr6.ifra_prefixmask.sin6_len = sizeof ifr6.ifra_prefixmask;
        memset(&ifr6.ifra_prefixmask.sin6_addr, 0xFF, prefix_len / 8);

        // Set infinite lifetime
        ifr6.ifra_lifetime.ia6t_vltime = ifr6.ifra_lifetime.ia6t_pltime = ND6_INFINITE_LIFETIME;

        // Perform ioctl to add the address
        if (ioctl(sockfd, SIOCAIFADDR_IN6, &ifr6) < 0) {
            perror("Failed to add IPv6 address");
            fprintf(stderr, "Error: %s\n", strerror(errno));
            free(ip_address);
            break; // Exit loop on error
        }

        printf("IPv6 address added successfully: %s\n", ip_address);
        free(ip_address);
    }

    close(sockfd);
    return 0;
}

$ sudo ktrace trace -Ss -f C4 -c ./output/bind_ipv6_using_syscall | grep bind_ipv6_using_sys

...
IPv6 address added successfully: fc00:2452:fc7e:5604:3d5c:ee50:a275:1ee3
IPv6 address added successfully: fc00:a332:1532:86bc:4f09:c01:b845:1685
IPv6 address added successfully: fc00:5f4c:dcfe:100:4542:9626:430c:de7f
IPv6 address added successfully: fc00:eb40:218a:6561:8f5a:ebab:7ed1:d3dc
IPv6 address added successfully: fc00:3422:b629:5d3a:7b69:caf0:4524:a18d
IPv6 address added successfully: fc00:cd47:a34b:f045:9620:2edd:9bc4:49f5
IPv6 address added successfully: fc00:305d:582c:94ff:d0fe:6d6f:b7df:31e1
IPv6 address added successfully: fc00:42e2:4920:93cc:6dec:8b3c:f34f:b7ef
IPv6 address added successfully: fc00:e1c3:dd39:53e:64f2:f047:8135:ee6c
IPv6 address added successfully: fc00:91ee:9b52:7cd0:862f:9101:84fd:9935
Failed to add IPv6 address: Cannot allocate memory
Error: Cannot allocate memory

I am trying to understand what might be causing this issue and whether there is a limit on the number of IPv6 addresses that can be assigned to my en0 interface.

Could this be due to an internal kernel or system limit on the number of IPv6 addresses per interface?

Is there a way to increase this limit, or is it hardcoded within macOS?

Are there any logs or diagnostic tools that could provide more insight into why the system refuses to allocate more addresses?


Solution

  • After digging in deep, I’ve figured out the root cause of the ENOMEM error when assigning many ULA IPv6 addresses on macOS(Silicon).

    The issue wasn’t due to DAD, userland ifconfig behavior, or address duplication, but rather an internal kernel limitation on the number of IPv6 prefixes allowed per interface.

    $ sysctl net.inet6.ip6.maxifprefixes
    16
    

    The error originated from within the kernel's nd6_prelist_add() function:

    if (ndi->nprefixes >= ip6_maxifprefixes) {
        return ENOMEM;
    }
    

    The sysctl controls this limit:

    $ sysctl net.inet6.ip6.maxifprefixes
    16
    

    On my system, the default was 16. Once I added more than 16 unique /64 prefixes (not just addresses), kernel function nd6_prelist_add() started returning ENOMEM. Interestingly, even though the error is returned, the IPv6 address does get added to the default interface (en0) shortly after, possibly by a deferred background operation.

    I used DTrace to track the full kernel path:

    ioctl
    └── fo_ioctl
        └── soioctl
            └── ifioctllocked
                └── ifioctl
                    └── in6_control
                        └── in6ctl_aifaddr
                            └── nd6_prelist_add ❌ ← ENOMEM triggered here
    

    Relevant kernel code:

    nd6_rtr.c:2282

    if (ndi->nprefixes >= ip6_maxifprefixes) {
        return ENOMEM;
    }
    

    Even though the kernel was returning ENOMEM from nd6_prelist_add, I noticed the IPv6 address still showed up in ifconfig. Here's why that happens.

    Inside the in6ctl_aifaddr() function, the kernel first attempts to add the IPv6 address structure itself with:

    in6.c:1746

    error = in6_update_ifa(ifp, ifra, 0, &ia);
    

    This runs before any prefix registration is attempted. So the address is added to the interface at this point — which is why it appears in ifconfig, even if something fails afterward.

    After that, the kernel tries to look up or register the prefix with:

    in6.c:1977

    if ((pr = nd6_prefix_lookup(&pr0, ND6_PREFIX_EXPIRY_NEVER)) == NULL) {
        error = nd6_prelist_add(&pr0, NULL, &pr, FALSE);
        if (error != 0) {
            goto done;
        }
    }
    

    This is where things go wrong. If I’ve already added too many unique prefixes, the kernel hits the limit defined by ip6_maxifprefixes. Before it can add the new prefix, it checks:

    nd6_rtr.c:2278

    if (ip6_maxifprefixes >= 0) {
        ndi = ND_IFINFO(ifp);
        VERIFY((NULL != ndi) && (TRUE == ndi->initialized));
        lck_mtx_lock(&ndi->lock);
        if (ndi->nprefixes >= ip6_maxifprefixes) {
            lck_mtx_unlock(&ndi->lock);
            return ENOMEM;  // ← This is the line I was hitting
        }
        lck_mtx_unlock(&ndi->lock);
    }
    

    This is the exact check that was causing the ENOMEM in my case.

    So what actually happened was:

    As a result, the interface showed the address, but internally, the kernel state was incomplete. That’s why it returned ENOMEM — the configuration wasn’t fully successful, even though the address looked fine from the outside.


    To Double-check

    I increased the limit using:

    sudo sysctl -w net.inet6.ip6.maxifprefixes=64
    

    and re-ran the test. The ENOMEM error disappeared, and I was able to add more than 16 prefixes without issue.


    Extra Debugging Details

    Code used to test this:

    #include <unistd.h>
    #include <sys/syscall.h>
    #include <sys/socket.h>
    #include <stdint.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/ioctl.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>
    #include <netinet/in_var.h>
    #include <sys/errno.h>
    
    
    int main() {
       const int fd = syscall(SYS_socket, AF_INET6, SOCK_DGRAM, 0);
       if (fd < 0) return 1;
    
    
       // Generated IP: fc00:339:1514:da9a:a5de:750f:415a:67d0
       const uint8_t req[128] = {
           0x65, 0x6e, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00,
           0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
           0x1c, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
           0xfc, 0x00, 0x03, 0x39, 0x15, 0x14, 0xda, 0x9a,
           0xa5, 0xde, 0x75, 0x0f, 0x41, 0x5a, 0x67, 0xd0,
           0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
           0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
           0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
           0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
           0x1c, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
           0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
           0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
           0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
           0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
           0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
           0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
       };
    
       // SIOCAIFADDR_IN6 -> 0x8080691a
       long ret = syscall(SYS_ioctl, fd, (unsigned long) 0x8080691a, &req);
    
    
       if (errno == 12) {
        fprintf(stderr, "❌ ENOMEM occurred in process %s (PID %d)\n", "clean_run_minimal_SIOCAIFADDR_IN", getpid());
       }
    
    
       return ret != 0;
    }
    
    // sudo gcc ./ipv6_sys_call_debugging/clean_run_minimal_SIOCAIFADDR_IN6_ioctl_syscall.c -o ./output/clean_run_minimal_SIOCAIFADDR_IN6_ioctl_syscall && sudo ./output/clean_run_minimal_SIOCAIFADDR_IN6_ioctl_syscall
    

    dtrace

    I used DTrace:

    sudo dtrace -s ipv6_tracer.d -x dynvarsize=64m
    

    Here's a trimmed version of my DTrace script that traced this live from the kernel:

    ipv6_tracer.d

    #pragma D option quiet
    
    /* ─────────── Entry to ioctl handler ─────────── */
    fbt:mach_kernel:in6ctl_aifaddr:entry
    {
        self->track = 1;
        printf("\n🔵 in6ctl_aifaddr(ifra=0x%p) → PID %d (%s)\n", arg1, pid, execname);
    
        /* Commented: risky if arg1 is kernel memory
        this->data = copyin(arg1, 64);
        printf("📦 ifra_name = %s\n", stringof(*(string *)this->data));
        */
        stack();
    }
    
    /* ─────────── Internal control path tracing ─────────── */
    fbt:mach_kernel:in6_update_ifa:entry
    /self->track/
    {
        printf("🟠 in6_update_ifa(ifra=0x%p) → PID %d\n", arg1, pid);
        stack();
    }
    
    fbt:mach_kernel:in6_update_ifa:return
    /self->track/
    {
        printf("🔴 RETURN from in6_update_ifa → ret = %d (PID %d)\n", (int)arg1, pid);
        stack();
    }
    
    fbt:mach_kernel:nd6_prelist_add:entry
    /self->track/
    {
        printf("🟣 nd6_prelist_add() → PID: %d\n", pid);
        stack();
    }
    
    fbt:mach_kernel:nd6_prelist_add:return
    {
        /* Only print ENOMEM case */
        /* ENOMEM = 12 */
        /* This check is inside the block, not as predicate */
        /* No /... && .../ to avoid syntax issues */
    
        /* Skip if not tracking */
        /* Guard to avoid unrelated calls */
        /* This must be runtime-checked */
        /* self->track may be unset on other calls */
        /* So check explicitly before printing */
        /* arg1 holds return value */
        /* Print only if ENOMEM and we were tracking */
    
        /* This version works on macOS */
        /* Return value in arg1, not retval */
        /* Predicate inside block */
    
        /* Safe wrapper */
        this->is_enomem = (self->track && arg1 == 12);
        if (this->is_enomem) {
            printf("❌ nd6_prelist_add → ENOMEM (12) → PID %d\n", pid);
            stack();
        }
    }
    
    /* ─────────── Track allocation failures ─────────── */
    fbt:mach_kernel:kalloc_ext:entry,
    fbt:mach_kernel:kalloc_large:entry
    {
        self->k_size = arg0;
    }
    
    fbt:mach_kernel:kalloc_ext:return,
    fbt:mach_kernel:kalloc_large:return
    {
        /* Return value is in arg1 */
        /* Check if size was set and return is 0 */
        /* Again avoid using /... && .../ in predicate */
    
        this->failed = (self->k_size && arg1 == 0);
        if (this->failed) {
            printf("❌ Memory alloc FAILED (%s size: %d) → PID %d (%s)\n",
                   probefunc, self->k_size, pid, execname);
            stack();
        }
        self->k_size = 0;
    }
    
    /* ─────────── Done ─────────── */
    fbt:mach_kernel:in6ctl_aifaddr:return
    /self->track/
    {
        printf("🔚 RETURN from in6ctl_aifaddr → PID %d (%s)\n", pid, execname);
        self->track = 0;
    }
    

    Output:

    🔵 in6ctl_aifaddr(ifra=0xfffffe91d243fa20) → PID 1622 (clean_run_minimal_SIOCAIFADDR_IN)
    
                  kernel.release.vmapple`in6_control+0xcd8
                  ...
                  kernel.release.vmapple`fleh_dispatch64+0x19c
    🟠 in6_update_ifa(ifra=0xfffffe91d243fa20) → PID 1622
    
                  kernel.release.vmapple`in6ctl_aifaddr+0xdc
                  ...
                  kernel.release.vmapple`fleh_dispatch64+0x19c
    🔴 RETURN from in6_update_ifa → ret = 0 (PID 1622)
    
                  kernel.release.vmapple`in6ctl_aifaddr+0xdc
                  ...
                  kernel.release.vmapple`fleh_dispatch64+0x19c
    🟣 nd6_prelist_add() → PID: 1622
    
                  kernel.release.vmapple`in6ctl_aifaddr+0x678
                  ...
                  kernel.release.vmapple`fleh_dispatch64+0x19c
    ❌ nd6_prelist_add → ENOMEM (12) → PID 1622          <----- ENOMEM in nd6_prelist_add
    
                  kernel.release.vmapple`in6ctl_aifaddr+0x678
                  kernel.release.vmapple`ifioctl+0x1060
                  kernel.release.vmapple`ifioctllocked+0x40
                  kernel.release.vmapple`soioctl+0x2b8
                  kernel.release.vmapple`fo_ioctl+0xc4
                  kernel.release.vmapple`ioctl+0x51c
                  kernel.release.vmapple`unix_syscall+0x304
                  kernel.release.vmapple`sleh_synchronous+0x3e0
                  kernel.release.vmapple`fleh_synchronous+0x28
                  kernel.release.vmapple`fleh_dispatch64+0x19c
    🔚 RETURN from in6ctl_aifaddr → PID 1622 (clean_run_minimal_SIOCAIFADDR_IN)
    

    Setup for dtrace

    To properly run dtrace, I had to disable SIP (System Integrity Protection) and authenticated-root from Recovery Mode. To get stack() output with resolved method symbols(including hex offsets), I also needed to install the Kernel Debug Kit (KDK).

    From macOS Recovery Mode > Utilities > Terminal:

    csrutil disable
    csrutil authenticated-root disable
    

    To resolve stack() output in dtrace: