From c98eaf796e2f00b590a7d21108eac7d2f428ea82 Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Thu, 2 Jul 2026 10:37:18 +0900 Subject: [PATCH 01/24] wscript: Add missing CMP sources Commit cc0483fa00e7 split the CMP services into separate source files, but the waf source list was not updated at the same time. Add the new src/cmp/*.c files to wscript so waf builds include the CMP handlers as well. Fixes: cc0483fa00e7 ("split cmp services into separate files") Signed-off-by: Yasushi SHOJI --- wscript | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/wscript b/wscript index b7e6bea20..091896e83 100644 --- a/wscript +++ b/wscript @@ -124,6 +124,13 @@ def configure(ctx): 'src/csp_services.c', 'src/csp_id.c', 'src/csp_sfp.c', + 'src/cmp/csp_cmp_clock.c', + 'src/cmp/csp_cmp_dispatch.c', + 'src/cmp/csp_cmp_ident.c', + 'src/cmp/csp_cmp_if_stats.c', + 'src/cmp/csp_cmp_mem.c', + 'src/cmp/csp_cmp_peek_poke.c', + 'src/cmp/csp_cmp_route.c', 'src/interfaces/csp_if_lo.c', 'src/interfaces/csp_if_can.c', 'src/interfaces/csp_if_can_pbuf.c', From c5b2d6e32a41ccdb339ed1387a401dbb56b678f8 Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Wed, 1 Jul 2026 11:27:38 +0900 Subject: [PATCH 02/24] meson.build: Fix incorrect license identifier libcsp was previously licensed under LGPL, but it has since been relicensed under the MIT license. The Meson project definition still used the old license identifier. Update the license parameter in meson.build. Signed-off-by: Yasushi SHOJI --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 5c5d44b64..5593df003 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -project('csp', 'c', version: '2.2', license: 'LGPL', meson_version : '>=0.61.2', default_options : [ +project('csp', 'c', version: '2.2', license: 'MIT', meson_version : '>=0.61.2', default_options : [ 'c_std=gnu11', 'optimization=s', 'warning_level=3', From 24486a17713a7db5a7745cc22b6a47bec2b6eaba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 04:05:46 +0000 Subject: [PATCH 03/24] build(deps): bump actions/checkout from 6 to 7 Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/abi-checker.yml | 4 ++-- .github/workflows/build-test-freertos.yml | 6 +++--- .github/workflows/build-test-python.yml | 2 +- .github/workflows/build-test-zephyr.yml | 2 +- .github/workflows/build-test.yml | 2 +- .github/workflows/check-merge-commits.yml | 2 +- .github/workflows/codespell.yml | 2 +- .github/workflows/develop-build-sphinx-docs.yml | 2 +- .github/workflows/gitlint.yml | 2 +- .github/workflows/linelint.yaml | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/abi-checker.yml b/.github/workflows/abi-checker.yml index a472fcbc6..3ede0c15d 100644 --- a/.github/workflows/abi-checker.yml +++ b/.github/workflows/abi-checker.yml @@ -22,7 +22,7 @@ jobs: sudo apt-get install abi-compliance-checker abi-dumper - name: Checkout merged-base - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.base.ref }} @@ -32,7 +32,7 @@ jobs: abi-dumper build/libcsp.so -lver "merged-base" -o ../tmp/libcsp-merged-base.dump - name: Checkout Current PR - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 ref: ${{github.event.pull_request.head.ref}} diff --git a/.github/workflows/build-test-freertos.yml b/.github/workflows/build-test-freertos.yml index b9060a9ef..46a8f87f1 100644 --- a/.github/workflows/build-test-freertos.yml +++ b/.github/workflows/build-test-freertos.yml @@ -17,18 +17,18 @@ jobs: sudo apt-get install ninja-build meson tree - name: Checkout Test App - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: libcsp/libcsp-freertos - name: Checkout FreeRTOS Kernel - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: FreeRTOS/FreeRTOS-Kernel path: freertos - name: Checkout libcsp under subprojects - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: path: subprojects/libcsp diff --git a/.github/workflows/build-test-python.yml b/.github/workflows/build-test-python.yml index 8bbd830af..64827e2b5 100644 --- a/.github/workflows/build-test-python.yml +++ b/.github/workflows/build-test-python.yml @@ -39,7 +39,7 @@ jobs: sudo apt-get install ninja-build ${{ matrix.buildsystem }} - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Build libcsp examples run: python3 examples/buildall.py --build-system=${{ matrix.buildsystem }} diff --git a/.github/workflows/build-test-zephyr.yml b/.github/workflows/build-test-zephyr.yml index 2d9d51c5d..bcab1f042 100644 --- a/.github/workflows/build-test-zephyr.yml +++ b/.github/workflows/build-test-zephyr.yml @@ -35,7 +35,7 @@ jobs: python3 --version - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: path: libcsp-zephyr repository: yashi/libcsp-zephyr diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 5784ac126..0bc000368 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -64,7 +64,7 @@ jobs: sudo apt-get install ninja-build ${{ matrix.buildsystem }} - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Build env: diff --git a/.github/workflows/check-merge-commits.yml b/.github/workflows/check-merge-commits.yml index 310c9fbd8..4ab184a32 100644 --- a/.github/workflows/check-merge-commits.yml +++ b/.github/workflows/check-merge-commits.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout PR head - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 219f55141..ffa6fc6a6 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install codespell run: pip install codespell diff --git a/.github/workflows/develop-build-sphinx-docs.yml b/.github/workflows/develop-build-sphinx-docs.yml index 8e5af2159..f46f00fdd 100644 --- a/.github/workflows/develop-build-sphinx-docs.yml +++ b/.github/workflows/develop-build-sphinx-docs.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout the repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup packages on Linux if: ${{ runner.os == 'Linux' }} diff --git a/.github/workflows/gitlint.yml b/.github/workflows/gitlint.yml index 318b3cb9f..8598e7b68 100644 --- a/.github/workflows/gitlint.yml +++ b/.github/workflows/gitlint.yml @@ -16,7 +16,7 @@ jobs: pip3 install gitlint - name: Checkout the code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 diff --git a/.github/workflows/linelint.yaml b/.github/workflows/linelint.yaml index ee6190415..375a8a149 100644 --- a/.github/workflows/linelint.yaml +++ b/.github/workflows/linelint.yaml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Linelint uses: fernandrone/linelint@master From 7a162a594b13297366e2a5fc41b9ce5f3dc5a8dc Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Thu, 2 Jul 2026 19:40:53 +0900 Subject: [PATCH 04/24] drivers: eth: eth_linux: Initialize SO_REUSEADDR option Initialize the integer value passed to setsockopt() for SO_REUSEADDR. SO_REUSEADDR expects an integer boolean option value. Set it to 1 so address reuse is enabled instead of passing an uninitialized value. Signed-off-by: Yasushi SHOJI --- src/drivers/eth/eth_linux.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/drivers/eth/eth_linux.c b/src/drivers/eth/eth_linux.c index fdbf17d6b..8cbe03de3 100644 --- a/src/drivers/eth/eth_linux.c +++ b/src/drivers/eth/eth_linux.c @@ -149,7 +149,7 @@ int csp_eth_init(const char * device, const char * ifname, int mtu, unsigned int ((uint8_t *)if_mac.ifr_hwaddr.sa_data)[5]); /* Allow the socket to be reused - in case connection is closed prematurely */ - int sockopt; + const int sockopt = 1; if (setsockopt(ctx->sockfd, SOL_SOCKET, SO_REUSEADDR, &sockopt, sizeof sockopt) == -1) { perror("setsockopt"); close(ctx->sockfd); From d559678696ecb5b120e65ff62b3503ed0e6617e8 Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Thu, 2 Jul 2026 13:38:21 +0900 Subject: [PATCH 05/24] github: build-test: Add Ubuntu 26.04 Add Ubuntu 26.04 to the build-test matrix. The GitHub-hosted Ubuntu 26.04 runner image is now available in public preview. Replace the separate OS and compiler matrix axes with explicit Ubuntu/compiler targets so each runner is paired only with GCC versions available on that image. This removes the need for an exclude list. Signed-off-by: Yasushi SHOJI --- .github/workflows/build-test.yml | 83 ++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 0bc000368..50a4fb527 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -10,54 +10,73 @@ jobs: strategy: fail-fast: false matrix: - os: - - ubuntu-24.04 - - ubuntu-22.04 - buildsystem: - - meson - - cmake - - waf - compiler: - - CC: gcc-10 + target: + # Ubuntu 22.04 only has GCC 10, 11, 12 + - os: ubuntu-22.04 + CC: gcc-10 CXX: g++-10 - - CC: gcc-11 + - os: ubuntu-22.04 + CC: gcc-11 CXX: g++-11 - - CC: gcc-12 + - os: ubuntu-22.04 + CC: gcc-12 CXX: g++-12 - - CC: gcc-13 + - os: ubuntu-22.04 + CC: clang + CXX: clang++ + # Ubuntu 24.04 only has GCC 12, 13, 14 + - os: ubuntu-24.04 + CC: gcc-12 + CXX: g++-12 + - os: ubuntu-24.04 + CC: gcc-13 CXX: g++-13 - - CC: gcc-14 + - os: ubuntu-24.04 + CC: gcc-14 CXX: g++-14 - - CC: clang + - os: ubuntu-24.04 + CC: clang CXX: clang++ + # Ubuntu 26.04 only has GCC 13, 14, 15 + - os: ubuntu-26.04 + CC: gcc-13 + CXX: g++-13 + - os: ubuntu-26.04 + CC: gcc-14 + CXX: g++-14 + - os: ubuntu-26.04 + CC: gcc-15 + CXX: g++-15 + - os: ubuntu-26.04 + CC: clang + CXX: clang++ + buildsystem: + - meson + - cmake + - waf csp_version: - 1 - 2 - exclude: - # Ubuntu 22.04 only has GCC 10, 11, 12 - - os: ubuntu-22.04 - compiler: - CC: gcc-13 - - os: ubuntu-22.04 - compiler: - CC: gcc-14 - # Ubuntu 24.04 only has GCC 12, 13, 14 - - os: ubuntu-24.04 - compiler: - CC: gcc-10 - - os: ubuntu-24.04 - compiler: - CC: gcc-11 - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.target.os }} steps: - name: Setup packages on Linux if: ${{ runner.os == 'Linux' }} run: | sudo apt-get update sudo apt-get install libzmq3-dev libsocketcan-dev socat iproute2 + sudo apt-cache search linux-modules + + - name: Setup packages on Ubuntu 22.04/24.04 + if: ${{ runner.os == 'Linux' && (matrix.target.os == 'ubuntu-22.04' || matrix.target.os == 'ubuntu-24.04') }} + run: | sudo apt-get install linux-modules-extra-$(uname -r) + - name: Setup packages on Linux 26.04 + if: ${{ runner.os == 'Linux' && (matrix.target.os == 'ubuntu-26.04') }} + run: | + sudo apt-get install linux-modules-$(uname -r) + - name: Setup build system packages on Linux if: ${{ runner.os == 'Linux' && matrix.buildsystem != 'waf' }} run: | @@ -68,8 +87,8 @@ jobs: - name: Build env: - CC: ${{ matrix.compiler.CC }} - CXX: ${{ matrix.compiler.CXX }} + CC: ${{ matrix.target.CC }} + CXX: ${{ matrix.target.CXX }} run: python3 ./examples/buildall.py --build-system=${{ matrix.buildsystem }} - name: Run Loopback Test From 2fc1e7566031b7e3ccaa923bfc56f533725505e0 Mon Sep 17 00:00:00 2001 From: Sebastiano Lewental Date: Mon, 22 Jun 2026 14:30:05 +0200 Subject: [PATCH 06/24] fix: compilation error when CSP_ENABLE_CSP_PRINT=0 Compiling with CSP_ENABLE_CSP_PRINT set to false fails because compiler reports unused arguments as in this case csp_print does nothing with them. This fix uses the arguments in an `if(0)` or in a void cast to keep them referenced by the compiler and suppress the errors. --- include/csp/csp.h | 7 ++++++- include/csp/csp_debug.h | 2 +- src/csp_debug.c | 6 ++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/include/csp/csp.h b/include/csp/csp.h index ea190e605..f28e1139d 100644 --- a/include/csp/csp.h +++ b/include/csp/csp.h @@ -510,7 +510,12 @@ void csp_hex_dump(const char *desc, const void *addr, int len); #else inline void csp_conn_print_table(void) {} -inline void csp_hex_dump(const char *desc, void *addr, int len) {} +inline void csp_hex_dump(const char *desc, void *addr, int len) { + /* Avoid compiler warnings about unused parameters when CSP_ENABLE_CSP_PRINT=0 */ + (void)desc; + (void)addr; + (void)len; +} #endif diff --git a/include/csp/csp_debug.h b/include/csp/csp_debug.h index acaeaaf0e..4d4da8d59 100644 --- a/include/csp/csp_debug.h +++ b/include/csp/csp_debug.h @@ -73,7 +73,7 @@ void csp_print_func(const char * fmt, ...); #if (CSP_ENABLE_CSP_PRINT) #define csp_print(...) csp_print_func(__VA_ARGS__); #else -#define csp_print(...) do {} while(0) +#define csp_print(...) do { if(0) csp_print_func(__VA_ARGS__); /* avoids unused-parameter compiler errors */ } while(0) #endif #define csp_rdp_error(format, ...) { if (csp_dbg_rdp_print >= 1) { csp_print("\033[31m" format "\033[0m", ##__VA_ARGS__); }} diff --git a/src/csp_debug.c b/src/csp_debug.c index acb67c02e..86a2e1b25 100644 --- a/src/csp_debug.c +++ b/src/csp_debug.c @@ -14,10 +14,10 @@ uint8_t csp_dbg_rdp_print; uint8_t csp_dbg_packet_print; #if (CSP_ENABLE_CSP_PRINT) +#include "csp/csp_debug.h" #if (CSP_PRINT_STDIO) #include #include -#include "csp/csp_debug.h" __weak void csp_print_func(const char * fmt, ...) { va_list args; va_start(args, fmt); @@ -25,6 +25,8 @@ __weak void csp_print_func(const char * fmt, ...) { va_end(args); } #else -__weak void csp_print_func(const char * fmt, ...) {} +__weak void csp_print_func(const char * fmt, ...) { + (void)fmt; /* Avoid compiler warnings about unused parameter */ +} #endif #endif From 0a80f9fba869a61419a80654784e4ad3ec3e0e9b Mon Sep 17 00:00:00 2001 From: Sebastiano Lewental Date: Wed, 8 Jul 2026 12:35:51 +0200 Subject: [PATCH 07/24] github: workflow: Add compile time options check Add check-compile-options.yml that checks compilation with a defined set of options. --- .github/workflows/check-compile-options.yml | 34 +++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/check-compile-options.yml diff --git a/.github/workflows/check-compile-options.yml b/.github/workflows/check-compile-options.yml new file mode 100644 index 000000000..9ec26c912 --- /dev/null +++ b/.github/workflows/check-compile-options.yml @@ -0,0 +1,34 @@ +name: Check Compile Options +on: [push, pull_request] +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true +jobs: + check-compile-options: + strategy: + fail-fast: false + matrix: + os: + - ubuntu-22.04 + - ubuntu-24.04 + options: + - "-D enable_csp_print=false -D print_stdio=false" + - "-D enable_csp_print=false -D print_stdio=true" + - "-D enable_csp_print=true -D print_stdio=false" + - "-D enable_csp_print=true -D print_stdio=true" + runs-on: ${{ matrix.os }} + steps: + - name: Setup packages on Linux + run: | + sudo apt-get update + sudo apt-get install libzmq3-dev libsocketcan-dev ninja-build meson + + - name: Checkout + uses: actions/checkout@v7 + + - name: Build with compile time options + run: | + meson setup ${{ matrix.options }} builddir + meson compile -C builddir/ From e4000a216a5fe63ec4cc5bf8b44472b201c56a0d Mon Sep 17 00:00:00 2001 From: Luigi Colluto Date: Fri, 3 Jul 2026 16:17:11 +0200 Subject: [PATCH 08/24] fix(service): avoid use-after-free in the CSP_PS handler The CSP_PS branch of csp_service_handler() frees the packet when csp_ps_hook() returns 0 (the shipped default) but then break()s, so the shared post-switch csp_sendto_reply() runs on the freed buffer: a use-after-free / double-ownership of a buffer-pool slot, triggerable by a single CSP_PS packet. Return after the free, mirroring the CSP_CMP, CSP_REBOOT and default branches. --- src/csp_service_handler.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/csp_service_handler.c b/src/csp_service_handler.c index 6503af1e7..ab17fbabb 100644 --- a/src/csp_service_handler.c +++ b/src/csp_service_handler.c @@ -28,6 +28,7 @@ void csp_service_handler(csp_packet_t * packet) { packet->length = csp_ps_hook(packet); if (packet->length == 0) { csp_buffer_free(packet); + return; } break; } From bf336e9601879506b213de2a3c9e6fbc9ac6b98a Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Fri, 10 Jul 2026 19:48:43 +0900 Subject: [PATCH 09/24] csp_service_handler: Refactor out 32-bit service reply handling Refactor out the repeated byte-order conversion, buffer copy, and length assignment used by the MEMFREE, BUF_FREE, and UPTIME services. No functionality has been changed. Signed-off-by: Yasushi SHOJI --- src/csp_service_handler.c | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/csp_service_handler.c b/src/csp_service_handler.c index ab17fbabb..e652253ac 100644 --- a/src/csp_service_handler.c +++ b/src/csp_service_handler.c @@ -8,6 +8,15 @@ #include "cmp/csp_cmp_internal.h" + +static void set_u32_reply(csp_packet_t * packet, uint32_t value) { + + const uint32_t value_be = htobe32(value); + + memcpy(packet->data, &value_be, sizeof(value_be)); + packet->length = sizeof(value_be); +} + void csp_service_handler(csp_packet_t * packet) { switch (packet->id.dport) { @@ -34,14 +43,7 @@ void csp_service_handler(csp_packet_t * packet) { } case CSP_MEMFREE: { - - uint32_t total = 0; - total = csp_memfree_hook(); - - total = htobe32(total); - memcpy(packet->data, &total, sizeof(total)); - packet->length = sizeof(total); - + set_u32_reply(packet, csp_memfree_hook()); break; } @@ -63,18 +65,12 @@ void csp_service_handler(csp_packet_t * packet) { } case CSP_BUF_FREE: { - uint32_t size = csp_buffer_remaining(); - size = htobe32(size); - memcpy(packet->data, &size, sizeof(size)); - packet->length = sizeof(size); + set_u32_reply(packet, (uint32_t)csp_buffer_remaining()); break; } case CSP_UPTIME: { - uint32_t time = csp_get_s(); - time = htobe32(time); - memcpy(packet->data, &time, sizeof(time)); - packet->length = sizeof(time); + set_u32_reply(packet, csp_get_s()); break; } From c46591bd237fbf0d3accfb26ee1c660107e83da9 Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Fri, 10 Jul 2026 19:49:07 +0900 Subject: [PATCH 10/24] csp_service_handler: Remove redundant packet null check Remove the unnecessary null check before sending the service reply, since all paths that do not send a reply return earlier. No functionality has been changed. Signed-off-by: Yasushi SHOJI --- src/csp_service_handler.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/csp_service_handler.c b/src/csp_service_handler.c index e652253ac..362c06f8b 100644 --- a/src/csp_service_handler.c +++ b/src/csp_service_handler.c @@ -79,7 +79,5 @@ void csp_service_handler(csp_packet_t * packet) { return; } - if (packet != NULL) { - csp_sendto_reply(packet, packet, CSP_O_SAME); - } + csp_sendto_reply(packet, packet, CSP_O_SAME); } From 2e7d50d8cf8c3ea82dc5cc8c62bc8b73e80871ed Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Fri, 10 Jul 2026 19:52:43 +0900 Subject: [PATCH 11/24] csp_service_handler: Centralize discarded packet cleanup Replace repeated buffer-free and return sequences with a shared discard path in the service handler. No functionality has been changed. Signed-off-by: Yasushi SHOJI --- src/csp_service_handler.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/csp_service_handler.c b/src/csp_service_handler.c index 362c06f8b..035dcb9e3 100644 --- a/src/csp_service_handler.c +++ b/src/csp_service_handler.c @@ -24,8 +24,7 @@ void csp_service_handler(csp_packet_t * packet) { case CSP_CMP: /* Pass to CMP handler */ if (csp_cmp_handler(packet) != CSP_ERR_NONE) { - csp_buffer_free(packet); - return; + goto discard; } break; @@ -36,8 +35,7 @@ void csp_service_handler(csp_packet_t * packet) { case CSP_PS: { packet->length = csp_ps_hook(packet); if (packet->length == 0) { - csp_buffer_free(packet); - return; + goto discard; } break; } @@ -60,8 +58,7 @@ void csp_service_handler(csp_packet_t * packet) { csp_shutdown_hook(); } - csp_buffer_free(packet); - return; + goto discard; } case CSP_BUF_FREE: { @@ -75,9 +72,12 @@ void csp_service_handler(csp_packet_t * packet) { } default: - csp_buffer_free(packet); - return; + goto discard; } csp_sendto_reply(packet, packet, CSP_O_SAME); + return; + +discard: + csp_buffer_free(packet); } From b5e228643b06da4a5e5a8145ddfd663165aa9151 Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Sat, 11 Jul 2026 16:22:28 +0900 Subject: [PATCH 12/24] meson: Set Shared Library Version Set the library version and ABI version from the Meson project version. This produces libcsp.so.2.2 with the SONAME libcsp.so.2, matching the CMake build. Signed-off-by: Yasushi SHOJI --- meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/meson.build b/meson.build index 5593df003..6b9b68b1d 100644 --- a/meson.build +++ b/meson.build @@ -101,6 +101,7 @@ csp_lib = library('csp', c_args : csp_c_args, install : true, pic:true, + version : meson.project_version(), ) # The following dependency variable is for parent projects to link From 4b3c08d98ed863353dc41eb26ccd4cf10c4e9ea6 Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Sat, 11 Jul 2026 16:26:44 +0900 Subject: [PATCH 13/24] waf: Set Shared Library Version Set vnum and cnum from the Waf project version. This produces an installed libcsp.so.2.2 with the SONAME libcsp.so.2, matching the CMake build. Signed-off-by: Yasushi SHOJI --- wscript | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wscript b/wscript index 091896e83..6d96af371 100644 --- a/wscript +++ b/wscript @@ -245,7 +245,8 @@ def build(ctx): name='csp_shlib', target='csp', use=['csp_h', 'util_shlib'], - lib=ctx.env.LIBS) + lib=ctx.env.LIBS, + vnum=VERSION) # Build Python bindings if ctx.env.HAVE_PYEXT: From bbbfd16888c5a266cb8cfd326066b92cc1f07164 Mon Sep 17 00:00:00 2001 From: Guillaume Jacob Date: Tue, 12 May 2026 15:00:33 +0200 Subject: [PATCH 14/24] cmake: set proper SOVERSION This is necessary to build libcsp with Yocto --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2752465aa..4621671c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,11 @@ add_library(csp) set_target_properties(csp PROPERTIES C_STANDARD 11) set_target_properties(csp PROPERTIES C_EXTENSIONS ON) +set_target_properties(csp PROPERTIES + VERSION ${CSP_VERSION} + SOVERSION ${CSP_VERSION_MAJOR} +) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release MinSizeRel) if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "No build type selected, default to MinSizeRel") From f2a2c1e4a100dd2e61b0e0faf13541c1bffa79ec Mon Sep 17 00:00:00 2001 From: Guillaume Jacob Date: Tue, 12 May 2026 15:09:59 +0200 Subject: [PATCH 15/24] bindings: add missing python bindings Add bindings to: * add a custom interface (iface_add) * manipulate a packet ID (id_prepend, id_strip, id_setup_rx) * read a packet ID (packet_get*) * read and write a packet frame data (packet_get_frame_length, packet_set_frame_data) --- src/bindings/python/pycsp.c | 242 ++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) diff --git a/src/bindings/python/pycsp.c b/src/bindings/python/pycsp.c index f0c2d8118..90ad92483 100644 --- a/src/bindings/python/pycsp.c +++ b/src/bindings/python/pycsp.c @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -617,6 +618,117 @@ static PyObject * pycsp_rdp_get_opt(PyObject * self, PyObject * args) { ack_delay_count); } +typedef struct { + csp_iface_t iface; + PyObject * py_nexthop; +} pycsp_iface_wrapper_t; + +static int pycsp_nexthop_trampoline(csp_iface_t * iface, + uint16_t via, + csp_packet_t * packet, + int from_me) { + + pycsp_iface_wrapper_t * wrapper = + (pycsp_iface_wrapper_t *) iface->interface_data; + + if (!wrapper || !wrapper->py_nexthop) { + return CSP_ERR_INVAL; + } + + PyGILState_STATE gstate = PyGILState_Ensure(); + + PyObject * iface_capsule = + PyCapsule_New(iface, "csp_iface_t", NULL); + + PyObject * packet_capsule = + PyCapsule_New(packet, PACKET_CAPSULE, NULL); + + PyObject * result = PyObject_CallFunction( + wrapper->py_nexthop, + "NHNi", // PyObject (steals ref), uint16, PyObject (steals ref), int + iface_capsule, + via, + packet_capsule, + from_me + ); + + int ret = CSP_ERR_NONE; + + if (!result) { + PyErr_Print(); + ret = CSP_ERR_INVAL; + } else { + if (PyLong_Check(result)) { + ret = (int) PyLong_AsLong(result); + } + Py_DECREF(result); + } + + PyGILState_Release(gstate); + return ret; +} + +static PyObject * pycsp_iface_add(PyObject * self, PyObject * args) { + + uint16_t addr; + uint16_t netmask; + const char * name; + PyObject * py_nexthop; + + if (!PyArg_ParseTuple(args, "HHsO", &addr, &netmask, &name, &py_nexthop)) { + return NULL; + } + + if (!PyCallable_Check(py_nexthop)) { + PyErr_SetString(PyExc_TypeError, "nexthop must be callable"); + return NULL; + } + + pycsp_iface_wrapper_t * wrapper = + PyMem_RawCalloc(1, sizeof(*wrapper)); + + if (!wrapper) { + return PyErr_NoMemory(); + } + + // Fill iface + wrapper->iface.addr = addr; + wrapper->iface.netmask = netmask; + wrapper->iface.name = strdup(name); // ensure lifetime! + wrapper->iface.interface_data = wrapper; + wrapper->iface.driver_data = NULL; + wrapper->iface.nexthop = pycsp_nexthop_trampoline; + wrapper->iface.is_default = 0; + + // Store Python callback + Py_INCREF(py_nexthop); + wrapper->py_nexthop = py_nexthop; + + // Register with CSP + csp_iflist_add(&wrapper->iface); + + Py_RETURN_NONE; +} + +static PyObject * pycsp_qfifo_write(PyObject * self, PyObject * args) { + PyObject * packet_capsule; + char * interface_name; + if (!PyArg_ParseTuple(args, "Os", &packet_capsule, &interface_name)) { + return NULL; // TypeError is thrown + } + + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + + csp_iface_t * iface = csp_iflist_get_by_name(interface_name); + + csp_qfifo_write(packet, iface, NULL); // todo: handle pxTaskWoken != NULL case? + + Py_RETURN_NONE; +} + #if CSP_USE_RTABLE static PyObject * pycsp_rtable_set(PyObject * self, PyObject * args) { @@ -701,6 +813,34 @@ static PyObject * pycsp_buffer_remaining(PyObject * self, PyObject * args) { return Py_BuildValue("i", csp_buffer_remaining()); } +static PyObject * pycsp_id_prepend(PyObject * self, PyObject * packet_capsule) { + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + csp_id_prepend(packet); + + Py_RETURN_NONE; +} + +static PyObject * pycsp_id_strip(PyObject * self, PyObject * packet_capsule) { + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + int res = csp_id_strip(packet); + return Py_BuildValue("i", res); +} + +static PyObject * pycsp_id_setup_rx(PyObject * self, PyObject * packet_capsule) { + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + int res = csp_id_setup_rx(packet); + return Py_BuildValue("i", res); +} + static PyObject * pycsp_cmp_ident(PyObject * self, PyObject * args) { uint16_t node; uint32_t timeout = 1000; @@ -967,6 +1107,27 @@ static PyObject * pycsp_packet_set_data(PyObject * self, PyObject * args) { Py_RETURN_NONE; } +static PyObject * pycsp_packet_set_frame_data(PyObject * self, PyObject * args) { + PyObject * packet_capsule; + Py_buffer data; + if (!PyArg_ParseTuple(args, "Oy*", &packet_capsule, &data)) { + return NULL; // TypeError is thrown + } + + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + if (data.len > csp_id_get_header_size() + (int)sizeof(packet->data)) { + return PyErr_Error("packet_set_frame_data() - exceeding frame size", CSP_ERR_INVAL); + } + + memcpy(packet->frame_begin, data.buf, data.len); + packet->frame_length = data.len; + + Py_RETURN_NONE; +} + static PyObject * pycsp_packet_get_data(PyObject * self, PyObject * packet_capsule) { csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); if (packet == NULL) { @@ -975,6 +1136,14 @@ static PyObject * pycsp_packet_get_data(PyObject * self, PyObject * packet_capsu return Py_BuildValue("y#", packet->data, (size_t)packet->length); } +static PyObject * pycsp_packet_get_frame_data(PyObject * self, PyObject * packet_capsule) { + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + return Py_BuildValue("y#", packet->frame_begin, (size_t)packet->frame_length); +} + static PyObject * pycsp_packet_get_length(PyObject * self, PyObject * packet_capsule) { csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); if (packet == NULL) { @@ -983,6 +1152,62 @@ static PyObject * pycsp_packet_get_length(PyObject * self, PyObject * packet_cap return Py_BuildValue("H", packet->length); } +static PyObject * pycsp_packet_get_frame_length(PyObject * self, PyObject * packet_capsule) { + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + return Py_BuildValue("H", packet->frame_length); +} + +static PyObject * pycsp_packet_get_pri(PyObject * self, PyObject * packet_capsule) { + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + return Py_BuildValue("B", packet->id.pri); +} + +static PyObject * pycsp_packet_get_flags(PyObject * self, PyObject * packet_capsule) { + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + return Py_BuildValue("B", packet->id.flags); +} + +static PyObject * pycsp_packet_get_src(PyObject * self, PyObject * packet_capsule) { + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + return Py_BuildValue("H", packet->id.src); +} + +static PyObject * pycsp_packet_get_dst(PyObject * self, PyObject * packet_capsule) { + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + return Py_BuildValue("H", packet->id.dst); +} + +static PyObject * pycsp_packet_get_dport(PyObject * self, PyObject * packet_capsule) { + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + return Py_BuildValue("B", packet->id.dport); +} + +static PyObject * pycsp_packet_get_sport(PyObject * self, PyObject * packet_capsule) { + csp_packet_t * packet = get_obj_as_packet(packet_capsule, false); + if (packet == NULL) { + return NULL; // TypeError is thrown + } + return Py_BuildValue("B", packet->id.sport); +} + static PyObject * pycsp_print_connections(PyObject * self, PyObject * args) { csp_conn_print_table(); Py_RETURN_NONE; @@ -1026,6 +1251,9 @@ static PyMethodDef methods[] = { {"rdp_set_opt", pycsp_rdp_set_opt, METH_VARARGS, ""}, {"rdp_get_opt", pycsp_rdp_get_opt, METH_NOARGS, ""}, + {"iface_add", pycsp_iface_add, METH_VARARGS, "Add CSP interface"}, + {"qfifo_write", pycsp_qfifo_write, METH_VARARGS, ""}, + #if CSP_USE_RTABLE /* csp/csp_rtable.h */ {"rtable_set", pycsp_rtable_set, METH_VARARGS, ""}, @@ -1040,6 +1268,11 @@ static PyMethodDef methods[] = { {"buffer_get", pycsp_buffer_get, METH_VARARGS, ""}, {"buffer_remaining", pycsp_buffer_remaining, METH_NOARGS, ""}, + /* csp/csp_id.h */ + {"id_prepend", pycsp_id_prepend, METH_O, ""}, + {"id_strip", pycsp_id_strip, METH_O, ""}, + {"id_setup_rx", pycsp_id_setup_rx, METH_O, ""}, + /* csp/csp_cmp.h */ {"cmp_ident", pycsp_cmp_ident, METH_VARARGS, ""}, {"cmp_route_set", pycsp_cmp_route_set, METH_VARARGS, ""}, @@ -1061,8 +1294,17 @@ static PyMethodDef methods[] = { /* helpers */ {"packet_get_length", pycsp_packet_get_length, METH_O, ""}, + {"packet_get_frame_length", pycsp_packet_get_frame_length, METH_O, ""}, {"packet_get_data", pycsp_packet_get_data, METH_O, ""}, + {"packet_get_frame_data", pycsp_packet_get_frame_data, METH_O, ""}, + {"packet_get_pri", pycsp_packet_get_pri, METH_O, ""}, + {"packet_get_flags", pycsp_packet_get_flags, METH_O, ""}, + {"packet_get_src", pycsp_packet_get_src, METH_O, ""}, + {"packet_get_dst", pycsp_packet_get_dst, METH_O, ""}, + {"packet_get_dport", pycsp_packet_get_dport, METH_O, ""}, + {"packet_get_sport", pycsp_packet_get_sport, METH_O, ""}, {"packet_set_data", pycsp_packet_set_data, METH_VARARGS, ""}, + {"packet_set_frame_data", pycsp_packet_set_frame_data, METH_VARARGS, ""}, {"print_connections", pycsp_print_connections, METH_NOARGS, ""}, {"print_interfaces", pycsp_print_interfaces, METH_NOARGS, ""}, From a47b4d7302360a01993c83bdb0de514240002fc4 Mon Sep 17 00:00:00 2001 From: Sammy Dabbas Date: Thu, 9 Jul 2026 21:29:21 -0400 Subject: [PATCH 16/24] csp_yaml: Copy UDP host string to fix use-after-free The UDP interface aliased the parsed server string, which the loader then freed, leaving a dangling host pointer in the interface driver state. Duplicate it with strdup so the interface owns its own copy, matching the existing interface name handling. --- src/csp_yaml.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/csp_yaml.c b/src/csp_yaml.c index 2402546a6..d1ab695dd 100644 --- a/src/csp_yaml.c +++ b/src/csp_yaml.c @@ -104,7 +104,7 @@ static void csp_yaml_end_if(struct data_s * data, unsigned int * dfl_addr) { iface = malloc(sizeof(csp_iface_t)); memset(iface, 0, sizeof(csp_iface_t)); csp_if_udp_conf_t * udp_conf = malloc(sizeof(csp_if_udp_conf_t)); - udp_conf->host = data->server; + udp_conf->host = strdup(data->server); udp_conf->lport = atoi(data->listen_port); udp_conf->rport = atoi(data->remote_port); csp_if_udp_init(iface, udp_conf); From d874cbb79d690cfa2579bf9221a3d4ba2e1053ba Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Mon, 10 Aug 2026 09:46:15 +0900 Subject: [PATCH 17/24] gitlint: Ignore B1 for Dependabot commits Dependabot-generated commit messages can contain lines longer than gitlint's 80-character body limit due to package URLs. Ignore only the body line-length rule for Dependabot commits while keeping the remaining gitlint checks enabled. Signed-off-by: Yasushi SHOJI --- .gitlint | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitlint b/.gitlint index 87587e6f4..bed235a85 100644 --- a/.gitlint +++ b/.gitlint @@ -3,3 +3,7 @@ regex-style-search=true [ignore-body-lines] regex=^(?:(?:Signed-off|Acked|Co-Authored|Reported|Tested)-by: |\[\d+\]: https:\/\/) + +[ignore-by-author-name] +regex=^dependabot\[bot\]$ +ignore=B1 From 651dca975c72ea7adfc870f12628cfdec734e779 Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Mon, 10 Aug 2026 13:01:05 +0900 Subject: [PATCH 18/24] github: Avoid duplicate checks for pull requests A branch update for a pull request triggers both the push and pull_request workflows. This causes the same checks to run twice. Limit push workflows to develop and libcsp-* branches so pull requests run only the pull_request workflows. Assisted-by: Codex:GPT-5 Signed-off-by: Yasushi SHOJI --- .github/workflows/build-test-freertos.yml | 7 ++++++- .github/workflows/build-test-python.yml | 7 ++++++- .github/workflows/build-test-zephyr.yml | 5 ++++- .github/workflows/build-test.yml | 7 ++++++- .github/workflows/check-compile-options.yml | 7 ++++++- .github/workflows/codespell.yml | 5 ++++- 6 files changed, 32 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test-freertos.yml b/.github/workflows/build-test-freertos.yml index 46a8f87f1..54e308b0e 100644 --- a/.github/workflows/build-test-freertos.yml +++ b/.github/workflows/build-test-freertos.yml @@ -1,5 +1,10 @@ name: FreeRTOS Build and Test -on: [push, pull_request] +on: + pull_request: + push: + branches: + - develop + - 'libcsp-*' permissions: contents: read concurrency: diff --git a/.github/workflows/build-test-python.yml b/.github/workflows/build-test-python.yml index 64827e2b5..f14bdd3df 100644 --- a/.github/workflows/build-test-python.yml +++ b/.github/workflows/build-test-python.yml @@ -1,5 +1,10 @@ name: Python Bindings -on: [push, pull_request] +on: + pull_request: + push: + branches: + - develop + - 'libcsp-*' permissions: contents: read concurrency: diff --git a/.github/workflows/build-test-zephyr.yml b/.github/workflows/build-test-zephyr.yml index bcab1f042..29b7c9069 100644 --- a/.github/workflows/build-test-zephyr.yml +++ b/.github/workflows/build-test-zephyr.yml @@ -1,7 +1,10 @@ name: Zephyr Build and Test on: - push: pull_request: + push: + branches: + - develop + - 'libcsp-*' schedule: - cron: '0 10 * * 0' # Run it every Sunday 10am UTC diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 50a4fb527..91f84d7f7 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1,5 +1,10 @@ name: Build and Test -on: [push, pull_request] +on: + pull_request: + push: + branches: + - develop + - 'libcsp-*' permissions: contents: read concurrency: diff --git a/.github/workflows/check-compile-options.yml b/.github/workflows/check-compile-options.yml index 9ec26c912..a6a8982be 100644 --- a/.github/workflows/check-compile-options.yml +++ b/.github/workflows/check-compile-options.yml @@ -1,5 +1,10 @@ name: Check Compile Options -on: [push, pull_request] +on: + pull_request: + push: + branches: + - develop + - 'libcsp-*' permissions: contents: read concurrency: diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index ffa6fc6a6..7c0a8241e 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -1,8 +1,11 @@ name: Codespell Check -on: +on: pull_request: push: + branches: + - develop + - 'libcsp-*' permissions: contents: read From c86262079556d41a44b4e425f28aa3f20d78ecbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 04:26:16 +0000 Subject: [PATCH 19/24] build(deps): bump actions/setup-python from 6 to 7 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build-test-python.yml | 2 +- .github/workflows/build-test-zephyr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test-python.yml b/.github/workflows/build-test-python.yml index f14bdd3df..bd138cfe2 100644 --- a/.github/workflows/build-test-python.yml +++ b/.github/workflows/build-test-python.yml @@ -30,7 +30,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-zephyr.yml b/.github/workflows/build-test-zephyr.yml index 29b7c9069..9f26582eb 100644 --- a/.github/workflows/build-test-zephyr.yml +++ b/.github/workflows/build-test-zephyr.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} From 4aed655a81cb591f84c547c30fc25ed70cb852a2 Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Mon, 10 Aug 2026 17:59:11 +0900 Subject: [PATCH 20/24] csp: Fix csp_hex_dump() stub signature mismatch When CSP_ENABLE_CSP_PRINT is 0, the inline csp_hex_dump() stub declares its addr parameter as `void *` while the public prototype and the implementation use `const void *`. This causes a "conflicting types for csp_hex_dump" compile error when CSP print is disabled. Change the stub to `const void *` to match the public API. Assisted-by: GitHub Copilot CLI:claude-fable-5 Signed-off-by: Yasushi SHOJI --- include/csp/csp.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/csp/csp.h b/include/csp/csp.h index f28e1139d..f8d20ac45 100644 --- a/include/csp/csp.h +++ b/include/csp/csp.h @@ -510,7 +510,7 @@ void csp_hex_dump(const char *desc, const void *addr, int len); #else inline void csp_conn_print_table(void) {} -inline void csp_hex_dump(const char *desc, void *addr, int len) { +inline void csp_hex_dump(const char *desc, const void *addr, int len) { /* Avoid compiler warnings about unused parameters when CSP_ENABLE_CSP_PRINT=0 */ (void)desc; (void)addr; From c5207a63e7674607dbba3e0bbfa4938d7040566c Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Mon, 10 Aug 2026 17:49:17 +0900 Subject: [PATCH 21/24] csp_io: Fix doxygen comment for csp_send_direct() The doxygen comment above csp_send_direct() documented a from_me parameter that the function does not take and a return value for a void function, which is confusing for library users. The from_me parameter actually belongs to csp_send_direct_iface(), which had no documentation at all. Rewrite the comment to match the actual prototype and add a proper doxygen comment for csp_send_direct_iface(). No functionality has been changed. Assisted-by: GitHub Copilot CLI:claude-fable-5 Signed-off-by: Yasushi SHOJI --- src/csp_io.h | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/csp_io.h b/src/csp_io.h index 7e829ee4d..16f8c6a8a 100644 --- a/src/csp_io.h +++ b/src/csp_io.h @@ -3,11 +3,29 @@ #include /** + * Send a packet, resolving the outgoing interface(s) from the destination + * address in @p idout. * - * @param packet packet to send - this may not be freed if error code is returned - * @param from_me 1 if from me, 0 if routed message - * @return #CSP_ERR_NONE on success, otherwise an error code. + * The packet is always consumed by this call, either handed over to one or + * more interfaces or freed on error. * + * @param idout header (identifier) to send with + * @param packet packet to send + * @param routed_from interface the packet was received on, or NULL if the + * packet originates from the local node */ void csp_send_direct(csp_id_t* idout, csp_packet_t * packet, csp_iface_t * routed_from); + +/** + * Send a packet on a specific interface. + * + * The packet is always consumed by this call, either handed over to the + * interface or freed on error. + * + * @param idout header (identifier) to send with + * @param packet packet to send + * @param iface interface to send the packet on + * @param via via address, or #CSP_NO_VIA_ADDRESS to send directly to the destination + * @param from_me 1 if the packet originates from the local node, 0 if it is a routed message + */ void csp_send_direct_iface(const csp_id_t* idout, csp_packet_t * packet, csp_iface_t * iface, uint16_t via, int from_me); From cd07298ca0bc406aae5ad70459ad1b63012fcadd Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Mon, 10 Aug 2026 20:03:34 +0900 Subject: [PATCH 22/24] csp_route: Extract csp_socket_is_conn_less() predicate Name the connection-less delivery condition in csp_route_work() so the routing code states its intent instead of testing option bits inline. No functionality has been changed. Assisted-by: GitHub Copilot CLI:claude-fable-5 Signed-off-by: Yasushi SHOJI --- src/csp_port.c | 4 ++++ src/csp_port.h | 1 + src/csp_route.c | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/csp_port.c b/src/csp_port.c index cbec0e127..b23a1b0ba 100644 --- a/src/csp_port.c +++ b/src/csp_port.c @@ -74,6 +74,10 @@ csp_socket_t * csp_port_get_socket(unsigned int port) { return NULL; } +bool csp_socket_is_conn_less(const csp_socket_t * socket) { + return (socket->opts & CSP_SO_CONN_LESS) != 0; +} + int csp_listen(csp_socket_t * socket, size_t backlog) { (void)backlog; /* Avoid compiler warnings about unused parameter */ socket->rx_queue = csp_queue_create_static(CSP_CONN_RXQUEUE_LEN, sizeof(csp_packet_t *), socket->rx_queue_static_data, &socket->rx_queue_static); diff --git a/src/csp_port.h b/src/csp_port.h index 5ee3dacd3..79410312a 100644 --- a/src/csp_port.h +++ b/src/csp_port.h @@ -6,3 +6,4 @@ csp_socket_t * csp_port_get_socket(unsigned int dport); csp_callback_t csp_port_get_callback(unsigned int port); +bool csp_socket_is_conn_less(const csp_socket_t * socket); diff --git a/src/csp_route.c b/src/csp_route.c index 79cd23d24..f2f266ca0 100644 --- a/src/csp_route.c +++ b/src/csp_route.c @@ -198,7 +198,7 @@ int csp_route_work(void) { socket = csp_port_get_socket(packet->id.dport); /* If the socket is connection-less, deliver now */ - if (socket && (socket->opts & CSP_SO_CONN_LESS)) { + if (socket && csp_socket_is_conn_less(socket)) { if (csp_route_security_check(socket->opts, input.iface, packet) != CSP_ERR_NONE) { csp_buffer_free(packet); From c9efc267bb6bd7435fff28c411867e1292eda406 Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Mon, 10 Aug 2026 20:04:17 +0900 Subject: [PATCH 23/24] csp_route: Extract delivery helpers from csp_route_work() csp_route_work() mixes routing decisions with the details of each local delivery mode, making the overall packet flow hard to follow. Extract the three local delivery paths into named functions so that the routine reads as its actual pipeline: forward if not mine, then deliver to a callback, a connection-less socket, or a connection. The helpers document that a connection-less socket is itself the endpoint, while for connection-oriented operation the connection is the endpoint and the socket only accepts new server-side connections. No functionality has been changed. Assisted-by: GitHub Copilot CLI:claude-fable-5 Signed-off-by: Yasushi SHOJI --- src/csp_route.c | 224 ++++++++++++++++++++++++++---------------------- 1 file changed, 122 insertions(+), 102 deletions(-) diff --git a/src/csp_route.c b/src/csp_route.c index f2f266ca0..7bdee47fc 100644 --- a/src/csp_route.c +++ b/src/csp_route.c @@ -109,113 +109,49 @@ __weak void csp_input_hook(csp_iface_t * iface, csp_packet_t * packet) { packet->id.sport, packet->id.pri, packet->id.flags, packet->length, iface->name, csp_get_ms()); } -int csp_route_work(void) { - - csp_qfifo_t input; - csp_packet_t * packet; - csp_conn_t * conn; - csp_socket_t * socket; +/* Deliver the packet to a bound callback, if any. Returns true if + * the port is served by a callback and the packet was consumed. */ +static bool csp_route_deliver_callback(csp_iface_t * iface, csp_packet_t * packet) { -#if (CSP_USE_RDP) - /* Check connection timeouts (currently only for RDP) */ - csp_conn_check_timeouts(); -#endif - - /* Get next packet to route */ - if (csp_qfifo_read(&input) != CSP_ERR_NONE) { - return CSP_ERR_TIMEDOUT; - } - - packet = input.packet; - if (packet == NULL) { - return CSP_ERR_TIMEDOUT; + csp_callback_t callback = csp_port_get_callback(packet->id.dport); + if (callback == NULL) { + return false; } - csp_input_hook(input.iface, packet); - - /* Count the message */ - input.iface->rx++; - input.iface->rxbytes += packet->length; - - /* The packet is to me, if the address matches that of any interface, - * or the address matches the broadcast address of the incoming interface */ - int is_to_me = ((csp_iflist_get_by_addr(packet->id.dst) != NULL || - (csp_id_is_broadcast(packet->id.dst, input.iface))) || - (csp_addr_is_alias(packet->id.dst))); - - /* Deduplication */ - if ((csp_conf.dedup == CSP_DEDUP_ALL) || - (is_to_me && (csp_conf.dedup == CSP_DEDUP_INCOMING)) || - ((!is_to_me) && (csp_conf.dedup == CSP_DEDUP_FWD))) { - if (csp_dedup_is_duplicate(packet)) { - /* Discard packet */ - input.iface->drop++; - csp_buffer_free(packet); - return CSP_ERR_NONE; - } + if (csp_route_security_check(CSP_SO_CRC32REQ, iface, packet) != CSP_ERR_NONE) { + csp_buffer_free(packet); + return true; } - /* Here there be promiscuous mode */ -#if (CSP_USE_PROMISC) - csp_promisc_add(packet); -#endif - - /* If the message is not to me, route the message to the correct interface */ - if (!is_to_me) { - - /* Otherwise, actually send the message */ - csp_send_direct(&packet->id, packet, input.iface); - return CSP_ERR_NONE; + callback(packet); + return true; +} - } +/* Deliver the packet to a connection-less socket. The socket is the + * endpoint, so delivery terminates here. */ +static void csp_route_deliver_conn_less(csp_socket_t * socket, csp_iface_t * iface, csp_packet_t * packet) { - /* Discard packets with unsupported options */ - if (csp_route_check_options(input.iface, packet) != CSP_ERR_NONE) { + if (csp_route_security_check(socket->opts, iface, packet) != CSP_ERR_NONE) { csp_buffer_free(packet); - return CSP_ERR_NONE; + return; } - /** - * Callbacks - */ - csp_callback_t callback = csp_port_get_callback(packet->id.dport); - if (callback) { - - if (csp_route_security_check(CSP_SO_CRC32REQ, input.iface, packet) != CSP_ERR_NONE) { - csp_buffer_free(packet); - return CSP_ERR_NONE; - } - - callback(packet); - return CSP_ERR_NONE; + if (csp_queue_enqueue(socket->rx_queue, &packet, 0) != CSP_QUEUE_OK) { + csp_dbg_conn_ovf++; + csp_buffer_free(packet); + return; } +} - /** - * Sockets - */ - - /* The message is to me, search for incoming socket */ - socket = csp_port_get_socket(packet->id.dport); - - /* If the socket is connection-less, deliver now */ - if (socket && csp_socket_is_conn_less(socket)) { - - if (csp_route_security_check(socket->opts, input.iface, packet) != CSP_ERR_NONE) { - csp_buffer_free(packet); - return CSP_ERR_NONE; - } - - if (csp_queue_enqueue(socket->rx_queue, &packet, 0) != CSP_QUEUE_OK) { - csp_dbg_conn_ovf++; - csp_buffer_free(packet); - return CSP_ERR_NONE; - } - - return CSP_ERR_NONE; - } +/* Deliver the packet to a connection. The connection is the + * endpoint: an existing connection is matched by identifier (client + * or server side), otherwise the socket acts as the factory for a + * new server-side connection, which is posted to the socket rx_queue + * for csp_accept(). */ +static void csp_route_deliver_connection(csp_socket_t * socket, csp_iface_t * iface, csp_packet_t * packet) { /* Search for an existing connection */ - conn = csp_conn_find_existing(&packet->id); + csp_conn_t * conn = csp_conn_find_existing(&packet->id); /* If this is an incoming packet on a new connection */ if (conn == NULL) { @@ -223,13 +159,13 @@ int csp_route_work(void) { /* Reject packet if no matching socket is found */ if (!socket) { csp_buffer_free(packet); - return CSP_ERR_NONE; + return; } /* Run security check on incoming packet */ - if (csp_route_security_check(socket->opts, input.iface, packet) != CSP_ERR_NONE) { + if (csp_route_security_check(socket->opts, iface, packet) != CSP_ERR_NONE) { csp_buffer_free(packet); - return CSP_ERR_NONE; + return; } /* New incoming connection accepted */ @@ -247,7 +183,7 @@ int csp_route_work(void) { if (!conn) { csp_dbg_conn_out++; csp_buffer_free(packet); - return CSP_ERR_NONE; + return; } /* Store the socket queue and options */ @@ -258,9 +194,9 @@ int csp_route_work(void) { } else { /* Run security check on incoming packet */ - if (csp_route_security_check(conn->opts, input.iface, packet) != CSP_ERR_NONE) { + if (csp_route_security_check(conn->opts, iface, packet) != CSP_ERR_NONE) { csp_buffer_free(packet); - return CSP_ERR_NONE; + return; } } @@ -271,7 +207,7 @@ int csp_route_work(void) { if (close_connection) { csp_close(conn); } - return CSP_ERR_NONE; + return; } #endif @@ -279,7 +215,7 @@ int csp_route_work(void) { if (csp_conn_enqueue_packet(conn, packet) != CSP_ERR_NONE) { csp_dbg_conn_ovf++; csp_buffer_free(packet); - return CSP_ERR_NONE; + return; } /* Try to queue up the new connection pointer */ @@ -287,12 +223,96 @@ int csp_route_work(void) { if (csp_queue_enqueue(conn->dest_socket->rx_queue, &conn, 0) != CSP_QUEUE_OK) { csp_dbg_conn_ovf++; csp_close(conn); - return CSP_ERR_NONE; + return; } /* Ensure that this connection will not be posted to this socket again */ conn->dest_socket = NULL; } +} + +int csp_route_work(void) { + + csp_qfifo_t input; + csp_packet_t * packet; + csp_socket_t * socket; + +#if (CSP_USE_RDP) + /* Check connection timeouts (currently only for RDP) */ + csp_conn_check_timeouts(); +#endif + + /* Get next packet to route */ + if (csp_qfifo_read(&input) != CSP_ERR_NONE) { + return CSP_ERR_TIMEDOUT; + } + + packet = input.packet; + if (packet == NULL) { + return CSP_ERR_TIMEDOUT; + } + + csp_input_hook(input.iface, packet); + + /* Count the message */ + input.iface->rx++; + input.iface->rxbytes += packet->length; + + /* The packet is to me, if the address matches that of any interface, + * or the address matches the broadcast address of the incoming interface */ + int is_to_me = ((csp_iflist_get_by_addr(packet->id.dst) != NULL || + (csp_id_is_broadcast(packet->id.dst, input.iface))) || + (csp_addr_is_alias(packet->id.dst))); + + /* Deduplication */ + if ((csp_conf.dedup == CSP_DEDUP_ALL) || + (is_to_me && (csp_conf.dedup == CSP_DEDUP_INCOMING)) || + ((!is_to_me) && (csp_conf.dedup == CSP_DEDUP_FWD))) { + if (csp_dedup_is_duplicate(packet)) { + /* Discard packet */ + input.iface->drop++; + csp_buffer_free(packet); + return CSP_ERR_NONE; + } + } + + /* Here there be promiscuous mode */ +#if (CSP_USE_PROMISC) + csp_promisc_add(packet); +#endif + + /* If the message is not to me, route the message to the correct interface */ + if (!is_to_me) { + + /* Otherwise, actually send the message */ + csp_send_direct(&packet->id, packet, input.iface); + return CSP_ERR_NONE; + + } + + /* Discard packets with unsupported options */ + if (csp_route_check_options(input.iface, packet) != CSP_ERR_NONE) { + csp_buffer_free(packet); + return CSP_ERR_NONE; + } + + /* Callback delivery */ + if (csp_route_deliver_callback(input.iface, packet)) { + return CSP_ERR_NONE; + } + + /* Socket delivery */ + socket = csp_port_get_socket(packet->id.dport); + + /* If the socket is connection-less, deliver now */ + if (socket && csp_socket_is_conn_less(socket)) { + /* Connection-less delivery, the socket is the endpoint */ + csp_route_deliver_conn_less(socket, input.iface, packet); + return CSP_ERR_NONE; + } + /* Connection-oriented delivery, a connection is the endpoint and + * the socket, when present, only accepts new connections */ + csp_route_deliver_connection(socket, input.iface, packet); return CSP_ERR_NONE; } From addcf145a837efe41d72a6988465e244a033d1fe Mon Sep 17 00:00:00 2001 From: Yasushi SHOJI Date: Mon, 10 Aug 2026 20:04:56 +0900 Subject: [PATCH 24/24] csp_route: Hoist endpoint resolution and security check The security check was duplicated in every delivery path, and each path had to know which endpoint type it was handling before the packet had even been validated. Resolve the endpoint first: an existing connection, or the bound socket. Run csp_route_security_check() once with the resolved endpoint options, then branch on connection-less versus connection-oriented delivery. This makes the dual endpoint types of libcsp explicit: a connection-less socket is an endpoint itself, while connection-oriented traffic terminates in a connection. A connection-less socket port never matches an existing connection, because connections use ephemeral or server-side port pairs, so the lookup order does not change behavior. No functionality has been changed. Assisted-by: GitHub Copilot CLI:claude-fable-5 Signed-off-by: Yasushi SHOJI --- src/csp_route.c | 75 ++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/src/csp_route.c b/src/csp_route.c index 7bdee47fc..ee83da74e 100644 --- a/src/csp_route.c +++ b/src/csp_route.c @@ -129,12 +129,7 @@ static bool csp_route_deliver_callback(csp_iface_t * iface, csp_packet_t * packe /* Deliver the packet to a connection-less socket. The socket is the * endpoint, so delivery terminates here. */ -static void csp_route_deliver_conn_less(csp_socket_t * socket, csp_iface_t * iface, csp_packet_t * packet) { - - if (csp_route_security_check(socket->opts, iface, packet) != CSP_ERR_NONE) { - csp_buffer_free(packet); - return; - } +static void csp_route_deliver_conn_less(csp_socket_t * socket, csp_packet_t * packet) { if (csp_queue_enqueue(socket->rx_queue, &packet, 0) != CSP_QUEUE_OK) { csp_dbg_conn_ovf++; @@ -144,30 +139,15 @@ static void csp_route_deliver_conn_less(csp_socket_t * socket, csp_iface_t * ifa } /* Deliver the packet to a connection. The connection is the - * endpoint: an existing connection is matched by identifier (client - * or server side), otherwise the socket acts as the factory for a - * new server-side connection, which is posted to the socket rx_queue - * for csp_accept(). */ -static void csp_route_deliver_connection(csp_socket_t * socket, csp_iface_t * iface, csp_packet_t * packet) { - - /* Search for an existing connection */ - csp_conn_t * conn = csp_conn_find_existing(&packet->id); + * endpoint: an existing connection, when given, was matched by + * identifier (client or server side), otherwise the socket acts as + * the factory for a new server-side connection, which is posted to + * the socket rx_queue for csp_accept(). */ +static void csp_route_deliver_connection(csp_conn_t * conn, csp_socket_t * socket, csp_packet_t * packet) { /* If this is an incoming packet on a new connection */ if (conn == NULL) { - /* Reject packet if no matching socket is found */ - if (!socket) { - csp_buffer_free(packet); - return; - } - - /* Run security check on incoming packet */ - if (csp_route_security_check(socket->opts, iface, packet) != CSP_ERR_NONE) { - csp_buffer_free(packet); - return; - } - /* New incoming connection accepted */ csp_id_t idout; idout.pri = packet->id.pri; @@ -189,15 +169,6 @@ static void csp_route_deliver_connection(csp_socket_t * socket, csp_iface_t * if /* Store the socket queue and options */ conn->dest_socket = socket; conn->opts = socket->opts; - - /* Packet to existing connection */ - } else { - - /* Run security check on incoming packet */ - if (csp_route_security_check(conn->opts, iface, packet) != CSP_ERR_NONE) { - csp_buffer_free(packet); - return; - } } #if (CSP_USE_RDP) @@ -304,15 +275,35 @@ int csp_route_work(void) { /* Socket delivery */ socket = csp_port_get_socket(packet->id.dport); - /* If the socket is connection-less, deliver now */ - if (socket && csp_socket_is_conn_less(socket)) { - /* Connection-less delivery, the socket is the endpoint */ - csp_route_deliver_conn_less(socket, input.iface, packet); + /* Resolve the endpoint: an existing connection first, otherwise + * the bound socket. Drop the packet if neither exists. */ + csp_conn_t * conn = csp_conn_find_existing(&packet->id); + + if (conn == NULL && socket == NULL) { + csp_buffer_free(packet); return CSP_ERR_NONE; } - /* Connection-oriented delivery, a connection is the endpoint and - * the socket, when present, only accepts new connections */ - csp_route_deliver_connection(socket, input.iface, packet); + /* Security check with the endpoint options */ + uint32_t opts = conn ? conn->opts : socket->opts; + if (csp_route_security_check(opts, input.iface, packet) != CSP_ERR_NONE) { + csp_buffer_free(packet); + return CSP_ERR_NONE; + } + + /* The packet itself does not tell connection-less from + * connection-oriented; only the bound socket does. Without a + * conn-less socket, the connection pool is the last possible + * match. */ + if (socket != NULL && csp_socket_is_conn_less(socket)) { + /* Connection-less delivery, the socket is the endpoint */ + csp_route_deliver_conn_less(socket, packet); + } else { + /* Connection-oriented delivery, a connection is the + * endpoint and the socket, when present, only accepts new + * connections */ + csp_route_deliver_connection(conn, socket, packet); + } + return CSP_ERR_NONE; }