From: Ana Bobillo <[email protected]>
A gpsd reading another gpsd (a gpsd:// source) puts the source's path
and a '#' in front of every "device" and "path" value it passes on.
path_rewrite() copied the path in at the value with a bound of
sizeof(dev.path), not of what is left of lexer.outbuffer.  A JSON
packet can be nearly as long as outbuffer, so for a value near its end
the path was written past outbuffer, over outbuflen and the lexer
state after it, and the scan then ran on to the corrupted outbuflen.

Insert the path only when the grown packet and its NUL fit, else log
and pass the rest of the packet as it came.  The insert is a memmove
in place, so the stack copy of outbuffer goes too: with a NUL inside a
string, that copy was filled only up to the NUL, and its uninitialized
rest was appended to the packet.

The scan also stopped at the packet's old length, so in a DEVICES
reply a path near the end, pushed past that by the inserts before it,
was not rewritten.  outbuflen now follows each insert.

tests/test_passthrough.c feeds packets to the JSON passthrough driver:
the usual rewrites, a NUL in a string, and packets up to the longest
the packetizer passes, the last one ending in a device name.  It runs
under "scons check".

Signed-off-by: Ana Bobillo <[email protected]>
---
Without the drivers.c change tests/test_passthrough crashes (SIGSEGV);
with it, "scons check" passes.

 SConscript               |  12 +++
 drivers/drivers.c        |  45 ++++----
 tests/test_passthrough.c | 223 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 259 insertions(+), 21 deletions(-)

diff --git a/SConscript b/SConscript
index 2f52662a7..edaf72c9a 100644
--- a/SConscript
+++ b/SConscript
@@ -2175,6 +2175,11 @@ test_packet = env.Program('tests/test_packet',
                            'tests/test_packet.c'],
                           LIBS=[libgpsd_static, libgps_static],
                           parse_flags=gpsdflags)
+test_passthrough = env.Program('tests/test_passthrough',
+                               [libgpsd_static, libgps_static,
+                                'tests/test_passthrough.c'],
+                               LIBS=[libgpsd_static, libgps_static],
+                               parse_flags=gpsdflags)
 test_timespec = env.Program('tests/test_timespec', ['tests/test_timespec.c'],
                             LIBS=[libgpsd_static, libgps_static],
                             parse_flags=gpsdflags)
@@ -2208,6 +2213,7 @@ testprogs = [test_bits,
              test_matrix,
              test_mktime,
              test_packet,
+             test_passthrough,
              test_timespec,
              test_trig]
 if env["libgpsmm"] or cleaning:
@@ -2917,6 +2923,11 @@ matrix_regress = Utility('matrix-regress', 
[test_matrix], [
     '"${SRCDIR}/tests/test_matrix" --quiet'
 ])
 
