Files
RetroArch/samples/tasks/netplay_nat/nat_task_serialisation_test.c
T
LibretroAdmin b9be38fd55 strl: copy string literals without scanning them at runtime
strlcpy() begins with strlen() of its source. It lives in another
translation unit, so with no LTO that scan happens at runtime even when
the source is a literal whose length the compiler already measured, and
the call itself cannot be folded away either.

strlcpy_lit() passes the length over as sizeof(). When the destination
size is also a constant -- sizeof(buf), the usual case -- nothing is
left but the stores memcpy() compiles to:

  strlcpy      "core_options"  sizeof(buf)   7.01 ns
  strlcpy_lit  "core_options"  sizeof(buf)   0.42 ns
  strlcpy      41-char literal sizeof(buf)   7.74 ns
  strlcpy_lit  41-char literal sizeof(buf)   0.71 ns
  strlcpy      "core_options"  runtime len   7.11 ns
  strlcpy_lit  "core_options"  runtime len   0.70 ns

The destination bound is kept. When the literal does not fit it defers
to strlcpy(), so the copy truncates as before rather than overrunning;
a form that took only the literal would have turned every one of these
sites into an overflow waiting for a small buffer. The literal is
concatenated with "" so a non-literal source is a compile error rather
than a sizeof() that silently measures a pointer.

818 sites across 104 files were converted by parsing arguments rather
than by pattern-matching text: only a three-argument call whose second
argument is a bare string literal is rewritten, so a macro that happens
to expand to a literal is left alone. Line endings are preserved, so
the CRLF files carry only their own changed lines.

test/compat/test_strl.c compares the two against each other over every
destination size from 0 past the literal's length, for literals that
are empty, single-character, path-shaped, adjacently concatenated and
escape-bearing, checking the return, the terminator and the whole
buffer so an overrun past the terminator is caught. ASan+UBSan+LSan and
TSan both 4/4. The full menu_displaylist_build_list walk over every
displaylist type is byte-identical to before the change
(md5 a2a081bcac09cdc8d72d67efd14ac40e).
2026-08-30 02:10:20 +02:00

368 lines
11 KiB
C

