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"

Attachment: control_send_repro.c
Description: Binary data

Attachment: 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.

Reply via email to