+# Unit-test the JSON passthrough driver
+passthrough_regress = Utility('passthrough-regress', [test_passthrough], [
+    '"${SRCDIR}/tests/test_passthrough" --quiet'
+])
+
 # MIB test
 if have_smilint:
     mib_regress = Utility('mib-regress', [mib_lint], [
@@ -3183,6 +3194,7 @@ test_nondaemon = [
     method_regress,
     mib_regress,
     packet_regress,
+    passthrough_regress,
     rtcm_regress,
     test_xgps_deps,
     time_regress,
diff --git a/drivers/drivers.c b/drivers/drivers.c
index 16a19565c..3dae4fb7b 100644
--- a/drivers/drivers.c
+++ b/drivers/drivers.c
@@ -1590,41 +1590,44 @@ static void path_rewrite(struct gps_device_t *session, 
char *prefix)
      * beginning of the path attribute, followed by a # to separate it
      * from the device.
      */
+    char *outbuf = (char *)session->lexer.outbuffer;
     char *prefloc;
+    // only the path before any '#', to avoid accumulating device names
+    size_t pathlen = strcspn(session->gpsdata.dev.path, "#");
 
     assert(prefix != NULL && session->lexer.outbuffer != NULL);
 
     // possibly the rewrite has been done already, this comw up in gpsmon
-    if (strstr((char *)session->lexer.outbuffer,
-               session->gpsdata.dev.path) != NULL) {
+    if (strstr(outbuf, session->gpsdata.dev.path) != NULL) {
         return;
     }
 
-    for (prefloc = (char *)session->lexer.outbuffer;
-         prefloc < (char *)session->lexer.outbuffer+session->lexer.outbuflen;
+    for (prefloc = outbuf;
+         prefloc < outbuf + session->lexer.outbuflen;
          prefloc++) {
         if (str_starts_with(prefloc, prefix)) {
-            char *sfxloc;
-            char copy[sizeof(session->lexer.outbuffer)+1];
-            (void)strlcpy(copy,
-                          (char *)session->lexer.outbuffer,
-                          sizeof(copy));
+            size_t sfxlen;
+
             prefloc += strlen(prefix);
-            (void)strlcpy(prefloc,
-                          session->gpsdata.dev.path,
-                          sizeof(session->gpsdata.dev.path));
-            sfxloc = strchr(prefloc, '#');
-            if (sfxloc) {
-                *sfxloc = '\0';  // Avoid accumulating multiple device names
+            sfxlen = strlen(prefloc);
+            // the path, the '#', the rest and the NUL must all fit
+            if (sizeof(session->lexer.outbuffer) <=
+                (size_t)(prefloc - outbuf) + pathlen + 1 + sfxlen) {
+                GPSD_LOG(LOG_WARN, &session->context->errout,
+                         "JSON: no room for %zu more bytes, "
+                         "rest passed as is\n", pathlen + 1);
+                break;
             }
-            (void)strlcat((char *)session->lexer.outbuffer, "#",
-                          sizeof(session->lexer.outbuffer));
-            (void)strlcat((char *)session->lexer.outbuffer,
-                          copy + (prefloc-(char *)session->lexer.outbuffer),
-                          sizeof(session->lexer.outbuffer));
+            memmove(prefloc + pathlen + 1, prefloc, sfxlen + 1);
+            memcpy(prefloc, session->gpsdata.dev.path, pathlen);
+            prefloc += pathlen;
+            *prefloc = '#';
+            // keep scanning to the end of the grown packet
+            session->lexer.outbuflen = (size_t)(prefloc - outbuf) +
+                                       1 + sfxlen;
         }
     }
-    session->lexer.outbuflen = strnlen((char *)session->lexer.outbuffer,
+    session->lexer.outbuflen = strnlen(outbuf,
                                        sizeof(session->lexer.outbuffer));
 }
 
diff --git a/tests/test_passthrough.c b/tests/test_passthrough.c
new file mode 100644
index 000000000..60ec7b8f6
--- /dev/null
+++ b/tests/test_passthrough.c
@@ -0,0 +1,223 @@
+/*
+ * Unit test for the JSON passthrough driver.
+ *
+ * A gpsd reading another gpsd (a gpsd:// source) passes its JSON on to
+ * clients, after json_pass_packet() puts the source's path and a '#'
+ * in front of each device name, so clients can tell where it came
+ * from.  Packets go in through driver_json_passthrough.parse_packet(),
+ * left in lexer.outbuffer as the packetizer would leave them.
+ *
+ * This file is Copyright by the GPSD project
+ * SPDX-License-Identifier: BSD-2-clause
+ */
+
+#include "../include/gpsd_config.h"  // must be before all includes
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "../include/gpsd.h"
+
+#define SOURCE          "gpsd://example.com:2947"
+// the field after outbuffer and outbuflen, which must not change
+#define SENTINEL        0x5a5a5a5aUL
+
+static bool quiet = false;
+
+extern const struct gps_type_t driver_json_passthrough;
+
+static void session_init(struct gps_context_t *context,
+                         struct gps_device_t *session)
+{
+    memset(context, 0, sizeof(*context));
+    memset(session, 0, sizeof(*session));
+    errout_reset(&context->errout);
+    session->context = context;
+    (void)strlcpy(session->gpsdata.dev.path, SOURCE,
+                  sizeof(session->gpsdata.dev.path));
+    session->lexer.char_counter = SENTINEL;
+}
+
+/* Hand the driver one packet and compare what it passes on.  NULL as
+ * expect means the packet must pass unchanged.
+ */
+static int pass(const char *legend, struct gps_device_t *session,
+                const char *expect)
+{
+    char *outbuf = (char *)session->lexer.outbuffer;
+    char sent[sizeof(session->lexer.outbuffer)];
+    gps_mask_t mask;
+
+    (void)strlcpy(sent, outbuf, sizeof(sent));
+    if (NULL == expect) {
+        expect = sent;
+    }
+    mask = driver_json_passthrough.parse_packet(session);
+
+    if (PASSTHROUGH_IS != mask ||
+        SENTINEL != session->lexer.char_counter ||
+        strlen(outbuf) != session->lexer.outbuflen ||
+        0 != strcmp(outbuf, expect)) {
+        size_t elen = strlen(expect);
+        size_t glen = strlen(outbuf);
+
+        // the ends of the packets, where the rewrites near the bound are
+        (void)printf("%s: mask %#llx, outbuflen %zu, strlen %zu, "
+                     "char_counter %#lx\n  expected ...%s\n  got      ...%s\n",
+                     legend, (unsigned long long)mask,
+                     session->lexer.outbuflen, glen,
+                     session->lexer.char_counter,
+                     expect + (80 < elen ? elen - 80 : 0),
+                     outbuf + (80 < glen ? glen - 80 : 0));
+        return 1;
+    }
+    return 0;
+}
+
+static struct {
+    const char *legend;
+    const char *packet;
+    const char *expect;         // NULL: passed unchanged
+} rewrite_tests[] = {
+    {"TPV",
+     "{\"class\":\"TPV\",\"device\":\"/dev/ttyS0\",\"mode\":3}",
+     "{\"class\":\"TPV\",\"device\":\"" SOURCE "#/dev/ttyS0\",\"mode\":3}"},
+    {"DEVICE",
+     "{\"class\":\"DEVICE\",\"path\":\"/dev/ttyS0\",\"driver\":\"u-blox\"}",
+     "{\"class\":\"DEVICE\",\"path\":\"" SOURCE "#/dev/ttyS0\","
+     "\"driver\":\"u-blox\"}"},
+    // every path, the last one ending within a path's length of the end
+    {"DEVICES",
+     "{\"class\":\"DEVICES\",\"devices\":["
+     "{\"class\":\"DEVICE\",\"path\":\"/dev/ttyS0\"},"
+     "{\"class\":\"DEVICE\",\"path\":\"/dev/ttyS1\"}]}",
+     "{\"class\":\"DEVICES\",\"devices\":["
+     "{\"class\":\"DEVICE\",\"path\":\"" SOURCE "#/dev/ttyS0\"},"
+     "{\"class\":\"DEVICE\",\"path\":\"" SOURCE "#/dev/ttyS1\"}],"
+     "\"remote\":\"" SOURCE "\"}"},
+    // already rewritten, as gpsmon may hand it back
+    {"TPV seen before",
+     "{\"class\":\"TPV\",\"device\":\"" SOURCE "#/dev/ttyS0\"}",
+     NULL},
+};
+
+static int check_rewrite(void)
+{
+    struct gps_context_t context;
+    struct gps_device_t session;
+    int errors = 0;
+    size_t i;
+
+    for (i = 0; i < ROWS(rewrite_tests); i++) {
+        session_init(&context, &session);
+        session.lexer.outbuflen = strlcpy((char *)session.lexer.outbuffer,
+                                          rewrite_tests[i].packet,
+                                          sizeof(session.lexer.outbuffer));
+        errors += pass(rewrite_tests[i].legend, &session,
+                       rewrite_tests[i].expect);
+    }
+    return errors;
+}
+
+/* The packetizer takes a NUL inside a string like any other byte, but
+ * the packet passed on ends there, whatever follows is rewritten or not.
+ */
+static int check_embedded_nul(void)
+{
+    static const char packet[] =
+        "{\"class\":\"TPV\",\"tag\":\"a\0\",\"device\":\"/dev/ttyS0\"}";
+    struct gps_context_t context;
+    struct gps_device_t session;
+
+    session_init(&context, &session);
+    memcpy(session.lexer.outbuffer, packet, sizeof(packet));
+    session.lexer.outbuflen = sizeof(packet) - 1;
+    return pass("TPV with a NUL in its tag", &session,
+                "{\"class\":\"TPV\",\"tag\":\"a");
+}
+
+/* Packets near the size of lexer.outbuffer, up to the longest the
+ * packetizer passes: a TPV padded out with its tag, the device last.
+ * over is how many bytes the rewritten packet and its NUL would take
+ * past the end of outbuffer; unless 0, the packet must pass unchanged.
+ */
+#define PAD_HEAD        "{\"class\":\"TPV\",\"tag\":\""
+#define REWRITE         (sizeof(SOURCE "#") - 1)
+
+static struct {
+    const char *legend;
+    const char *tail;           // the packet's end, after the tag
+    size_t over;
+} bound_tests[] = {
+    {"path_rewrite fits exactly", "\",\"device\":\"/dev/ttyS0\"}", 0},
+    {"path_rewrite one byte over", "\",\"device\":\"/dev/ttyS0\"}", 1},
+    // the longest packet, ending in a device name shorter than the path
+    {"path_rewrite bound, device last", "\",\"device\":\"x\"}", REWRITE},
+};
+
+static int check_path_rewrite_bound(void)
+{
+    struct gps_context_t context;
+    struct gps_device_t session;
+    char expect[sizeof(session.lexer.outbuffer) + REWRITE];
+    int errors = 0;
+    size_t i;
+
+    for (i = 0; i < ROWS(bound_tests); i++) {
+        char *outbuf = (char *)session.lexer.outbuffer;
+        const char *tail = bound_tests[i].tail;
+        size_t len = sizeof(session.lexer.outbuffer) - 1 - REWRITE +
+                     bound_tests[i].over;
+        size_t pad = len - strlen(PAD_HEAD) - strlen(tail);
+        const char *dev = strstr(tail, "\"device\":\"") +
+                          strlen("\"device\":\"");
+
+        session_init(&context, &session);
+        memcpy(outbuf, PAD_HEAD, strlen(PAD_HEAD));
+        memset(outbuf + strlen(PAD_HEAD), 'x', pad);
+        (void)strlcpy(outbuf + strlen(PAD_HEAD) + pad, tail,
+                      sizeof(session.lexer.outbuffer) -
+                      strlen(PAD_HEAD) - pad);
+        session.lexer.outbuflen = len;
+
+        if (0 == bound_tests[i].over) {
+            // the packet up to the device name, the source, the rest
+            size_t head = len - strlen(dev);
+
+            memcpy(expect, outbuf, head);
+            (void)strlcpy(expect + head, SOURCE "#", sizeof(expect) - head);
+            (void)strlcat(expect, dev, sizeof(expect));
+            errors += pass(bound_tests[i].legend, &session, expect);
+        } else {
+            errors += pass(bound_tests[i].legend, &session, NULL);
+        }
+    }
+    return errors;
+}
+
+int main(int argc, char **argv)
+{
+    int errors = 0;
+
+    if (1 < argc &&
+        0 == strcmp("--quiet", argv[1])) {
+        quiet = true;
+    }
+
+    errors += check_rewrite();
+    errors += check_embedded_nul();
+    errors += check_path_rewrite_bound();
+
+    if (0 < errors) {
+        (void)printf("test_passthrough: %d errors\n", errors);
+        return 1;
+    }
+    if (!quiet) {
+        (void)printf("test_passthrough: %zu cases, all OK\n",
+                     ROWS(rewrite_tests) + 1 + ROWS(bound_tests));
+    }
+    return 0;
+}
+
+// vim: set expandtab shiftwidth=4

Reply via email to