/* Oracle for the NAT traversal task's request serialisation
* (tasks/task_netplay_nat_traversal.c), compiled from the shipping
* translation unit against the real task queue.
*
* Only one NAT task may run at a time, because both requests mutate
* the single shared nat_traversal_request object. That exclusion
* used to be enforced by blocking the calling thread - the one
* driving the menu - until the in-flight task finished, which for
* UPnP discovery is seconds of frozen UI. Requests are now queued
* and started from the task callbacks instead.
*
* What these lanes pin:
*
* exclusion - a push issued while a task is in flight does not
* start a second task, and does not block: it returns
* immediately, before the in-flight task has run to
* completion.
* ordering - a close followed by an open runs as both, in that
* order. Coalescing them would leave the port
* mapping the close was meant to release open.
* resumption - the queued request actually starts once the
* in-flight task retires, for a task that finishes
* through either callback (the open task's, which
* also reports the result, and the close task's,
* which exists only to resume the queue).
* overflow - a request beyond the queue's depth is refused
* rather than silently dropped.
*
* The natt_* device layer is stubbed with a deterministic fake that
* takes a fixed number of handler invocations to complete, standing
* in for the network round trips. The task queue is the real one,
* so the assumption the design rests on - that a task's callback
* runs after the task has been popped off the queue, leaving the
* queue free for the resumed push - is exercised rather than
* assumed.
*
* No threads: the non-threaded task queue is driven explicitly, so
* the sweep is ASan+UBSan+LSan. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <boolean.h>
#include <string/stdstring.h>
#include <queues/task_queue.h>
#include "../../../network/natt.h"
#include "../../../network/netplay/netplay.h"
static unsigned failures = 0;
#define CHECK(cond, ...) \
do { \
if (!(cond)) \
{ \
fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
failures++; \
} \
} while (0)
/* ------------------------------------------------------------------ */
/* Fake NAT device layer */
/* ------------------------------------------------------------------ */
/* Handler invocations still needed before the current operation
* reports success - the stand-in for network round trips. */
static int fake_steps_remaining;
static unsigned fake_open_calls;
static unsigned fake_close_calls;
/* Observed order of completed operations, e.g. "CO" for a close
* followed by an open. */
static char fake_order[16];
static size_t fake_order_len;
static void fake_record(char c)
{
if (fake_order_len + 1 < sizeof(fake_order))
fake_order[fake_order_len++] = c;
fake_order[fake_order_len] = '\0';
}
bool natt_init(struct natt_discovery *discovery)
{
return true;
}
bool natt_device_next(struct natt_discovery *discovery,
struct natt_device *device)
{
/* Returning false means discovery is OVER - the handler ends the
* task. "Still searching" is a device with no description: the
* handler skips it and comes back on its next invocation, which
* is what makes the fake take a known number of ticks. */
if (fake_steps_remaining > 0)
{
fake_steps_remaining--;
memset(device, 0, sizeof(*device));
return true;
}
memset(device, 0, sizeof(*device));
/* Enough of a device for the handler to accept: a description, a
* service type, and a loopback address that find_local_address()
* can connect a UDP socket to in order to learn a local address. */
strlcpy_lit(device->desc, "fake-device", sizeof(device->desc));
strlcpy_lit(device->service_type, "fake-service",
sizeof(device->service_type));
strlcpy_lit(device->control, "fake-control", sizeof(device->control));
device->addr.sin_family = AF_INET;
device->addr.sin_port = htons(1900);
device->addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
device->busy = false;
return true;
}
void natt_device_end(struct natt_discovery *discovery)
{
}
bool natt_query_device(struct natt_device *device, bool block)
{
device->busy = false;
return true;
}
bool natt_external_address(struct natt_device *device, bool block)
{
device->busy = false;
device->ext_addr.sin_family = AF_INET;
return true;
}
bool natt_open_port(struct natt_device *device,
struct natt_request *request, enum natt_forward_type forward_type,
bool block)
{
fake_open_calls++;
fake_record('O');
device->busy = false;
/* The real natt_open_port publishes the device on the request;
* the close path validates against it. */
request->device = device;
request->success = true;
return true;
}
bool natt_close_port(struct natt_device *device,
struct natt_request *request, bool block)
{
fake_close_calls++;
fake_record('C');
device->busy = false;
request->success = true;
return true;
}
/* ------------------------------------------------------------------ */
/* Netplay stub */
/* ------------------------------------------------------------------ */
static unsigned finished_notifications;
bool netplay_driver_ctl(enum rarch_netplay_ctl_state state, void *data)
{
if (state == RARCH_NETPLAY_CTL_FINISHED_NAT_TRAVERSAL)
finished_notifications++;
return true;
}
/* ------------------------------------------------------------------ */
static struct nat_traversal_data req;
static void reset_all(void)
{
/* Drain anything left over so lanes do not leak state into each
* other. */
task_queue_check();
memset(&req, 0, sizeof(req));
fake_steps_remaining = 0;
fake_open_calls = 0;
fake_close_calls = 0;
fake_order_len = 0;
fake_order[0] = '\0';
finished_notifications = 0;
}
static bool any_nat_task_finder(retro_task_t *task, void *userdata)
{
return true;
}
/* True while any task is still queued or running. */
static bool queue_busy(void)
{
task_finder_data_t find_data;
find_data.func = any_nat_task_finder;
find_data.userdata = NULL;
return task_queue_find(&find_data);
}
/* Run the queue until it is idle, or the guard trips. */
static unsigned pump(unsigned max_ticks)
{
unsigned ticks = 0;
while (ticks < max_ticks)
{
task_queue_check();
ticks++;
if (!queue_busy())
break;
}
return ticks;
}
/* ------------------------------------------------------------------ */
/* Lanes */
/* ------------------------------------------------------------------ */
static void lane_open_completes(void)
{
unsigned had = failures;
reset_all();
fake_steps_remaining = 3;
CHECK(task_push_netplay_nat_traversal(&req, 55435),
"open push refused");
pump(64);
CHECK(fake_open_calls == 1, "expected one open, got %u",
fake_open_calls);
CHECK(finished_notifications == 1,
"expected one completion notification, got %u",
finished_notifications);
CHECK(req.status == NAT_TRAVERSAL_STATUS_OPENED,
"expected OPENED, got %d", (int)req.status);
if (failures == had)
fprintf(stderr, "[pass] open lane\n");
}
static void lane_push_while_busy_does_not_block(void)
{
unsigned had = failures;
reset_all();
/* Long enough that the task is unmistakably still in flight. */
fake_steps_remaining = 8;
CHECK(task_push_netplay_nat_traversal(&req, 55435),
"first open push refused");
/* One tick: the task has started but cannot have finished. */
task_queue_check();
CHECK(fake_open_calls == 0, "task finished too early to test this");
/* The second push must return without running the first task to
* completion - that is the whole point. If it blocked, the open
* would already have happened by the time it returns. */
CHECK(task_push_netplay_nat_traversal(&req, 55436),
"queued open push refused");
CHECK(fake_open_calls == 0,
"push blocked until the in-flight task finished");
pump(128);
CHECK(fake_open_calls == 2,
"expected the queued open to run too, got %u opens",
fake_open_calls);
if (failures == had)
fprintf(stderr, "[pass] non-blocking push lane\n");
}
static void lane_close_then_open_order(void)
{
unsigned had = failures;
reset_all();
fake_steps_remaining = 4;
/* An open runs to completion first, so a close has something to
* close. */
CHECK(task_push_netplay_nat_traversal(&req, 55435), "open refused");
pump(64);
CHECK(req.status == NAT_TRAVERSAL_STATUS_OPENED, "not opened");
/* Now start another open and, while it is in flight, ask for a
* close and then another open. All three must run, in order. */
fake_steps_remaining = 8;
CHECK(task_push_netplay_nat_traversal(&req, 55437),
"second open refused");
task_queue_check();
CHECK(task_push_netplay_nat_close(&req), "queued close refused");
CHECK(task_push_netplay_nat_traversal(&req, 55438),
"queued open refused");
pump(256);
/* "O" from the first open, then the in-flight open, the close,
* and the trailing open - in exactly that order. A design that
* coalesced the close away would show "OOO". */
CHECK(strcmp(fake_order, "OOCO") == 0,
"operation order was \"%s\", expected \"OOCO\"", fake_order);
if (failures == had)
fprintf(stderr, "[pass] ordering lane\n");
}
static void lane_overflow_refused(void)
{
unsigned had = failures;
unsigned i;
unsigned accepted = 0;
reset_all();
fake_steps_remaining = 16;
CHECK(task_push_netplay_nat_traversal(&req, 55435), "open refused");
task_queue_check();
/* Queue depth is finite: past it, pushes must report failure
* rather than drop the request on the floor. */
for (i = 0; i < 16; i++)
if (task_push_netplay_nat_traversal(&req, (uint16_t)(55500 + i)))
accepted++;
CHECK(accepted > 0, "no request could be queued at all");
CHECK(accepted < 16, "queue accepted every request - overflow is "
"not being refused");
pump(512);
/* Everything accepted must actually have run: the in-flight one
* plus each queued one. */
CHECK(fake_open_calls == accepted + 1,
"ran %u opens for %u accepted queued requests",
fake_open_calls, accepted);
if (failures == had)
fprintf(stderr, "[pass] overflow lane\n");
}
int main(void)
{
task_queue_init(false, NULL);
lane_open_completes();
lane_push_while_busy_does_not_block();
lane_close_then_open_order();
lane_overflow_refused();
task_queue_deinit();
if (failures)
{
fprintf(stderr, "FAIL nat_task_serialisation_test: %u failures\n",
failures);
return 1;
}
fprintf(stderr, "PASS nat_task_serialisation_test\n");
return 0;
}