The FreeRADIUS server $Id: f3670dba8951ca10eb4948feb3dc3db9423a334f $
Loading...
Searching...
No Matches
bio_retry_tests.c
Go to the documentation of this file.
1/*
2 * This library is free software; you can redistribute it and/or
3 * modify it under the terms of the GNU Lesser General Public
4 * License as published by the Free Software Foundation; either
5 * version 2.1 of the License, or (at your option) any later version.
6 *
7 * This library is distributed in the hope that it will be useful,
8 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10 * Lesser General Public License for more details.
11 *
12 * You should have received a copy of the GNU Lesser General Public
13 * License along with this library; if not, write to the Free Software
14 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
15 */
16
17/** Tests for the retry bio running out of entries
18 *
19 * @file src/lib/bio/test/bio_retry_tests.c
20 *
21 * @copyright 2026 Network RADIUS SAS (legal@networkradius.com)
22 */
23#include <freeradius-devel/util/test/acutest_common_init.h>
24#include <freeradius-devel/util/test/acutest_helpers.h>
25
26#define _BIO_PRIVATE 1
27#include <freeradius-devel/bio/bio_priv.h>
28#include <freeradius-devel/bio/null.h>
29#include <freeradius-devel/bio/retry.h>
30
31static int blocked_count;
32static int resume_count;
33static int release_count;
35
36static fr_bio_t *resume_write_bio; //!< when set, cb_write_resume() writes packet2 to this bio
38
39static uint8_t packet1[4] = { 1, 2, 3, 4 };
40static uint8_t packet2[4] = { 5, 6, 7, 8 };
41
42static int cb_noop(fr_bio_t *bio) { (void) bio; return 0; }
43static void cb_noop_void(fr_bio_t *bio) { (void) bio; }
44static int cb_write_blocked(fr_bio_t *bio) { (void) bio; blocked_count++; return 1; }
45
46static int cb_write_resume(fr_bio_t *bio)
47{
48 (void) bio;
50
52
53 return 1;
54}
55
58 .write_resume = cb_write_resume,
59 .read_blocked = cb_noop,
60 .write_blocked = cb_write_blocked,
61 .eof = cb_noop_void,
62};
63
64static void retry_sent(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, UNUSED const void *buffer, UNUSED size_t size,
65 fr_bio_retry_entry_t *retry_ctx)
66{
67 saved_item = retry_ctx;
68}
69
71 UNUSED const void *buffer, UNUSED size_t size)
72{
73 return false;
74}
75
81
82static size_t stub_bytes; //!< bytes of packet data which the stub has accepted
83
84/** A transport which accepts every write in full, and never has anything to read.
85 */
86static ssize_t stub_write_all(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, void const *buffer, size_t size)
87{
88 if (!buffer) return 0; /* nothing to flush */
89
90 stub_bytes += size;
91 return size;
92}
93
94/** A transport which accepts one byte of each write, so the retry bio saves the rest of the packet.
95 */
96static ssize_t stub_write_one(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, void const *buffer, UNUSED size_t size)
97{
98 if (!buffer) return 0;
99
100 stub_bytes++;
101 return 1;
102}
103
104static fr_bio_t *stub_alloc(TALLOC_CTX *ctx)
105{
106 fr_bio_common_t *my;
107
108 my = talloc_zero(ctx, fr_bio_common_t);
109 if (!my) return NULL;
110
111 my->bio.read = fr_bio_null_read;
112 my->bio.write = stub_write_all;
113
114 return &my->bio;
115}
116
117/** Allocate a retry bio with one entry, in front of a stub.
118 *
119 * Only test_partial_retransmit() runs the timer list, so no timer fires in the other tests.
120 */
121static fr_bio_t *test_retry_alloc_full(TALLOC_CTX *ctx, fr_bio_retry_config_t *cfg, fr_time_delta_t mrd, size_t max_saved)
122{
123 fr_bio_t *retry, *stub;
124
126 stub_bytes = 0;
127 saved_item = NULL;
128 resume_write_bio = NULL;
130
131 cfg->el = fr_event_list_alloc(ctx, NULL, NULL);
132 if (!cfg->el) return NULL;
133
136 .mrt = fr_time_delta_from_sec(16),
137 .mrd = mrd,
138 .mrc = 5,
139 };
140
141 stub = stub_alloc(ctx);
142 if (!stub) return NULL;
143
144 retry = fr_bio_retry_alloc(ctx, max_saved, retry_sent, retry_response, NULL, retry_release, cfg, stub);
145 if (!retry) return NULL;
146
147 fr_bio_cb_set(retry, &test_cb);
148
149 return retry;
150}
151
153{
154 return test_retry_alloc_full(ctx, cfg, mrd, 1);
155}
156
157static fr_bio_t *test_retry_alloc(TALLOC_CTX *ctx, fr_bio_retry_config_t *cfg)
158{
159 return test_retry_alloc_mrd(ctx, cfg, fr_time_delta_from_sec(30));
160}
161
162/** Running out of entries blocks writes, and freeing an entry resumes writes.
163 *
164 * See finding 1 in retry.md. Only fr_bio_retry_write_resume() clears write_blocked, and
165 * fr_bio_retry_write_resume() runs only when the socket becomes writable. If running out of
166 * entries set write_blocked, then the application would never resume writes.
167 */
168static void test_all_used_resumes(void)
169{
170 TALLOC_CTX *ctx = talloc_init_const("test");
172 fr_bio_t *retry;
173
174 retry = test_retry_alloc(ctx, &cfg);
175 TEST_CHECK(retry != NULL);
176 if (!retry) goto done;
177
178 TEST_CASE("the first packet uses the only entry");
179 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet1, sizeof(packet1)), (int) sizeof(packet1));
180 TEST_CHECK(saved_item != NULL);
181 if (!saved_item) goto done;
182
183 TEST_CASE("the second write returns IO_WOULD_BLOCK, and the retry bio calls the write_blocked callback");
184 TEST_CHECK(fr_bio_write(retry, NULL, packet2, sizeof(packet2)) == fr_bio_error(IO_WOULD_BLOCK));
186
187 TEST_CASE("running out of entries does not set write_blocked");
188 TEST_CHECK(!fr_bio_retry_info(retry)->write_blocked);
189
190 TEST_CASE("cancelling the entry calls the release callback and the resume callback");
194
195 TEST_CASE("the retry bio now accepts the second packet");
196 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet2, sizeof(packet2)), (int) sizeof(packet2));
197
198done:
199 talloc_free(ctx);
200}
201
202/** The application may write from inside the resume callback, so fr_bio_retry_release() must put
203 * the freed entry on the free list before calling the resume callback.
204 */
205static void test_resume_can_write(void)
206{
207 TALLOC_CTX *ctx = talloc_init_const("test");
209 fr_bio_t *retry;
210
211 retry = test_retry_alloc(ctx, &cfg);
212 TEST_CHECK(retry != NULL);
213 if (!retry) goto done;
214
215 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet1, sizeof(packet1)), (int) sizeof(packet1));
216 TEST_CHECK(fr_bio_write(retry, NULL, packet2, sizeof(packet2)) == fr_bio_error(IO_WOULD_BLOCK));
217 if (!saved_item) goto done;
218
219 TEST_CASE("a write from inside the resume callback succeeds");
220 resume_write_bio = retry;
223 TEST_CHECK_RET((int) resume_write_rcode, (int) sizeof(packet2));
224
225 TEST_CASE("the retry bio did not call the write_blocked callback a second time");
227
228done:
229 talloc_free(ctx);
230}
231
232/** The retry bio saves the rest of a partly written retransmission once, and a flush sends the rest once.
233 *
234 * See finding 2 in retry.md. Before the fix, fr_bio_retry_rewrite() saved the rest of the packet,
235 * and then fr_bio_retry_write_item() saved the rest of the packet again.
236 */
237static void test_partial_retransmit(void)
238{
239 TALLOC_CTX *ctx = talloc_init_const("test");
241 fr_bio_t *retry, *stub;
242 fr_time_t when;
243
244 retry = test_retry_alloc(ctx, &cfg);
245 TEST_CHECK(retry != NULL);
246 if (!retry) goto done;
247
248 stub = fr_bio_next(retry);
249
250 TEST_CASE("the stub accepts the first transmission in full");
251 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet1, sizeof(packet1)), (int) sizeof(packet1));
252 TEST_CHECK_RET((int) stub_bytes, (int) sizeof(packet1));
253
254 TEST_CASE("the retransmission timer fires, and the stub accepts one byte");
255 TEST_MSG("the initial retransmission time (irt) is 2s, so running the timer list 3s ahead fires exactly one retransmission");
256 stub->write = stub_write_one;
258 (void) fr_timer_list_run(cfg.el->tl, &when);
259 TEST_CHECK_RET((int) stub_bytes, (int) sizeof(packet1) + 1);
260
261 TEST_CASE("the retry bio has saved the rest of the packet and has blocked writes");
262 TEST_CHECK(fr_bio_retry_info(retry)->write_blocked);
263
264 TEST_CASE("a flush sends the rest of the packet exactly once");
265 stub->write = stub_write_all;
266 (void) fr_bio_write(retry, NULL, NULL, SIZE_MAX);
267 TEST_CHECK_RET((int) stub_bytes, (int) (2 * sizeof(packet1)));
268 TEST_CHECK(!fr_bio_retry_info(retry)->write_blocked);
269
270done:
271 talloc_free(ctx);
272}
273
274/** When the retry bio cannot save the rest of a partly written packet, the retry bio releases the entry of the packet.
275 *
276 * See 'A related defect: a failed save kept the entry' in retry.md. Without the release, the entry
277 * stays on both timer lists after the sent() callback has passed the entry to the application. The
278 * write returns OOM, so an application which then frees the packet leaves the retry bio holding a
279 * pointer to freed memory.
280 */
282{
283 TALLOC_CTX *ctx = talloc_init_const("test");
285 fr_bio_t *retry, *stub;
286
287 retry = test_retry_alloc(ctx, &cfg);
288 TEST_CHECK(retry != NULL);
289 if (!retry) goto done;
290
291 stub = fr_bio_next(retry);
292
293 TEST_CASE("a partial write returns OOM when the retry bio cannot allocate the buffer for the rest of the packet");
294 TEST_MSG("a talloc memory limit on the retry bio makes the buffer allocation fail");
295DIAG_OFF(deprecated-declarations)
296 TEST_CHECK(talloc_set_memlimit(retry, talloc_total_size(retry)) == 0);
297DIAG_ON(deprecated-declarations)
298 stub->write = stub_write_one;
299 TEST_CHECK(fr_bio_write(retry, NULL, packet1, sizeof(packet1)) == fr_bio_error(OOM));
300
301 TEST_CASE("the retry bio releases the entry");
303
304 TEST_CASE("the only entry is free again, so the retry bio accepts the next packet");
305DIAG_OFF(deprecated-declarations)
306 TEST_CHECK(talloc_set_memlimit(retry, 0) == 0);
307DIAG_ON(deprecated-declarations)
308 stub->write = stub_write_all;
309 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet2, sizeof(packet2)), (int) sizeof(packet2));
311
312done:
313 talloc_free(ctx);
314}
315
316/** Blocking writes expires an overdue entry, and the expiry sees writes as blocked.
317 *
318 * See finding 1 in retry.md. Arming the expiry list runs any expiry which is already due, and
319 * fr_bio_retry_expiry_timer() asserts that writes are blocked. fr_bio_retry_write_blocked() used to
320 * set the flag only after arming the list.
321 */
323{
324 TALLOC_CTX *ctx = talloc_init_const("test");
326 fr_bio_t *retry;
327 fr_bio_common_t *common;
328 fr_time_t until;
329
330 /*
331 * A maximum duration of one microsecond makes the entry overdue as soon as the entry is
332 * written. fr_bio_retry_alloc() copies the retry configuration, so the duration has to be set
333 * before the bio is allocated.
334 */
335 retry = test_retry_alloc_mrd(ctx, &cfg, fr_time_delta_from_usec(1));
336 TEST_CHECK(retry != NULL);
337 if (!retry) goto done;
338
339 TEST_CASE("the packet is written");
340 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet1, sizeof(packet1)), (int) sizeof(packet1));
341
342 /*
343 * Make sure that the maximum duration has passed. The wait is a few microseconds, so a busy
344 * loop is enough, and the result does not depend on how fast the test runs.
345 */
347 while (fr_time_lt(fr_time(), until)) { /* nothing */ }
348
349 TEST_CASE("blocking writes runs the overdue expiry, which releases the entry");
350 common = (fr_bio_common_t *) retry;
351 TEST_CHECK(common->priv_cb.write_blocked != NULL);
352 if (!common->priv_cb.write_blocked) goto done;
353
354 TEST_CHECK_RET(common->priv_cb.write_blocked(retry), 1);
356 TEST_CHECK(fr_bio_retry_info(retry)->write_blocked);
357
358 TEST_CASE("a released entry while blocked does not resume writes");
360
361done:
362 talloc_free(ctx);
363}
364
365/** When one retransmission blocks writes, the other retransmissions due in the same pass wait.
366 *
367 * See finding 1 in retry.md. fr_bio_retry_write_blocked() disarms the retry list, and the timer
368 * list used to keep running the other entries which were due in the same pass.
369 */
371{
372 TALLOC_CTX *ctx = talloc_init_const("test");
374 fr_bio_t *retry, *stub;
375 fr_time_t when;
376
377 retry = test_retry_alloc_full(ctx, &cfg, fr_time_delta_from_sec(30), 2);
378 TEST_CHECK(retry != NULL);
379 if (!retry) goto done;
380
381 stub = fr_bio_next(retry);
382
383 TEST_CASE("two packets are written in full");
384 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet1, sizeof(packet1)), (int) sizeof(packet1));
385 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet2, sizeof(packet2)), (int) sizeof(packet2));
386
387 TEST_CASE("both retransmissions are due, and the first one blocks writes");
388 TEST_MSG("irt is 2s, so running the timer list 3s ahead makes both retransmissions due");
389 stub->write = stub_write_one;
391 (void) fr_timer_list_run(cfg.el->tl, &when);
392
393 TEST_CASE("only the first retransmission was written, and writes are blocked");
394 TEST_CHECK_RET((int) stub_bytes, (int) (sizeof(packet1) + sizeof(packet2) + 1));
395 TEST_CHECK(fr_bio_retry_info(retry)->write_blocked);
396
397done:
398 talloc_free(ctx);
399}
400
402 { "all_used_resumes", test_all_used_resumes },
403 { "resume_can_write", test_resume_can_write },
404 { "partial_retransmit", test_partial_retransmit },
405 { "partial_oom_releases", test_partial_oom_releases },
406 { "block_expires_overdue", test_block_expires_overdue },
407 { "block_stops_retries", test_block_stops_retries },
409};
static int const char char buffer[256]
Definition acutest.h:635
#define TEST_CHECK(cond)
Definition acutest.h:101
#define TEST_CASE(name)
Definition acutest.h:203
#define TEST_TERMINATOR
Definition acutest.h:77
#define TEST_MSG(...)
Definition acutest.h:234
#define TEST_CHECK_RET(_got, _exp)
fr_bio_write_t _CONST write
write to the underlying bio
Definition base.h:117
static ssize_t fr_bio_write(fr_bio_t *bio, void *packet_ctx, void const *buffer, size_t size)
Write raw data to a bio.
Definition base.h:184
static fr_bio_t * fr_bio_next(fr_bio_t *bio)
Definition base.h:131
fr_bio_io_t read_resume
"unblocked" is too similar to "blocked"
Definition base.h:97
#define fr_bio_error(_x)
Definition base.h:200
fr_bio_t * fr_bio_retry_alloc(TALLOC_CTX *ctx, size_t max_saved, fr_bio_retry_sent_t sent, fr_bio_retry_response_t response, fr_bio_retry_rewrite_t rewrite, fr_bio_retry_release_t release, fr_bio_retry_config_t const *cfg, fr_bio_t *next)
Allocate a fr_bio_retry_t.
Definition retry.c:899
int fr_bio_retry_entry_cancel(fr_bio_t *bio, fr_bio_retry_entry_t *item)
Cancel one item.
Definition retry.c:815
fr_bio_retry_info_t const * fr_bio_retry_info(fr_bio_t *bio)
Definition retry.c:986
fr_retry_config_t retry_config
base retry config
Definition retry.h:47
fr_bio_retry_release_reason_t
Definition retry.h:82
fr_event_list_t * el
event list
Definition retry.h:45
Definition retry.c:63
static int packet_ctx
the tests pass only the address of packet_ctx
static bool retry_response(UNUSED fr_bio_t *bio, UNUSED fr_bio_retry_entry_t **item_p, UNUSED void *packet_ctx, UNUSED const void *buffer, UNUSED size_t size)
TEST_LIST
static void test_partial_oom_releases(void)
When the retry bio cannot save the rest of a partly written packet, the retry bio releases the entry ...
static int release_count
static int cb_write_blocked(fr_bio_t *bio)
static fr_bio_t * test_retry_alloc_full(TALLOC_CTX *ctx, fr_bio_retry_config_t *cfg, fr_time_delta_t mrd, size_t max_saved)
Allocate a retry bio with one entry, in front of a stub.
static void test_resume_can_write(void)
The application may write from inside the resume callback, so fr_bio_retry_release() must put the fre...
static int cb_write_resume(fr_bio_t *bio)
static int blocked_count
static int resume_count
static void cb_noop_void(fr_bio_t *bio)
static fr_bio_cb_funcs_t test_cb
static uint8_t packet2[4]
static fr_bio_t * test_retry_alloc_mrd(TALLOC_CTX *ctx, fr_bio_retry_config_t *cfg, fr_time_delta_t mrd)
static ssize_t stub_write_one(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, void const *buffer, UNUSED size_t size)
A transport which accepts one byte of each write, so the retry bio saves the rest of the packet.
static void test_partial_retransmit(void)
The retry bio saves the rest of a partly written retransmission once, and a flush sends the rest once...
static uint8_t packet1[4]
static fr_bio_t * resume_write_bio
when set, cb_write_resume() writes packet2 to this bio
static void test_block_expires_overdue(void)
Blocking writes expires an overdue entry, and the expiry sees writes as blocked.
static fr_bio_t * test_retry_alloc(TALLOC_CTX *ctx, fr_bio_retry_config_t *cfg)
static fr_bio_retry_entry_t * saved_item
static ssize_t stub_write_all(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, void const *buffer, size_t size)
A transport which accepts every write in full, and never has anything to read.
static fr_bio_t * stub_alloc(TALLOC_CTX *ctx)
static void retry_release(UNUSED fr_bio_t *bio, UNUSED fr_bio_retry_entry_t *retry_ctx, UNUSED fr_bio_retry_release_reason_t reason)
static void test_block_stops_retries(void)
When one retransmission blocks writes, the other retransmissions due in the same pass wait.
static void test_all_used_resumes(void)
Running out of entries blocks writes, and freeing an entry resumes writes.
static size_t stub_bytes
bytes of packet data which the stub has accepted
static void retry_sent(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, UNUSED const void *buffer, UNUSED size_t size, fr_bio_retry_entry_t *retry_ctx)
static ssize_t resume_write_rcode
static int cb_noop(fr_bio_t *bio)
#define DIAG_ON(_x)
Definition build.h:535
#define UNUSED
Definition build.h:384
#define DIAG_OFF(_x)
Definition build.h:534
talloc_free(hp)
void fr_bio_cb_set(fr_bio_t *bio, fr_bio_cb_funcs_t const *cb)
Definition base.c:260
#define fr_time()
Definition event.c:60
fr_event_list_t * fr_event_list_alloc(TALLOC_CTX *ctx, fr_event_status_cb_t status, void *status_uctx)
Initialise a new event list.
Definition event.c:2553
long int ssize_t
unsigned char uint8_t
ssize_t fr_bio_null_read(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, UNUSED void *buffer, UNUSED size_t size)
Always return 0 on read.
Definition null.c:31
static bool done
Definition radclient.c:80
static TALLOC_CTX * talloc_init_const(char const *name)
Allocate a top level chunk with a constant name.
Definition talloc.h:127
static fr_time_delta_t fr_time_delta_from_sec(int64_t sec)
Definition time.h:590
#define fr_time_add(_a, _b)
Add a time/time delta together.
Definition time.h:196
static fr_time_delta_t fr_time_delta_from_usec(int64_t usec)
Definition time.h:568
#define fr_time_lt(_a, _b)
Definition time.h:239
A time delta, a difference in time measured in nanoseconds.
Definition time.h:80
"server local" time.
Definition time.h:69
int fr_timer_list_run(fr_timer_list_t *tl, fr_time_t *when)
Execute any pending events in the event loop.
Definition timer.c:934
fr_time_delta_t irt
Initial transmission time.
Definition retry.h:33