Hi Gary, Short summary up front so I don't waste your time before creating an official issue: this is a *local* crash bug, not remote, and not in the packet-parse path. It's a small integer underflow in the outbound control_send() wrappers, separate from the parse-path reports in #397/#400/#387/#398/#401. A one-line-per-driver fix is attached and verified.
The bug
-------
ubx_control_send()/control_send() compute the payload length as
"data_len - 2" (Zodiac: "len/2 - 1") with no check that data_len >= 2. A
control message shorter than the 2-byte class/id header underflows size_t to
~SIZE_MAX, which the *_write() helper then passes to memcpy()/a loop:
- u-blox ubx_write -> memcpy(session->msgbuf, .., ~SIZE_MAX)
OOB write
- AllyStar ally_write -> same
OOB write
- Zodiac end_write -> stack buf[BUFSIZ] loop, huge count
stack overflow
- CASIC control_send -> no crash; casic_write() already rejects the
oversize length (1-byte over-read of
msg[1] only)
In ubx_write()/ally_write() the "TOO LONG!" guard only logs and falls
through to
the memcpy; casic_write() correctly return false's, which is why CASIC is
safe.
The idiom started in ubx_write (2010) and was copied into the 2024 AllyStar
and
CASIC drivers.
Reachability
------------
handle_control() in gpsd.c never calls control_send() - "!dev=" and "&dev="
do
raw write()s - so this is not reachable through the daemon socket. The
callers
with a caller-controlled length are the local tools: gpsctl -x CONTROL
<device>
(gpsctl.c:986) and gpsmon (gpsmon.c:708). So the practical impact is
crashing
gpsctl/gpsmon, or a wrapper that feeds untrusted data to "gpsctl -x". The
underflowed size is always ~SIZE_MAX (not tunable), so it faults
immediately -
a reliable crash, not a controlled-write primitive.
Reproduce (no fuzzing framework needed)
---------------------------------------
control_send_repro.c is a ~70-line standalone program that does exactly what
gpsctl -x does (a char cooked[BUFSIZ] with cooklen == 1); it links only
against
your driver sources:
CC=clang sh repro_build.sh . ./control_send_repro
./control_send_repro ubx # AddressSanitizer: negative-size-param,
ubx_write:5886
./control_send_repro allystar # AddressSanitizer: negative-size-param,
control_send:1882
./control_send_repro zodiac # AddressSanitizer:
stack-buffer-overflow, end_write:55
./control_send_repro casic # "returned without crashing" (guarded)
Equivalent real-tool trigger: gpsctl -t "u-blox" -x "41" <writable
pty/device>.
Reproduced on master @ 55e184b40 (and it's present in all older revisions).
Fix
---
Attached patch adds a 2-byte minimum-length guard to each wrapper. With it
applied all four cases return cleanly and a short fuzz over the control
path is
clean. Optional defence-in-depth: make the "TOO LONG!" checks in ubx_write
and
ally_write return false like casic_write already does.
If you'd like, I can send this as a proper git commit (format-patch, with a
commit message and my sign-off) or open a merge request - whichever is
easier
for you to apply.
Attachments:
- 0001-drivers-guard-control_send-min-length.patch (the fix)
- control_send_repro.c, repro_build.sh (standalone
reproducer)
Thanks for gpsd,
Patrick
--
*Patrick Ventuzelo* | *CEO, FuzzingLabs <https://fuzzinglabs.com/>* |
[email protected]
Research, Training & Offensive Security Engineering
*FuzzForge* <https://fuzzforge.ai/> | AI agent orchestration platform for
Continuous Offensive Validation
#!/bin/sh
# repro_build.sh -- build the standalone control_send reproducer with ASan.
# Usage: fuzz/repro_build.sh [srcdir] [out] (defaults: repo root, ./control_send_repro)
set -eu
CC="${CC:-clang}" # any clang with AddressSanitizer
here=$(cd "$(dirname "$0")" && pwd)
SRC=$(cd "${1:-$(dirname "$here")}" && pwd)
OUT="${2:-./control_send_repro}"
# generated headers (normally produced by scons); harmless if already present
[ -f "$SRC/include/gpsd_config.h" ] || cp "$here/gpsd_config.h.fuzz" "$SRC/include/gpsd_config.h"
[ -f "$SRC/include/packet_names.h" ] || \
sed -e '/^ *\([A-Z][A-Z0-9_]*\),/s// "\1",/' \
< "$SRC/include/packet_states.h" > "$SRC/include/packet_names.h"
DEFS="-DSIRF_ENABLE -DGEOSTAR_ENABLE -DONCORE_ENABLE -DITRAX_ENABLE \
-DSUPERSTAR2_ENABLE -DSKYTRAQ_ENABLE -DZODIAC_ENABLE -DAIVDM_ENABLE -DGREIS_ENABLE \
-DFV18_ENABLE -DEARTHMATE_ENABLE -DTRIPMATE_ENABLE -DGARMINTXT_ENABLE -DFURY_ENABLE \
-DGPSCLOCK_ENABLE -DFUZZ_REAL_GPSUTILS -DFUZZ_REAL_RTCM2"
DRV="$SRC/drivers/drivers.c $SRC/drivers/driver_ubx.c $SRC/drivers/driver_allystar.c \
$SRC/drivers/driver_casic.c $SRC/drivers/driver_zodiac.c $SRC/drivers/driver_sirf.c \
$SRC/drivers/driver_tsip.c $SRC/drivers/driver_garmin.c $SRC/drivers/driver_garmin_txt.c \
$SRC/drivers/driver_evermore.c $SRC/drivers/driver_navcom.c $SRC/drivers/driver_geostar.c \
$SRC/drivers/driver_oncore.c $SRC/drivers/driver_italk.c $SRC/drivers/driver_superstar2.c \
$SRC/drivers/driver_skytraq.c $SRC/drivers/driver_greis.c $SRC/drivers/driver_greis_checksum.c \
$SRC/drivers/driver_ais.c $SRC/drivers/driver_nmea0183.c $SRC/drivers/driver_spartn.c \
$SRC/drivers/driver_rtcm2.c $SRC/drivers/driver_rtcm3.c"
CORE="$SRC/gpsd/packet.c $SRC/gpsd/isgps.c $SRC/gpsd/crc24q.c $SRC/gpsd/subframe.c \
$SRC/gpsd/timebase.c $SRC/gpsd/geoid.c $SRC/libgps/gpsutils.c $SRC/libgps/timespec_str.c \
$SRC/libgps/os_compat.c $SRC/libgps/bits.c $SRC/libgps/hex.c"
# shellcheck disable=SC2086
$CC -g -O1 -fsanitize=address -fno-omit-frame-pointer -I "$SRC/include" $DEFS \
"$here/control_send_repro.c" "$here/fuzz_stubs.c" "$here/fuzz_stubs_drivers.c" \
$DRV $CORE -lm -o "$OUT"
echo "built $OUT"
control_send_repro.c
Description: Binary data
0001-drivers-guard-control_send-min-length.patch
Description: Binary data
# gpsd driver control-send bugs — detailed vulnerability report **Author:** [email protected] **Target:** gpsd `master` @ `55e184b40` (re-validated; also present on `43dddd734`) **Discovery:** `fuzz_control` harness (libFuzzer + AddressSanitizer) **Class:** CWE-191 (integer underflow) → CWE-787 (OOB write) / CWE-125 (OOB read) **Overall severity:** **Low** (local, denial-of-service; not remotely reachable through the gpsd network daemon — see §5) --- ## 1. Executive summary Four gpsd binary-protocol drivers build an outgoing control frame using the caller-supplied control-message length **without validating a minimum length**. A control message shorter than the driver's fixed header (typically < 2 bytes) makes an unsigned length computation **underflow to ~`SIZE_MAX`**, which then drives a `memcpy`/loop with an enormous size: | # | Driver | Function | Effect | Confirmed | |---|--------|----------|--------|-----------| | 1 | u-blox UBX | `ubx_write` (via `ubx_control_send`) | `memcpy(session->msgbuf, …, ~SIZE_MAX)` → **OOB write** | crash (ASan negative-size-param) | | 2 | AllyStar | `ally_write` (via `control_send`) | `memcpy(session->msgbuf, …, ~SIZE_MAX)` → **OOB write** | crash (ASan negative-size-param) | | 3 | Zodiac | `end_write` (via `zodiac_control_send`→`zodiac_spew`) | loop writes stack `buf[BUFSIZ]` with huge count → **stack overflow** | crash (ASan stack-buffer-overflow) | | 4 | CASIC | `control_send` | 1-byte **OOB read** of the caller args (`msg[1]`) — `casic_write` itself is guarded | bounded over-read (ASan, strict mode) | Bugs #1–#3 are memory-corrupting crashes; #4 is a bounded 1-byte over-read. All are reached only through the **local** diagnostic tools `gpsctl -x` and `gpsmon`, **not** through the gpsd network daemon. Impact is a reliable **denial-of-service (crash)**; the corrupting size is always ~`SIZE_MAX` (uncontrollable), so this is not a practical path to code execution. --- ## 2. Affected code The drivers wrap a caller payload into a framed packet in a helper `*_write()` that copies `payload_len` bytes into the fixed `session->msgbuf`. The `control_send` method computes `payload_len = data_len - 2` (skip the 2 header bytes the caller prepends) **with no check that `data_len >= 2`**: ```c /* drivers/driver_ubx.c — "not used by gpsd, it's for gpsctl and friends" */ static ssize_t ubx_control_send(struct gps_device_t *session, char *msg, size_t data_len) { return ubx_write(session, (unsigned)msg[0], (unsigned)msg[1], (unsigned char *)msg + 2, (size_t)(data_len - 2)) ? ... ; /* data_len<2 → ~SIZE_MAX */ } bool ubx_write(..., const unsigned char *msg, size_t data_len) { ... if ((sizeof(session->msgbuf) - 8) <= data_len) { GPSD_LOG(... "TOO LONG!" ...); /* WARNS but does NOT return */ } (void)memcpy(&session->msgbuf[6], msg, data_len); /* OOB write */ ... } ``` `drivers/driver_allystar.c` (`control_send` → `ally_write`) is byte-for-byte the same pattern — the `data_len - 2` idiom and the non-fatal "TOO LONG!" guard were **copied** from the 2010-era UBX code into the 2024 AllyStar driver. Zodiac underflows differently but equivalently: ```c /* drivers/driver_zodiac.c */ static ssize_t zodiac_control_send(struct gps_device_t *session, char *msg, size_t len) { ... return zodiac_spew(session, shortwords[0], shortwords + 1, (int)(len / 2 - 1)); /* len<2 → len/2-1 underflows → -1 */ } /* zodiac_spew: datlen = sizeof(short) * dlen (dlen=-1 → huge) → end_write() loops for (n < len/2) writing stack buf[BUFSIZ] → overflow */ ``` CASIC (`driver_casic.c`) has the same `data_len - 2` in `control_send`, **but** `casic_write()` correctly `return false`s when `payload_len` is oversized, so the only defect is `control_send` dereferencing `msg[1]` before the length is validated (a 1-byte over-read of the caller buffer). **Provenance:** `ubx_write` dates to 2010 (ESR); the AllyStar (2024) and CASIC (2024) drivers copied the idiom. So this is a long-lived pattern recently propagated to new drivers. --- ## 3. Trigger & root cause Root cause: **unsigned integer underflow of an attacker-influenced length.** `control_send(session, buf, data_len)` is documented as taking a caller frame whose first two bytes are the message class/ID; the payload is `data_len - 2`. Nothing enforces `data_len >= 2`. With `data_len ∈ {0, 1}`: * `data_len - 2` → `SIZE_MAX-1` / `SIZE_MAX` (size_t), or * `len/2 - 1` → `SIZE_MAX` (Zodiac), which is then used as a `memcpy` size or loop bound → out-of-bounds access. Minimal trigger: a control string that decodes to **0 or 1 byte**. --- ## 4. Per-bug detail & ASan evidence Reproducers are 4-byte harness inputs in `fuzz/crashes/control_real/` (`<driver-idx> <ops> <1-byte-payload>`), replayable with `fuzz/bin/fuzz_control <file>`. **#1 UBX** — `driver_ubx.c:621` (`ubx_write`) ``` ERROR: AddressSanitizer: negative-size-param: (size=-1) #0 __asan_memcpy #1 ubx_write driver_ubx.c:621 #2 ubx_control_send driver_ubx.c:5886 ``` `memcpy(&session->msgbuf[6], msg, (size_t)-1)` — OOB write into the device struct's `msgbuf`, then segfault. **#2 AllyStar** — `driver_allystar.c:342` (`ally_write`) ``` ERROR: AddressSanitizer: negative-size-param: (size=-1) #0 __asan_memcpy #1 ally_write driver_allystar.c:342 #2 control_send driver_allystar.c:1882 ``` Identical mechanism. **#3 Zodiac** — `driver_zodiac.c:55` (`end_write`) ``` ERROR: AddressSanitizer: stack-buffer-overflow #0 end_write driver_zodiac.c:55 (putle16 into stack buf[BUFSIZ]) ``` `(int)(len/2 - 1)` underflows to `-1`; the negative word count propagates to `end_write`, whose `for (n < len/2)` loop runs against a huge `size_t` and writes past the stack buffer `buf[BUFSIZ]` — a **stack** memory-corruption (return address is on the same frame). **#4 CASIC** — `driver_casic.c:424` (`control_send`) Bounded 1-byte over-read of `msg[1]` when `data_len == 1`; `casic_write` blocks the large copy. Robustness defect, not a crash in the daemon-realistic buffer model. --- ## 5. Attack surface & reachability (honest assessment) `control_send` is **not** invoked by the gpsd network daemon on the client/socket path. I verified the daemon's control socket handler (`handle_control` in `gpsd/gpsd.c`): * `!dev=bytes` → `write(gps_fd, …)` — **raw** device write, no `control_send`. * `&dev=hex` → `write_gps()` → `gps_hexpack()` + `write()` — **raw**, no `control_send`. The only callers of `device_type->control_send` with a **caller-controlled length** are the local tools: * **`gpsctl -x CONTROL <device>`** (a.k.a. `--ship`): the CONTROL argument is hex-decoded to `cooked`/`cooklen` and passed to `session.device_type->control_send(&session, cooked, cooklen)` (`gpsctl.c:985`). `gpsctl` in direct mode opens the serial device itself. * **`gpsmon`** (interactive monitor): a typed control command → `monitor_control_send()` → `control_send` (`gpsmon.c:708`). Drivers also call their *own* `*_control_send` internally (speed/rate/mode switchers) — but always with **fixed buffers ≥ 2 bytes**, so the daemon never self-triggers it. **Consequence:** this is a **local** defect. The "attacker" is whoever supplies the `-x` control string to `gpsctl` (or the command in `gpsmon`). It is **not** remotely exploitable via `gpsd` itself, and does **not** cross the network trust boundary that gpsd's socket protocol defines. --- ## 6. Exploitability * **Primary impact: denial-of-service (crash).** The underflowed size is always ~`SIZE_MAX`, i.e. **not attacker-controllable to a small, useful value**. A `memcpy(dst, src, 2^64-1)` (UBX/AllyStar) or a `for (n < ~2^63)` write loop (Zodiac) overruns the destination and faults almost immediately. There is no practical primitive to write a *chosen* number of *chosen* bytes, so this is **not a realistic route to controlled memory corruption or RCE** — it is a reliable crash. * **UBX / AllyStar:** OOB **write** into `session->msgbuf` (a member of the heap-allocated `gps_device_t`), overrunning subsequent struct members / heap until an unmapped page → SIGSEGV. * **Zodiac:** OOB **write** on the **stack** (`buf[BUFSIZ]`); the same frame holds the saved return address, so in the absence of the huge-size instant fault this would be the more dangerous one — but again the count is ~`SIZE_MAX`, so it faults before any controlled overwrite. * **CASIC:** 1-byte OOB **read** of the argument buffer — at most an info-robustness issue, no crash under a realistic (≥ 2-byte, buffered) caller. Because the tools are typically run by an administrator/integrator against a local device, the realistic risk is **crashing a diagnostic/provisioning tool**, or **crashing an automated wrapper** that forwards untrusted data into `gpsctl -x` (see §7). --- ## 7. Real-world systems that expose this `gpsctl` and `gpsmon` ship in essentially every gpsd deployment. The path becomes security-relevant wherever a program feeds **untrusted or externally-derived data** into `gpsctl -x` (or drives `gpsmon`/the driver `control_send` API): * **GNSS provisioning / device-management automation.** Fleet tooling that configures u-blox / AllyStar / CASIC receivers by shelling out to `gpsctl -x "$CONTROL"` where `$CONTROL` comes from a config file, REST/MQTT message, or a device-management server → a malformed/short value crashes the provisioning step (and, if wrapped in a service, that service). * **Embedded & IoT GNSS modules** (routers, trackers, asset-monitoring gateways) that expose a "send raw command to GPS" feature in a local web UI / CLI backed by `gpsctl` — the classic pattern where a web form field reaches a shell tool. * **Timing / PTP-GNSS appliances and NTP stratum-1 servers** (data centres, telecom, finance) that use u-blox timing receivers; provisioning scripts using `gpsctl` to push receiver config are common. * **Marine / AIS and automotive/rail** integrations using AllyStar/CASIC/u-blox where an operator console runs `gpsmon` or a control wrapper. * **CI / build pipelines and test rigs** that exercise gpsd drivers via `gpsctl -x`/`gpsmon` with fuzzed or templated inputs. In all of these the practical outcome is the same: a crash of the local tool or its wrapper (availability impact), not remote compromise. --- ## 8. Severity * **CVSS 3.1 (indicative):** `AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:L` ≈ **3.3 (Low)** — local, requires invoking the tool with a crafted control string, availability impact only. Zodiac's stack write is theoretically higher-impact (`I:` potential) but not practically controllable, so still Low. * **CWE:** CWE-191 (integer underflow) leading to CWE-787 (OOB write, #1–#3) and CWE-125 (OOB read, #4). This is best characterised as a **robustness / local-DoS defect**, not a remote vulnerability. It is nonetheless a clear correctness bug worth fixing, and the pattern is actively spreading (2024 drivers copied the 2010 idiom). --- ## 9. Reproduction ```sh # build (Homebrew LLVM clang with libFuzzer) fuzz/build.sh . fuzz/bin # each 4-byte reproducer: <driver-index> <ops-byte> <1-byte control payload> fuzz/bin/fuzz_control fuzz/crashes/control_real/ubx_write-underflow.bin # UBX crash fuzz/bin/fuzz_control fuzz/crashes/control_real/ally_write-underflow.bin # AllyStar crash fuzz/bin/fuzz_control fuzz/crashes/control_real/end_write-underflow.bin # Zodiac stack overflow # equivalently, against a real device: a control string decoding to 1 byte # gpsctl -t ublox -x '41' /dev/ttyACM0 # 1 payload byte -> data_len=1 -> underflow ``` The harness models the daemon-faithful buffer (BUFSIZ backing); these three still crash there, confirming they are genuine OOB accesses, not harness artefacts. --- ## 10. Fix Add a minimum-length check in each `control_send` wrapper (and make the `*_write` "TOO LONG" guards fatal): ```c static ssize_t ubx_control_send(struct gps_device_t *session, char *msg, size_t data_len) { if (data_len < 2) { /* need class + id */ return -1; } return ubx_write(...); } ``` * `driver_ubx.c` `ubx_control_send`: require `data_len >= 2`. * `driver_allystar.c` `control_send`: require `data_len >= 2`. * `driver_casic.c` `control_send`: require `data_len >= 2`. * `driver_zodiac.c` `zodiac_control_send`: require `len >= 2` (and treat the `len/2 - 1` word count as signed-safe). * Defence-in-depth: in `ubx_write`/`ally_write`, make the `(sizeof(msgbuf)-N) <= payload_len` check **`return false`** (as `casic_write` already does) instead of only logging "TOO LONG!". A one-line guard per wrapper closes all four. --- ## 11. Disclosure notes * Found by fuzzing, on current `master`; no evidence of exploitation. * Not remotely reachable via the gpsd daemon; local DoS in `gpsctl`/`gpsmon` and any third-party consumer of the driver `control_send` API. * Recommend a coordinated report to the gpsd maintainers (gpsd-dev / GitLab issue) with the reproducers and the one-line fixes above.
