Skip to content

Commit f2ecb07

Browse files
Restrict TX emission to a runtime-available interface bitmap
Add udpard_tx_t.iface_bitmap, a runtime-mutable subset of UDPARD_IFACE_BITMAP_ALL naming the locally available interfaces; udpard_tx_new() gains a matching parameter (breaking change). Each push intersects the requested iface set with it, so transfer replicas are no longer enqueued for interfaces the application cannot drain (where they previously expired silently). Zero means listen-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent aa11d98 commit f2ecb07

10 files changed

Lines changed: 194 additions & 41 deletions

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Please read `README.md` for general information about LibUDPard, and `CONTRIBUTING.md` for development-related notes.
44

5-
Keep the code and comments very brief. Be sure every significant code block is preceded with a brief comment.
5+
DO NOT COMMENT THE CODE unless comments add critical information that is impossible to infer from reading the code (design rationale, gotchas, etc), in which case extremely terse comments are allowed.
66

77
If you need a build directory, create one in the project root named with a `build` prefix;
88
you can also use existing build directories if you prefer so,

libudpard/udpard.c

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -800,9 +800,11 @@ static bool tx_push(udpard_tx_t* const tx,
800800
UDPARD_ASSERT(now <= deadline);
801801
UDPARD_ASSERT(tx != NULL);
802802

803-
const uint16_t iface_bitmap = valid_ep_bitmap(endpoints);
803+
uint16_t iface_bitmap = valid_ep_bitmap(endpoints);
804804
UDPARD_ASSERT((iface_bitmap & UDPARD_IFACE_BITMAP_ALL) != 0);
805805
UDPARD_ASSERT((iface_bitmap & UDPARD_IFACE_BITMAP_ALL) == iface_bitmap);
806+
iface_bitmap &= tx->iface_bitmap;
807+
UDPARD_ASSERT(iface_bitmap != 0U);
806808

807809
// Purge expired transfers before accepting a new one to make room in the queue.
808810
tx_purge_expired_transfers(tx, now);
@@ -902,18 +904,20 @@ bool udpard_tx_new(udpard_tx_t* const self,
902904
const uint64_t local_uid,
903905
const uint64_t unicast_transfer_id_seed,
904906
const size_t enqueued_frames_limit,
907+
const uint16_t iface_bitmap,
905908
const udpard_tx_mem_resources_t memory,
906909
const udpard_tx_vtable_t* const vtable)
907910
{
908-
const bool ok = (NULL != self) && (local_uid != 0) && tx_validate_mem_resources(memory) && (vtable != NULL) &&
909-
(vtable->eject != NULL);
911+
const bool ok = (NULL != self) && (local_uid != 0) && ((iface_bitmap & UDPARD_IFACE_BITMAP_ALL) == iface_bitmap) &&
912+
tx_validate_mem_resources(memory) && (vtable != NULL) && (vtable->eject != NULL);
910913
if (ok) {
911914
mem_zero(sizeof(*self), self);
912915
self->vtable = vtable;
913916
self->local_uid = local_uid;
914917
self->unicast_transfer_id = unicast_transfer_id_seed + local_uid; // extra entropy
915918
self->enqueued_frames_limit = enqueued_frames_limit;
916919
self->enqueued_frames_count = 0;
920+
self->iface_bitmap = iface_bitmap;
917921
self->memory = memory;
918922
self->index_deadline = NULL;
919923
self->agewise = (udpard_list_t){ NULL, NULL };
@@ -941,8 +945,9 @@ bool udpard_tx_push(udpard_tx_t* const self,
941945
{
942946
// Only the head payload fragment is validated; inner fragments of the caller-owned chain are not checked.
943947
bool ok = (self != NULL) && (deadline >= now) && (now >= 0) && (self->local_uid != 0) &&
944-
((iface_bitmap & UDPARD_IFACE_BITMAP_ALL) != 0) && (priority < UDPARD_PRIORITY_COUNT) &&
945-
udpard_is_valid_endpoint(endpoint) && ((payload.bytes.data != NULL) || (payload.bytes.size == 0U));
948+
((iface_bitmap & UDPARD_IFACE_BITMAP_ALL & self->iface_bitmap) != 0) &&
949+
(priority < UDPARD_PRIORITY_COUNT) && udpard_is_valid_endpoint(endpoint) &&
950+
((payload.bytes.data != NULL) || (payload.bytes.size == 0U));
946951
if (ok) {
947952
const meta_t meta = {
948953
.priority = priority,
@@ -971,7 +976,7 @@ bool udpard_tx_push_unicast(udpard_tx_t* const self,
971976
{
972977
// Only the head payload fragment is validated; inner fragments of the caller-owned chain are not checked.
973978
bool ok = (self != NULL) && (deadline >= now) && (now >= 0) && (self->local_uid != 0) &&
974-
(valid_ep_bitmap(endpoints) != 0) && (priority < UDPARD_PRIORITY_COUNT) &&
979+
((valid_ep_bitmap(endpoints) & self->iface_bitmap) != 0) && (priority < UDPARD_PRIORITY_COUNT) &&
975980
((payload.bytes.data != NULL) || (payload.bytes.size == 0U));
976981
if (ok) {
977982
const meta_t meta = {

libudpard/udpard.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,6 @@ typedef struct udpard_tx_mem_resources_t
300300
} udpard_tx_mem_resources_t;
301301

302302
/// Request to transmit a UDP datagram over the specified interface.
303-
/// Which interface indexes are available is determined by the user when pushing the transfer.
304303
/// If Berkeley sockets or similar API is used, the application should use a dedicated socket per redundant interface.
305304
typedef struct udpard_tx_ejection_t
306305
{
@@ -356,6 +355,9 @@ struct udpard_tx_t
356355
/// able to avoid frame duplication and instead reuse each frame across all interfaces.
357356
size_t mtu[UDPARD_IFACE_COUNT_MAX];
358357

358+
/// Transfers are enqueued only on this subset of UDPARD_IFACE_BITMAP_ALL (applied at push); zero = listen-only.
359+
uint16_t iface_bitmap;
360+
359361
/// Optional user-managed mapping from the Cyphal priority level in [0,7] (highest priority at index 0)
360362
/// to the IP DSCP field value for use by the application when transmitting. By default, all entries are zero.
361363
uint_least8_t dscp_value_per_priority[UDPARD_PRIORITY_COUNT];
@@ -403,11 +405,14 @@ struct udpard_tx_t
403405
/// If the limit is reached, the library will apply heuristics to sacrifice some older transfers to make room
404406
/// for the new one. This behavior allows the library to make progress even when some interfaces are stalled.
405407
///
408+
/// iface_bitmap must be a subset of UDPARD_IFACE_BITMAP_ALL (zero = listen-only); see its field docs.
409+
///
406410
/// True on success, false if any of the arguments are invalid.
407411
bool udpard_tx_new(udpard_tx_t* const self,
408412
const uint64_t local_uid,
409413
const uint64_t unicast_transfer_id_seed,
410414
const size_t enqueued_frames_limit,
415+
const uint16_t iface_bitmap,
411416
const udpard_tx_mem_resources_t memory,
412417
const udpard_tx_vtable_t* const vtable);
413418

@@ -425,7 +430,7 @@ bool udpard_tx_new(udpard_tx_t* const self,
425430
/// Excess most significant bits are ignored.
426431
/// Related thread on random transfer-ID init: https://forum.opencyphal.org/t/improve-the-transfer-id-timeout/2375
427432
///
428-
/// The enqueued transfer will be emitted over all interfaces specified in the iface_bitmap.
433+
/// The transfer is emitted over iface_bitmap masked by udpard_tx_t.iface_bitmap; an empty result returns false.
429434
///
430435
/// The user context value is carried through to the callbacks.
431436
///
@@ -448,6 +453,7 @@ bool udpard_tx_push(udpard_tx_t* const self,
448453
/// This is a specialization of the general push function for unicast transfers.
449454
/// The transfer-ID counter is managed automatically.
450455
/// Endpoints may be empty (zero) for some ifaces, in which case no transmission over those ifaces will be attempted.
456+
/// The iface set is also masked by udpard_tx_t.iface_bitmap.
451457
bool udpard_tx_push_unicast(udpard_tx_t* const self,
452458
const udpard_us_t now,
453459
const udpard_us_t deadline,
@@ -466,7 +472,7 @@ void udpard_tx_poll(udpard_tx_t* const self, const udpard_us_t now, const uint16
466472

467473
/// Returns a bitmap of interfaces that have pending transmissions. This is useful for IO multiplexing loops.
468474
/// Zero indicates that there are no pending transmissions.
469-
/// Which interfaces are usable is defined by the remote endpoints provided when pushing transfers.
475+
/// Which interfaces can carry transfers is set at push time by the remote endpoints and udpard_tx_t.iface_bitmap.
470476
uint16_t udpard_tx_pending_ifaces(const udpard_tx_t* const self);
471477

472478
/// When a datagram is ejected and the application opts to keep it, these functions must be used to manage the

tests/src/test_e2e_api.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ void test_subject_roundtrip()
9292
res = instrumented_allocator_make_resource(&tx_alloc_payload);
9393
}
9494
udpard_tx_t tx{};
95-
TEST_ASSERT_TRUE(udpard_tx_new(&tx, 0x1010101010101010ULL, 123U, 32U, tx_mem, &tx_vtable));
95+
TEST_ASSERT_TRUE(udpard_tx_new(&tx, 0x1010101010101010ULL, 123U, 32U, UDPARD_IFACE_BITMAP_ALL, tx_mem, &tx_vtable));
9696
tx.mtu[0] = 256U;
9797
tx.mtu[1] = 256U;
9898
tx.mtu[2] = 256U;

tests/src/test_e2e_edge.cpp

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,13 @@ void test_zero_payload_transfer()
117117

118118
udpard_tx_t tx{};
119119
std::vector<CapturedFrame> frames;
120-
TEST_ASSERT_TRUE(udpard_tx_new(
121-
&tx, 0x1111222233334444ULL, 123U, 8U, make_tx_mem(tx_alloc_transfer, tx_alloc_payload), &tx_vtable));
120+
TEST_ASSERT_TRUE(udpard_tx_new(&tx,
121+
0x1111222233334444ULL,
122+
123U,
123+
8U,
124+
UDPARD_IFACE_BITMAP_ALL,
125+
make_tx_mem(tx_alloc_transfer, tx_alloc_payload),
126+
&tx_vtable));
122127
tx.mtu[0] = 128U;
123128
tx.mtu[1] = 128U;
124129
tx.mtu[2] = 128U;
@@ -182,8 +187,13 @@ void test_out_of_order_multiframe_reassembly()
182187

183188
udpard_tx_t tx{};
184189
std::vector<CapturedFrame> frames;
185-
TEST_ASSERT_TRUE(udpard_tx_new(
186-
&tx, 0xAAAABBBBCCCCDDDDULL, 321U, 32U, make_tx_mem(tx_alloc_transfer, tx_alloc_payload), &tx_vtable));
190+
TEST_ASSERT_TRUE(udpard_tx_new(&tx,
191+
0xAAAABBBBCCCCDDDDULL,
192+
321U,
193+
32U,
194+
UDPARD_IFACE_BITMAP_ALL,
195+
make_tx_mem(tx_alloc_transfer, tx_alloc_payload),
196+
&tx_vtable));
187197
tx.mtu[0] = 96U;
188198
tx.mtu[1] = 96U;
189199
tx.mtu[2] = 96U;
@@ -259,8 +269,13 @@ void test_stateless_single_frame_acceptance()
259269

260270
udpard_tx_t tx{};
261271
std::vector<CapturedFrame> frames;
262-
TEST_ASSERT_TRUE(udpard_tx_new(
263-
&tx, 0x1234123412341234ULL, 777U, 8U, make_tx_mem(tx_alloc_transfer, tx_alloc_payload), &tx_vtable));
272+
TEST_ASSERT_TRUE(udpard_tx_new(&tx,
273+
0x1234123412341234ULL,
274+
777U,
275+
8U,
276+
UDPARD_IFACE_BITMAP_ALL,
277+
make_tx_mem(tx_alloc_transfer, tx_alloc_payload),
278+
&tx_vtable));
264279
tx.mtu[0] = 128U;
265280
tx.mtu[1] = 128U;
266281
tx.mtu[2] = 128U;
@@ -325,8 +340,13 @@ void test_stateless_multiframe_first_frame_handling(const std::size_t extent, co
325340

326341
udpard_tx_t tx{};
327342
std::vector<CapturedFrame> frames;
328-
TEST_ASSERT_TRUE(udpard_tx_new(
329-
&tx, 0x5555666677778888ULL, 999U, 16U, make_tx_mem(tx_alloc_transfer, tx_alloc_payload), &tx_vtable));
343+
TEST_ASSERT_TRUE(udpard_tx_new(&tx,
344+
0x5555666677778888ULL,
345+
999U,
346+
16U,
347+
UDPARD_IFACE_BITMAP_ALL,
348+
make_tx_mem(tx_alloc_transfer, tx_alloc_payload),
349+
&tx_vtable));
330350
tx.mtu[0] = 128U;
331351
tx.mtu[1] = 128U;
332352
tx.mtu[2] = 128U;

tests/src/test_e2e_random.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,13 @@ void test_randomized_deduplication()
118118
instrumented_allocator_new(&tx_alloc_payload);
119119
udpard_tx_t tx{};
120120
std::vector<CapturedFrame> frames;
121-
TEST_ASSERT_TRUE(udpard_tx_new(
122-
&tx, 0x1010101010101010ULL, 123U, 512U, make_tx_mem(tx_alloc_transfer, tx_alloc_payload), &tx_vtable));
121+
TEST_ASSERT_TRUE(udpard_tx_new(&tx,
122+
0x1010101010101010ULL,
123+
123U,
124+
512U,
125+
UDPARD_IFACE_BITMAP_ALL,
126+
make_tx_mem(tx_alloc_transfer, tx_alloc_payload),
127+
&tx_vtable));
123128
tx.mtu[0] = 192U;
124129
tx.mtu[1] = 192U;
125130
tx.mtu[2] = 192U;

tests/src/test_e2e_responses.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,13 @@ void test_unicast_response_roundtrip()
108108
instrumented_allocator_new(&b_tx_payload);
109109
udpard_tx_t b_tx{};
110110
std::vector<CapturedFrame> b_frames;
111-
TEST_ASSERT_TRUE(
112-
udpard_tx_new(&b_tx, 0xBBBBBBBBBBBBBBBBULL, 10U, 16U, make_tx_mem(b_tx_transfer, b_tx_payload), &tx_vtable));
111+
TEST_ASSERT_TRUE(udpard_tx_new(&b_tx,
112+
0xBBBBBBBBBBBBBBBBULL,
113+
10U,
114+
16U,
115+
UDPARD_IFACE_BITMAP_ALL,
116+
make_tx_mem(b_tx_transfer, b_tx_payload),
117+
&tx_vtable));
113118
b_tx.mtu[0] = 256U;
114119
b_tx.mtu[1] = 256U;
115120
b_tx.mtu[2] = 256U;

tests/src/test_integration_sockets.cpp

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,13 @@ void test_reordered_multiframe_delivery()
114114
instrumented_allocator_new(&tx_alloc_payload);
115115
udpard_tx_t tx{};
116116
std::vector<CapturedFrame> frames;
117-
TEST_ASSERT_TRUE(
118-
udpard_tx_new(&tx, 0xAAAAAAAABBBBBBBBULL, 1U, 32U, make_tx_mem(tx_alloc_transfer, tx_alloc_payload), &tx_vtable));
117+
TEST_ASSERT_TRUE(udpard_tx_new(&tx,
118+
0xAAAAAAAABBBBBBBBULL,
119+
1U,
120+
32U,
121+
UDPARD_IFACE_BITMAP_ALL,
122+
make_tx_mem(tx_alloc_transfer, tx_alloc_payload),
123+
&tx_vtable));
119124
tx.mtu[0] = 96U;
120125
tx.mtu[1] = 96U;
121126
tx.mtu[2] = 96U;
@@ -198,10 +203,20 @@ void test_two_publishers()
198203
udpard_tx_t b_tx{};
199204
std::vector<CapturedFrame> a_frames;
200205
std::vector<CapturedFrame> b_frames;
201-
TEST_ASSERT_TRUE(
202-
udpard_tx_new(&a_tx, 0x1111111111111111ULL, 2U, 16U, make_tx_mem(a_tx_transfer, a_tx_payload), &tx_vtable));
203-
TEST_ASSERT_TRUE(
204-
udpard_tx_new(&b_tx, 0x2222222222222222ULL, 3U, 16U, make_tx_mem(b_tx_transfer, b_tx_payload), &tx_vtable));
206+
TEST_ASSERT_TRUE(udpard_tx_new(&a_tx,
207+
0x1111111111111111ULL,
208+
2U,
209+
16U,
210+
UDPARD_IFACE_BITMAP_ALL,
211+
make_tx_mem(a_tx_transfer, a_tx_payload),
212+
&tx_vtable));
213+
TEST_ASSERT_TRUE(udpard_tx_new(&b_tx,
214+
0x2222222222222222ULL,
215+
3U,
216+
16U,
217+
UDPARD_IFACE_BITMAP_ALL,
218+
make_tx_mem(b_tx_transfer, b_tx_payload),
219+
&tx_vtable));
205220
a_tx.mtu[0] = 128U;
206221
a_tx.mtu[1] = 128U;
207222
a_tx.mtu[2] = 128U;

tests/src/test_intrusive_guards.c

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,20 +82,25 @@ static void test_tx_new_guards(void)
8282

8383
// Validate constructor argument checks.
8484
udpard_tx_t tx = { 0 };
85-
TEST_ASSERT_FALSE(udpard_tx_new(NULL, 1U, 1U, 1U, mem_ok, &tx_vtable));
86-
TEST_ASSERT_FALSE(udpard_tx_new(&tx, 0U, 1U, 1U, mem_ok, &tx_vtable));
87-
TEST_ASSERT_FALSE(udpard_tx_new(&tx, 1U, 1U, 1U, mem_ok, NULL));
88-
TEST_ASSERT_FALSE(udpard_tx_new(&tx, 1U, 1U, 1U, mem_ok, &tx_vtable_null_eject));
85+
TEST_ASSERT_FALSE(udpard_tx_new(NULL, 1U, 1U, 1U, UDPARD_IFACE_BITMAP_ALL, mem_ok, &tx_vtable));
86+
TEST_ASSERT_FALSE(udpard_tx_new(&tx, 0U, 1U, 1U, UDPARD_IFACE_BITMAP_ALL, mem_ok, &tx_vtable));
87+
TEST_ASSERT_FALSE(udpard_tx_new(&tx, 1U, 1U, 1U, UDPARD_IFACE_BITMAP_ALL, mem_ok, NULL));
88+
TEST_ASSERT_FALSE(udpard_tx_new(&tx, 1U, 1U, 1U, UDPARD_IFACE_BITMAP_ALL, mem_ok, &tx_vtable_null_eject));
8989

9090
// Reject invalid payload memory resources.
9191
const udpard_mem_vtable_t bad_alloc = { .base = { .free = free_heap }, .alloc = NULL };
9292
const udpard_tx_mem_resources_t mem_bad_payload = {
9393
.transfer = make_mem(transfer_pool),
9494
.payload = { make_mem(payload_pool), { .vtable = &bad_alloc, .context = NULL }, make_mem(payload_pool) },
9595
};
96-
TEST_ASSERT_FALSE(udpard_tx_new(&tx, 1U, 1U, 1U, mem_bad_payload, &tx_vtable));
96+
TEST_ASSERT_FALSE(udpard_tx_new(&tx, 1U, 1U, 1U, UDPARD_IFACE_BITMAP_ALL, mem_bad_payload, &tx_vtable));
9797

98-
TEST_ASSERT_TRUE(udpard_tx_new(&tx, 1U, 1U, 4U, mem_ok, &tx_vtable));
98+
TEST_ASSERT_FALSE(udpard_tx_new(&tx, 1U, 1U, 4U, (uint16_t)(1U << UDPARD_IFACE_COUNT_MAX), mem_ok, &tx_vtable));
99+
TEST_ASSERT_FALSE(udpard_tx_new(&tx, 1U, 1U, 4U, 0xFFFFU, mem_ok, &tx_vtable));
100+
TEST_ASSERT_TRUE(udpard_tx_new(&tx, 1U, 1U, 4U, 0U, mem_ok, &tx_vtable));
101+
TEST_ASSERT_TRUE(udpard_tx_new(&tx, 1U, 1U, 4U, 1U, mem_ok, &tx_vtable));
102+
103+
TEST_ASSERT_TRUE(udpard_tx_new(&tx, 1U, 1U, 4U, UDPARD_IFACE_BITMAP_ALL, mem_ok, &tx_vtable));
99104
udpard_tx_free(&tx);
100105
}
101106

@@ -109,7 +114,7 @@ static void test_tx_push_guards(void)
109114
.payload = { make_mem(payload_pool), make_mem(payload_pool), make_mem(payload_pool) },
110115
};
111116
udpard_tx_t tx = { 0 };
112-
TEST_ASSERT_TRUE(udpard_tx_new(&tx, 1U, 1U, 4U, mem_ok, &tx_vtable));
117+
TEST_ASSERT_TRUE(udpard_tx_new(&tx, 1U, 1U, 4U, UDPARD_IFACE_BITMAP_ALL, mem_ok, &tx_vtable));
113118

114119
// Validate argument checks for subject push.
115120
const udpard_bytes_scattered_t empty_payload = make_scattered("", 0U);
@@ -148,7 +153,7 @@ static void test_tx_push_unicast_guards(void)
148153
.payload = { make_mem(payload_pool), make_mem(payload_pool), make_mem(payload_pool) },
149154
};
150155
udpard_tx_t tx = { 0 };
151-
TEST_ASSERT_TRUE(udpard_tx_new(&tx, 2U, 2U, 4U, mem_ok, &tx_vtable));
156+
TEST_ASSERT_TRUE(udpard_tx_new(&tx, 2U, 2U, 4U, UDPARD_IFACE_BITMAP_ALL, mem_ok, &tx_vtable));
152157

153158
// Validate argument checks for unicast push.
154159
const udpard_bytes_scattered_t empty_payload = make_scattered("", 0U);
@@ -180,7 +185,7 @@ static void test_tx_poll_and_free_guards(void)
180185
.payload = { make_mem(payload_pool), make_mem(payload_pool), make_mem(payload_pool) },
181186
};
182187
udpard_tx_t tx = { 0 };
183-
TEST_ASSERT_TRUE(udpard_tx_new(&tx, 10U, 11U, 4U, mem_ok, &tx_vtable));
188+
TEST_ASSERT_TRUE(udpard_tx_new(&tx, 10U, 11U, 4U, UDPARD_IFACE_BITMAP_ALL, mem_ok, &tx_vtable));
184189
udpard_tx_poll(NULL, 0, UDPARD_IFACE_BITMAP_ALL);
185190
udpard_tx_poll(&tx, -1, UDPARD_IFACE_BITMAP_ALL);
186191
TEST_ASSERT_EQUAL_UINT16(0U, udpard_tx_pending_ifaces(NULL));

0 commit comments

Comments
 (0)