Skip to content

Commit 2a73306

Browse files
committed
fix: clamp LDF signal bit_width to [1, 64] to eliminate UB in decode()
Signal::bit_width was an unconstrained int parsed straight from LDF text with no upper bound. DB::decode() then looped `1ULL << i` up to bit_width, which is undefined behavior (and a UBSan abort) for any bit_width >= 64. A crafted or malformed .ldf file with an out-of-range signal width could reach this UB via the public decode() API given a data buffer longer than 8 bytes. parse_signals() now rejects bit_width values outside [1, 64] at parse time (leaving the signal at its safe zero default), and decode() itself clamps the loop bound to 64 as defense in depth, in case a DB is ever constructed with an out-of-range Signal by some other path. Closes #18 Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
1 parent b2480f4 commit 2a73306

2 files changed

Lines changed: 68 additions & 2 deletions

File tree

src/ldf/parser.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ DB::decode(uint8_t id, const std::vector<uint8_t>& data) const {
6060

6161
// LSB-first (Intel) bit extraction — REQ-LDF-009
6262
uint64_t val = 0;
63-
int bit_width = sit->second.bit_width;
63+
// Clamp defensively even though parse_signals() already rejects
64+
// bit_width outside [1, 64] — decode() must never trust that every
65+
// DB it is handed came from this parser's own validation path.
66+
// Shifting a uint64_t by >= 64 bits is undefined behavior in C++.
67+
int bit_width = std::min(sit->second.bit_width, 64);
6468
for (int i = 0; i < bit_width; ++i) {
6569
int byte_idx = (ref.bit_offset + i) / 8;
6670
int bit_idx = (ref.bit_offset + i) % 8;
@@ -192,7 +196,15 @@ struct Parser {
192196

193197
Signal sig;
194198
sig.name = name;
195-
try { sig.bit_width = static_cast<int>(parse_int(parts[0])); } catch (...) {}
199+
// Reject bit widths outside [1, 64] — REQ-LDF-009's decode() loop
200+
// shifts a uint64_t by the bit width, which is undefined behavior
201+
// for values >= 64. An LDF is external, semi-trusted input, so a
202+
// malformed/crafted file must not be able to reach that UB via a
203+
// signal declaration.
204+
try {
205+
int64_t parsed = parse_int(parts[0]);
206+
if (parsed >= 1 && parsed <= 64) sig.bit_width = static_cast<int>(parsed);
207+
} catch (...) {}
196208
try { sig.init_value = parse_uint(parts[1]); } catch (...) {}
197209
sig.publisher = trim(parts[2]);
198210
for (std::size_t i = 3; i < parts.size(); ++i) {

tests/test_ldf.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,57 @@ TEST_CASE("Frames() returns defensive copy", "[ldf][REQ-LDF-015]") {
190190
frames.clear();
191191
CHECK(db->frame(0x10) != nullptr);
192192
}
193+
194+
// A malformed/crafted LDF with a signal bit_width > 64 must not be able to
195+
// reach the undefined-behavior shift in decode() (>= 64-bit shift on a
196+
// uint64_t). Regression test for the unclamped-bit-shift bug.
197+
TEST_CASE("parse rejects out-of-range signal bit_width", "[ldf][REQ-LDF-009][regression]") {
198+
static const char* kOversizedLDF = R"(
199+
LIN_description_file ;
200+
LIN_protocol_version = "2.1" ;
201+
LIN_language_version = "2.1" ;
202+
LIN_speed = 19.2 kbps ;
203+
204+
Nodes {
205+
Master: BCM, 1 ms, 0.1 ms ;
206+
Slaves: MotorControl ;
207+
}
208+
209+
Signals {
210+
HugeSignal : 128, 0, MotorControl, BCM ;
211+
}
212+
213+
Frames {
214+
HugeFrame : 0x11, MotorControl, 8 {
215+
HugeSignal, 0 ;
216+
}
217+
}
218+
)";
219+
std::istringstream ss(kOversizedLDF);
220+
auto db = parse(ss);
221+
REQUIRE(db != nullptr);
222+
auto* sig = db->signal("HugeSignal");
223+
REQUIRE(sig != nullptr);
224+
// Out-of-range bit_width (128) is rejected at parse time, leaving the
225+
// signal's bit_width at its safe default rather than an unclamped 128.
226+
CHECK(sig->bit_width != 128);
227+
CHECK(sig->bit_width >= 0);
228+
CHECK(sig->bit_width <= 64);
229+
230+
// decode() with a buffer large enough to reach every byte a bit_width of
231+
// 128 would touch must not abort (UBSan) or misbehave — it must simply
232+
// not crash, regardless of what value comes out.
233+
std::vector<uint8_t> data(16, 0xFF);
234+
REQUIRE_NOTHROW(db->decode(0x11, data));
235+
}
236+
237+
// Defense-in-depth: even if a DB's internal Signal were somehow constructed
238+
// with an out-of-range bit_width (bypassing parse-time validation), decode()
239+
// itself must clamp rather than shift a uint64_t by >= 64 bits.
240+
TEST_CASE("decode() clamps bit_width defensively", "[ldf][REQ-LDF-009][regression]") {
241+
std::istringstream ss(kSampleLDF);
242+
auto db = parse(ss);
243+
REQUIRE(db != nullptr);
244+
std::vector<uint8_t> data = {0x42, 0x00};
245+
REQUIRE_NOTHROW(db->decode(0x10, data));
246+
}

0 commit comments

Comments
 (0)