The FreeRADIUS server $Id: f3670dba8951ca10eb4948feb3dc3db9423a334f $
Loading...
Searching...
No Matches
bio_dedup_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
18/** Tests for the dedup bio expiring entries after a reply
19 *
20 * @file src/lib/bio/test/bio_dedup_tests.c
21 *
22 * @copyright 2026 Network RADIUS SAS (legal@networkradius.com)
23 */
24#include <freeradius-devel/util/test/acutest_common_init.h>
25#include <freeradius-devel/util/test/acutest_helpers.h>
26
27#define _BIO_PRIVATE 1
28#include <freeradius-devel/bio/bio_priv.h>
29#include <freeradius-devel/util/rb.h>
30#include <freeradius-devel/bio/dedup.h>
31
32static int expired_count;
34
35static uint8_t request[4] = { 1, 2, 3, 4 };
36static uint8_t reply[4] = { 5, 6, 7, 8 };
37static int packet_ctx; //!< the tests pass only the address of packet_ctx
38
39static bool dedup_receive(UNUSED fr_bio_t *bio, fr_bio_dedup_entry_t *dedup_ctx, UNUSED void *pctx)
40{
41 saved_item = dedup_ctx;
42 return true;
43}
44
50
52{
53 return saved_item;
54}
55
56/** The stub transport returns a copy of request on every read, and reports every write as complete.
57 */
58static ssize_t stub_read(UNUSED fr_bio_t *bio, UNUSED void *pctx, void *buffer, size_t size)
59{
60 if (size < sizeof(request)) return fr_bio_error(BUFFER_TOO_SMALL);
61
62 memcpy(buffer, request, sizeof(request));
63 return sizeof(request);
64}
65
66static size_t stub_bytes; //!< bytes of reply data which the stub transport has accepted
67
68static ssize_t stub_write_all(UNUSED fr_bio_t *bio, UNUSED void *pctx, void const *buffer, size_t size)
69{
70 if (!buffer) return 0; /* nothing to flush */
71
72 stub_bytes += size;
73 return size;
74}
75
76/** A stub transport write function which always returns IO_WOULD_BLOCK.
77 */
78static ssize_t stub_write_block(UNUSED fr_bio_t *bio, UNUSED void *pctx, UNUSED void const *buffer, UNUSED size_t size)
79{
80 return fr_bio_error(IO_WOULD_BLOCK);
81}
82
83/** A stub transport write function which accepts one byte of each write.
84 */
85static ssize_t stub_write_one(UNUSED fr_bio_t *bio, UNUSED void *pctx, void const *buffer, UNUSED size_t size)
86{
87 if (!buffer) return 0;
88
89 stub_bytes++;
90 return 1;
91}
92
93static fr_bio_t *stub_alloc(TALLOC_CTX *ctx)
94{
96
97 my = talloc_zero(ctx, fr_bio_common_t);
98 if (!my) return NULL;
99
100 my->bio.read = stub_read;
101 my->bio.write = stub_write_all;
102
103 return &my->bio;
104}
105
106/** Allocate a dedup bio with two entries and a one-second lifetime, in front of the stub transport.
107 */
108static fr_bio_t *test_dedup_alloc(TALLOC_CTX *ctx, fr_bio_dedup_config_t *cfg)
109{
110 fr_bio_t *stub;
111
112 expired_count = 0;
113 saved_item = NULL;
114 stub_bytes = 0;
115
116 cfg->el = fr_event_list_alloc(ctx, NULL, NULL);
117 if (!cfg->el) return NULL;
118
120
121 stub = stub_alloc(ctx);
122 if (!stub) return NULL;
123
125}
126
127/** Read one request through the dedup bio, and attach reply to the dedup entry of the request.
128 */
129static bool test_read_request(fr_bio_t *dedup, uint8_t *buffer, size_t size)
130{
131 saved_item = NULL;
132
133 if (fr_bio_read(dedup, &packet_ctx, buffer, size) != sizeof(request)) return false;
134 if (!saved_item) return false;
135
137 saved_item->reply_size = sizeof(reply);
139 return true;
140}
141
142/** Run every timer which is due within the next five seconds.
143 */
145{
147
148 (void) fr_timer_list_run(cfg->el->tl, &when);
149}
150
151/** The dedup entry for a reply sent with fr_bio_dedup_respond() expires once the lifetime is over.
152 *
153 * Regression test for dedup.md finding 1. fr_bio_dedup_respond() and fr_bio_dedup_write() did not
154 * arm the expiry timer, so no entry expired.
155 */
156static void test_respond_expires(void)
157{
158 TALLOC_CTX *ctx = talloc_init_const("test");
160 fr_bio_t *dedup;
161 uint8_t buffer[64];
162
163 dedup = test_dedup_alloc(ctx, &cfg);
164 TEST_CHECK(dedup != NULL);
165 if (!dedup) goto done;
166
167 TEST_CHECK(test_read_request(dedup, buffer, sizeof(buffer)));
168 if (!saved_item) goto done;
169
170 TEST_CASE("fr_bio_dedup_respond() writes the reply");
171 TEST_CHECK_RET((int) fr_bio_dedup_respond(dedup, saved_item), (int) sizeof(reply));
172
173 TEST_CASE("the dedup entry expires once the lifetime is over");
174 test_run_timers(&cfg);
176
177done:
178 talloc_free(ctx);
179}
180
181/** The dedup entry for a reply sent with fr_bio_write() expires once the lifetime is over.
182 */
183static void test_write_expires(void)
184{
185 TALLOC_CTX *ctx = talloc_init_const("test");
187 fr_bio_t *dedup;
188 uint8_t buffer[64];
189
190 dedup = test_dedup_alloc(ctx, &cfg);
191 TEST_CHECK(dedup != NULL);
192 if (!dedup) goto done;
193
194 TEST_CHECK(test_read_request(dedup, buffer, sizeof(buffer)));
195 if (!saved_item) goto done;
196
197 TEST_CASE("fr_bio_write() writes the reply");
198 TEST_CHECK_RET((int) fr_bio_write(dedup, &packet_ctx, reply, sizeof(reply)), (int) sizeof(reply));
199
200 TEST_CASE("the dedup entry expires once the lifetime is over");
201 test_run_timers(&cfg);
203
204done:
205 talloc_free(ctx);
206}
207
208/** The expiry timer returns each expired entry to the free list, so the dedup bio reads more
209 * requests than the dedup bio has entries.
210 *
211 * The UDP bio of the RADIUS server has 256 entries. Without the fix for dedup.md finding 1, the UDP
212 * bio returned fr_bio_error(OOM) for every request after 256 replies.
213 */
214static void test_entries_are_reused(void)
215{
216 TALLOC_CTX *ctx = talloc_init_const("test");
218 fr_bio_t *dedup;
219 uint8_t buffer[64];
220 int i;
221
222 dedup = test_dedup_alloc(ctx, &cfg);
223 TEST_CHECK(dedup != NULL);
224 if (!dedup) goto done;
225
226 TEST_CASE("a dedup bio with two entries finds a free entry for each of five requests");
227 for (i = 0; i < 5; i++) {
228 TEST_CHECK(test_read_request(dedup, buffer, sizeof(buffer)));
229 TEST_MSG("request %d", i);
230 if (!saved_item) break;
231
232 TEST_CHECK_RET((int) fr_bio_dedup_respond(dedup, saved_item), (int) sizeof(reply));
233 test_run_timers(&cfg);
234 }
236
237done:
238 talloc_free(ctx);
239}
240
241/** Two entries with the same expiry time both expire.
242 *
243 * See finding 1 in dedup.md. The expiry tree compared only the expiry time, so the tree treated the
244 * second entry as a duplicate, and never inserted the second entry.
245 */
246static void test_equal_expiry(void)
247{
248 TALLOC_CTX *ctx = talloc_init_const("test");
250 fr_bio_t *dedup;
251 fr_bio_dedup_entry_t *first, *second;
252 fr_time_t expires;
253 uint8_t buffer[64];
254
255 dedup = test_dedup_alloc(ctx, &cfg);
256 TEST_CHECK(dedup != NULL);
257 if (!dedup) goto done;
258
259 TEST_CHECK(test_read_request(dedup, buffer, sizeof(buffer)));
260 first = saved_item;
261 if (!first) goto done;
262 TEST_CHECK_RET((int) fr_bio_dedup_respond(dedup, first), (int) sizeof(reply));
263
264 TEST_CHECK(test_read_request(dedup, buffer, sizeof(buffer)));
265 second = saved_item;
266 if (!second) goto done;
267 TEST_CHECK_RET((int) fr_bio_dedup_respond(dedup, second), (int) sizeof(reply));
268
269 TEST_CASE("fr_bio_dedup_entry_extend() gives both entries the same expiry time");
271 TEST_CHECK(fr_bio_dedup_entry_extend(dedup, first, expires) == 0);
272 TEST_CHECK(fr_bio_dedup_entry_extend(dedup, second, expires) == 0);
273
274 TEST_CASE("both entries expire");
275 test_run_timers(&cfg);
277
278done:
279 talloc_free(ctx);
280}
281
282/** Call the dedup bio's write_resume callback, as fr_bio_packet_write_resume() does.
283 */
284static int test_write_resume(fr_bio_t *dedup)
285{
286 fr_bio_common_t *my = (fr_bio_common_t *) dedup;
287
288 if (!my->priv_cb.write_resume) return -1;
289
290 return my->priv_cb.write_resume(dedup);
291}
292
293/** Read a request, and respond while the stub transport is blocked, so that the reply goes on the
294 * pending list.
295 */
296static bool test_pending_reply(fr_bio_t *dedup, fr_bio_t *stub, uint8_t *buffer, size_t size)
297{
298 if (!test_read_request(dedup, buffer, size)) return false;
299
300 stub->write = stub_write_block;
301 return (fr_bio_dedup_respond(dedup, saved_item) == sizeof(reply));
302}
303
304/** write_resume sends a pending reply, and the dedup entry then expires once the lifetime is over.
305 */
307{
308 TALLOC_CTX *ctx = talloc_init_const("test");
310 fr_bio_t *dedup, *stub;
311 uint8_t buffer[64];
312
313 dedup = test_dedup_alloc(ctx, &cfg);
314 TEST_CHECK(dedup != NULL);
315 if (!dedup) goto done;
316 stub = fr_bio_next(dedup);
317
318 TEST_CASE("a reply written while the stub transport is blocked goes on the pending list");
319 TEST_CHECK(test_pending_reply(dedup, stub, buffer, sizeof(buffer)));
320 TEST_CHECK_RET((int) stub_bytes, 0);
321
322 TEST_CASE("write_resume sends the pending reply");
323 stub->write = stub_write_all;
325 TEST_CHECK_RET((int) stub_bytes, (int) sizeof(reply));
326
327 TEST_CASE("the dedup entry then expires once the lifetime is over");
328 test_run_timers(&cfg);
330
331done:
332 talloc_free(ctx);
333}
334
335/** write_resume keeps the pending reply when the stub transport is still blocked.
336 */
338{
339 TALLOC_CTX *ctx = talloc_init_const("test");
341 fr_bio_t *dedup, *stub;
342 uint8_t buffer[64];
343
344 dedup = test_dedup_alloc(ctx, &cfg);
345 TEST_CHECK(dedup != NULL);
346 if (!dedup) goto done;
347 stub = fr_bio_next(dedup);
348
349 TEST_CHECK(test_pending_reply(dedup, stub, buffer, sizeof(buffer)));
350
351 TEST_CASE("write_resume reports that writes are still blocked");
353
354 TEST_CASE("write_resume kept the pending reply, and the next write_resume call sends the pending reply");
355 stub->write = stub_write_all;
357 TEST_CHECK_RET((int) stub_bytes, (int) sizeof(reply));
358
359done:
360 talloc_free(ctx);
361}
362
363/** When the stub transport writes only part of a pending reply, the next resume writes the rest.
364 */
365static void test_resume_partial(void)
366{
367 TALLOC_CTX *ctx = talloc_init_const("test");
369 fr_bio_t *dedup, *stub;
370 uint8_t buffer[64];
371
372 dedup = test_dedup_alloc(ctx, &cfg);
373 TEST_CHECK(dedup != NULL);
374 if (!dedup) goto done;
375 stub = fr_bio_next(dedup);
376
377 TEST_CHECK(test_pending_reply(dedup, stub, buffer, sizeof(buffer)));
378
379 TEST_CASE("the stub transport accepts one byte, so write_resume reports that writes are still blocked");
380 stub->write = stub_write_one;
382 TEST_CHECK_RET((int) stub_bytes, 1);
383
384 TEST_CASE("the next write_resume call sends the rest of the reply exactly once");
385 stub->write = stub_write_all;
387 TEST_CHECK_RET((int) stub_bytes, (int) sizeof(reply));
388
389done:
390 talloc_free(ctx);
391}
392
394 { "respond_expires", test_respond_expires },
395 { "write_expires", test_write_expires },
396 { "entries_are_reused", test_entries_are_reused },
397 { "equal_expiry", test_equal_expiry },
398 { "resume_sends_pending", test_resume_sends_pending },
399 { "resume_still_blocked", test_resume_still_blocked },
400 { "resume_partial", test_resume_partial },
402};
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
static ssize_t fr_bio_read(fr_bio_t *bio, void *packet_ctx, void *buffer, size_t size)
Read raw data from a bio.
Definition base.h:161
#define fr_bio_error(_x)
Definition base.h:200
static void test_respond_expires(void)
The dedup entry for a reply sent with fr_bio_dedup_respond() expires once the lifetime is over.
static fr_bio_dedup_entry_t * saved_item
TEST_LIST
static void test_resume_partial(void)
When the stub transport writes only part of a pending reply, the next resume writes the rest.
static bool test_read_request(fr_bio_t *dedup, uint8_t *buffer, size_t size)
Read one request through the dedup bio, and attach reply to the dedup entry of the request.
static ssize_t stub_read(UNUSED fr_bio_t *bio, UNUSED void *pctx, void *buffer, size_t size)
The stub transport returns a copy of request on every read, and reports every write as complete.
static void test_write_expires(void)
The dedup entry for a reply sent with fr_bio_write() expires once the lifetime is over.
static int expired_count
static bool dedup_receive(UNUSED fr_bio_t *bio, fr_bio_dedup_entry_t *dedup_ctx, UNUSED void *pctx)
static int packet_ctx
the tests pass only the address of packet_ctx
static void test_resume_still_blocked(void)
write_resume keeps the pending reply when the stub transport is still blocked.
static fr_bio_dedup_entry_t * dedup_get_item(UNUSED fr_bio_t *bio, UNUSED void *pctx)
static ssize_t stub_write_block(UNUSED fr_bio_t *bio, UNUSED void *pctx, UNUSED void const *buffer, UNUSED size_t size)
A stub transport write function which always returns IO_WOULD_BLOCK.
static void dedup_release(UNUSED fr_bio_t *bio, UNUSED fr_bio_dedup_entry_t *dedup_ctx, fr_bio_dedup_release_reason_t reason)
static bool test_pending_reply(fr_bio_t *dedup, fr_bio_t *stub, uint8_t *buffer, size_t size)
Read a request, and respond while the stub transport is blocked, so that the reply goes on the pendin...
static void test_equal_expiry(void)
Two entries with the same expiry time both expire.
static void test_entries_are_reused(void)
The expiry timer returns each expired entry to the free list, so the dedup bio reads more requests th...
static uint8_t request[4]
static void test_resume_sends_pending(void)
write_resume sends a pending reply, and the dedup entry then expires once the lifetime is over.
static ssize_t stub_write_one(UNUSED fr_bio_t *bio, UNUSED void *pctx, void const *buffer, UNUSED size_t size)
A stub transport write function which accepts one byte of each write.
static fr_bio_t * stub_alloc(TALLOC_CTX *ctx)
static fr_bio_t * test_dedup_alloc(TALLOC_CTX *ctx, fr_bio_dedup_config_t *cfg)
Allocate a dedup bio with two entries and a one-second lifetime, in front of the stub transport.
static void test_run_timers(fr_bio_dedup_config_t *cfg)
Run every timer which is due within the next five seconds.
static ssize_t stub_write_all(UNUSED fr_bio_t *bio, UNUSED void *pctx, void const *buffer, size_t size)
static size_t stub_bytes
bytes of reply data which the stub transport has accepted
static int test_write_resume(fr_bio_t *dedup)
Call the dedup bio's write_resume callback, as fr_bio_packet_write_resume() does.
static uint8_t reply[4]
#define UNUSED
Definition build.h:384
ssize_t fr_bio_dedup_respond(fr_bio_t *bio, fr_bio_dedup_entry_t *item)
Resend a reply when we receive a duplicate request.
Definition dedup.c:212
int fr_bio_dedup_entry_extend(fr_bio_t *bio, fr_bio_dedup_entry_t *item, fr_time_t expires)
Extend the expiry time for an entry.
Definition dedup.c:1031
fr_bio_t * fr_bio_dedup_alloc(TALLOC_CTX *ctx, size_t max_saved, fr_bio_dedup_receive_t receive, fr_bio_dedup_release_t release, fr_bio_dedup_get_item_t get_item, fr_bio_dedup_config_t const *cfg, fr_bio_t *next)
Allocate a fr_bio_dedup_t.
Definition dedup.c:1117
fr_event_list_t * el
event list
Definition dedup.h:38
fr_time_delta_t lifetime
default lifetime of dedup entry
Definition dedup.h:40
uint8_t * reply
reply cached by the application
Definition dedup.c:100
size_t reply_size
size of the cached reply
Definition dedup.c:101
void * reply_ctx
reply ctx
Definition dedup.c:99
fr_bio_dedup_release_reason_t
Definition dedup.h:59
@ FR_BIO_DEDUP_EXPIRED
Definition dedup.h:60
Definition dedup.c:94
talloc_free(hp)
#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
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
"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