Skip to content

Commit ce0fede

Browse files
authored
fix(vdm): prevent crash on multi-fragment sentences and modernize CMake test [CVE-2026-56770] (#264)
Guards `VdmStream::AddLine` against malformed multi-fragment AIS messages with missing or out-of-bounds sequence numbers, resolving assertion failures and out-of-bounds array access vulnerabilities. Additionally, modernizes the CMake build system to support automated C++ unit testing and AddressSanitizer (ASan) instrumentation. **VDM Stream & Vulnerability Fixes (`libais/vdm.cpp`, `test/vdm_test.cpp`)** - Added boundary validation in `VdmStream::AddLine` to reject sentences with total sentence count exceeding `kMaxSentences` (10). - Added validation for multi-fragment messages (`tot > 1`) to reject out-of-bounds sequence numbers (`seq >= kNumSequenceChannels` / `kNoSequenceNumber`), preventing buffer overflows when indexing `incoming_sentences_[seq]`. - Fixed `assert(tot == 1 || seq < kNumSequenceChannels)` so that single-line messages (which default to `kNoSequenceNumber` / 999999) do not trigger assertion failures in Debug and ASan builds. - Added unit test coverage in `TEST_F(VdmTest, OverRangeSequence)` to verify that multi-fragment NMEA strings with empty sequence IDs (e.g., `!AIVDM,2,1,,A,177KQ@,0*4E`) are rejected cleanly. **CMake Build & Test Suite Modernization (`CMakeLists.txt`, `src/CMakeLists.txt`, `src/test/CMakeLists.txt`)** - Added `set(CMAKE_POLICY_VERSION_MINIMUM "3.5")` to ensure compatibility between CMake 4.x and embedded GoogleTest/GoogleMock rules in `third_party/`. - Corrected C++ standard feature detection flags to check `-std=c++17` and `-std=c++20` properly. - Added `ENABLE_ASAN` option to compile and link with AddressSanitizer (`-fsanitize=address -g -fno-omit-frame-pointer`). - Integrated CTest and created `src/test/CMakeLists.txt` to automatically build all 30 C++ unit tests linked against `gmock_main` with `-Wno-deprecated-copy` silenced for third-party headers. **AIS 7/13 Stream Formatting (`src/libais/ais7_13.cpp`)** - Added `operator<<` formatting for `Ais7_13` to output message ID and MMSI. Fixes this existing CI build failure that produced the following assertion abort: ``` [----------] 7 tests from VdmTest [ RUN ] VdmTest.IgnoreNonAisMessages vdm_test: vdm.cpp:325: bool libais::VdmStream::AddLine(const std::string&): Assertion `seq < kNumSequenceChannels' failed. [ OK ] VdmTest.IgnoreNonAisMessages (0 ms) [ RUN ] VdmTest.SingleLineAisMessage [ OK ] VdmTest.SingleLineAisMessage (0 ms) [ RUN ] VdmTest.QueueClean Aborted (core dumped) ``` Based on the proposed fix by Google Jules in schwehr/libais-jules#20. * Fixes #263 * Fixes https://nvd.nist.gov/vuln/detail/CVE-2026-56770
1 parent d108602 commit ce0fede

6 files changed

Lines changed: 52 additions & 6 deletions

File tree

CMakeLists.txt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# make
77

88
cmake_minimum_required(VERSION 3.25)
9+
set(CMAKE_POLICY_VERSION_MINIMUM "3.5")
910
project (
1011
libais
1112
DESCRIPTION "A library for parsing Automatic Identification System messages."
@@ -15,8 +16,8 @@ project (
1516

1617
include(CheckCXXCompilerFlag)
1718
CHECK_CXX_COMPILER_FLAG("-std=c++14" COMPILER_SUPPORTS_CXX14)
18-
CHECK_CXX_COMPILER_FLAG("-std=c++14" COMPILER_SUPPORTS_CXX17)
19-
CHECK_CXX_COMPILER_FLAG("-std=c++14" COMPILER_SUPPORTS_CXX20)
19+
CHECK_CXX_COMPILER_FLAG("-std=c++17" COMPILER_SUPPORTS_CXX17)
20+
CHECK_CXX_COMPILER_FLAG("-std=c++20" COMPILER_SUPPORTS_CXX20)
2021
if(COMPILER_SUPPORTS_CXX20)
2122
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20")
2223
elseif(COMPILER_SUPPORTS_CXX17)
@@ -27,6 +28,18 @@ else()
2728
message(STATUS "The compiler ${CMAKE_CXX_COMPILER} is too old. Please use a different C++ compiler.")
2829
endif()
2930

31+
option(ENABLE_ASAN "Enable AddressSanitizer (ASan)" OFF)
32+
if(ENABLE_ASAN)
33+
add_compile_options(-fsanitize=address -g -fno-omit-frame-pointer)
34+
add_link_options(-fsanitize=address)
35+
endif()
36+
37+
include(CTest)
38+
if(BUILD_TESTING)
39+
add_compile_options(-Wno-deprecated-copy)
40+
add_subdirectory(third_party/gmock)
41+
endif()
42+
3043
# include_directories("${PROJECT_BINARY_DIR}/src/libais")
3144

3245
add_subdirectory(src)

src/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
add_subdirectory(libais)
2+
if(BUILD_TESTING)
3+
add_subdirectory(test)
4+
endif()

src/libais/ais7_13.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,8 @@ Ais7_13::Ais7_13(const char *nmea_payload, const size_t pad)
3232
status = AIS_OK;
3333
}
3434

35+
std::ostream& operator<< (std::ostream &o, const Ais7_13 &msg) {
36+
return o << msg.message_id << ": " << msg.mmsi;
37+
}
38+
3539
} // namespace libais

src/libais/vdm.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,12 +321,16 @@ bool VdmStream::AddLine(const std::string &line) {
321321
size_t const seq = sentence->sequence_number();
322322
size_t const tot = sentence->sentence_total();
323323

324-
// These are enforced by NmeaSentence, so only check when debugging.
325-
assert(seq < kNumSequenceChannels);
326-
assert(tot < kMaxSentences);
324+
if (tot > kMaxSentences) {
325+
return false; // More sentences than allowed.
326+
}
327327

328328
// Convert multi-line message to single line.
329329
if (tot != 1) {
330+
if (tot > 1 && seq > kNumSequenceChannels) {
331+
return false; // Sequence number is too large or empty (kNoSequenceNumber).
332+
}
333+
330334
size_t const cnt = sentence->sentence_number();
331335

332336
// Beginning of a message.

src/test/CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
find_package(Threads REQUIRED)
2+
3+
include_directories(
4+
SYSTEM
5+
${CMAKE_SOURCE_DIR}/third_party/gmock/include
6+
${CMAKE_SOURCE_DIR}/third_party/gtest/include
7+
)
8+
9+
file(GLOB TEST_SRCS "*_test.cpp")
10+
11+
foreach(test_src ${TEST_SRCS})
12+
get_filename_component(test_name ${test_src} NAME_WE)
13+
add_executable(${test_name} ${test_src})
14+
target_link_libraries(${test_name} PRIVATE ais gmock_main Threads::Threads)
15+
target_compile_options(${test_name} PRIVATE -Wno-deprecated-copy)
16+
add_test(NAME ${test_name} COMMAND ${test_name})
17+
endforeach()

src/test/vdm_test.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,9 +467,14 @@ TEST_F(VdmTest, ThreeLineAisMessage) {
467467

468468
TEST_F(VdmTest, OverRangeSequence) {
469469
// The sequence number should be [0..9]. Reject others.
470-
stream_.AddLine("!BSVDM,1,1,10,B,36So=l5000o?uF0K>pnpV0Nf0000,0*56");
470+
EXPECT_FALSE(stream_.AddLine("!BSVDM,1,1,10,B,36So=l5000o?uF0K>pnpV0Nf0000,0*56"));
471471
auto ais_msg = stream_.PopOldestMessage();
472472
ASSERT_EQ(nullptr, ais_msg);
473+
474+
// Reject multi-fragment sentences with empty/missing sequence ID.
475+
EXPECT_FALSE(stream_.AddLine("!AIVDM,2,1,,A,177KQ@,0*4E"));
476+
ais_msg = stream_.PopOldestMessage();
477+
ASSERT_EQ(nullptr, ais_msg);
473478
}
474479

475480
} // namespace

0 commit comments

Comments
 (0)