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