The FreeRADIUS server $Id: a4e7f8c94e8038b7627bcf52356b7ed15955c1d7 $
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#include <freeradius-devel/bio/bio_priv.h>
27#include <freeradius-devel/bio/null.h>
28#include <freeradius-devel/bio/retry.h>
29
30static int blocked_count;
31static int resume_count;
32static int release_count;
34
35static fr_bio_t *resume_write_bio; //!< when set, cb_write_resume() writes packet2 to this bio
37
38static uint8_t packet1[4] = { 1, 2, 3, 4 };
39static uint8_t packet2[4] = { 5, 6, 7, 8 };
40
41static int cb_noop(fr_bio_t *bio) { (void) bio; return 0; }
42static void cb_noop_void(fr_bio_t *bio) { (void) bio; }
43static int cb_write_blocked(fr_bio_t *bio) { (void) bio; blocked_count++; return 1; }
44
45static int cb_write_resume(fr_bio_t *bio)
46{
47 (void) bio;
49
51
52 return 1;
53}
54
57 .write_resume = cb_write_resume,
58 .read_blocked = cb_noop,
59 .write_blocked = cb_write_blocked,
60 .eof = cb_noop_void,
61};
62
63static void retry_sent(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, UNUSED const void *buffer, UNUSED size_t size,
64 fr_bio_retry_entry_t *retry_ctx)
65{
66 saved_item = retry_ctx;
67}
68
70 UNUSED const void *buffer, UNUSED size_t size)
71{
72 return false;
73}
74
80
81static size_t stub_bytes; //!< bytes of packet data which the stub has accepted
82
83/** A transport which accepts every write in full, and never has anything to read.
84 */
85static ssize_t stub_write_all(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, void const *buffer, size_t size)
86{
87 if (!buffer) return 0; /* nothing to flush */
88
89 stub_bytes += size;
90 return size;
91}
92
93/** A transport which accepts one byte of each write, so the retry bio saves the rest of the packet.
94 */
95static ssize_t stub_write_one(UNUSED fr_bio_t *bio, UNUSED void *packet_ctx, void const *buffer, UNUSED size_t size)
96{
97 if (!buffer) return 0;
98
99 stub_bytes++;
100 return 1;
101}
102
103static fr_bio_t *stub_alloc(TALLOC_CTX *ctx)
104{
105 fr_bio_common_t *my;
106
107 my = talloc_zero(ctx, fr_bio_common_t);
108 if (!my) return NULL;
109
110 my->bio.read = fr_bio_null_read;
111 my->bio.write = stub_write_all;
112
113 return &my->bio;
114}
115
116/** Allocate a retry bio with one entry, in front of a stub.
117 *
118 * Only test_partial_retransmit() runs the timer list, so no timer fires in the other tests.
119 */
120static 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)
121{
122 fr_bio_t *retry, *stub;
123
125 stub_bytes = 0;
126 saved_item = NULL;
127 resume_write_bio = NULL;
129
130 cfg->el = fr_event_list_alloc(ctx, NULL, NULL);
131 if (!cfg->el) return NULL;
132
135 .mrt = fr_time_delta_from_sec(16),
136 .mrd = mrd,
137 .mrc = 5,
138 };
139
140 stub = stub_alloc(ctx);
141 if (!stub) return NULL;
142
143 retry = fr_bio_retry_alloc(ctx, max_saved, retry_sent, retry_response, NULL, retry_release, cfg, stub);
144 if (!retry) return NULL;
145
146 fr_bio_cb_set(retry, &test_cb);
147
148 return retry;
149}
150
152{
153 return test_retry_alloc_full(ctx, cfg, mrd, 1);
154}
155
156static fr_bio_t *test_retry_alloc(TALLOC_CTX *ctx, fr_bio_retry_config_t *cfg)
157{
158 return test_retry_alloc_mrd(ctx, cfg, fr_time_delta_from_sec(30));
159}
160
161/** Running out of entries blocks writes, and freeing an entry resumes writes.
162 *
163 * See finding 1 in retry.md. Only fr_bio_retry_write_resume() clears write_blocked, and
164 * fr_bio_retry_write_resume() runs only when the socket becomes writable. If running out of
165 * entries set write_blocked, then the application would never resume writes.
166 */
167static void test_all_used_resumes(void)
168{
169 TALLOC_CTX *ctx = talloc_init_const("test");
171 fr_bio_t *retry;
172
173 retry = test_retry_alloc(ctx, &cfg);
174 TEST_CHECK(retry != NULL);
175 if (!retry) goto done;
176
177 TEST_CASE("the first packet uses the only entry");
178 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet1, sizeof(packet1)), (int) sizeof(packet1));
179 TEST_CHECK(saved_item != NULL);
180 if (!saved_item) goto done;
181
182 TEST_CASE("the second write returns IO_WOULD_BLOCK, and the retry bio calls the write_blocked callback");
183 TEST_CHECK(fr_bio_write(retry, NULL, packet2, sizeof(packet2)) == fr_bio_error(IO_WOULD_BLOCK));
185
186 TEST_CASE("running out of entries does not set write_blocked");
187 TEST_CHECK(!fr_bio_retry_info(retry)->write_blocked);
188
189 TEST_CASE("cancelling the entry calls the release callback and the resume callback");
193
194 TEST_CASE("the retry bio now accepts the second packet");
195 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet2, sizeof(packet2)), (int) sizeof(packet2));
196
197done:
198 talloc_free(ctx);
199}
200
201/** The application may write from inside the resume callback, so fr_bio_retry_release() must put
202 * the freed entry on the free list before calling the resume callback.
203 */
204static void test_resume_can_write(void)
205{
206 TALLOC_CTX *ctx = talloc_init_const("test");
208 fr_bio_t *retry;
209
210 retry = test_retry_alloc(ctx, &cfg);
211 TEST_CHECK(retry != NULL);
212 if (!retry) goto done;
213
214 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet1, sizeof(packet1)), (int) sizeof(packet1));
215 TEST_CHECK(fr_bio_write(retry, NULL, packet2, sizeof(packet2)) == fr_bio_error(IO_WOULD_BLOCK));
216 if (!saved_item) goto done;
217
218 TEST_CASE("a write from inside the resume callback succeeds");
219 resume_write_bio = retry;
222 TEST_CHECK_RET((int) resume_write_rcode, (int) sizeof(packet2));
223
224 TEST_CASE("the retry bio did not call the write_blocked callback a second time");
226
227done:
228 talloc_free(ctx);
229}
230
231/** The retry bio saves the rest of a partly written retransmission once, and a flush sends the rest once.
232 *
233 * See finding 2 in retry.md. Before the fix, fr_bio_retry_rewrite() saved the rest of the packet,
234 * and then fr_bio_retry_write_item() saved the rest of the packet again.
235 */
236static void test_partial_retransmit(void)
237{
238 TALLOC_CTX *ctx = talloc_init_const("test");
240 fr_bio_t *retry, *stub;
241 fr_time_t when;
242
243 retry = test_retry_alloc(ctx, &cfg);
244 TEST_CHECK(retry != NULL);
245 if (!retry) goto done;
246
247 stub = fr_bio_next(retry);
248
249 TEST_CASE("the stub accepts the first transmission in full");
250 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet1, sizeof(packet1)), (int) sizeof(packet1));
251 TEST_CHECK_RET((int) stub_bytes, (int) sizeof(packet1));
252
253 TEST_CASE("the retransmission timer fires, and the stub accepts one byte");
254 TEST_MSG("the initial retransmission time (irt) is 2s, so running the timer list 3s ahead fires exactly one retransmission");
255 stub->write = stub_write_one;
257 (void) fr_timer_list_run(cfg.el->tl, &when);
258 TEST_CHECK_RET((int) stub_bytes, (int) sizeof(packet1) + 1);
259
260 TEST_CASE("the retry bio has saved the rest of the packet and has blocked writes");
261 TEST_CHECK(fr_bio_retry_info(retry)->write_blocked);
262
263 TEST_CASE("a flush sends the rest of the packet exactly once");
264 stub->write = stub_write_all;
265 (void) fr_bio_write(retry, NULL, NULL, SIZE_MAX);
266 TEST_CHECK_RET((int) stub_bytes, (int) (2 * sizeof(packet1)));
267 TEST_CHECK(!fr_bio_retry_info(retry)->write_blocked);
268
269done:
270 talloc_free(ctx);
271}
272
273/** When the retry bio cannot save the rest of a partly written packet, the retry bio releases the entry of the packet.
274 *
275 * See 'A related defect: a failed save kept the entry' in retry.md. Without the release, the entry
276 * stays on both timer lists after the sent() callback has passed the entry to the application. The
277 * write returns OOM, so an application which then frees the packet leaves the retry bio holding a
278 * pointer to freed memory.
279 */
281{
282 TALLOC_CTX *ctx = talloc_init_const("test");
284 fr_bio_t *retry, *stub;
285
286 retry = test_retry_alloc(ctx, &cfg);
287 TEST_CHECK(retry != NULL);
288 if (!retry) goto done;
289
290 stub = fr_bio_next(retry);
291
292 TEST_CASE("a partial write returns OOM when the retry bio cannot allocate the buffer for the rest of the packet");
293 TEST_MSG("a talloc memory limit on the retry bio makes the buffer allocation fail");
294DIAG_OFF(deprecated-declarations)
295 TEST_CHECK(talloc_set_memlimit(retry, talloc_total_size(retry)) == 0);
296DIAG_ON(deprecated-declarations)
297 stub->write = stub_write_one;
298 TEST_CHECK(fr_bio_write(retry, NULL, packet1, sizeof(packet1)) == fr_bio_error(OOM));
299
300 TEST_CASE("the retry bio releases the entry");
302
303 TEST_CASE("the only entry is free again, so the retry bio accepts the next packet");
304DIAG_OFF(deprecated-declarations)
305 TEST_CHECK(talloc_set_memlimit(retry, 0) == 0);
306DIAG_ON(deprecated-declarations)
307 stub->write = stub_write_all;
308 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet2, sizeof(packet2)), (int) sizeof(packet2));
310
311done:
312 talloc_free(ctx);
313}
314
315/** Blocking writes expires an overdue entry, and the expiry sees writes as blocked.
316 *
317 * See finding 1 in retry.md. Arming the expiry list runs any expiry which is already due, and
318 * fr_bio_retry_expiry_timer() asserts that writes are blocked. fr_bio_retry_write_blocked() used to
319 * set the flag only after arming the list.
320 */
322{
323 TALLOC_CTX *ctx = talloc_init_const("test");
325 fr_bio_t *retry;
326 fr_bio_common_t *common;
327 fr_time_t until;
328
329 /*
330 * A maximum duration of one microsecond makes the entry overdue as soon as the entry is
331 * written. fr_bio_retry_alloc() copies the retry configuration, so the duration has to be set
332 * before the bio is allocated.
333 */
334 retry = test_retry_alloc_mrd(ctx, &cfg, fr_time_delta_from_usec(1));
335 TEST_CHECK(retry != NULL);
336 if (!retry) goto done;
337
338 TEST_CASE("the packet is written");
339 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet1, sizeof(packet1)), (int) sizeof(packet1));
340
341 /*
342 * Make sure that the maximum duration has passed. The wait is a few microseconds, so a busy
343 * loop is enough, and the result does not depend on how fast the test runs.
344 */
346 while (fr_time_lt(fr_time(), until)) { /* nothing */ }
347
348 TEST_CASE("blocking writes runs the overdue expiry, which releases the entry");
349 common = (fr_bio_common_t *) retry;
350 TEST_CHECK(common->priv_cb.write_blocked != NULL);
351 if (!common->priv_cb.write_blocked) goto done;
352
353 TEST_CHECK_RET(common->priv_cb.write_blocked(retry), 1);
355 TEST_CHECK(fr_bio_retry_info(retry)->write_blocked);
356
357 TEST_CASE("a released entry while blocked does not resume writes");
359
360done:
361 talloc_free(ctx);
362}
363
364/** When one retransmission blocks writes, the other retransmissions due in the same pass wait.
365 *
366 * See finding 1 in retry.md. fr_bio_retry_write_blocked() disarms the retry list, and the timer
367 * list used to keep running the other entries which were due in the same pass.
368 */
370{
371 TALLOC_CTX *ctx = talloc_init_const("test");
373 fr_bio_t *retry, *stub;
374 fr_time_t when;
375
376 retry = test_retry_alloc_full(ctx, &cfg, fr_time_delta_from_sec(30), 2);
377 TEST_CHECK(retry != NULL);
378 if (!retry) goto done;
379
380 stub = fr_bio_next(retry);
381
382 TEST_CASE("two packets are written in full");
383 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet1, sizeof(packet1)), (int) sizeof(packet1));
384 TEST_CHECK_RET((int) fr_bio_write(retry, NULL, packet2, sizeof(packet2)), (int) sizeof(packet2));
385
386 TEST_CASE("both retransmissions are due, and the first one blocks writes");
387 TEST_MSG("irt is 2s, so running the timer list 3s ahead makes both retransmissions due");
388 stub->write = stub_write_one;
390 (void) fr_timer_list_run(cfg.el->tl, &when);
391
392 TEST_CASE("only the first retransmission was written, and writes are blocked");
393 TEST_CHECK_RET((int) stub_bytes, (int) (sizeof(packet1) + sizeof(packet2) + 1));
394 TEST_CHECK(fr_bio_retry_info(retry)->write_blocked);
395
396done:
397 talloc_free(ctx);
398}
399
401 { "all_used_resumes", test_all_used_resumes },
402 { "resume_can_write", test_resume_can_write },
403 { "partial_retransmit", test_partial_retransmit },
404 { "partial_oom_releases", test_partial_oom_releases },
405 { "block_expires_overdue", test_block_expires_overdue },
406 { "block_stops_retries", test_block_stops_retries },
408};
static int const char char buffer[256]
Definition acutest.h:641
#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_event_list_t * el
event list
Definition retry.h:45
fr_bio_retry_release_reason_t
Definition retry.h:82
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:576
#define UNUSED
Definition build.h:415
#define DIAG_OFF(_x)
Definition build.h:575
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:58
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:2551
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:599
#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:577
#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