Skip to content

Commit 19c26e6

Browse files
Fix 189, 196, 206 (#211)
Close #189 Close #196 Close #206
1 parent 8ce8ed2 commit 19c26e6

10 files changed

+133
-60
lines changed

.github/workflows/main.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ jobs:
132132
contains(github.event.head_commit.message, '#sonar')
133133
runs-on: ubuntu-latest
134134
env:
135-
SONAR_SCANNER_VERSION: 4.6.1.2450
135+
SONAR_SCANNER_VERSION: 4.8.0.2856
136136
SONAR_SERVER_URL: "https://sonarcloud.io"
137137
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory
138138
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -154,7 +154,7 @@ jobs:
154154
distribution: zulu
155155

156156
- name: Cache SonarCloud packages
157-
uses: actions/cache@v1
157+
uses: actions/cache@v3
158158
with:
159159
path: ~/.sonar/cache
160160
key: ${{ runner.os }}-sonar
@@ -193,7 +193,7 @@ jobs:
193193
--define sonar.projectName=libcanard
194194
--define sonar.projectKey=libcanard
195195
--define sonar.sources=libcanard
196-
--define sonar.exclusions=libcanard/cavl.h
196+
--define sonar.exclusions=libcanard/_canard_cavl.h
197197
--define sonar.cfamily.gcov.reportsPath=.
198198
--define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}"
199199
--define sonar.host.url="${{ env.SONAR_SERVER_URL }}"

CONTRIBUTING.md

+3
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ The following tools are required to conduct library development locally
5858
- CMake
5959
- An AMD64 machine
6060

