STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_elodin_read_path.cpp
Go to the documentation of this file.
1/**
2 * ElodinClient::read_packet() framing — what happens when a packet does not fit.
3 *
4 * The stream is length-prefixed packets back to back on one TCP connection, so the only thing
5 * keeping a reader aligned is consuming exactly as many bytes as each length announced.
6 *
7 * read_packet() used to break that. On a packet larger than the caller's buffer it returned -1
8 * having already consumed the 8-byte header but *not* the body, leaving the rest of that packet in
9 * the socket. Every later read then parsed payload bytes as a header, and since nothing marked the
10 * socket dead, is_connected() kept reporting true and callers looped on garbage forever. Observed
11 * against a real elodin-db: one 179-byte packet against a 160-byte buffer, then "Invalid packet
12 * length: 1638688", "134742016", "268436225" ... indefinitely.
13 *
14 * It also mis-sized its own bound. The check was `packet_len > max_len`, but the function writes
15 * 8 + (packet_len - 4) = packet_len + 4 bytes, so packet_len == max_len passed and overran the
16 * caller's buffer by 4. Confirmed with a guard page: the kernel returned EFAULT at exactly that
17 * boundary.
18 *
19 * These tests use a fake elodin-db rather than the real one so they can produce packet sizes on
20 * demand — nothing a live db emits today is big enough to trip either bug, which is why neither
21 * was noticed.
22 */
23#include <arpa/inet.h>
24#include <netinet/in.h>
25#include <sys/socket.h>
26#include <unistd.h>
27
28#include <atomic>
29#include <cstring>
30#include <iostream>
31#include <string>
32#include <thread>
33#include <vector>
34
36
37static int g_failures = 0;
38
39static void check(bool ok, const std::string& what) {
40 std::cout << (ok ? " ok " : " FAIL ") << what << std::endl;
41 if (!ok)
42 g_failures++;
43}
44
45/**
46 * One wire packet: len(4, LE) then len bytes of body, where the first 4 body bytes are
47 * ty/packet_id(2)/request_id. So `len` = payload + 4, and the whole frame is payload + 8.
48 * read_packet() reports packet_len + 4, i.e. the same payload + 8.
49 */
50static std::vector<uint8_t> frame(uint8_t hi, uint8_t lo, size_t payload_len, uint8_t fill) {
51 const uint32_t len = static_cast<uint32_t>(payload_len + 4);
52 std::vector<uint8_t> f(8 + payload_len, fill);
53 std::memcpy(f.data(), &len, 4);
54 f[4] = 0x01; // ty = TABLE
55 f[5] = hi;
56 f[6] = lo;
57 f[7] = 0x00;
58 return f;
59}
60
61/** Fake elodin-db: accept one connection, write `blob`, then hold the socket open. */
62struct FakeDb {
63 std::thread th;
64 std::atomic<bool> run{true};
65 std::atomic<uint16_t> port{0};
66 std::atomic<bool> closed_early{false};
67
68 void start(std::vector<uint8_t> blob, bool close_after_write) {
69 th = std::thread([this, blob = std::move(blob), close_after_write]() {
70 int srv = socket(AF_INET, SOCK_STREAM, 0);
71 int opt = 1;
72 setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
73 struct sockaddr_in a{};
74 a.sin_family = AF_INET;
75 a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
76 a.sin_port = 0; // let the OS pick, so parallel runs cannot collide
77 if (bind(srv, reinterpret_cast<struct sockaddr*>(&a), sizeof(a)) < 0) {
78 close(srv);
79 return;
80 }
81 socklen_t alen = sizeof(a);
82 getsockname(srv, reinterpret_cast<struct sockaddr*>(&a), &alen);
83 listen(srv, 1);
84 port.store(ntohs(a.sin_port));
85
86 struct timeval tv{.tv_sec = 5, .tv_usec = 0};
87 setsockopt(srv, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
88 int c = accept(srv, nullptr, nullptr);
89 if (c < 0) {
90 close(srv);
91 return;
92 }
93 size_t sent = 0;
94 while (sent < blob.size()) {
95 ssize_t n = send(c, blob.data() + sent, blob.size() - sent, MSG_NOSIGNAL);
96 if (n <= 0)
97 break;
98 sent += static_cast<size_t>(n);
99 }
100 if (close_after_write) {
101 close(c);
102 closed_early.store(true);
103 } else {
104 while (run)
105 std::this_thread::sleep_for(std::chrono::milliseconds(20));
106 close(c);
107 }
108 close(srv);
109 });
110 while (port.load() == 0)
111 std::this_thread::sleep_for(std::chrono::milliseconds(5));
112 }
113
114 void stop() {
115 run = false;
116 if (th.joinable())
117 th.join();
118 }
119};
120
121int main() {
122 std::cout << "=== ElodinClient read_packet framing ===" << std::endl;
123
124 // ── 1. An oversized packet is skipped, and the NEXT packet still reads correctly ──────────
125 // This is the regression that matters: pre-fix, read 2 returned garbage forever.
126 {
127 std::vector<uint8_t> blob;
128 const auto big = frame(0x20, 0x01, 200, 0xAA); // 208 wire bytes, will not fit
129 const auto small = frame(0x50, 0x00, 8, 0xBB); // 16 wire bytes, fits
130 blob.insert(blob.end(), big.begin(), big.end());
131 blob.insert(blob.end(), small.begin(), small.end());
132
133 FakeDb db;
134 db.start(blob, /*close_after_write=*/false);
136 if (!c.connect("127.0.0.1", db.port.load())) {
137 std::cerr << "SKIP: cannot connect to the fake db" << std::endl;
138 db.stop();
139 return 0; // environmental
140 }
141
142 uint8_t buf[64];
143 const ssize_t r1 = c.read_packet(buf, sizeof(buf));
144 check(r1 == 0,
145 "oversized packet is skipped, not an error (got " + std::to_string(r1) + ")");
146
147 const ssize_t r2 = c.read_packet(buf, sizeof(buf));
148 check(r2 == 16, "the packet AFTER an oversized one still reads (got " + std::to_string(r2) +
149 ", want 16)");
150 check(r2 == 16 && buf[5] == 0x50 && buf[6] == 0x00,
151 "and it is framed correctly — stream stayed aligned");
152 check(c.is_connected(), "the connection survives a skipped packet");
153 db.stop();
154 }
155
156 // ── 2. The bound accounts for all 8 + (packet_len - 4) bytes written ──────────────────────
157 // packet_len == max_len used to pass the check and overrun the caller's buffer by 4.
158 {
159 constexpr size_t kMax = 64;
160 // payload 56 → packet_len 60 → writes exactly 64. The largest that legitimately fits.
161 const auto exact = frame(0x50, 0x00, kMax - 8, 0xCC);
162 // payload 60 → packet_len 64 == max_len: passed the old check, wrote 68 bytes.
163 const auto over_by_four = frame(0x50, 0x00, kMax - 4, 0xDD);
164 std::vector<uint8_t> blob;
165 blob.insert(blob.end(), exact.begin(), exact.end());
166 blob.insert(blob.end(), over_by_four.begin(), over_by_four.end());
167
168 FakeDb db;
169 db.start(blob, /*close_after_write=*/false);
171 c.connect("127.0.0.1", db.port.load());
172
173 uint8_t buf[kMax];
174 const ssize_t r1 = c.read_packet(buf, sizeof(buf));
175 check(r1 == static_cast<ssize_t>(kMax), "a packet that exactly fills the buffer is read");
176
177 const ssize_t r2 = c.read_packet(buf, sizeof(buf));
178 check(r2 == 0, "packet_len == max_len is skipped, not written 4 bytes past the buffer");
179 db.stop();
180 }
181
182 // ── 3. A closed socket is reported as closed ──────────────────────────────────────────────
183 // read_exact() never cleared connected_, so a read-only consumer looped forever on a dead
184 // socket believing it was live.
185 {
186 FakeDb db;
187 db.start(frame(0x50, 0x00, 8, 0xEE), /*close_after_write=*/true);
189 c.connect("127.0.0.1", db.port.load());
190
191 uint8_t buf[64];
192 check(c.read_packet(buf, sizeof(buf)) == 16,
193 "reads the packet the peer sent before closing");
194 const ssize_t r = c.read_packet(buf, sizeof(buf));
195 check(r < 0, "a read past EOF fails");
196 check(!c.is_connected(), "and the client reports itself disconnected");
197 db.stop();
198 }
199
200 std::cout << (g_failures ? "FAILED" : "PASSED") << std::endl;
201 return g_failures ? 1 : 0;
202}
Standalone Elodin database client.
bool is_connected() const
Check if connected.
bool connect(const std::string &host, uint16_t port=2240)
Connect to Elodin database.
ssize_t read_packet(uint8_t *packet_buffer, size_t max_len)
Read complete packet from Elodin (header + payload).
static bool ok
Definition flight_e2e.c:435
static void send(void *ctx, const uint8_t *bytes, uint16_t len)
Definition gnss_task.c:27
*brief create JSON pointer *sa reference_tokens const string_t & a
Definition json.hpp:13877
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474
Definition run.py:1
Fake elodin-db: accept one connection, write blob, then hold the socket open.
void start(std::vector< uint8_t > blob, bool close_after_write)
std::atomic< bool > closed_early
std::atomic< uint16_t > port
static int g_failures
static boot_report_t r
static void check(bool ok, const std::string &what)
static std::vector< uint8_t > frame(uint8_t hi, uint8_t lo, size_t payload_len, uint8_t fill)
One wire packet: len(4, LE) then len bytes of body, where the first 4 body bytes are ty/packet_id(2)/...
static const size_t kMax
sensorBatchReady: when a sensor board ships its queued chunks.