Skip to content

fix: remediate P0 safety and mechanical clang-tidy violations (#222)#225

Merged
vtz merged 6 commits intomainfrom
feat/clang-tidy-remediation
Apr 13, 2026
Merged

fix: remediate P0 safety and mechanical clang-tidy violations (#222)#225
vtz merged 6 commits intomainfrom
feat/clang-tidy-remediation

Conversation

@vtz
Copy link
Copy Markdown
Owner

@vtz vtz commented Apr 13, 2026

Summary

First batch of clang-tidy remediation for #222. Reduces violations by ~325 (18% reduction).

  • Config fix: .clang-tidy used invalid snake_case option values (should be lower_case), silently disabling readability-identifier-naming checks
  • P0 Safety — all bugprone-* (28), cppcoreguidelines-init-variables (19), and cppcoreguidelines-pro-type-member-init (26) violations eliminated:
    • Narrowing conversions fixed with static_cast<ptrdiff_t> for iterator arithmetic
    • Uninitialized locals and struct records zero-initialized
    • RpcClientImpl destructor wrapped in noexcept try-catch for exception safety
    • Intentional empty catch in thread_impl.h justified with NOLINT
    • Duplicate branch merged in standard_profile.cpp counter validation
  • P2 Maintainability — mechanical auto-fixes:
    • modernize-concat-nested-namespaces (147 → 0): C++17 namespace foo::bar syntax
    • misc-const-correctness (304 → ~160): const added to non-mutated locals
    • readability-braces-around-statements (36 → 0): braces on bare if/else
    • modernize-use-override, readability-static-accessed-through-instance, performance-avoid-endl, and other small fixes

Pre-existing issues fixed (#226)

Addresses 9 pre-existing code quality issues flagged by CodeRabbit during review:

  • Critical — Data corruption: IPv4MulticastOption::serialize() wrote length to wrong byte indices; E2E counter validation accepted duplicate counters (replay risk)
  • Critical — Concurrency: pool_initialized data race in FreeRTOS/ThreadX memory allocators (broken DCLP); callbacks invoked under mutexes in TpManager::process_timeouts() and SdClient offer handling (deadlock risk)
  • Major — Type safety: TP offset narrowed from uint32_t to uint16_t; DeserializationResult default-constructed as success with empty optional; missing PTRDIFF_MAX overflow guard in CRC calculation

Note: Fixing the .clang-tidy config correctly activates readability-identifier-naming, surfacing ~165 new naming violations that were previously hidden. These are tracked for a future batch.

Remaining violations for subsequent batches

Check Count Priority
misc-include-cleaner 427 P2
hicpp-signed-bitwise 366 P1
readability-identifier-naming 180 P2
misc-const-correctness 160 P2
cppcoreguidelines-special-member-functions 90 P2

Test plan

  • Project compiles cleanly (cmake --build build)
  • Local clang-tidy quality gate passes (1505 < 1783 threshold)
  • CI clang-tidy quality gate passes
  • CI unit tests pass (no behavioral changes)

Closes #226

Summary by CodeRabbit

  • Refactor

    • Modernized C++ namespace syntax across the codebase.
    • Tightened const-correctness and added explicit initializations to improve robustness.
    • Standardized destructors and clarified lifecycle behavior.
  • Public API changes

    • CRC calculation now returns an optional result for failures/invalid input.
    • Some transport/segment offset types widened and deserialization result now defaults to an explicit "not initialized" state (may affect integrations/tests).
  • Chores

    • Updated linting rules for identifier naming.
    • Added build directory to .gitignore.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces nested C++ namespaces with C++17 inline namespaces across the tree; changes E2E CRC API to return std::optional and updates callers/tests; widens TP offset/length types; replaces pool init booleans with atomics; moves user callbacks out of mutexes; tightens const-correctness and initializes temporaries.

Changes

Cohort / File(s) Summary
Clang-tidy config
\.clang-tidy
Updated readability-identifier-naming options: VariableCase, MemberCase, ParameterCase changed from snake_caselower_case.
Namespace modernizations
include/..., src/... (many files)
Replaced nested namespace someip { namespace X { with namespace someip::X { across many headers and sources (e.g., include/e2e/*, include/events/*, include/platform/*, include/rpc/*, include/sd/*, include/tp/*, include/transport/*, and matching src/...).
E2E CRC & tests
include/e2e/e2e_crc.h, src/e2e/e2e_crc.cpp, tests/*, tests/shared/*
calculate_crc(...) now returns std::optional<uint32_t>; strengthened bounds/overflow guards; invalid/unknown cases return std::nullopt; tests and helpers updated to expect optional semantics.
E2E profiles & protection
src/e2e/e2e_profiles/*, src/e2e/e2e_protection.cpp
Defaulted constructors, more const-correct locals, changed counter validation semantics, and removed an unnecessary message copy when calling profile validation.
TP: types, segmenter, reassembler, manager
include/tp/*, src/tp/*
Widened TP offset/length types (uint16_tuint32_t), adjusted parse/parse-signatures, defaulted/removed destructors, made many locks const, and refactored process_timeouts to capture timed-out transfers under lock and invoke callbacks after unlocking.
SD: client/server/message
include/sd/*, src/sd/*
Value-initialized network structs (in_addr, ip_mreq), tightened constness on locks/locals, and capture-and-invoke pattern introduced so user callbacks run after releasing internal mutexes.
RPC & Events
include/rpc/*, src/rpc/*, include/events/*, src/events/*
Marked several implementation destructors override/noexcept, wrapped destructor shutdown calls to avoid exception escape, moved callback invocation out of locks, tightened lock constness, and removed some out-of-line defaulted destructor definitions.
Platform backends & pool init
include/platform/threadx/memory_internal.h, src/platform/*/memory.cpp, tests/threadx/*
Replaced global bool pool_initialized with std::atomic<bool> and added std::atomic_flag gating; ensure-init uses atomic acquire/release semantics; tests updated to use atomic loads.
Serialization & Message handling
include/serialization/serializer.h, src/serialization/*, src/someip/message.cpp
Explicitly initialized DeserializationResult members, zero-initialized temporaries before memcpy, used std::ptrdiff_t casts for iterator arithmetic, and made padding/temporary locals const.
Transport (UDP/TCP/endpoint)
include/transport/*, src/transport/*
Value-initialized socket/addr structs, tightened const on lock and error-code locals, adjusted iterator arithmetic to ptrdiff_t, simplified some parsing flows.
Core/service and misc impls
src/core/session_manager.cpp, many src/* files
Converted many platform::ScopedLock locals to const, converted various locals to const/default-initialized, small bracing/format changes, and removed several redundant out-of-line destructor definitions.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇
Names aligned in tidy rows I sow,
Locks made steady, calm, and slow,
Callbacks step out where fresh air blows,
Atomics guard the pool that grows,
A little rabbit hops — the patch now flows.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: fixing P0 safety violations and mechanical clang-tidy violations through remediation.
Linked Issues check ✅ Passed The PR successfully addresses all code-quality objectives from linked issue #226: data corruption bugs fixed, concurrency/data-race issues resolved, and type-safety improvements implemented.
Out of Scope Changes check ✅ Passed All changes are directly related to clang-tidy remediation and linked issue #226 objectives; no unrelated or out-of-scope modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/clang-tidy-remediation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/platform/threadx/memory.cpp (1)

69-76: ⚠️ Potential issue | 🔴 Critical

Remove const qualifier from block pointer—incompatible with ThreadX and placement new.

Line 69 declares block as void const*, but:

  • tx_block_allocate(&message_pool, &block, TX_NO_WAIT) expects a VOID** (non-const) parameter
  • new (block) Message() expects a void* (non-const) pointer

Passing a void const** to tx_block_allocate and a const void* to placement new are type mismatches that will cause compilation errors.

Fix
-    void const* block = nullptr;
+    void* block = nullptr;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/threadx/memory.cpp` around lines 69 - 76, The local pointer
`block` is declared as `void const*` which is incompatible with ThreadX's
tx_block_allocate (expects `VOID**`) and placement new (expects `void*`); change
the declaration of `block` to a non-const pointer (e.g., `void* block =
nullptr`) so you can call `tx_block_allocate(&message_pool, &block, TX_NO_WAIT)`
and use `new (block) Message()` safely; ensure subsequent uses (including the
lambda destructor in `MessagePtr`) also treat the pointer as non-const.
src/rpc/rpc_client.cpp (1)

80-89: ⚠️ Potential issue | 🟠 Major

Do not invoke user callbacks while holding pending_calls_mutex_.

This can deadlock if a callback re-enters RPC client APIs that also take the same mutex (downstream impact: cancel_call and on_message_received follow the same pattern). Move callback collection inside the lock and execute callbacks after unlocking.

Proposed pattern for `shutdown()` (apply similarly in `cancel_call` and `on_message_received`)
-        {
-            platform::ScopedLock const lock(pending_calls_mutex_);
-            for (auto& pair : pending_calls_) {
-                if (pair.second.callback) {
-                    RpcResponse response(pair.second.service_id, pair.second.method_id,
-                                       client_id_, pair.second.session_id, RpcResult::INTERNAL_ERROR);
-                    pair.second.callback(response);
-                }
-            }
-            pending_calls_.clear();
-        }
+        std::vector<std::pair<RpcCallback, RpcResponse>> callbacks;
+        {
+            platform::ScopedLock const lock(pending_calls_mutex_);
+            callbacks.reserve(pending_calls_.size());
+            for (auto const& pair : pending_calls_) {
+                if (pair.second.callback) {
+                    callbacks.emplace_back(
+                        pair.second.callback,
+                        RpcResponse(pair.second.service_id, pair.second.method_id,
+                                    client_id_, pair.second.session_id, RpcResult::INTERNAL_ERROR));
+                }
+            }
+            pending_calls_.clear();
+        }
+        for (auto& item : callbacks) {
+            item.first(item.second);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rpc/rpc_client.cpp` around lines 80 - 89, Do not invoke user callbacks
while holding pending_calls_mutex_: in shutdown() (and similarly in cancel_call
and on_message_received) acquire pending_calls_mutex_, iterate pending_calls_ to
collect the RpcResponse objects or std::function callbacks into a local vector,
clear pending_calls_ while still holding the lock, then release the lock and
finally iterate the local vector to invoke each callback (using RpcResponse
constructed with service_id, method_id, client_id_, session_id,
RpcResult::INTERNAL_ERROR as you do now). This moves callback execution out of
the locked region and prevents deadlocks when callbacks re-enter RPC client APIs
that also lock pending_calls_mutex_.
src/rpc/rpc_server.cpp (1)

127-133: ⚠️ Potential issue | 🟠 Major

Release methods_mutex_ before sending unknown-method response.

At Line 131, send_error_response runs while methods_mutex_ is still held. Move the send after the lock scope to avoid blocking method registration paths on network I/O.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rpc/rpc_server.cpp` around lines 127 - 133, The code holds
platform::ScopedLock methods_mutex_ while calling send_error_response, which can
block registration paths; change the logic in the request handling (around
methods_mutex_, method_handlers_, and the call that checks
message->get_method_id()) so the lock is released before performing network I/O:
acquire the lock, check method_handlers_ for message->get_method_id() and record
whether the method exists (or store the iterator/result) then let the ScopedLock
go out of scope; only after the lock is released, call
send_error_response(message, sender, ReturnCode::E_UNKNOWN_METHOD) when the
method was not found. Ensure you do not access method_handlers_ after releasing
methods_mutex_.
src/sd/sd_client.cpp (1)

377-391: ⚠️ Potential issue | 🔴 Critical

Invoke user callbacks after releasing internal mutexes.

These callbacks execute while subscriptions_mutex_ / pending_finds_mutex_ are held. Re-entrant client code can deadlock against subscribe/find APIs.

Also applies to: 418-422

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sd/sd_client.cpp` around lines 377 - 391, The code currently invokes user
callbacks while holding internal mutexes (subscriptions_mutex_ and
pending_finds_mutex_), risking deadlocks; modify the logic in the handler that
processes instance notifications so it captures the callback(s) and any data
(e.g., ServiceInstance) while under lock, then releases the lock(s) and invokes
available_callback and pending_finds_ callbacks outside the critical sections;
specifically, when you find sub_it->second.available_callback or an entry in
pending_finds_ with it->second.callback, copy the std::function and the
ServiceInstance into local variables/containers under the respective mutex
(subscriptions_mutex_ / pending_finds_mutex_), erase/advance the containers as
needed, then call the copied callbacks after the locks are dropped (do the same
for the other occurrence referenced in the review).
src/sd/sd_server.cpp (1)

112-143: ⚠️ Potential issue | 🟠 Major

Avoid network sends while offered_services_mutex_ is held.

The lock scope includes send_service_offer (Line 142). If send blocks, service-management APIs are blocked behind the mutex. Snapshot data under lock, then send outside it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sd/sd_server.cpp` around lines 112 - 143, The code currently holds
offered_services_mutex_ while calling send_service_offer, which can block;
reduce the lock scope by assembling and inserting the OfferedService under the
lock but capturing a copy (not a reference) of the element to send, then release
the mutex and call send_service_offer on that copy. Specifically, keep the
find/insert/eviction logic and offered_services_.push_back(std::move(offered))
protected by offered_services_mutex_, then create a local OfferedService copy
(e.g., copy the newly inserted element) while still holding the lock, let the
lock go out of scope, and finally call send_service_offer with that local copy
so send_service_offer is executed without holding offered_services_mutex_.
src/events/event_subscriber.cpp (1)

261-303: ⚠️ Potential issue | 🔴 Critical

Do not invoke external callbacks while holding internal mutexes.

At Line 261 and Line 290, locks are held while callback code is executed (Line 278, Line 299). Re-entrant callbacks can deadlock by calling APIs that need the same mutexes.

Suggested fix
-        platform::ScopedLock const subs_lock(subscriptions_mutex_);
+        EventNotificationCallback notify_cb;
+        EventNotification notify_copy(service_id, 0, event_id);
+        {
+            platform::ScopedLock const subs_lock(subscriptions_mutex_);
             ...
-                if (sub_info.notification_callback) {
-                    sub_info.notification_callback(notification);
-                }
+                notify_copy = notification;
+                notify_cb = sub_info.notification_callback;
             ...
-        platform::ScopedLock const field_lock(field_requests_mutex_);
+        }
+        if (notify_cb) {
+            notify_cb(notify_copy);
+        }
+
+        EventNotificationCallback field_cb;
+        EventNotification field_notify(service_id, 0, event_id);
+        {
+            platform::ScopedLock const field_lock(field_requests_mutex_);
             ...
-            if (field_it->second) {
-                field_it->second(notification);
-            }
+            field_notify = notification;
+            field_cb = field_it->second;
             field_requests_.erase(field_it);
-        }
+        }
+        if (field_cb) {
+            field_cb(field_notify);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/events/event_subscriber.cpp` around lines 261 - 303, You are calling
external callbacks while holding internal mutexes (subscriptions_mutex_ and
field_requests_mutex_) which can deadlock; fix by capturing necessary data and
the callback under the lock and then invoking the callback after releasing the
lock. For the subscriptions loop (subscriptions_, subscriptions_mutex_,
sub_pair/sub_info, notification_callback) create and populate EventNotification
and copy sub_info.notification_callback to a local std::function while still
holding the lock, update sub_info.subscription.state and last_notification under
the lock, then release the lock and call the copied callback. For the field
response path (field_requests_, field_requests_mutex_, make_field_key, field_it)
copy/construct the EventNotification and copy field_it->second into a local
callback, erase the entry while holding the lock, release the lock, then invoke
the copied callback. Ensure no external call paths run while either mutex is
held.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@include/events/event_publisher.h`:
- Line 46: The inline defaulted destructor ~EventPublisher() must be moved
out-of-line to avoid deleting an incomplete std::unique_ptr<EventPublisherImpl>;
change the header to declare ~EventPublisher(); (no inline definition) and add a
definition in the .cpp: implement EventPublisher::~EventPublisher() = default;
inside the someip::events namespace in the .cpp after includes where
EventPublisherImpl is fully defined so the unique_ptr deleter sees a complete
type.

In `@include/events/event_subscriber.h`:
- Line 45: Declare the destructor for EventSubscriber in the header (change
~EventSubscriber() = default; to a non-inline declaration ~EventSubscriber();)
and add an out-of-line definition in the implementation translation unit: define
EventSubscriber::~EventSubscriber() = default; in the .cpp where
EventSubscriberImpl is fully defined and ensure that .cpp includes the header
that provides the complete definition of EventSubscriberImpl so the unique_ptr
deleter can be instantiated correctly.

In `@include/rpc/rpc_client.h`:
- Line 44: The header currently has an inline defaulted destructor (~RpcClient()
= default;) which requires the complete type of RpcClientImpl to destroy the
std::unique_ptr impl_; change the header to declare the destructor
(~RpcClient();) without definition and add an out-of-line definition in the .cpp
(implement RpcClient::~RpcClient() = default; in the translation unit that
includes the RpcClientImpl definition) so the unique_ptr can be destroyed with a
complete type; update includes in the .cpp so RpcClientImpl is visible there.

In `@include/rpc/rpc_server.h`:
- Line 58: Declare RpcServer's destructor as non-defaulted in the header (remove
the inline "= default" from ~RpcServer()) and provide an out-of-line definition
in the corresponding .cpp after the RpcServerImpl type is complete;
specifically, add a definition RpcServer::~RpcServer() = default; in the .cpp
(inside the someip::rpc namespace) after including the header that defines
RpcServerImpl so the std::unique_ptr<RpcServerImpl> member can be destroyed with
a complete type.

In `@include/sd/sd_client.h`:
- Line 45: The inline defaulted destructor ~SdClient() = default; should be
declared in the header but defined out-of-line to avoid undefined behavior with
the incomplete type std::unique_ptr<SdClientImpl>; change the header to declare
SdClient::~SdClient(); and add an out-of-line definition SdClient::~SdClient() =
default; in the source file after SdClientImpl is fully defined so the
unique_ptr can properly destroy its pointee.

In `@include/sd/sd_server.h`:
- Line 45: The inline defaulted destructor ~SdServer() needs to be moved
out-of-line to avoid requiring the definition of SdServerImpl in the header;
change the header to declare ~SdServer(); (keeping the forward declaration of
SdServerImpl and std::unique_ptr<SdServerImpl> intact) and implement
SdServer::~SdServer() = default; in the corresponding .cpp where SdServerImpl is
defined so destruction of the std::unique_ptr<SdServerImpl> happens with a
complete type.

In `@src/e2e/e2e_crc.cpp`:
- Around line 120-121: The cast to std::ptrdiff_t for iterator arithmetic (auto
first = data.begin() + static_cast<std::ptrdiff_t>(offset); and the subsequent
std::vector<uint8_t> slice(...) construction) needs explicit checks against
PTRDIFF_MAX to avoid overflow: before casting, validate that offset and length
are each <= PTRDIFF_MAX and that offset + length <= PTRDIFF_MAX and <=
data.size(), then perform the static_casts and iterator arithmetic; update the
checks around offset, length, and the creation of first/slice to reject inputs
that violate these limits.

In `@src/e2e/e2e_profiles/standard_profile.cpp`:
- Around line 211-215: The current validation treats header.counter ==
last_counter as valid which allows replayed frames; update the validation in
standard_profile.cpp so that only header.counter > last_counter sets
counter_valid to true (i.e., reject equality), and adjust any related comments
to reflect that equal counters are considered duplicates and invalid; locate and
modify the condition using header.counter, last_counter, and counter_valid
accordingly.

In `@src/e2e/e2e_protection.cpp`:
- Around line 79-82: The comment and code disagree: remove the stale comment and
the unnecessary const reference alias msg_copy and call
profile->validate(message, config) directly; update or delete the comment above
the Message binding so validate() invocation uses the original message and the
code no longer declares Message const& msg_copy.

In `@src/platform/freertos/memory.cpp`:
- Around line 70-71: The storage pointer used for placement new is declared as
const (void const* block) but placement new (new (void*) Message) requires a
non-const void* so the constructor can write into the buffer; change the
declaration of block to a non-const pointer (void* block) or explicitly cast
pool_buffer + i * sizeof(Message) to void* before calling placement new on
Message (references: pool_buffer, Message, msg and the placement new call).

In `@src/sd/sd_message.cpp`:
- Around line 319-321: The multicast option's length is being stored into the
wrong bytes (data[2]/data[3]) which overwrites the option type; update the
writes to store the uint16_t length into the SdOption length field bytes data[0]
and data[1] (i.e., set data[0] = (length >> 8) & 0xFF; data[1] = length & 0xFF;)
so the option type and subsequent payload remain intact and deserialization
succeeds.

In `@src/tp/tp_manager.cpp`:
- Line 188: The completion_callback_ is being invoked while holding
transfers_mutex_ (via platform::ScopedLock) in TpManager, which risks deadlock;
fix by capturing/moving a local copy of completion_callback_ and any required
callback arguments inside the critical section protected by transfers_mutex_
(using platform::ScopedLock), then release the lock and invoke the copied
completion_callback_ outside the lock (apply same change for the other
occurrences around lines 201-203), ensuring no user callback runs while
transfers_mutex_ is held.

In `@src/tp/tp_reassembler.cpp`:
- Around line 56-57: The computation offset_units = tp_header >> 4 then offset =
offset_units * 16 can overflow the uint16_t destination; guard the narrowing
before assigning to offset (or change offset's type). Update the code around
tp_header, offset_units and offset so you compute the byte offset in a wider
type (e.g., uint32_t), check or clamp the result against UINT16_MAX (or validate
it against expected max segment size), and only assign a safe value to the
uint16_t offset to prevent wraparound and incorrect reassembly.

In `@src/transport/tcp_transport.cpp`:
- Around line 242-243: The sockaddr_in variable addr is already zero-initialized
via brace initialization (sockaddr_in addr = {}), so remove the redundant
memset(&addr, 0, sizeof(addr)); calls (found alongside the addr declaration and
the similar instance at the later occurrence) to avoid duplicate zeroing; just
keep the brace-initialized declarations (e.g., sockaddr_in addr = {}) in the
functions where these appear.

---

Outside diff comments:
In `@src/events/event_subscriber.cpp`:
- Around line 261-303: You are calling external callbacks while holding internal
mutexes (subscriptions_mutex_ and field_requests_mutex_) which can deadlock; fix
by capturing necessary data and the callback under the lock and then invoking
the callback after releasing the lock. For the subscriptions loop
(subscriptions_, subscriptions_mutex_, sub_pair/sub_info, notification_callback)
create and populate EventNotification and copy sub_info.notification_callback to
a local std::function while still holding the lock, update
sub_info.subscription.state and last_notification under the lock, then release
the lock and call the copied callback. For the field response path
(field_requests_, field_requests_mutex_, make_field_key, field_it)
copy/construct the EventNotification and copy field_it->second into a local
callback, erase the entry while holding the lock, release the lock, then invoke
the copied callback. Ensure no external call paths run while either mutex is
held.

In `@src/platform/threadx/memory.cpp`:
- Around line 69-76: The local pointer `block` is declared as `void const*`
which is incompatible with ThreadX's tx_block_allocate (expects `VOID**`) and
placement new (expects `void*`); change the declaration of `block` to a
non-const pointer (e.g., `void* block = nullptr`) so you can call
`tx_block_allocate(&message_pool, &block, TX_NO_WAIT)` and use `new (block)
Message()` safely; ensure subsequent uses (including the lambda destructor in
`MessagePtr`) also treat the pointer as non-const.

In `@src/rpc/rpc_client.cpp`:
- Around line 80-89: Do not invoke user callbacks while holding
pending_calls_mutex_: in shutdown() (and similarly in cancel_call and
on_message_received) acquire pending_calls_mutex_, iterate pending_calls_ to
collect the RpcResponse objects or std::function callbacks into a local vector,
clear pending_calls_ while still holding the lock, then release the lock and
finally iterate the local vector to invoke each callback (using RpcResponse
constructed with service_id, method_id, client_id_, session_id,
RpcResult::INTERNAL_ERROR as you do now). This moves callback execution out of
the locked region and prevents deadlocks when callbacks re-enter RPC client APIs
that also lock pending_calls_mutex_.

In `@src/rpc/rpc_server.cpp`:
- Around line 127-133: The code holds platform::ScopedLock methods_mutex_ while
calling send_error_response, which can block registration paths; change the
logic in the request handling (around methods_mutex_, method_handlers_, and the
call that checks message->get_method_id()) so the lock is released before
performing network I/O: acquire the lock, check method_handlers_ for
message->get_method_id() and record whether the method exists (or store the
iterator/result) then let the ScopedLock go out of scope; only after the lock is
released, call send_error_response(message, sender,
ReturnCode::E_UNKNOWN_METHOD) when the method was not found. Ensure you do not
access method_handlers_ after releasing methods_mutex_.

In `@src/sd/sd_client.cpp`:
- Around line 377-391: The code currently invokes user callbacks while holding
internal mutexes (subscriptions_mutex_ and pending_finds_mutex_), risking
deadlocks; modify the logic in the handler that processes instance notifications
so it captures the callback(s) and any data (e.g., ServiceInstance) while under
lock, then releases the lock(s) and invokes available_callback and
pending_finds_ callbacks outside the critical sections; specifically, when you
find sub_it->second.available_callback or an entry in pending_finds_ with
it->second.callback, copy the std::function and the ServiceInstance into local
variables/containers under the respective mutex (subscriptions_mutex_ /
pending_finds_mutex_), erase/advance the containers as needed, then call the
copied callbacks after the locks are dropped (do the same for the other
occurrence referenced in the review).

In `@src/sd/sd_server.cpp`:
- Around line 112-143: The code currently holds offered_services_mutex_ while
calling send_service_offer, which can block; reduce the lock scope by assembling
and inserting the OfferedService under the lock but capturing a copy (not a
reference) of the element to send, then release the mutex and call
send_service_offer on that copy. Specifically, keep the find/insert/eviction
logic and offered_services_.push_back(std::move(offered)) protected by
offered_services_mutex_, then create a local OfferedService copy (e.g., copy the
newly inserted element) while still holding the lock, let the lock go out of
scope, and finally call send_service_offer with that local copy so
send_service_offer is executed without holding offered_services_mutex_.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: adec4986-e544-46d5-923c-3e4aadc46db7

📥 Commits

Reviewing files that changed from the base of the PR and between 6cf69b9 and 0792a67.

📒 Files selected for processing (54)
  • .clang-tidy
  • include/e2e/e2e_config.h
  • include/e2e/e2e_crc.h
  • include/e2e/e2e_header.h
  • include/e2e/e2e_profile.h
  • include/e2e/e2e_profile_registry.h
  • include/e2e/e2e_protection.h
  • include/events/event_publisher.h
  • include/events/event_subscriber.h
  • include/events/event_types.h
  • include/platform/host/host_condition_variable.h
  • include/platform/posix/memory_impl.h
  • include/platform/posix/net_impl.h
  • include/platform/posix/thread_impl.h
  • include/platform/thread.h
  • include/rpc/rpc_client.h
  • include/rpc/rpc_server.h
  • include/rpc/rpc_types.h
  • include/sd/sd_client.h
  • include/sd/sd_message.h
  • include/sd/sd_server.h
  • include/sd/sd_types.h
  • include/serialization/serializer.h
  • include/tp/tp_manager.h
  • include/tp/tp_reassembler.h
  • include/tp/tp_segmenter.h
  • include/tp/tp_types.h
  • include/transport/endpoint.h
  • include/transport/tcp_transport.h
  • include/transport/transport.h
  • include/transport/udp_transport.h
  • src/core/session_manager.cpp
  • src/e2e/e2e_crc.cpp
  • src/e2e/e2e_header.cpp
  • src/e2e/e2e_profile_registry.cpp
  • src/e2e/e2e_profiles/standard_profile.cpp
  • src/e2e/e2e_protection.cpp
  • src/events/event_publisher.cpp
  • src/events/event_subscriber.cpp
  • src/platform/freertos/memory.cpp
  • src/platform/threadx/memory.cpp
  • src/rpc/rpc_client.cpp
  • src/rpc/rpc_server.cpp
  • src/sd/sd_client.cpp
  • src/sd/sd_message.cpp
  • src/sd/sd_server.cpp
  • src/serialization/serializer.cpp
  • src/someip/message.cpp
  • src/tp/tp_manager.cpp
  • src/tp/tp_reassembler.cpp
  • src/tp/tp_segmenter.cpp
  • src/transport/endpoint.cpp
  • src/transport/tcp_transport.cpp
  • src/transport/udp_transport.cpp

Comment thread include/events/event_publisher.h Outdated
Comment thread include/events/event_subscriber.h Outdated
Comment thread include/rpc/rpc_client.h Outdated
Comment thread include/rpc/rpc_server.h Outdated
Comment thread include/sd/sd_client.h Outdated
Comment thread src/platform/freertos/memory.cpp Outdated
Comment thread src/sd/sd_message.cpp Outdated
Comment thread src/tp/tp_manager.cpp Outdated
Comment thread src/tp/tp_reassembler.cpp
Comment thread src/transport/tcp_transport.cpp Outdated
@vtz vtz force-pushed the feat/clang-tidy-remediation branch 2 times, most recently from c4b2e0e to 383a399 Compare April 13, 2026 02:20
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/rpc/rpc_client.cpp (1)

80-86: ⚠️ Potential issue | 🔴 Critical

Do not invoke callbacks while holding pending_calls_mutex_.

Current paths call user callbacks under the lock, which can deadlock if callbacks call back into client APIs (cancel_call, new calls, shutdown).

🔧 Proposed pattern (unlock before callback)
-        {
-            platform::ScopedLock const lock(pending_calls_mutex_);
-            for (auto& pair : pending_calls_) {
-                if (pair.second.callback) {
-                    RpcResponse response(...);
-                    pair.second.callback(response);
-                }
-            }
-            pending_calls_.clear();
-        }
+        std::vector<std::pair<RpcCallback, RpcResponse>> callbacks;
+        {
+            platform::ScopedLock const lock(pending_calls_mutex_);
+            for (auto& pair : pending_calls_) {
+                if (pair.second.callback) {
+                    callbacks.emplace_back(
+                        pair.second.callback,
+                        RpcResponse(pair.second.service_id, pair.second.method_id,
+                                    client_id_, pair.second.session_id, RpcResult::INTERNAL_ERROR));
+                }
+            }
+            pending_calls_.clear();
+        }
+        for (auto& cb : callbacks) {
+            cb.first(cb.second);
+        }

Apply the same unlock-before-callback pattern to cancel_call() and on_message_received().

Also applies to: 182-193, 225-242

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rpc/rpc_client.cpp` around lines 80 - 86, Callbacks are being invoked
while holding pending_calls_mutex_ (see the loop that constructs RpcResponse and
calls pair.second.callback), which can deadlock; change these sites to capture
the callback (and any response object like RpcResponse constructed from
pair.second.service_id/method_id/client_id_/session_id/RpcResult::INTERNAL_ERROR)
and erase/update the pending_calls_ entry as needed while holding the lock, then
release the lock and invoke the captured callback outside the critical section.
Apply the same unlock-before-callback pattern to cancel_call() and
on_message_received() where pending_calls_ entries are completed or cancelled so
that mutex is not held during user callback execution.
♻️ Duplicate comments (8)
src/e2e/e2e_protection.cpp (1)

79-82: ⚠️ Potential issue | 🟡 Minor

Stale validate() comment and redundant alias still present.

At Line 79-82, the comment says a mutable copy is required, but the code uses a const reference alias. Drop the stale wording and call profile->validate(message, config) directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/e2e/e2e_protection.cpp` around lines 79 - 82, The code contains a stale
comment and a redundant const reference alias that suggests a mutable copy is
needed but actually creates a const alias; remove the comment and the alias and
call profile->validate(message, config) directly. Locate the block using Message
const& msg_copy = message and the surrounding comment, delete both, and replace
the redundant alias usage so the call uses the original message and config with
profile->validate(message, config). Ensure no other code depends on msg_copy
before removing it.
src/e2e/e2e_crc.cpp (1)

120-121: ⚠️ Potential issue | 🟡 Minor

size_tstd::ptrdiff_t cast still lacks overflow guard (unresolved).

Line 120/121 cast offsets directly to std::ptrdiff_t without checking against ptrdiff_t max, so very large inputs can still overflow on narrower ptrdiff_t targets.

Suggested fix
+#include <limits>
...
 uint32_t calculate_crc(const std::vector<uint8_t>& data, size_t offset, size_t length, uint8_t crc_type) {
     if (offset > data.size() || length > data.size() || offset > data.size() - length) {
         return 0;
     }
+    const size_t max_diff = static_cast<size_t>(std::numeric_limits<std::ptrdiff_t>::max());
+    if (offset > max_diff || length > max_diff || offset > max_diff - length) {
+        return 0;
+    }
 
     auto first = data.begin() + static_cast<std::ptrdiff_t>(offset);
     std::vector<uint8_t> slice(first, first + static_cast<std::ptrdiff_t>(length));
#!/bin/bash
# Verify whether ptrdiff_t-bound checks exist near calculate_crc iterator casts
sed -n '115,126p' src/e2e/e2e_crc.cpp
rg -n 'PTRDIFF_MAX|numeric_limits<\s*std::ptrdiff_t\s*>::max' src/e2e/e2e_crc.cpp
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/e2e/e2e_crc.cpp` around lines 120 - 121, The cast of offset and length
into std::ptrdiff_t when building the iterator slice (variables first and slice)
can overflow on platforms where ptrdiff_t is narrower; before casting, validate
that offset and length are <=
static_cast<size_t>(std::numeric_limits<std::ptrdiff_t>::max()) and that offset
+ length does not exceed data.size() (and check for offset + length overflow
using size_t arithmetic), then perform the safe cast; if the checks fail,
return/throw an appropriate error instead of performing the cast in the code
that constructs first and slice.
src/tp/tp_manager.cpp (1)

188-203: ⚠️ Potential issue | 🔴 Critical

Callback is still invoked under transfers_mutex_ (unresolved).

Line 188 holds the mutex, and Lines 201-203 call completion_callback_ inside that critical section, preserving the deadlock/re-entrancy risk flagged earlier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tp/tp_manager.cpp` around lines 188 - 203, Held transfers_mutex_ while
invoking completion_callback_ creates re-entrancy/deadlock risk: modify the loop
in tp_manager.cpp so you update transfer.state (TpTransfer), increment
statistics_.timeouts and record the transfer.transfer_id and TpResult::TIMEOUT
into a local list (vector) while holding transfers_mutex_, then release
transfers_mutex_ before actually calling completion_callback_ for each recorded
transfer_id; ensure you still remove/leave active_transfers_ as intended inside
the locked section but perform completion_callback_ invocations only after the
lock is released.
src/sd/sd_message.cpp (1)

320-321: ⚠️ Potential issue | 🔴 Critical

Write multicast option length into header bytes 0/1, not 2/3.

Lines 320-321 currently overwrite the option type/reserved fields. The length field belongs in bytes 0-1.

Proposed fix
-    data[2] = (length >> 8) & 0xFF;
-    data[3] = length & 0xFF;
+    data[0] = (length >> 8) & 0xFF;
+    data[1] = length & 0xFF;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sd/sd_message.cpp` around lines 320 - 321, The multicast option length is
being written to data[2]/data[3], overwriting the option type/reserved bytes;
change the writes so the length is stored in header bytes data[0] and data[1]
instead (keep using the existing length variable and data buffer), preserving
data[2]/data[3] for option type/reserved—update the code around where data and
length are set in sd_message.cpp to use data[0] = (length >> 8) & 0xFF and
data[1] = length & 0xFF instead of data[2]/data[3].
src/platform/freertos/memory.cpp (1)

70-71: ⚠️ Potential issue | 🔴 Critical

Placement-new target must be non-const storage.

Line 70 uses void const* for placement-new storage, but Line 71 constructs into that buffer. Placement new requires writable void* storage.

Proposed fix
-            void const* block = pool_buffer + i * sizeof(Message);
+            void* block = static_cast<void*>(pool_buffer + i * sizeof(Message));
             auto* msg = new (block) Message();
#!/bin/bash
set -euo pipefail

# Verify placement-new call site and storage pointer type
rg -n -C2 'void\s+const\*\s+block|new\s*\(\s*block\s*\)\s*Message\s*\(' src/platform/freertos/memory.cpp

# Verify there is no custom placement-new overload accepting const void*
rg -n -C2 'operator\s+new\s*\([^)]*const\s+void\s*\*' include src
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/freertos/memory.cpp` around lines 70 - 71, The placement-new is
constructing a Message into storage pointed to by a const pointer; change the
storage pointer to writable storage so placement-new receives a void* (not void
const*). Locate the allocation logic using pool_buffer and the local variable
named block in the function that constructs Message with new (block) Message(),
remove the const qualifier (or otherwise ensure pool_buffer is non-const) so
block is void* before calling placement-new, and verify there are no custom
placement-new overloads accepting const void* that would mask the error.
src/tp/tp_reassembler.cpp (1)

56-57: ⚠️ Potential issue | 🟠 Major

Guard narrowing before assigning TP offset to uint16_t.

Line 57 can truncate when offset_units * 16 exceeds 0xFFFF, causing incorrect reassembly offsets.

Proposed fix
     uint32_t const offset_units = tp_header >> 4;
-    offset = offset_units * 16;  // Convert back to bytes
+    uint32_t const offset_bytes = offset_units * 16U;  // Convert back to bytes
+    if (offset_bytes > 0xFFFFU) {
+        return false;
+    }
+    offset = static_cast<uint16_t>(offset_bytes);
#!/bin/bash
set -euo pipefail

# Verify current conversion path and absence of bounds check
rg -n -C3 'offset_units|offset\s*=' src/tp/tp_reassembler.cpp
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tp/tp_reassembler.cpp` around lines 56 - 57, The assignment computing
offset from tp_header currently does "offset = offset_units * 16" which can
overflow/narrow when offset (uint16_t) cannot hold offset_units*16; in the
function handling TP reassembly (look for tp_header, offset_units, and offset in
tp_reassembler.cpp) compute the product in a wider integer (e.g., uint32_t),
check against UINT16_MAX (or the expected max reassembly offset) and handle
out-of-range values explicitly (return an error, drop the packet, or clamp as
your policy) before casting to uint16_t so no silent truncation occurs.
src/transport/tcp_transport.cpp (1)

242-243: 🧹 Nitpick | 🔵 Trivial

Remove redundant memset after brace initialization.

addr is already zero-initialized via sockaddr_in addr = {};, so the extra memset is unnecessary in both sites.

♻️ Proposed cleanup
-    sockaddr_in addr = {};
-    memset(&addr, 0, sizeof(addr));
+    sockaddr_in addr{};
@@
-    sockaddr_in addr = {};
-    memset(&addr, 0, sizeof(addr));
+    sockaddr_in addr{};

Also applies to: 296-297

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/transport/tcp_transport.cpp` around lines 242 - 243, The sockaddr_in
variable is already zero-initialized with "sockaddr_in addr = {}" so remove the
redundant "memset(&addr, 0, sizeof(addr))" calls; update the code sites where
"sockaddr_in addr = {}" is immediately followed by "memset(&addr, ...)" (e.g.,
the two occurrences around the TCP transport initialization) by deleting the
memset lines and keeping the brace initialization only.
src/e2e/e2e_profiles/standard_profile.cpp (1)

211-215: ⚠️ Potential issue | 🔴 Critical

Reject duplicate counters during validation.

Line 211 now accepts header.counter == last_counter, which allows duplicate/replayed frames to pass the counter check.

🛡️ Proposed fix
-            } else if (header.counter >= last_counter) {
-                // Equal: same message re-validated (e.g. in tests).
-                // Greater: new message with higher counter.
-                // Both cases are valid.
-                counter_valid = true;
+            } else if (header.counter > last_counter) {
+                // Monotonic increase.
+                counter_valid = true;
+            } else if (header.counter == last_counter) {
+                // Duplicate frame/counter.
+                return Result::INVALID_ARGUMENT;
             } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/e2e/e2e_profiles/standard_profile.cpp` around lines 211 - 215, The
current validation accepts header.counter == last_counter which allows
duplicate/replayed frames; change the check that sets counter_valid so it only
becomes true when header.counter > last_counter (strictly greater) and treat
equal as invalid (set counter_valid = false and handle/log as a
duplicate/replay). Update the surrounding comment to reflect that equal counters
are now rejected; the change should be made where header.counter, last_counter,
and counter_valid are used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@include/serialization/serializer.h`:
- Around line 91-92: The default DeserializationResult currently sets error_ to
Result::SUCCESS while value_ is empty, causing is_success() to report success
but get_value()/move_value() to throw; fix this by updating the success logic so
that is_success() returns true only when error_ == Result::SUCCESS AND
value_.has_value() (or equivalently check value_ non-empty) or, alternatively,
initialize error_ to a non-success sentinel (e.g., Result::NO_VALUE) instead of
Result::SUCCESS; update references to value_, error_, is_success(), get_value(),
and move_value() accordingly so default-constructed objects never report success
without a contained value.

In `@src/platform/freertos/memory.cpp`:
- Around line 40-42: The read of pool_initialized is unsynchronized and can race
with the write done under the pool init mutex; fix by making the flag access
consistent: either turn pool_initialized into a std::atomic<bool> and use an
atomic load/store (or proper memory_order) or perform the read under the same
mutex used when writing it (i.e., implement double-checked locking: atomically
read, if false then lock the pool init mutex, re-check pool_initialized, perform
init, and set the flag while holding the mutex). Ensure all reads/writes of
pool_initialized use the chosen synchronized approach so no unsynchronized
access remains.

In `@src/platform/threadx/memory.cpp`:
- Around line 40-42: The fast-path check reads pool_initialized without
synchronization which races with the write guarded by the mutex later (write at
the init path around the code that sets pool_initialized). Fix by making
pool_initialized atomic<bool> (or by loading it with acquire semantics) and use
a second checked lock pattern: load with acquire, if false take the existing
mutex and re-check pool_initialized before initializing and storing true with
release; alternatively always read/set under the same mutex. Update uses of
pool_initialized to the chosen synchronized access so the unsynchronized read is
removed.
- Around line 69-70: The pointer `block` is declared as `void const*` but
`tx_block_allocate(&message_pool, &block, TX_NO_WAIT)` and the subsequent
placement-new need a mutable pointer; change the declaration of `block` to a
non-const pointer (e.g., `void* block` or `VOID* block`) so the call to
`tx_block_allocate` receives a `VOID**`-compatible address and the placement-new
at line 75 can write into the allocated memory.

In `@src/sd/sd_client.cpp`:
- Around line 377-381: The code currently invokes user callbacks (e.g.,
service_subscriptions_.second.available_callback and unavailable_callback, and
pending-find callbacks) while holding internal mutexes (subscriptions_mutex_,
pending_finds_mutex_), which can deadlock; fix by copying the callback (and any
small data needed, e.g., instance) out while under the lock, then release the
ScopedLock before calling the copied callback so the call happens without
holding subscriptions_mutex_ or pending_finds_mutex_; apply the same pattern to
all sites that call available_callback, unavailable_callback, and pending-find
callbacks so only a minimal check/assignment occurs under lock and the actual
external invocation happens after unlocking, taking care to keep a local copy to
ensure callback lifetime and to re-check existence under lock if necessary.

---

Outside diff comments:
In `@src/rpc/rpc_client.cpp`:
- Around line 80-86: Callbacks are being invoked while holding
pending_calls_mutex_ (see the loop that constructs RpcResponse and calls
pair.second.callback), which can deadlock; change these sites to capture the
callback (and any response object like RpcResponse constructed from
pair.second.service_id/method_id/client_id_/session_id/RpcResult::INTERNAL_ERROR)
and erase/update the pending_calls_ entry as needed while holding the lock, then
release the lock and invoke the captured callback outside the critical section.
Apply the same unlock-before-callback pattern to cancel_call() and
on_message_received() where pending_calls_ entries are completed or cancelled so
that mutex is not held during user callback execution.

---

Duplicate comments:
In `@src/e2e/e2e_crc.cpp`:
- Around line 120-121: The cast of offset and length into std::ptrdiff_t when
building the iterator slice (variables first and slice) can overflow on
platforms where ptrdiff_t is narrower; before casting, validate that offset and
length are <= static_cast<size_t>(std::numeric_limits<std::ptrdiff_t>::max())
and that offset + length does not exceed data.size() (and check for offset +
length overflow using size_t arithmetic), then perform the safe cast; if the
checks fail, return/throw an appropriate error instead of performing the cast in
the code that constructs first and slice.

In `@src/e2e/e2e_profiles/standard_profile.cpp`:
- Around line 211-215: The current validation accepts header.counter ==
last_counter which allows duplicate/replayed frames; change the check that sets
counter_valid so it only becomes true when header.counter > last_counter
(strictly greater) and treat equal as invalid (set counter_valid = false and
handle/log as a duplicate/replay). Update the surrounding comment to reflect
that equal counters are now rejected; the change should be made where
header.counter, last_counter, and counter_valid are used.

In `@src/e2e/e2e_protection.cpp`:
- Around line 79-82: The code contains a stale comment and a redundant const
reference alias that suggests a mutable copy is needed but actually creates a
const alias; remove the comment and the alias and call
profile->validate(message, config) directly. Locate the block using Message
const& msg_copy = message and the surrounding comment, delete both, and replace
the redundant alias usage so the call uses the original message and config with
profile->validate(message, config). Ensure no other code depends on msg_copy
before removing it.

In `@src/platform/freertos/memory.cpp`:
- Around line 70-71: The placement-new is constructing a Message into storage
pointed to by a const pointer; change the storage pointer to writable storage so
placement-new receives a void* (not void const*). Locate the allocation logic
using pool_buffer and the local variable named block in the function that
constructs Message with new (block) Message(), remove the const qualifier (or
otherwise ensure pool_buffer is non-const) so block is void* before calling
placement-new, and verify there are no custom placement-new overloads accepting
const void* that would mask the error.

In `@src/sd/sd_message.cpp`:
- Around line 320-321: The multicast option length is being written to
data[2]/data[3], overwriting the option type/reserved bytes; change the writes
so the length is stored in header bytes data[0] and data[1] instead (keep using
the existing length variable and data buffer), preserving data[2]/data[3] for
option type/reserved—update the code around where data and length are set in
sd_message.cpp to use data[0] = (length >> 8) & 0xFF and data[1] = length & 0xFF
instead of data[2]/data[3].

In `@src/tp/tp_manager.cpp`:
- Around line 188-203: Held transfers_mutex_ while invoking completion_callback_
creates re-entrancy/deadlock risk: modify the loop in tp_manager.cpp so you
update transfer.state (TpTransfer), increment statistics_.timeouts and record
the transfer.transfer_id and TpResult::TIMEOUT into a local list (vector) while
holding transfers_mutex_, then release transfers_mutex_ before actually calling
completion_callback_ for each recorded transfer_id; ensure you still
remove/leave active_transfers_ as intended inside the locked section but perform
completion_callback_ invocations only after the lock is released.

In `@src/tp/tp_reassembler.cpp`:
- Around line 56-57: The assignment computing offset from tp_header currently
does "offset = offset_units * 16" which can overflow/narrow when offset
(uint16_t) cannot hold offset_units*16; in the function handling TP reassembly
(look for tp_header, offset_units, and offset in tp_reassembler.cpp) compute the
product in a wider integer (e.g., uint32_t), check against UINT16_MAX (or the
expected max reassembly offset) and handle out-of-range values explicitly
(return an error, drop the packet, or clamp as your policy) before casting to
uint16_t so no silent truncation occurs.

In `@src/transport/tcp_transport.cpp`:
- Around line 242-243: The sockaddr_in variable is already zero-initialized with
"sockaddr_in addr = {}" so remove the redundant "memset(&addr, 0, sizeof(addr))"
calls; update the code sites where "sockaddr_in addr = {}" is immediately
followed by "memset(&addr, ...)" (e.g., the two occurrences around the TCP
transport initialization) by deleting the memset lines and keeping the brace
initialization only.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2dca58ea-16bc-4f20-bdd3-dbc378aa9dc8

📥 Commits

Reviewing files that changed from the base of the PR and between 0792a67 and c4b2e0e.

📒 Files selected for processing (54)
  • .clang-tidy
  • include/e2e/e2e_config.h
  • include/e2e/e2e_crc.h
  • include/e2e/e2e_header.h
  • include/e2e/e2e_profile.h
  • include/e2e/e2e_profile_registry.h
  • include/e2e/e2e_protection.h
  • include/events/event_publisher.h
  • include/events/event_subscriber.h
  • include/events/event_types.h
  • include/platform/host/host_condition_variable.h
  • include/platform/posix/memory_impl.h
  • include/platform/posix/net_impl.h
  • include/platform/posix/thread_impl.h
  • include/platform/thread.h
  • include/rpc/rpc_client.h
  • include/rpc/rpc_server.h
  • include/rpc/rpc_types.h
  • include/sd/sd_client.h
  • include/sd/sd_message.h
  • include/sd/sd_server.h
  • include/sd/sd_types.h
  • include/serialization/serializer.h
  • include/tp/tp_manager.h
  • include/tp/tp_reassembler.h
  • include/tp/tp_segmenter.h
  • include/tp/tp_types.h
  • include/transport/endpoint.h
  • include/transport/tcp_transport.h
  • include/transport/transport.h
  • include/transport/udp_transport.h
  • src/core/session_manager.cpp
  • src/e2e/e2e_crc.cpp
  • src/e2e/e2e_header.cpp
  • src/e2e/e2e_profile_registry.cpp
  • src/e2e/e2e_profiles/standard_profile.cpp
  • src/e2e/e2e_protection.cpp
  • src/events/event_publisher.cpp
  • src/events/event_subscriber.cpp
  • src/platform/freertos/memory.cpp
  • src/platform/threadx/memory.cpp
  • src/rpc/rpc_client.cpp
  • src/rpc/rpc_server.cpp
  • src/sd/sd_client.cpp
  • src/sd/sd_message.cpp
  • src/sd/sd_server.cpp
  • src/serialization/serializer.cpp
  • src/someip/message.cpp
  • src/tp/tp_manager.cpp
  • src/tp/tp_reassembler.cpp
  • src/tp/tp_segmenter.cpp
  • src/transport/endpoint.cpp
  • src/transport/tcp_transport.cpp
  • src/transport/udp_transport.cpp

Comment thread include/serialization/serializer.h Outdated
Comment thread src/platform/freertos/memory.cpp Outdated
Comment thread src/platform/threadx/memory.cpp Outdated
Comment thread src/platform/threadx/memory.cpp Outdated
Comment thread src/sd/sd_client.cpp Outdated
First batch of clang-tidy remediation per issue #222 plan.

Config fix:
- Fix .clang-tidy: snake_case -> lower_case (invalid option value,
  silently broke readability-identifier-naming checks)

P0 — Safety (bugprone-*, init-variables, member-init):
- bugprone-empty-catch: justify NOLINT for intentional thread creation
  catch in thread_impl.h
- bugprone-exception-escape: wrap RpcClientImpl destructor in noexcept
  try-catch
- bugprone-narrowing-conversions: cast size_t to ptrdiff_t for iterator
  arithmetic across 6 files
- bugprone-branch-clone: merge identical if/else branches in
  standard_profile.cpp counter validation
- cppcoreguidelines-init-variables: zero-initialize 19 uninitialized
  locals (memcpy targets, loop accumulators, output params)
- cppcoreguidelines-pro-type-member-init: zero-init 18 struct records
  (sockaddr_in, ip_mreq, timeval) and add default member initializers

P2 — Maintainability (mechanical, auto-fixed):
- misc-const-correctness: add const to ~144 non-mutated locals
- modernize-concat-nested-namespaces: convert all nested namespace
  declarations to C++17 syntax across 50+ files
- readability-braces-around-statements: add braces to bare if/else
- modernize-use-override, readability-static-accessed-through-instance,
  performance-avoid-endl, cppcoreguidelines-prefer-member-initializer,
  and other mechanical fixes

Quality gate: PASSED (1505 violations, threshold 1783).

Made-with: Cursor
Addresses pre-existing code quality issues identified during clang-tidy
review (CodeRabbit findings on PR #225):

Critical data corruption / logic bugs:
- sd_message: fix IPv4MulticastOption length written to wrong indices
  (data[2]/data[3] → data[0]/data[1])
- standard_profile: reject duplicate E2E counters to prevent replay
  attacks (header.counter == last_counter now returns INVALID_ARGUMENT)

Concurrency / data race fixes:
- freertos/memory: make pool_initialized std::atomic<bool> with proper
  memory ordering to fix broken double-checked locking pattern
- threadx/memory: same fix for pool_initialized and guard_created
- tp_manager: collect timed-out transfers under lock, invoke
  completion_callback_ after releasing transfers_mutex_
- sd_client: copy callbacks from subscriptions/pending_finds maps under
  their respective locks, invoke them after lock release

Type safety / narrowing fixes:
- tp_reassembler: widen TP offset from uint16_t to uint32_t (28-bit
  field × 16 can exceed 64 KB)
- serializer: default DeserializationResult error_ to NOT_INITIALIZED
  instead of SUCCESS to prevent get_value() on empty optional
- e2e_crc: add PTRDIFF_MAX overflow guard before size_t → ptrdiff_t
  cast in iterator arithmetic

Closes #226

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/rpc/rpc_client.cpp (1)

80-89: ⚠️ Potential issue | 🔴 Critical

Extract callbacks outside pending_calls_mutex_ scope before invoking them to prevent deadlock on re-entrance.

All three locations (lines 80-89, 182-193, 225-242) invoke callbacks while holding pending_calls_mutex_. Since RpcCallback = std::function<void(const RpcResponse&)> is user-provided and the mutex is non-recursive (std::mutex on POSIX/WIN32, binary semaphore on FreeRTOS, k_mutex on Zephyr, TX_INHERIT on ThreadX), a callback re-entering cancel_call(), shutdown(), or call_method_*() will deadlock.

Suggested fix pattern
 bool cancel_call(RpcCallHandle handle) {
-    platform::ScopedLock const lock(pending_calls_mutex_);
-    auto it = pending_calls_.find(handle);
-    if (it == pending_calls_.end()) {
-        return false;
-    }
-
-    // Call callback with cancellation result
-    if (it->second.callback) {
-        RpcResponse response(it->second.service_id, it->second.method_id,
-                           client_id_, it->second.session_id, RpcResult::INTERNAL_ERROR);
-        it->second.callback(response);
-    }
-
-    pending_calls_.erase(it);
-    return true;
+    RpcCallback cb;
+    RpcResponse response(0, 0, client_id_, 0, RpcResult::INTERNAL_ERROR);
+    {
+        platform::ScopedLock const lock(pending_calls_mutex_);
+        auto it = pending_calls_.find(handle);
+        if (it == pending_calls_.end()) {
+            return false;
+        }
+        response = RpcResponse(it->second.service_id, it->second.method_id,
+                               client_id_, it->second.session_id, RpcResult::INTERNAL_ERROR);
+        cb = std::move(it->second.callback);
+        pending_calls_.erase(it);
+    }
+    if (cb) {
+        cb(response);
+    }
+    return true;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rpc/rpc_client.cpp` around lines 80 - 89, The code currently invokes
user-provided RpcCallback while holding pending_calls_mutex_ (see occurrences
around the loop that constructs RpcResponse using pending_calls_ and invoking
pair.second.callback), which can deadlock if callbacks re-enter cancel_call(),
shutdown(), or any call_method_*; fix by collecting the callbacks and their
RpcResponse (or minimal call identifiers) into a local vector while holding
pending_calls_mutex_, then release the mutex and iterate that vector to invoke
each callback; update the three sites (the block using pending_calls_mutex_ at
lines ~80-89, and the blocks at ~182-193 and ~225-242) to use this pattern and
ensure pending_calls_.clear() (or removal) happens while mutex is held before
invoking callbacks.
src/tp/tp_reassembler.cpp (1)

45-57: ⚠️ Potential issue | 🔴 Critical

Cast each payload byte to uint32_t before the left shifts.

Lines 52-53 shift promoted uint8_t values as int. When the high byte is >= 0x80, the result exceeds INT_MAX (e.g., 255 << 24 = 4278190080 > 2147483647), triggering undefined behavior per the C++ standard [expr.shift]/2. Widen each byte to uint32_t first, then compose tp_header.

Suggested fix
-    uint32_t tp_header = (payload[16] << 24) | (payload[17] << 16) |
-                        (payload[18] << 8) | payload[19];
+    uint32_t const tp_header =
+        (static_cast<uint32_t>(payload[16]) << 24U) |
+        (static_cast<uint32_t>(payload[17]) << 16U) |
+        (static_cast<uint32_t>(payload[18]) << 8U) |
+        static_cast<uint32_t>(payload[19]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tp/tp_reassembler.cpp` around lines 45 - 57, In
TpReassembler::parse_tp_header the expression building tp_header shifts uint8_t
values which are promoted to signed int and can trigger undefined behavior for
bytes >= 0x80; fix by casting each payload element to uint32_t before shifting
(e.g., static_cast<uint32_t>(payload[16]) << 24, etc.) when composing tp_header
and ensure any subsequent uses of tp_header (offset_units calculation and offset
assignment) operate on the corrected unsigned value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/e2e/e2e_crc.cpp`:
- Around line 117-119: The calculate_crc function currently returns 0 on invalid
input which is ambiguous with a valid CRC; change its return type to
std::optional<uint32_t> (or a Result-like type) and have calculate_crc return
std::nullopt on error instead of 0 so callers (e.g.,
standard_profile.cpp::validate()) can distinguish an error from a legitimate CRC
value; update all call sites to check the optional before comparing expected vs
received CRC and propagate or log errors appropriately.

In `@src/events/event_subscriber.cpp`:
- Line 261: In on_message_received, the code currently takes
subscriptions_mutex_ then later takes field_requests_mutex_, which is safe now
but risky if future code acquires them in the opposite order; update the
implementation or comments: either (A) change on_message_received (or the helper
that needs field_requests_) to release subscriptions_mutex_ before acquiring
field_requests_mutex_, or (B) add a clear comment near the mutex declarations
and at the top of on_message_received documenting the required lock ordering
("always take subscriptions_mutex_ before field_requests_mutex_") so future
authors preserve the order; reference subscriptions_mutex_,
field_requests_mutex_, and on_message_received when making the change.

In `@src/platform/freertos/memory.cpp`:
- Around line 40-56: The lazy creation of pool_mutex in ensure_pool_init() is
racy because the check if (!pool_mutex) is unsynchronized; replace that ad-hoc
creation with a true one-time init using std::once_flag and std::call_once
(declare a static std::once_flag e.g. pool_mutex_once) and perform
xSemaphoreCreateMutex() inside the call_once lambda so only one task creates
pool_mutex; keep the rest of ensure_pool_init() logic (taking the mutex,
initializing block_used, and setting pool_initialized) intact and ensure
pool_mutex is the mutex created by the call_once, and update any references from
allocate_message() to rely on ensure_pool_init() for safe initialization.

In `@src/platform/threadx/memory.cpp`:
- Around line 45-50: The one-time init using the atomic guard (guard_created) is
racy and ignores ThreadX return values; replace the double-check with
std::call_once (use a static std::once_flag) to ensure tx_mutex_create is
executed exactly once, and validate the return codes from tx_mutex_create,
tx_mutex_get and tx_block_pool_create (and any other ThreadX calls) before
setting the published initialization flag (pool_initialized); only mark the pool
as initialized after all API calls succeed and handle/log failures accordingly
while referencing pool_guard, guard_created (remove), tx_mutex_create,
tx_mutex_get, tx_block_pool_create, and pool_initialized so reviewers can find
the changes.

In `@src/serialization/serializer.cpp`:
- Around line 149-152: Guard against alignment == 0 in Serializer::align_to (and
any other places in this file that do `... % alignment`) to avoid undefined
behavior: add an early check (e.g., if (alignment == 0) return; or assert/throw)
before performing `(current_size % alignment)` so the modulus is never computed
with zero; apply the same guard to the other modulus operation(s) in this file
that use `alignment`.

In `@src/tp/tp_manager.cpp`:
- Around line 189-219: The code reads completion_callback_ without
synchronization in TpManager::process_timeouts(), causing a data race with
set_completion_callback(); fix by taking transfers_mutex_ lock to snapshot
completion_callback_ into a local variable while still inside the locked scope
(before releasing the lock and invoking callbacks), and modify
set_completion_callback() to acquire the same transfers_mutex_ before assigning
to completion_callback_; use the local copy for invoking completion callbacks
outside the lock to preserve the unlock-before-callback behavior.

---

Outside diff comments:
In `@src/rpc/rpc_client.cpp`:
- Around line 80-89: The code currently invokes user-provided RpcCallback while
holding pending_calls_mutex_ (see occurrences around the loop that constructs
RpcResponse using pending_calls_ and invoking pair.second.callback), which can
deadlock if callbacks re-enter cancel_call(), shutdown(), or any call_method_*;
fix by collecting the callbacks and their RpcResponse (or minimal call
identifiers) into a local vector while holding pending_calls_mutex_, then
release the mutex and iterate that vector to invoke each callback; update the
three sites (the block using pending_calls_mutex_ at lines ~80-89, and the
blocks at ~182-193 and ~225-242) to use this pattern and ensure
pending_calls_.clear() (or removal) happens while mutex is held before invoking
callbacks.

In `@src/tp/tp_reassembler.cpp`:
- Around line 45-57: In TpReassembler::parse_tp_header the expression building
tp_header shifts uint8_t values which are promoted to signed int and can trigger
undefined behavior for bytes >= 0x80; fix by casting each payload element to
uint32_t before shifting (e.g., static_cast<uint32_t>(payload[16]) << 24, etc.)
when composing tp_header and ensure any subsequent uses of tp_header
(offset_units calculation and offset assignment) operate on the corrected
unsigned value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 346f2f42-0390-4cf6-a513-768ebd801919

📥 Commits

Reviewing files that changed from the base of the PR and between c4b2e0e and c9c6c45.

📒 Files selected for processing (56)
  • .clang-tidy
  • include/e2e/e2e_config.h
  • include/e2e/e2e_crc.h
  • include/e2e/e2e_header.h
  • include/e2e/e2e_profile.h
  • include/e2e/e2e_profile_registry.h
  • include/e2e/e2e_protection.h
  • include/events/event_publisher.h
  • include/events/event_subscriber.h
  • include/events/event_types.h
  • include/platform/host/host_condition_variable.h
  • include/platform/posix/memory_impl.h
  • include/platform/posix/net_impl.h
  • include/platform/posix/thread_impl.h
  • include/platform/thread.h
  • include/platform/threadx/memory_internal.h
  • include/rpc/rpc_client.h
  • include/rpc/rpc_server.h
  • include/rpc/rpc_types.h
  • include/sd/sd_client.h
  • include/sd/sd_message.h
  • include/sd/sd_server.h
  • include/sd/sd_types.h
  • include/serialization/serializer.h
  • include/tp/tp_manager.h
  • include/tp/tp_reassembler.h
  • include/tp/tp_segmenter.h
  • include/tp/tp_types.h
  • include/transport/endpoint.h
  • include/transport/tcp_transport.h
  • include/transport/transport.h
  • include/transport/udp_transport.h
  • src/core/session_manager.cpp
  • src/e2e/e2e_crc.cpp
  • src/e2e/e2e_header.cpp
  • src/e2e/e2e_profile_registry.cpp
  • src/e2e/e2e_profiles/standard_profile.cpp
  • src/e2e/e2e_protection.cpp
  • src/events/event_publisher.cpp
  • src/events/event_subscriber.cpp
  • src/platform/freertos/memory.cpp
  • src/platform/threadx/memory.cpp
  • src/rpc/rpc_client.cpp
  • src/rpc/rpc_server.cpp
  • src/sd/sd_client.cpp
  • src/sd/sd_message.cpp
  • src/sd/sd_server.cpp
  • src/serialization/serializer.cpp
  • src/someip/message.cpp
  • src/tp/tp_manager.cpp
  • src/tp/tp_reassembler.cpp
  • src/tp/tp_segmenter.cpp
  • src/transport/endpoint.cpp
  • src/transport/tcp_transport.cpp
  • src/transport/udp_transport.cpp
  • tests/threadx/test_threadx_core.cpp

Comment thread src/e2e/e2e_crc.cpp Outdated
Comment thread src/events/event_subscriber.cpp
Comment thread src/platform/freertos/memory.cpp
Comment thread src/platform/threadx/memory.cpp Outdated
Comment thread src/serialization/serializer.cpp
Comment thread src/tp/tp_manager.cpp
Fixes test failures and resolves all remaining open review findings:

Test fix:
- Revert E2E counter equality rejection — protect() and validate()
  share the same counters_ map, so header.counter == last_counter is
  expected behavior, not a replay

Concurrency fixes:
- rpc_client: extract callbacks from pending_calls_mutex_ before
  invoking in shutdown(), cancel_call(), and on_message_received()
- tp_manager: synchronize set_completion/progress/message_callback()
  under transfers_mutex_; snapshot callback under lock in
  process_timeouts() before invoking outside
- freertos/memory: replace racy double-checked locking with
  std::atomic_flag spinlock for one-time pool initialization
- threadx/memory: same spinlock pattern; remove guard_created flag

Safety / correctness fixes:
- serializer: guard Serializer::align_to and Deserializer::align_to
  against zero alignment (UB from % 0)
- e2e_crc: change calculate_crc() return type to std::optional<uint32_t>
  so error cases are distinguishable from valid CRC value 0; update
  callers in standard_profile.cpp and all tests

Closes #226

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
include/e2e/e2e_crc.h (1)

65-73: 🧹 Nitpick | 🔵 Trivial

Documentation should reflect the new std::optional return type.

The return type change to std::optional<uint32_t> is the correct fix for the ambiguity where 0 could be both an error and a valid CRC. However, the @return documentation at line 71 still says "CRC value (size depends on crc_type)" without mentioning that std::nullopt is returned on invalid inputs.

📝 Suggested documentation update
 /**
  * `@brief` Calculate CRC for a specific range of data
  * `@param` data Data vector
  * `@param` offset Start offset
  * `@param` length Length of data to process
  * `@param` crc_type 0 = SAE-J1850 (8-bit), 1 = ITU-T X.25 (16-bit), 2 = CRC32
- * `@return` CRC value (size depends on crc_type)
+ * `@return` CRC value (size depends on crc_type), or std::nullopt on invalid inputs
+ *         (out-of-bounds offset/length, unknown crc_type, or values exceeding PTRDIFF_MAX)
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/e2e/e2e_crc.h` around lines 65 - 73, Update the calculate_crc
documentation to state that it now returns std::optional<uint32_t> and that
std::nullopt is returned for invalid inputs (e.g., out-of-range offset/length or
unsupported crc_type); clarify that when set, the uint32_t contains the CRC
value whose size depends on crc_type (8/16/32 bits), and mention which crc_type
values are supported (0,1,2) and that other values yield std::nullopt.
src/e2e/e2e_crc.cpp (1)

78-101: ⚠️ Potential issue | 🟡 Minor

CRC32 table initialization is not thread-safe.

init_crc32_table() uses a plain bool crc32_table_initialized flag without synchronization. If multiple threads call calculate_crc32() concurrently for the first time, they may race on writing to crc32_table[], potentially causing corrupted table entries.

This is a pre-existing issue, but given the PR's focus on fixing concurrency bugs in other files (e.g., platform allocators), it may be worth addressing here as well.

🔒 Suggested fix using std::call_once
+#include <mutex>
+
 // Precomputed CRC32 lookup table
 static uint32_t crc32_table[256];
-
-// Initialize CRC32 lookup table (called once)
-static bool crc32_table_initialized = false;
+static std::once_flag crc32_table_once;

 static void init_crc32_table() {
-    if (crc32_table_initialized) {
-        return;
-    }
-
-    for (uint32_t i = 0; i < 256; ++i) {
-        uint32_t crc = i << 24;
-        for (int j = 0; j < 8; ++j) {
-            if (crc & 0x80000000) {
-                crc = (crc << 1) ^ CRC32_POLY;
-            } else {
-                crc <<= 1;
+    std::call_once(crc32_table_once, []() {
+        for (uint32_t i = 0; i < 256; ++i) {
+            uint32_t crc = i << 24;
+            for (int j = 0; j < 8; ++j) {
+                if (crc & 0x80000000) {
+                    crc = (crc << 1) ^ CRC32_POLY;
+                } else {
+                    crc <<= 1;
+                }
             }
+            crc32_table[i] = crc;
         }
-        crc32_table[i] = crc;
-    }
-
-    crc32_table_initialized = true;
+    });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/e2e/e2e_crc.cpp` around lines 78 - 101, The crc32 table initialization in
init_crc32_table() is not thread-safe; replace the manual bool
crc32_table_initialized check with a thread-safe one-time initialization (use
std::call_once and a static std::once_flag) so multiple threads cannot
concurrently populate crc32_table; keep the table population logic inside the
call_once-protected function and remove or repurpose crc32_table_initialized,
referencing init_crc32_table, crc32_table, and any callers like calculate_crc32
to ensure they trigger the call_once initialization.
tests/test_e2e.cpp (1)

286-299: ⚠️ Potential issue | 🔴 Critical

Type mismatch: test assigns std::optional<uint32_t> to uint32_t variables.

Lines 289-292 assign the result of calculate_crc() (now std::optional<uint32_t>) directly to uint32_t variables. This will fail to compile since std::optional does not implicitly convert to its contained type. Additionally, line 298 compares unk against 0u, but unk is now an std::optional and should be checked with !has_value().

🐛 Proposed fix
 TEST_F(E2ETest, CRC_AllTypeBranches) {
     std::vector<uint8_t> data = {0x01, 0x02, 0x03, 0x04};

-    uint32_t crc8  = E2ECRC::calculate_crc(data, 0, 4, 0);
-    uint32_t crc16 = E2ECRC::calculate_crc(data, 0, 4, 1);
-    uint32_t crc32 = E2ECRC::calculate_crc(data, 0, 4, 2);
-    uint32_t unk   = E2ECRC::calculate_crc(data, 0, 4, 255);
+    auto crc8  = E2ECRC::calculate_crc(data, 0, 4, 0);
+    auto crc16 = E2ECRC::calculate_crc(data, 0, 4, 1);
+    auto crc32 = E2ECRC::calculate_crc(data, 0, 4, 2);
+    auto unk   = E2ECRC::calculate_crc(data, 0, 4, 255);

     // Ranged CRC should match direct calls
-    EXPECT_EQ(crc8,  static_cast<uint32_t>(E2ECRC::calculate_crc8_sae_j1850(data)));
-    EXPECT_EQ(crc16, static_cast<uint32_t>(E2ECRC::calculate_crc16_itu_x25(data)));
-    EXPECT_EQ(crc32, E2ECRC::calculate_crc32(data));
-    EXPECT_EQ(unk, 0u);  // Unknown type returns 0
+    ASSERT_TRUE(crc8.has_value());
+    ASSERT_TRUE(crc16.has_value());
+    ASSERT_TRUE(crc32.has_value());
+    EXPECT_EQ(crc8.value(),  static_cast<uint32_t>(E2ECRC::calculate_crc8_sae_j1850(data)));
+    EXPECT_EQ(crc16.value(), static_cast<uint32_t>(E2ECRC::calculate_crc16_itu_x25(data)));
+    EXPECT_EQ(crc32.value(), E2ECRC::calculate_crc32(data));
+    EXPECT_FALSE(unk.has_value());  // Unknown type returns nullopt
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_e2e.cpp` around lines 286 - 299, The test assigns the return of
E2ECRC::calculate_crc (now returning std::optional<uint32_t>) to plain uint32_t
variables and compares an optional to 0; update the test to store results in
std::optional<uint32_t> (e.g., crc8, crc16, crc32, unk as
std::optional<uint32_t>), assert that the known CRC optionals have values (use
has_value()/operator bool) and compare their contained values via .value() to
the direct-call results, and assert that unk has no value (use !unk.has_value()
or equivalent) instead of comparing it to 0u; reference calculate_crc and the
direct comparators calculate_crc8_sae_j1850, calculate_crc16_itu_x25, and
calculate_crc32 to locate the checks.
♻️ Duplicate comments (2)
src/platform/threadx/memory.cpp (1)

48-56: ⚠️ Potential issue | 🟠 Major

ThreadX API return values are unchecked.

tx_mutex_create() and tx_block_pool_create() both return UINT status codes that should be validated. If either fails, pool_initialized is still set to true, which will cause subsequent tx_block_allocate() calls to operate on an uninitialized pool—leading to undefined behavior or crashes.

🛡️ Proposed fix to validate return codes
     if (!pool_initialized.load(std::memory_order_relaxed)) {
-        tx_mutex_create(&pool_guard, const_cast<CHAR*>("someip_pool_guard"),
-                        TX_NO_INHERIT);
-        tx_block_pool_create(&message_pool,
-                             const_cast<CHAR*>("someip_msg"),
-                             sizeof(someip::Message),
-                             pool_buffer,
-                             sizeof(pool_buffer));
-        pool_initialized.store(true, std::memory_order_release);
+        UINT status = tx_mutex_create(&pool_guard, const_cast<CHAR*>("someip_pool_guard"),
+                                      TX_NO_INHERIT);
+        if (status == TX_SUCCESS) {
+            status = tx_block_pool_create(&message_pool,
+                                          const_cast<CHAR*>("someip_msg"),
+                                          sizeof(someip::Message),
+                                          pool_buffer,
+                                          sizeof(pool_buffer));
+        }
+        if (status == TX_SUCCESS) {
+            pool_initialized.store(true, std::memory_order_release);
+        }
+        // If init failed, pool_initialized remains false; subsequent calls will retry.
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/threadx/memory.cpp` around lines 48 - 56, Check the UINT return
values from tx_mutex_create and tx_block_pool_create (compare against
TX_SUCCESS) before setting pool_initialized; if either call fails, log or handle
the error, undo any partially-created resource (e.g., delete the mutex if pool
creation fails), and do not set pool_initialized to true so that subsequent
tx_block_allocate calls won't use an uninitialized pool. Update the
initialization block around tx_mutex_create, tx_block_pool_create,
pool_initialized, and any cleanup paths to validate return codes and perform
proper teardown on failure.
src/serialization/serializer.cpp (1)

403-405: ⚠️ Potential issue | 🟠 Major

Add a PTRDIFF_MAX guard before narrowing casts in iterator arithmetic.

position_ (size_t) and length (uint32_t) are narrowed to std::ptrdiff_t without an explicit representability check. On systems where size_t exceeds ptrdiff_t width, values larger than PTRDIFF_MAX will cause undefined behavior in iterator arithmetic. The codebase already demonstrates this guard pattern in src/e2e/e2e_crc.cpp (lines 118–119).

Proposed fix
 `#include` "serialization/serializer.h"
 `#include` <cstring>
 `#include` <algorithm>
+#include <limits>
 `#include` "platform/byteorder.h"
@@
 DeserializationResult<std::string> Deserializer::deserialize_string() {
@@
-    if (position_ + length > buffer_.size()) {
+    if (length > buffer_.size() - position_) {
         return DeserializationResult<std::string>::error(Result::MALFORMED_MESSAGE);
     }
 
-    auto first = buffer_.begin() + static_cast<std::ptrdiff_t>(position_);
-    std::string result(first, first + static_cast<std::ptrdiff_t>(length));
+    constexpr auto kMaxDiff = static_cast<size_t>(std::numeric_limits<std::ptrdiff_t>::max());
+    const size_t length_sz = static_cast<size_t>(length);
+    if (position_ > kMaxDiff || length_sz > (kMaxDiff - position_)) {
+        return DeserializationResult<std::string>::error(Result::MALFORMED_MESSAGE);
+    }
+
+    auto first = buffer_.begin() + static_cast<std::ptrdiff_t>(position_);
+    std::string result(first, first + static_cast<std::ptrdiff_t>(length_sz));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/serialization/serializer.cpp` around lines 403 - 405, Before performing
iterator arithmetic that casts position_ and length to std::ptrdiff_t in the
serializer code (the block that computes "auto first = buffer_.begin() +
static_cast<std::ptrdiff_t>(position_);" and constructs result from that range),
add a guard that checks both position_ and length do not exceed PTRDIFF_MAX (and
that position_ + length <= PTRDIFF_MAX) before narrowing; if the check fails,
return or throw a clear error/exception (consistent with surrounding error
handling) instead of performing the cast. Ensure the check references the
existing members/variables position_, length and buffer_ and mirrors the
PTRDIFF_MAX pattern used in e2e_crc.cpp so iterator arithmetic uses only
representable ptrdiff_t values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/platform/freertos/memory.cpp`:
- Around line 48-55: The mutex allocation can fail and configASSERT is a no-op
in release builds, so after calling xSemaphoreCreateMutex() you must explicitly
check pool_mutex for nullptr and handle the failure instead of setting
pool_initialized to true; update the initialization block around pool_mutex,
pool_initialized.store(...) and init_lock.clear() so that if
xSemaphoreCreateMutex() returns nullptr you do not mark the pool as initialized
(and optionally report/log the error or return a failure), leaving
pool_initialized false to prevent allocate_message() from calling
xSemaphoreTake(nullptr,...); ensure the check references pool_mutex and
functions xSemaphoreCreateMutex, pool_initialized.store, init_lock.clear and
that allocate_message will only proceed when pool_initialized is true.

In `@src/rpc/rpc_client.cpp`:
- Around line 80-95: The callbacks in shutdown/erase paths (code around
pending_calls_ handling that builds shutdown_cbs and later invokes cb(resp)) can
outlive stack-local synchronization state used by call_method_sync() (e.g.,
sync_mtx, sync_resp, response_ready), causing use-after-free; change the sync
completion state to shared ownership (e.g., wrap the mutex, response storage and
readiness flag in a shared_ptr/struct) and store that shared state in the
RpcCallback capture so the callback writes into heap-owned state, or
alternatively ensure the pending entry is not erased (or block until cb
finishes) before call_method_sync() returns; update locations that build
RpcResponse and invoke RpcCallback (the shutdown_cbs creation and the similar
blocks at the other noted ranges) to capture or wait on the shared state so
cancel_call(handle) and call_method_sync() cannot return while a queued callback
may access stack locals.

In `@src/tp/tp_manager.cpp`:
- Around line 192-227: Hoist the invariant null-check on the captured completion
callback to avoid repeated evaluation: in TpManager::process_timeouts keep
capturing completion_callback_ into local cb and building the timed_out vector
as-is, but move the if (cb) check outside the for-loop that iterates over
timed_out so you only test once before invoking cb(id, result); this touches the
local variable cb, the timed_out collection and the invocation loop.

---

Outside diff comments:
In `@include/e2e/e2e_crc.h`:
- Around line 65-73: Update the calculate_crc documentation to state that it now
returns std::optional<uint32_t> and that std::nullopt is returned for invalid
inputs (e.g., out-of-range offset/length or unsupported crc_type); clarify that
when set, the uint32_t contains the CRC value whose size depends on crc_type
(8/16/32 bits), and mention which crc_type values are supported (0,1,2) and that
other values yield std::nullopt.

In `@src/e2e/e2e_crc.cpp`:
- Around line 78-101: The crc32 table initialization in init_crc32_table() is
not thread-safe; replace the manual bool crc32_table_initialized check with a
thread-safe one-time initialization (use std::call_once and a static
std::once_flag) so multiple threads cannot concurrently populate crc32_table;
keep the table population logic inside the call_once-protected function and
remove or repurpose crc32_table_initialized, referencing init_crc32_table,
crc32_table, and any callers like calculate_crc32 to ensure they trigger the
call_once initialization.

In `@tests/test_e2e.cpp`:
- Around line 286-299: The test assigns the return of E2ECRC::calculate_crc (now
returning std::optional<uint32_t>) to plain uint32_t variables and compares an
optional to 0; update the test to store results in std::optional<uint32_t>
(e.g., crc8, crc16, crc32, unk as std::optional<uint32_t>), assert that the
known CRC optionals have values (use has_value()/operator bool) and compare
their contained values via .value() to the direct-call results, and assert that
unk has no value (use !unk.has_value() or equivalent) instead of comparing it to
0u; reference calculate_crc and the direct comparators calculate_crc8_sae_j1850,
calculate_crc16_itu_x25, and calculate_crc32 to locate the checks.

---

Duplicate comments:
In `@src/platform/threadx/memory.cpp`:
- Around line 48-56: Check the UINT return values from tx_mutex_create and
tx_block_pool_create (compare against TX_SUCCESS) before setting
pool_initialized; if either call fails, log or handle the error, undo any
partially-created resource (e.g., delete the mutex if pool creation fails), and
do not set pool_initialized to true so that subsequent tx_block_allocate calls
won't use an uninitialized pool. Update the initialization block around
tx_mutex_create, tx_block_pool_create, pool_initialized, and any cleanup paths
to validate return codes and perform proper teardown on failure.

In `@src/serialization/serializer.cpp`:
- Around line 403-405: Before performing iterator arithmetic that casts
position_ and length to std::ptrdiff_t in the serializer code (the block that
computes "auto first = buffer_.begin() +
static_cast<std::ptrdiff_t>(position_);" and constructs result from that range),
add a guard that checks both position_ and length do not exceed PTRDIFF_MAX (and
that position_ + length <= PTRDIFF_MAX) before narrowing; if the check fails,
return or throw a clear error/exception (consistent with surrounding error
handling) instead of performing the cast. Ensure the check references the
existing members/variables position_, length and buffer_ and mirrors the
PTRDIFF_MAX pattern used in e2e_crc.cpp so iterator arithmetic uses only
representable ptrdiff_t values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: eed814be-e183-49d2-9ba6-895cde467afd

📥 Commits

Reviewing files that changed from the base of the PR and between c9c6c45 and d0fb137.

📒 Files selected for processing (10)
  • include/e2e/e2e_crc.h
  • src/e2e/e2e_crc.cpp
  • src/e2e/e2e_profiles/standard_profile.cpp
  • src/platform/freertos/memory.cpp
  • src/platform/threadx/memory.cpp
  • src/rpc/rpc_client.cpp
  • src/serialization/serializer.cpp
  • src/tp/tp_manager.cpp
  • tests/shared/test_e2e_common.inc
  • tests/test_e2e.cpp

Comment thread src/platform/freertos/memory.cpp Outdated
Comment thread src/rpc/rpc_client.cpp
Comment thread src/tp/tp_manager.cpp
CI fix:
- test_e2e: fix CRC_AllTypeBranches test that was still using uint32_t
  instead of auto for std::optional<uint32_t> return type

Review findings:
- freertos/memory: guard pool_initialized against mutex creation
  failure — configASSERT is a no-op in release builds, so also check
  pool_mutex != nullptr before marking initialized
- threadx/memory: check tx_mutex_create and tx_block_pool_create
  return status; bail out without marking initialized on failure
- rpc_client: fix use-after-free in call_method_sync — move sync
  state (mutex, response, ready flag) to shared_ptr so the callback
  closure outlives the stack frame if cancel_call races
- tp_manager: hoist invariant if(cb) check outside the loop
- event_subscriber: document lock ordering (subscriptions_mutex_
  before field_requests_mutex_) to prevent future deadlocks

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/events/event_subscriber.cpp (2)

261-279: ⚠️ Potential issue | 🔴 Critical

Move user callback invocation out of internal mutex critical sections.

Line 261/278 and Line 290/299 invoke user-provided callbacks while subscriptions_mutex_ / field_requests_mutex_ are held. This can deadlock on re-entrancy (e.g., callback calls unsubscribe_eventgroup() or request_field()) and stalls message handling.

🔧 Suggested fix (two-phase: capture under lock, invoke after unlock)
 void on_message_received(MessagePtr message, const transport::Endpoint& /*sender*/) override {
@@
-    platform::ScopedLock const subs_lock(subscriptions_mutex_);
+    EventNotificationCallback notification_cb{};
+    EventNotification notification(0, 0, 0);
+    bool has_notification = false;
+    {
+        platform::ScopedLock const subs_lock(subscriptions_mutex_);
@@
-                if (sub_info.notification_callback) {
-                    sub_info.notification_callback(notification);
-                }
+                notification_cb = sub_info.notification_callback;
+                has_notification = static_cast<bool>(notification_cb);
@@
-                break;
+                break;
             }
         }
+    }
+
+    if (has_notification) {
+        notification_cb(notification);
+    }
@@
-    platform::ScopedLock const field_lock(field_requests_mutex_);
-    std::string field_key = make_field_key(service_id, 0, event_id);  // Simplified
-
-    auto field_it = field_requests_.find(field_key);
-    if (field_it != field_requests_.end()) {
-        EventNotification notification(service_id, 0, event_id);
-        notification.event_data = message->get_payload();
-
-        if (field_it->second) {
-            field_it->second(notification);
-        }
-
-        field_requests_.erase(field_it);
-    }
+    EventNotificationCallback field_cb{};
+    EventNotification field_notification(service_id, 0, event_id);
+    bool has_field_cb = false;
+    {
+        platform::ScopedLock const field_lock(field_requests_mutex_);
+        std::string field_key = make_field_key(service_id, 0, event_id);  // Simplified
+        auto field_it = field_requests_.find(field_key);
+        if (field_it != field_requests_.end()) {
+            field_notification.event_data = message->get_payload();
+            field_cb = field_it->second;
+            has_field_cb = static_cast<bool>(field_cb);
+            field_requests_.erase(field_it);
+        }
+    }
+    if (has_field_cb) {
+        field_cb(field_notification);
+    }
 }

Also applies to: 290-300

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/events/event_subscriber.cpp` around lines 261 - 279, The code is calling
user callbacks while holding internal locks (subscriptions_mutex_ and
field_requests_mutex_), which can deadlock on re-entrancy; fix by doing a
two-phase approach: inside the critical section (in the loop over subscriptions_
and in the field_requests_ handling) capture all necessary data into local
variables/objects (e.g., construct EventNotification, copy
notification.client_id/session_id/event_data and store the std::function
callback references like sub_info.notification_callback or
field_request_callback) and then release the mutex before invoking the
callbacks; update the blocks that create EventNotification and call
sub_info.notification_callback as well as the field request path that calls
field_request_callback to follow this capture-then-unlock-then-invoke pattern.

71-75: ⚠️ Potential issue | 🟠 Major

Reduce lock scopes around transport calls to avoid blocking receive thread.

Transport calls (send_message(), stop()) are executed while holding subscriptions_mutex_ (lines 71, 98–123, 131–159) or field_requests_mutex_ (line 170–188). Since the transport's receive thread invokes listener callbacks (on_message_received(), on_connection_lost()) that contend for these same locks, holding them during network I/O creates unnecessary blocking and lock contention. Collect data under lock, then release before calling transport; reacquire only for post-send cleanup if needed.

Also applies to: 98–123, 131–159, 170–188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/events/event_subscriber.cpp` around lines 71 - 75, Current code holds
subscriptions_mutex_ or field_requests_mutex_ while calling transport methods
(e.g., transport_->stop(), send_message()), which blocks the transport receive
thread callbacks (on_message_received(), on_connection_lost()); change each such
site to: acquire the mutex, copy or move out only the minimal data needed for
the transport call (e.g., list of subscriptions or message payload), release the
mutex, perform transport_->stop() or send_message(), and then re-acquire the
mutex only if you must perform post-call cleanup or mutation of subscriptions_ /
field_requests_. Ensure this pattern is applied to the blocks touching
subscriptions_mutex_ (around subscriptions_.clear() and the send_message sites)
and field_requests_mutex_ (lines handling field requests) to avoid holding locks
during network I/O.
♻️ Duplicate comments (2)
src/platform/freertos/memory.cpp (1)

48-54: ⚠️ Potential issue | 🔴 Critical

Propagate pool-init failure before taking pool_mutex.

If xSemaphoreCreateMutex() returns nullptr, Lines 51-54 correctly avoid publishing pool_initialized, but allocate_message() still calls xSemaphoreTake(pool_mutex, ...) on Line 66. In release builds where configASSERT is compiled out, that turns an init failure into a null-handle dereference. Make ensure_pool_init() return success/failure and bail out here when initialization did not complete.

Proposed fix
-static void ensure_pool_init() {
+static bool ensure_pool_init() {
     if (pool_initialized.load(std::memory_order_acquire)) {
-        return;
+        return true;
     }

     while (init_lock.test_and_set(std::memory_order_acquire)) { }

     if (!pool_initialized.load(std::memory_order_relaxed)) {
         pool_mutex = xSemaphoreCreateMutex();
         configASSERT(pool_mutex != nullptr);
         if (pool_mutex != nullptr) {
             std::memset(block_used, 0, sizeof(block_used));
             pool_initialized.store(true, std::memory_order_release);
         }
     }

+    bool const ready = pool_initialized.load(std::memory_order_acquire);
     init_lock.clear(std::memory_order_release);
+    return ready;
 }

 /** `@implements` REQ_PLATFORM_FREERTOS_002 */
 MessagePtr allocate_message() {
-    ensure_pool_init();
+    if (!ensure_pool_init()) {
+        return nullptr;
+    }

     xSemaphoreTake(pool_mutex, portMAX_DELAY);

Also applies to: 63-66

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/freertos/memory.cpp` around lines 48 - 54, The pool init can
fail (xSemaphoreCreateMutex may return nullptr) but ensure_pool_init currently
doesn't signal failure, causing callers like allocate_message() to call
xSemaphoreTake(pool_mutex, ...) and potentially dereference nullptr; change
ensure_pool_init() to return a bool (or error code) indicating success/failure,
set pool_initialized only on success, and update all callers (notably
allocate_message() and any code paths around the second init block at the other
site) to check the return value and bail out when initialization failed instead
of taking/using pool_mutex; ensure any early returns maintain thread-safety and
do not rely on pool_mutex if it is null.
src/platform/threadx/memory.cpp (1)

48-65: ⚠️ Potential issue | 🔴 Critical

Partial init failures still leave allocate_message() on an invalid path.

ensure_pool_init() now checks tx_mutex_create() and tx_block_pool_create(), but it still returns void, so Line 78 runs even when message_pool was never created. There is a second bug here too: if tx_mutex_create() succeeds once and tx_block_pool_create() fails, the next retry will call tx_mutex_create() again on the already-created pool_guard. Please return success/failure from ensure_pool_init() and track guard creation separately from pool readiness.

Also applies to: 74-79

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/threadx/memory.cpp` around lines 48 - 65, ensure_pool_init()
currently returns void which allows allocate_message() to proceed even when
message_pool wasn't created; change ensure_pool_init() to return bool, introduce
a separate atomic<bool> guard_initialized (or similar) to track whether
pool_guard was successfully created, and only call tx_mutex_create() when
guard_initialized is false; on tx_block_pool_create() failure, if the mutex was
just created either destroy it or leave guard_initialized true but ensure
pool_initialized stays false and return false; callers like allocate_message()
must check the new boolean result and avoid using message_pool when
ensure_pool_init() returns false; update references to pool_initialized,
init_lock, pool_guard, message_pool, tx_mutex_create, and tx_block_pool_create
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitignore:
- Line 103: Move the gitignore entry "build_lib/" out of the file end and place
it into the existing "Build directories" section alongside the other build
patterns (e.g., "build/", "build_test/", "build-*/") so all build-related
entries are grouped; locate the "Build directories" block and add "build_lib/"
there, then remove the duplicate entry at the end of the file.

In `@src/platform/freertos/memory.cpp`:
- Around line 46-57: The current busy-spin using init_lock.test_and_set causes
starvation in FreeRTOS; replace this with a blocking one-time init using a
FreeRTOS synchronization primitive (e.g., a binary semaphore or mutex) so tasks
call xSemaphoreTake to wait instead of spinning, and perform initialization only
when pool_initialized is false; specifically modify the init sequence around
init_lock, pool_initialized and pool_mutex so callers of allocate_message()
block on the semaphore until initialization completes. Also handle
xSemaphoreCreateMutex() failure: if creating pool_mutex fails do not leave
pool_mutex nullptr—either retry creation, set an explicit initialization-failed
flag and return an error to callers of allocate_message(), or assert/propagate
the error so subsequent xSemaphoreTake(pool_mutex, ...) is never called on a
null pointer; ensure block_used is only memset after successful mutex creation
and pool_initialized.store(true) only after that.

In `@src/platform/threadx/memory.cpp`:
- Around line 46-68: Replace the busy-spin on init_lock with a blocking
synchronization primitive (e.g., create and wait on a ThreadX semaphore or use
tx_event_flags) so threads block instead of busy-waiting while initialization is
in progress; specifically change the loop that uses init_lock.test_and_set() to
acquire a tx_semaphore (or equivalent) and release it when initialization
completes. Move the pool setup (tx_mutex_create(&pool_guard,...),
tx_block_pool_create(&message_pool,...)) out of hot allocation paths if
possible, or ensure the init path is protected by the blocking primitive so only
one initializer runs. Add proper rollback on failure: if tx_block_pool_create
fails after tx_mutex_create succeeded, call tx_mutex_delete(&pool_guard) (or
handle TX_ALREADY_CREATED) and clear pool_initialized so subsequent attempts can
retry cleanly. Ensure pool_initialized (std::atomic) is set only after both
tx_mutex_create and tx_block_pool_create succeed and release the blocking
primitive on all code paths.

---

Outside diff comments:
In `@src/events/event_subscriber.cpp`:
- Around line 261-279: The code is calling user callbacks while holding internal
locks (subscriptions_mutex_ and field_requests_mutex_), which can deadlock on
re-entrancy; fix by doing a two-phase approach: inside the critical section (in
the loop over subscriptions_ and in the field_requests_ handling) capture all
necessary data into local variables/objects (e.g., construct EventNotification,
copy notification.client_id/session_id/event_data and store the std::function
callback references like sub_info.notification_callback or
field_request_callback) and then release the mutex before invoking the
callbacks; update the blocks that create EventNotification and call
sub_info.notification_callback as well as the field request path that calls
field_request_callback to follow this capture-then-unlock-then-invoke pattern.
- Around line 71-75: Current code holds subscriptions_mutex_ or
field_requests_mutex_ while calling transport methods (e.g., transport_->stop(),
send_message()), which blocks the transport receive thread callbacks
(on_message_received(), on_connection_lost()); change each such site to: acquire
the mutex, copy or move out only the minimal data needed for the transport call
(e.g., list of subscriptions or message payload), release the mutex, perform
transport_->stop() or send_message(), and then re-acquire the mutex only if you
must perform post-call cleanup or mutation of subscriptions_ / field_requests_.
Ensure this pattern is applied to the blocks touching subscriptions_mutex_
(around subscriptions_.clear() and the send_message sites) and
field_requests_mutex_ (lines handling field requests) to avoid holding locks
during network I/O.

---

Duplicate comments:
In `@src/platform/freertos/memory.cpp`:
- Around line 48-54: The pool init can fail (xSemaphoreCreateMutex may return
nullptr) but ensure_pool_init currently doesn't signal failure, causing callers
like allocate_message() to call xSemaphoreTake(pool_mutex, ...) and potentially
dereference nullptr; change ensure_pool_init() to return a bool (or error code)
indicating success/failure, set pool_initialized only on success, and update all
callers (notably allocate_message() and any code paths around the second init
block at the other site) to check the return value and bail out when
initialization failed instead of taking/using pool_mutex; ensure any early
returns maintain thread-safety and do not rely on pool_mutex if it is null.

In `@src/platform/threadx/memory.cpp`:
- Around line 48-65: ensure_pool_init() currently returns void which allows
allocate_message() to proceed even when message_pool wasn't created; change
ensure_pool_init() to return bool, introduce a separate atomic<bool>
guard_initialized (or similar) to track whether pool_guard was successfully
created, and only call tx_mutex_create() when guard_initialized is false; on
tx_block_pool_create() failure, if the mutex was just created either destroy it
or leave guard_initialized true but ensure pool_initialized stays false and
return false; callers like allocate_message() must check the new boolean result
and avoid using message_pool when ensure_pool_init() returns false; update
references to pool_initialized, init_lock, pool_guard, message_pool,
tx_mutex_create, and tx_block_pool_create accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f71d54b1-0e50-47c4-b427-c450c84fa866

📥 Commits

Reviewing files that changed from the base of the PR and between d0fb137 and 63f3e54.

📒 Files selected for processing (7)
  • .gitignore
  • src/events/event_subscriber.cpp
  • src/platform/freertos/memory.cpp
  • src/platform/threadx/memory.cpp
  • src/rpc/rpc_client.cpp
  • src/tp/tp_manager.cpp
  • tests/test_e2e.cpp

Comment thread .gitignore Outdated
Comment thread src/platform/freertos/memory.cpp Outdated
Comment thread src/platform/threadx/memory.cpp Outdated
vtz added 2 commits April 13, 2026 02:27
…tors

- freertos/memory: replace std::atomic_flag spin with
  taskENTER_CRITICAL/taskEXIT_CRITICAL — prevents priority-inversion
  starvation in preemptive scheduling
- threadx/memory: replace spin with TX_DISABLE/TX_RESTORE for
  consistency
- .gitignore: move build_lib/ to Build directories section

Made-with: Cursor
If tx_mutex_create succeeds but tx_block_pool_create fails,
delete the already-created mutex so subsequent retries start
from a clean state instead of permanently failing with
TX_MUTEX_ERROR.

Ref: #226
Made-with: Cursor
@vtz vtz merged commit fe04536 into main Apr 13, 2026
43 checks passed
@vtz vtz deleted the feat/clang-tidy-remediation branch April 13, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix pre-existing code quality and safety issues identified during clang-tidy review

1 participant