CVE-2025-43353: A Heap Buffer Overflow in Libinfo’s clnt_sperror()
Apple Security Advisory: macOS Tahoe 26
Introduction
This is a heap buffer overflow I found in clnt_sperror(), part of Libinfo’s rpc.subproj/clnt_perror.c. Libinfo is one of Apple’s open-source repositories on GitHub, which makes its RPC client code easy to read even though it ships compiled into a closed-source system library.
I got here by cross-referencing past security releases against the frameworks Apple publishes on GitHub. That turned up several libraries worth reading, including libc, Libinfo, and libxml2. I found memory-corrupting bugs in more than one of them, but this is the one I want to walk through.
clnt_sperror() is the routine that formats a human-readable string describing an RPC client error. The problem is that it builds that string in a fixed-size buffer while trusting a caller-supplied prefix it should not trust.
The Vulnerability
The vulnerability lives in clnt_sperror(), which allocates a static 256-byte buffer via _buf() and then writes into it with sprintf and strcpy. Below is the relevant code from rpc.subproj/clnt_perror.c:
static char *buf;
static char *
_buf()
{
if (buf == 0)
buf = (char *)malloc(256);
return (buf);
}
...
char *str = _buf();
...
(void) sprintf(str, "%s: ", s);
str += strlen(str);
(void) strcpy(str, clnt_sperrno(e.re_status));
The buffer is a single 256-byte allocation returned by _buf(). The first write into it is sprintf(str, "%s: ", s), where s is a caller-supplied string. Nothing checks the length of s before it is formatted into the buffer, so a long s alone can already run past 256 bytes. The pointer is then advanced by strlen(str) and a second value is copied in with strcpy. Neither the sprintf nor the strcpy is bounded, and there is no truncation.
Root Cause
The root cause is that unbounded caller input is written into a fixed 256-byte buffer with no length check. The s argument passed to clnt_sperror() is entirely under the caller’s control and can be arbitrarily long, yet sprintf(str, "%s: ", s) formats it into a buffer sized for a short, well-formed error string. The subsequent strcpy of the error description compounds the problem by appending more bytes with no regard for how much space is left.
The buffer itself comes from a single malloc(256) inside _buf(), so the overflow lands on the heap. Because Libinfo compiles into the closed-source libsystem_info.dylib, this code runs inside a shipping system library where the fixed size is baked in and not visible to callers.
Reproduction
Conceptually, the trigger is any application that calls clnt_sperror with an oversized message. When s is longer than the buffer can hold, sprintf(str, "%s: ", s) writes past the end of the 256-byte allocation returned by _buf(), corrupting adjacent heap memory before the strcpy even runs. No malformed RPC state is required; the caller’s own prefix string is enough.
Running a proof-of-concept that hands clnt_sperror a 1023-byte message produces the following AddressSanitizer crash:
=================================================================
==3651==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x611000000140 at pc 0x000103601f6c bp 0x00016d175cb0 sp 0x00016d175450
WRITE of size 1023 at 0x611000000140 thread T0
#0 0x000103601f68 in memcpy+0x2ac (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x85f68)
#1 0x00018638bac8 in __sfvwrite+0x14c (libsystem_c.dylib:arm64e+0x4ac8)
#2 0x00018638b5f8 in __vfprintf+0x2b90 (libsystem_c.dylib:arm64e+0x45f8)
#3 0x000186393188 in vsprintf_l+0xc8 (libsystem_c.dylib:arm64e+0xc188)
#4 0x000103597ebc in vsprintf+0x74 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x1bebc)
#5 0x0001035986cc in sprintf+0x38 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x1c6cc)
#6 0x000186552f94 in clnt_sperror+0x7c (libsystem_info.dylib:arm64e+0x1df94)
#7 0x000102c88994 in main clnt_sperror_overflow.c:28
#8 0x000186156b94 in start+0x17b8 (dyld:arm64e+0xfffffffffff3ab94)
0x611000000140 is located 0 bytes after 256-byte region [0x611000000040,0x611000000140)
allocated by thread T0 here:
#0 0x0001035b938c in malloc+0x78 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x3d38c)
#1 0x000186313a80 in _malloc_type_malloc_outlined+0x60 (libsystem_malloc.dylib:arm64e+0x1da80)
#2 0x000186552f58 in clnt_sperror+0x40 (libsystem_info.dylib:arm64e+0x1df58)
#3 0x000102c88994 in main clnt_sperror_overflow.c:28
#4 0x000186156b94 in start+0x17b8 (dyld:arm64e+0xfffffffffff3ab94)
SUMMARY: AddressSanitizer: heap-buffer-overflow (libsystem_c.dylib:arm64e+0x4ac8) in __sfvwrite+0x14c
Shadow bytes around the buggy address:
0x610ffffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x610fffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x610fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x611000000000: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
0x611000000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x611000000100: 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa fa fa
0x611000000180: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x611000000200: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fa
0x611000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x611000000300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x611000000380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==3651==ABORTING
Any application calling clnt_sperror with an unbounded message (s) can corrupt heap memory, turning a routine error-formatting call into a memory-corruption primitive.