61+
You may want to use the [toolshed](https://github.com/OpenCyphal/docker_toolchains/pkgs/container/toolshed)
62+
container for this.
63+
6164
### Clang-Tidy
6265

6366
Clang-Tidy is used to enforce compliance with MISRA and Zubax Coding Conventions.

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,13 @@ If you find the examples to be unclear or incorrect, please, open a ticket.
244244
245245
- Remove UB as described in [203](https://github.com/OpenCyphal/libcanard/issues/203).
246246
247+
#### v3.0.2
248+
249+
- Robustify the multi-frame transfer reassembler state machine
250+
([#189](https://github.com/OpenCyphal/libcanard/issues/189)).
251+
- Eliminate the risk of a header file name collision by renaming the vendored Cavl header to `_canard_cavl.h`
252+
([#196](https://github.com/OpenCyphal/libcanard/issues/196)).
253+
247254
### v2.0
248255
249256
- Dedicated transmission queues per redundant CAN interface with depth limits.
File renamed without changes.

libcanard/canard.c

+12-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/// Author: Pavel Kirienko <pavel@opencyphal.org>
44

55
#include "canard.h"
6-
#include "cavl.h"
6+
#include "_canard_cavl.h"
77
#include <string.h>
88

99
// --------------------------------------------- BUILD CONFIGURATION ---------------------------------------------
@@ -849,10 +849,20 @@ CANARD_PRIVATE int8_t rxSessionUpdate(CanardInstance* const ins,
849849
}
850850
else
851851
{
852+
// The purpose of the correct_start check is to reduce the possibility of accepting a malformed multi-frame
853+
// transfer in the event of a CRC collision. The scenario where this failure mode would manifest is as follows:
854+
// 1. A valid transfer (whether single- or multi-frame) is accepted with TID=X.
855+
// 2. All frames of the subsequent multi-frame transfer with TID=X+1 are lost except for the last one.
856+
// 3. The CRC of said multi-frame transfer happens to yield the correct residue when applied to the fragment
857+
// of the payload contained in the last frame of the transfer (a CRC collision is in effect).
858+
// 4. The last frame of the multi-frame transfer is erroneously accepted even though it is malformed.
859+
// The correct_start check eliminates this failure mode by ensuring that the first frame is observed.
860+
// See https://github.com/OpenCyphal/libcanard/issues/189.
852861
const bool correct_transport = (rxs->redundant_transport_index == redundant_transport_index);
853862
const bool correct_toggle = (frame->toggle == rxs->toggle);
854863
const bool correct_tid = (frame->transfer_id == rxs->transfer_id);
855-
if (correct_transport && correct_toggle && correct_tid)
864+
const bool correct_start = frame->start_of_transfer || (rxs->total_payload_size > 0);
865+
if (correct_transport && correct_toggle && correct_tid && correct_start)
856866
{
857867
out = rxSessionAcceptFrame(ins, rxs, frame, extent, out_transfer);
858868
}

tests/test_private_cavl.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Copyright (c) 2016-2020 OpenCyphal Development Team.
33
// These tests have been adapted from the Cavl test suite that you can find at https://github.com/pavel-kirienko/cavl
44

5-
#include <cavl.h>
5+
#include <_canard_cavl.h>
66
#include "catch.hpp"
77
#include <algorithm>
88
#include <array>

tests/test_public_rx.cpp

+107
Original file line numberDiff line numberDiff line change
@@ -331,3 +331,110 @@ TEST_CASE("RxSubscriptionErrors")
331331
REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardRxAccept(&ins.getInstance(), 0, &frame, 0, nullptr, nullptr));
332332
REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardRxAccept(nullptr, 0, nullptr, 0, nullptr, nullptr));
333333
}
334+
335+
TEST_CASE("Issue189") // https://github.com/OpenCyphal/libcanard/issues/189
336+
{
337+
using helpers::Instance;
338+
using exposed::RxSession;
339+
340+
Instance ins;
341+
CanardRxTransfer transfer{};
342+
CanardRxSubscription* subscription = nullptr;
343+
const std::uint8_t redundant_transport_index = 0;
344+
345+
const auto accept = [&](const CanardMicrosecond timestamp_usec,
346+
const std::uint32_t extended_can_id,
347+
const std::vector<std::uint8_t>& payload) {
348+
static std::vector<std::uint8_t> payload_storage;
349+
payload_storage = payload;
350+
CanardFrame frame{};
351+
frame.extended_can_id = extended_can_id;
352+
frame.payload_size = std::size(payload);
353+
frame.payload = payload_storage.data();
354+
return ins.rxAccept(timestamp_usec, frame, redundant_transport_index, transfer, &subscription);
355+
};
356+
357+
ins.getAllocator().setAllocationCeiling(sizeof(RxSession) + 50); // A session and the payload buffer.
358+
359+
// Create a message subscription.
360+
CanardRxSubscription sub_msg{};
361+
REQUIRE(1 == ins.rxSubscribe(CanardTransferKindMessage, 0b0110011001100, 50, 1'000'000, sub_msg));
362+
REQUIRE(ins.getMessageSubs().at(0) == &sub_msg);
363+
REQUIRE(ins.getMessageSubs().at(0)->port_id == 0b0110011001100);
364+
REQUIRE(ins.getMessageSubs().at(0)->extent == 50);
365+
REQUIRE(ins.getMessageSubs().at(0)->transfer_id_timeout_usec == 1'000'000);
366+
REQUIRE(ensureAllNullptr(ins.getMessageSubs().at(0)->sessions));
367+
REQUIRE(ins.getResponseSubs().empty());
368+
REQUIRE(ins.getRequestSubs().empty());
369+
370+
// First, ensure that the reassembler is initialized, by feeding it a valid transfer at least once.
371+
subscription = nullptr;
372+
REQUIRE(1 == accept(100'000'001, 0b001'00'0'11'0110011001100'0'0100111, {0x42, 0b111'00000}));
373+
REQUIRE(subscription != nullptr);
374+
REQUIRE(subscription->port_id == 0b0110011001100);
375+
REQUIRE(transfer.timestamp_usec == 100'000'001);
376+
REQUIRE(transfer.metadata.priority == CanardPriorityImmediate);
377+
REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindMessage);
378+
REQUIRE(transfer.metadata.port_id == 0b0110011001100);
379+
REQUIRE(transfer.metadata.remote_node_id == 0b0100111);
380+
REQUIRE(transfer.metadata.transfer_id == 0);
381+
REQUIRE(transfer.payload_size == 1);
382+
REQUIRE(0 == std::memcmp(transfer.payload, "\x42", 1));
383+
REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 2); // The SESSION and the PAYLOAD BUFFER.
384+
REQUIRE(ins.getAllocator().getTotalAllocatedAmount() == (sizeof(RxSession) + 50));
385+
REQUIRE(ins.getMessageSubs().at(0)->sessions[0b0100111] != nullptr);
386+
ins.getAllocator().deallocate(transfer.payload);
387+
REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); // The payload buffer is gone.
388+
REQUIRE(ins.getAllocator().getTotalAllocatedAmount() == sizeof(RxSession));
389+
390+
// Next, feed the last frame of another transfer whose TID/TOG match the expected state of the reassembler,
391+
// and the CRC matches the payload available in the last frame.
392+
// This frame should be rejected because we didn't observe the first frame of this transfer.
393+
// This failure mode may occur when the first frame is lost.
394+
//
395+
// Here's how we compute the reference value of the transfer CRC:
396+
// >>> from pycyphal.transport.commons.crc import CRC16CCITT
397+
// >>> CRC16CCITT.new(b'DUCK').value_as_bytes
398+
// b'4\xa3'
399+
subscription = nullptr;
400+
REQUIRE(0 == accept(100'001'001, // The result should be zero because the transfer is rejected.
401+
0b001'00'0'11'0110011001100'0'0100111, //
402+
{'D', 'U', 'C', 'K', '4', 0xA3, 0b011'00001})); // SOF=0, EOF=1, TOG=1, TID=1, CRC=0x4A34
403+
REQUIRE(subscription != nullptr); // Subscription exists.
404+
REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); // The SESSION only.
405+
REQUIRE(ins.getAllocator().getTotalAllocatedAmount() == sizeof(RxSession));
406+
REQUIRE(ins.getMessageSubs().at(0)->sessions[0b0100111] != nullptr);
407+
408+
// Now feed a similar transfer that is not malformed. It should be accepted.
409+
// Here's how we compute the reference value of the transfer CRC:
410+
// >>> from pycyphal.transport.commons.crc import CRC16CCITT
411+
// >>> CRC16CCITT.new(b'\x01\x02\x03\x04\x05\x06\x07DUCK').value_as_bytes
412+
// b'\xd3\x14'
413+
subscription = nullptr;
414+
REQUIRE(0 == accept(100'002'001, //
415+
0b001'00'0'11'0110011001100'0'0100111,
416+
{1, 2, 3, 4, 5, 6, 7, 0b101'00010}));
417+
REQUIRE(subscription != nullptr); // Subscription exists.
418+
REQUIRE(1 == accept(100'002'002, // Accepted!
419+
0b001'00'0'11'0110011001100'0'0100111,
420+
{'D', 'U', 'C', 'K', 0xD3, 0x14, 0b010'00010}));
421+
REQUIRE(subscription != nullptr); // Subscription exists.
422+
REQUIRE(subscription->port_id == 0b0110011001100);
423+
REQUIRE(transfer.timestamp_usec == 100'002'001);
424+
REQUIRE(transfer.metadata.priority == CanardPriorityImmediate);
425+
REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindMessage);
426+
REQUIRE(transfer.metadata.port_id == 0b0110011001100);
427+
REQUIRE(transfer.metadata.remote_node_id == 0b0100111);
428+
REQUIRE(transfer.metadata.transfer_id == 2);
429+
REQUIRE(transfer.payload_size == 11);
430+
REQUIRE(0 == std::memcmp(transfer.payload,
431+
"\x01\x02\x03\x04\x05\x06\x07"
432+
"DUCK",
433+
11));
434+
REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 2); // The SESSION and the PAYLOAD BUFFER.
435+
REQUIRE(ins.getAllocator().getTotalAllocatedAmount() == (sizeof(RxSession) + 50));
436+
REQUIRE(ins.getMessageSubs().at(0)->sessions[0b0100111] != nullptr);
437+
ins.getAllocator().deallocate(transfer.payload);
438+
REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); // The payload buffer is gone.
439+
REQUIRE(ins.getAllocator().getTotalAllocatedAmount() == sizeof(RxSession));
440+
}

tools/Dockerfile

-31
This file was deleted.

tools/entrypoint.sh

-17
This file was deleted.

tools/run-docker.sh

-6
This file was deleted.

0 commit comments

Comments
 (0)