Index: /trunk/BNC/tests/cases/decoder_limits/case.json
===================================================================
--- /trunk/BNC/tests/cases/decoder_limits/case.json	(revision 11125)
+++ /trunk/BNC/tests/cases/decoder_limits/case.json	(revision 11125)
@@ -0,0 +1,22 @@
+{
+  "description": "03_BrdcCorr replay of crafted frames at the upper field limits (tools/mkcrafted.py): VTEC degree/order 16 in IGS-SSR and RTCM-SSR incl. 4 layers, RTCM 1300/1301/1302 with 31-character names. Overflowed the VTEC coefficient and CRS name arrays (crash in sendResults()). Expected C(d,o) = (100*d+o)/200, S = -C, in both .ssr files",
+  "config": "Example_Configs/03_BrdcCorr.bnc",
+  "raw": "data/crafted_limits.raw",
+  "args": [
+    "-key",
+    "miscMount",
+    "ALL",
+    "-key",
+    "miscScanRTCM",
+    "2"
+  ],
+  "exit_code": 5,
+  "timeout": 120,
+  "outputs": [
+    "Output/*.ssr",
+    {
+      "glob": "Output/BrdcCorr.log_261023",
+      "name": "BrdcCorr.log"
+    }
+  ]
+}
Index: /trunk/BNC/tests/tools/mkcrafted.py
===================================================================
--- /trunk/BNC/tests/tools/mkcrafted.py	(revision 11125)
+++ /trunk/BNC/tests/tools/mkcrafted.py	(revision 11125)
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+"""Write a BNC raw file with crafted, CRC-valid RTCM3 frames that use the
+upper end of the field ranges allowed by the format (decoder robustness test).
+
+Streams (one station ID each):
+  CRAFT0IGS0  IGS-SSR VTEC (4076/201), decoded by SsrCorrIgs, followed by
+              RTCM 1300/1301/1302 with 31-character names (BNC's RTCM scan
+              lists them only for streams that also decode successfully)
+  CRAFT0RTC0  RTCM-SSR VTEC (1264), decoded by SsrCorrRtcm
+  CRAFT0NEW0  new RTCM-SSR metadata (60), not dispatched by BNC at present;
+              decoded by ssr_harness.cpp with SsrCorrRtcmNew:
+                - 7 entries (maximum), additional parameter of 40 and 0 bits
+                - 31 entries (5-bit count, more than META_MAXNUMENTRIES=7)
+
+VTEC epochs (every 5 s):
+  t0      2 layers: layer 1 degree 16/order 2, layer 2 degree 2/order 2
+  t0+5    4 layers: layers 1-3 degree 1/order 1, layer 4 degree 16/order 16
+  t0+10   1 layer degree 2/order 2 (decoding continues normally)
+Degree and order are transmitted as N-1 in 4 bits, so 16 is the maximum
+(DF474/DF475, IDF037/IDF038). Coefficient C(d,o) is (100*d + o)/200 TECU
+and S(d,o) its negative, so every value in the .ssr output can be checked
+by eye; the heights are 100 km * layer number.
+
+Usage: mkcrafted.py <out.raw>
+"""
+
+import datetime
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from bncraw import crc24q, write_chunks  # noqa: E402
+
+T0 = datetime.datetime(2026, 10, 23, 12, 0, 0)  # GPS time, a Friday
+NAME31 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ01234"
+
+
+class BitWriter:
+    def __init__(self):
+        self.bits = []
+
+    def u(self, value, n):
+        assert 0 <= value < (1 << n), (value, n)
+        self.bits += [(value >> (n - 1 - i)) & 1 for i in range(n)]
+
+    def s(self, value, n):
+        assert -(1 << (n - 1)) <= value < (1 << (n - 1)), (value, n)
+        self.u(value & ((1 << n) - 1), n)
+
+    def text(self, string, nlen):
+        self.u(len(string), nlen)
+        for c in string.encode("ascii"):
+            self.u(c, 8)
+
+    def payload(self):
+        bits = self.bits + [0] * (-len(self.bits) % 8)
+        return bytes(int("".join(map(str, bits[i:i + 8])), 2)
+                     for i in range(0, len(bits), 8))
+
+
+def frame(payload):
+    assert len(payload) <= 1023, len(payload)
+    head = bytes([0xD3, len(payload) >> 8, len(payload) & 0xFF]) + payload
+    return head + crc24q(head).to_bytes(3, "big")
+
+
+def gps_sow(t):
+    return (t.weekday() + 1) % 7 * 86400 + t.hour * 3600 + t.minute * 60 + t.second
+
+
+def vtec_body(w, t, layers):
+    """VTEC content after the message number(s): IDF003-IDF041 / DF385-DF478."""
+    w.u(gps_sow(t), 20)      # epoch time 1 s
+    w.u(2, 4)                # update interval 5 s
+    w.u(0, 1)                # multiple message indicator
+    w.u(1, 4)                # SSR IOD
+    w.u(4711, 16)            # provider ID
+    w.u(1, 4)                # solution ID
+    w.u(20, 9)               # VTEC quality 1.0 TECU
+    w.u(len(layers) - 1, 2)
+    for n, (degree, order) in enumerate(layers, 1):
+        w.u(10 * n, 8)       # height, 10 km units
+        w.u(degree - 1, 4)
+        w.u(order - 1, 4)
+        for o in range(order + 1):
+            for d in range(o, degree + 1):
+                w.s(100 * d + o, 16)
+        for o in range(1, order + 1):
+            for d in range(o, degree + 1):
+                w.s(-(100 * d + o), 16)
+
+
+def vtec_igs(t, layers):
+    w = BitWriter()
+    w.u(4076, 12)
+    w.u(1, 3)                # IGS SSR version
+    w.u(201, 8)              # IGS message subtype: VTEC
+    vtec_body(w, t, layers)
+    return frame(w.payload())
+
+
+def vtec_rtcm(t, layers):
+    w = BitWriter()
+    w.u(1264, 12)
+    vtec_body(w, t, layers)
+    return frame(w.payload())
+
+
+def service_crs():
+    w = BitWriter()
+    w.u(1300, 12)
+    w.text(NAME31, 5)
+    w.u(2000, 16)            # coordinate epoch 1900 + 20.00
+    return frame(w.payload())
+
+
+def helmert():
+    w = BitWriter()
+    w.u(1301, 12)
+    w.text(NAME31, 5)        # source name
+    w.text(NAME31[::-1], 5)  # target name
+    w.u(1, 8)                # system identification number
+    w.u(0, 10)               # utilized transformation message indicator
+    w.u(61000 - 44244, 16)   # MJD of t0
+    for n in (23, 23, 23, 32, 32, 32, 25, 17, 17, 17, 17, 17, 17, 14):
+        w.s(0, n)
+    return frame(w.payload())
+
+
+def rtcm_crs():
+    w = BitWriter()
+    w.u(1302, 12)
+    w.text(NAME31, 5)
+    w.u(1, 1)                # anchor: plate-fixed
+    w.u(7, 5)                # plate number: Eurasia
+    w.u(2, 3)                # number of database links
+    w.text(NAME31, 5)
+    w.text(NAME31[::-1], 5)
+    return frame(w.payload())
+
+
+def metadata(entries):
+    """entries: list of None (default model) or number of additional bits."""
+    w = BitWriter()
+    w.u(60, 12)
+    w.u(1, 4)                # SSR IOD
+    w.u(4711, 16)            # provider ID
+    w.u(1, 4)                # solution ID
+    w.u(len(entries), 5)
+    for n, bits in enumerate(entries):
+        w.u(n % 32, 5)       # model correction type
+        w.u(1, 1)            # applied
+        w.u(bits is not None, 1)
+        if bits is not None:
+            w.u(1, 3)        # non-default model identifier
+        w.u(0, 1)            # no data IOD
+        if bits is not None:
+            w.u(1, 1)        # additional parameter follows
+            w.u(bits, 8)
+            for i in range(bits):
+                w.u(i & 1, 1)
+    return frame(w.payload())
+
+
+def main():
+    if len(sys.argv) != 2:
+        sys.exit(__doc__)
+    epochs = [
+        [(16, 2), (2, 2)],
+        [(1, 1), (1, 1), (1, 1), (16, 16)],
+        [(2, 2)],
+    ]
+    chunks = []
+    for i, layers in enumerate(epochs):
+        t = T0 + datetime.timedelta(seconds=5 * i)
+        stamp = t.isoformat()
+        chunks.append((stamp, "CRAFT0IGS0", "RTCM_3", vtec_igs(t, layers)
+                       + service_crs() + helmert() + rtcm_crs()))
+        chunks.append((stamp, "CRAFT0RTC0", "RTCM_3", vtec_rtcm(t, layers)))
+    stamp = (T0 + datetime.timedelta(seconds=15)).isoformat()
+    chunks.append((stamp, "CRAFT0NEW0", "RTCM_3",
+                   metadata([40, 0] + [None] * 5) + metadata([None] * 31)))
+    write_chunks(sys.argv[1], chunks)
+
+
+if __name__ == "__main__":
+    main()
Index: /trunk/BNC/tests/tools/ssr_harness.cpp
===================================================================
--- /trunk/BNC/tests/tools/ssr_harness.cpp	(revision 11125)
+++ /trunk/BNC/tests/tools/ssr_harness.cpp	(revision 11125)
@@ -0,0 +1,114 @@
+// SSR decoder harness for AddressSanitizer/UBSan runs (see ssr_harness.sh).
+//
+// Decodes every CRC-valid RTCM3 frame of a file with each of the three SSR
+// format classes (SsrCorrIgs, SsrCorrRtcm, SsrCorrRtcmNew). Unlike in BNC,
+// where all target structs are members of one RTCM3coDecoder object, every
+// struct is a separate heap block here, so a write beyond a struct is
+// reported by AddressSanitizer instead of silently corrupting the next member.
+//
+// Usage: ssr_harness <file>   (BNC raw file or plain RTCM3 byte stream)
+
+#include <cstdio>
+#include <vector>
+
+#include "clock_orbit_igs.h"
+#include "clock_orbit_rtcm.h"
+#include "clock_orbit_rtcm_new.h"
+
+using namespace std;
+
+// from bncutils.cpp, which would pull in most of BNC
+unsigned long CRC24(long size, const unsigned char *buf) {
+  unsigned long crc = 0;
+  while (size--) {
+    crc ^= (*buf++) << (16);
+    for (int ii = 0; ii < 8; ii++) {
+      crc <<= 1;
+      if (crc & 0x1000000) {
+        crc ^= 0x01864cfb;
+      }
+    }
+  }
+  return crc;
+}
+
+static void decode(const char* name, SsrCorr* ssr, const unsigned char* frame, size_t size) {
+  SsrCorr::ClockOrbit* co = new SsrCorr::ClockOrbit();
+  SsrCorr::CodeBias*   cb = new SsrCorr::CodeBias();
+  SsrCorr::VTEC*       v  = new SsrCorr::VTEC();
+  SsrCorr::PhaseBias*  pb = new SsrCorr::PhaseBias();
+  SsrCorr::MetaData*   md = new SsrCorr::MetaData();
+  SsrCorr::Antenna*    a  = new SsrCorr::Antenna();
+  int bytesused = 0;
+
+  int irc = ssr->GetSSR(co, cb, v, pb, md, a, (const char*)frame, size, &bytesused);
+  printf("  %-8s irc %3d", name, irc);
+  if (irc >= 0 && v->NumLayers) {
+    double sum = 0.0;
+    printf("  VTEC");
+    for (unsigned l = 0; l < v->NumLayers; l++) {
+      const SsrCorr::VTEC::IonoLayers& layer = v->Layers[l];
+      printf(" %u/%u", layer.Degree, layer.Order);
+      for (unsigned d = 0; d <= layer.Degree; d++) {
+        for (unsigned o = 0; o <= layer.Order; o++) {
+          sum += layer.Cosinus[d][o] - layer.Sinus[d][o];
+        }
+      }
+    }
+    printf("  sum(C-S) %.3f", sum);
+  }
+  if (irc >= 0 && md->NumEntries) {
+    printf("  META %u entries, additional data", md->NumEntries);
+    for (unsigned i = 0; i < md->NumEntries; i++) {
+      if (md->Entries[i].additionalParameterIndicator) {
+        printf(" %u:0x%x", md->Entries[i].additionalParameterNumBits,
+               md->Entries[i].additionalParameterData);
+      }
+    }
+  }
+  printf("\n");
+
+  delete co; delete cb; delete v; delete pb; delete md; delete a;
+}
+
+int main(int argc, char* argv[]) {
+  if (argc != 2) {
+    fprintf(stderr, "usage: %s <file>\n", argv[0]);
+    return 2;
+  }
+  FILE* inp = fopen(argv[1], "rb");
+  if (!inp) {
+    perror(argv[1]);
+    return 2;
+  }
+  vector<unsigned char> buf;
+  int c;
+  while ((c = fgetc(inp)) != EOF) {
+    buf.push_back((unsigned char)c);
+  }
+  fclose(inp);
+
+  SsrCorrIgs     igs;
+  SsrCorrRtcm    rtcm;
+  SsrCorrRtcmNew rtcmNew;
+
+  size_t ii = 0;
+  while (ii + 6 <= buf.size()) {
+    size_t len = ((buf[ii + 1] & 0x03) << 8) | buf[ii + 2];
+    if (buf[ii] != 0xD3 || ii + len + 6 > buf.size() ||
+        CRC24(len + 3, &buf[ii]) != (unsigned long)((buf[ii + len + 3] << 16) |
+                                                    (buf[ii + len + 4] << 8) | buf[ii + len + 5])) {
+      ii++;
+      continue;
+    }
+    // exact-size copy, so reading beyond the frame is caught as well
+    vector<unsigned char> frame(buf.begin() + ii, buf.begin() + ii + len + 6);
+    int type = (frame[3] << 4) | (frame[4] >> 4);
+    printf("frame at %zu: type %d, %zu bytes\n", ii, type, len);
+    decode("IGS", &igs, frame.data(), frame.size());
+    decode("RTCM", &rtcm, frame.data(), frame.size());
+    decode("RTCMnew", &rtcmNew, frame.data(), frame.size());
+    ii += len + 6;
+  }
+  return 0;
+}
Index: /trunk/BNC/tests/tools/ssr_harness.sh
===================================================================
--- /trunk/BNC/tests/tools/ssr_harness.sh	(revision 11125)
+++ /trunk/BNC/tests/tools/ssr_harness.sh	(revision 11125)
@@ -0,0 +1,32 @@
+#!/bin/sh
+# Build ssr_harness.cpp with the SSR decoder sources under AddressSanitizer and
+# UBSan and run it on the crafted frames of data/crafted_limits.raw (or on
+# the files given as arguments). Exit code 0 only if no sanitizer error.
+#
+#   tests/tools/ssr_harness.sh [file ...]
+
+set -e
+TOOLS=$(cd "$(dirname "$0")" && pwd)
+TESTS=$(dirname "$TOOLS")
+ROOT=$(dirname "$TESTS")
+SRC="$ROOT/src"
+OUT="$TESTS/work/ssr_harness"
+mkdir -p "$OUT"
+
+${CXX:-g++} -std=c++17 -g -O1 -fno-omit-frame-pointer \
+  -fsanitize=address,undefined -fno-sanitize-recover=all \
+  -I"$SRC" -I"$SRC/RTCM3" -I"$SRC/RTCM3/clock_and_orbit" -I"$ROOT/newmat" \
+  $(pkg-config --cflags Qt5Core) -fPIC \
+  "$TOOLS/ssr_harness.cpp" \
+  "$SRC/RTCM3/clock_and_orbit/clock_orbit.cpp" \
+  "$SRC/RTCM3/clock_and_orbit/clock_orbit_igs.cpp" \
+  "$SRC/RTCM3/clock_and_orbit/clock_orbit_rtcm.cpp" \
+  "$SRC/RTCM3/clock_and_orbit/clock_orbit_rtcm_new.cpp" \
+  $(pkg-config --libs Qt5Core) -o "$OUT/ssr_harness"
+
+[ $# -gt 0 ] || set -- "$TESTS/data/crafted_limits.raw"
+for f in "$@"; do
+  echo "== $f"
+  "$OUT/ssr_harness" "$f"
+done
+echo "ssr_harness: no sanitizer errors"
