387 lines
15 KiB
C
387 lines
15 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_udrv_claim.c
|
|
* @brief Gate for ft_tt_udrv_claim.c — the multi-tenant ownership that
|
|
* replaces tt-kmd's ALLOCATE_TLB bitmap.
|
|
*
|
|
* The claim mechanism's whole promise is that it survives an abnormal death,
|
|
* so the load-bearing stage here FORKS A CHILD THAT CLAIMS AND IS THEN
|
|
* KILLED. A suite that only ever tests acquire/release in one process
|
|
* verifies the easy half and misses the reason flock was chosen over a
|
|
* bitmap.
|
|
*
|
|
* Everything runs inside a scratch root, so this never touches the real
|
|
* coordination directory and can run alongside live jobs.
|
|
*
|
|
* @status Off-hardware. Filesystem only; no device is opened.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
/* flock(2) is BSD, not POSIX, so a bare _POSIX_C_SOURCE hides it on both
|
|
* platforms this code is built on. Same shape as the _DEFAULT_SOURCE note in
|
|
* DEV_NOTES §5 for MADV_DONTNEED: macOS declares BSD interfaces under
|
|
* _DARWIN_C_SOURCE, glibc under _DEFAULT_SOURCE (which implies POSIX 2008). */
|
|
#if defined(__APPLE__)
|
|
#define _DARWIN_C_SOURCE
|
|
#else
|
|
#define _DEFAULT_SOURCE
|
|
#endif
|
|
|
|
#include <errno.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include "ft_tt_test_report.h"
|
|
#include "ft_tt_udrv.h"
|
|
#include "ft_tt_udrv_claim.h"
|
|
|
|
#define TEST_WINDOW_COUNT 8u
|
|
|
|
static char scratch_root[192];
|
|
|
|
static void build_scratch_root(void)
|
|
{
|
|
(void)snprintf(scratch_root, sizeof(scratch_root), "/tmp/ft_tt_udrv_claim_test.%ld",
|
|
(long)getpid());
|
|
}
|
|
|
|
static void remove_scratch_root(void)
|
|
{
|
|
char command[256];
|
|
|
|
(void)snprintf(command, sizeof(command), "rm -rf '%s'", scratch_root);
|
|
if (system(command) != 0)
|
|
(void)fprintf(stderr, "fixture: cleanup of %s failed\n", scratch_root);
|
|
}
|
|
|
|
static void stage_namespace_validation(void)
|
|
{
|
|
ft_tt_udrv_claim_namespace ns;
|
|
|
|
ft_tt_test_step("Stage 1 — a namespace becomes a directory, so it is validated like one");
|
|
|
|
ft_tt_test_check("a well-formed namespace is accepted",
|
|
ft_tt_udrv_claim_namespace_init(&ns, scratch_root, "0000:12:00.0", "tlb2m",
|
|
TEST_WINDOW_COUNT) == FT_TT_UDRV_OK,
|
|
"");
|
|
|
|
ft_tt_test_check("a card name containing '/' is refused",
|
|
ft_tt_udrv_claim_namespace_init(&ns, scratch_root, "../etc", "tlb2m",
|
|
8u) == FT_TT_UDRV_EINVAL,
|
|
"it would escape the root");
|
|
|
|
ft_tt_test_check("a card name of \"..\" is refused",
|
|
ft_tt_udrv_claim_namespace_init(&ns, scratch_root, "..", "tlb2m", 8u) ==
|
|
FT_TT_UDRV_EINVAL,
|
|
"");
|
|
|
|
ft_tt_test_check("an empty resource class is refused",
|
|
ft_tt_udrv_claim_namespace_init(&ns, scratch_root, "card", "", 8u) ==
|
|
FT_TT_UDRV_EINVAL,
|
|
"");
|
|
|
|
ft_tt_test_check("a zero-sized namespace is refused",
|
|
ft_tt_udrv_claim_namespace_init(&ns, scratch_root, "card", "tlb2m", 0u) ==
|
|
FT_TT_UDRV_EINVAL,
|
|
"nothing to claim");
|
|
}
|
|
|
|
static void stage_basic_ownership(void)
|
|
{
|
|
ft_tt_udrv_claim_namespace ns;
|
|
ft_tt_udrv_claim *first = NULL;
|
|
ft_tt_udrv_claim *second = NULL;
|
|
|
|
ft_tt_test_step("Stage 2 — one owner at a time");
|
|
|
|
(void)ft_tt_udrv_claim_namespace_init(&ns, scratch_root, "0000:12:00.0", "tlb2m",
|
|
TEST_WINDOW_COUNT);
|
|
|
|
ft_tt_test_check("window 3 can be claimed",
|
|
ft_tt_udrv_claim_acquire(&ns, 3u, "encoder gate", &first) == FT_TT_UDRV_OK,
|
|
"");
|
|
ft_tt_test_check_fmt("the claim knows its index", ft_tt_udrv_claim_index(first) == 3u, "%u",
|
|
ft_tt_udrv_claim_index(first));
|
|
|
|
/* A second independent open file description in the SAME process is
|
|
* exactly what a second process looks like to flock, which is why this is
|
|
* a meaningful check rather than a self-comparison. */
|
|
ft_tt_test_check("a second claim on window 3 is refused",
|
|
ft_tt_udrv_claim_acquire(&ns, 3u, "should fail", &second) ==
|
|
FT_TT_UDRV_EAGAIN,
|
|
"already held");
|
|
|
|
ft_tt_test_check("a different window is unaffected",
|
|
ft_tt_udrv_claim_acquire(&ns, 4u, "neighbour", &second) == FT_TT_UDRV_OK,
|
|
"");
|
|
|
|
ft_tt_test_check("an out-of-range index is refused",
|
|
ft_tt_udrv_claim_acquire(&ns, TEST_WINDOW_COUNT, "oob", &second) ==
|
|
FT_TT_UDRV_EINVAL,
|
|
"");
|
|
|
|
(void)ft_tt_udrv_claim_release(second);
|
|
ft_tt_test_check("releasing gives the window back",
|
|
ft_tt_udrv_claim_release(first) == FT_TT_UDRV_OK, "");
|
|
|
|
ft_tt_test_check("...and it can then be re-claimed",
|
|
ft_tt_udrv_claim_acquire(&ns, 3u, "again", &first) == FT_TT_UDRV_OK, "");
|
|
(void)ft_tt_udrv_claim_release(first);
|
|
|
|
ft_tt_test_check("releasing NULL is a no-op",
|
|
ft_tt_udrv_claim_release(NULL) == FT_TT_UDRV_OK, "");
|
|
}
|
|
|
|
static void stage_allocate_any(void)
|
|
{
|
|
ft_tt_udrv_claim_namespace ns;
|
|
ft_tt_udrv_claim *held[TEST_WINDOW_COUNT];
|
|
ft_tt_udrv_claim *extra = NULL;
|
|
const uint32_t reserved[] = {0u, 7u};
|
|
uint32_t index;
|
|
uint32_t count = 0u;
|
|
bool saw_reserved = false;
|
|
|
|
ft_tt_test_step("Stage 3 — allocate-any, with the reserved set tt-kmd's window 201 needs");
|
|
|
|
(void)ft_tt_udrv_claim_namespace_init(&ns, scratch_root, "0000:12:00.0", "anytest",
|
|
TEST_WINDOW_COUNT);
|
|
|
|
for (index = 0u; index < TEST_WINDOW_COUNT; index++)
|
|
held[index] = NULL;
|
|
|
|
for (index = 0u; index < TEST_WINDOW_COUNT; index++) {
|
|
if (ft_tt_udrv_claim_acquire_any(&ns, reserved, 2u, "sweep", &held[count]) !=
|
|
FT_TT_UDRV_OK)
|
|
break;
|
|
|
|
if (ft_tt_udrv_claim_index(held[count]) == 0u ||
|
|
ft_tt_udrv_claim_index(held[count]) == 7u)
|
|
saw_reserved = true;
|
|
|
|
count++;
|
|
}
|
|
|
|
ft_tt_test_check_fmt("6 of 8 windows are allocatable with 2 reserved", count == 6u, "%u",
|
|
count);
|
|
ft_tt_test_check("no reserved window was ever handed out", !saw_reserved,
|
|
"0 and 7 skipped");
|
|
ft_tt_test_check("exhaustion reports EAGAIN, which is the ioctl's -ENOMEM",
|
|
ft_tt_udrv_claim_acquire_any(&ns, reserved, 2u, "one too many", &extra) ==
|
|
FT_TT_UDRV_EAGAIN,
|
|
"");
|
|
|
|
for (index = 0u; index < count; index++)
|
|
(void)ft_tt_udrv_claim_release(held[index]);
|
|
|
|
ft_tt_test_check("with nothing reserved, window 0 is available again",
|
|
ft_tt_udrv_claim_acquire_any(&ns, NULL, 0u, "unreserved", &extra) ==
|
|
FT_TT_UDRV_OK,
|
|
"");
|
|
ft_tt_test_check_fmt("...and it is the lowest index", ft_tt_udrv_claim_index(extra) == 0u,
|
|
"%u", ft_tt_udrv_claim_index(extra));
|
|
(void)ft_tt_udrv_claim_release(extra);
|
|
}
|
|
|
|
static void stage_death_reclaims(void)
|
|
{
|
|
ft_tt_udrv_claim_namespace ns;
|
|
ft_tt_udrv_claim *claim = NULL;
|
|
ft_tt_udrv_claim_info info[TEST_WINDOW_COUNT];
|
|
uint32_t held = 0u;
|
|
int ready_pipe[2];
|
|
char ready = 0;
|
|
pid_t child;
|
|
int status = 0;
|
|
|
|
ft_tt_test_step("Stage 4 — THE LOAD-BEARING ONE: a killed holder gives the window back");
|
|
|
|
(void)ft_tt_udrv_claim_namespace_init(&ns, scratch_root, "0000:12:00.0", "death",
|
|
TEST_WINDOW_COUNT);
|
|
|
|
/* A pipe, not a poll loop. The first version of this stage had the parent
|
|
* POLL by attempting the claim, which meant the parent could win the race
|
|
* and the child then failed to acquire at all — the stage went red for a
|
|
* reason that had nothing to do with the mechanism. A handshake removes
|
|
* the race instead of widening the timeout. */
|
|
if (pipe(ready_pipe) != 0) {
|
|
ft_tt_test_check("pipe succeeded", false, "pipe failed");
|
|
return;
|
|
}
|
|
|
|
child = fork();
|
|
if (child == 0) {
|
|
ft_tt_udrv_claim *child_claim = NULL;
|
|
char signal_byte = 1;
|
|
|
|
(void)close(ready_pipe[0]);
|
|
|
|
if (ft_tt_udrv_claim_acquire(&ns, 2u, "child that dies", &child_claim) != FT_TT_UDRV_OK)
|
|
_exit(3);
|
|
|
|
/* Say the claim is held, then wait to be killed WITHOUT releasing
|
|
* anything. SIGKILL is the case that matters: no cleanup path of ours
|
|
* can run at all, so anything that gets the window back is the
|
|
* kernel's doing. */
|
|
if (write(ready_pipe[1], &signal_byte, 1u) != 1)
|
|
_exit(4);
|
|
|
|
for (;;)
|
|
pause();
|
|
}
|
|
|
|
if (child < 0) {
|
|
ft_tt_test_check("fork succeeded", false, "fork failed");
|
|
return;
|
|
}
|
|
|
|
(void)close(ready_pipe[1]);
|
|
ft_tt_test_check("the child says it holds window 2",
|
|
read(ready_pipe[0], &ready, 1u) == 1 && ready == 1, "handshake");
|
|
(void)close(ready_pipe[0]);
|
|
|
|
ft_tt_test_check("...and the parent is refused while it lives",
|
|
ft_tt_udrv_claim_acquire(&ns, 2u, "parent probe", &claim) ==
|
|
FT_TT_UDRV_EAGAIN,
|
|
"held by another process");
|
|
|
|
(void)ft_tt_udrv_claim_scan(&ns, info, TEST_WINDOW_COUNT, &held);
|
|
ft_tt_test_check_fmt("a scan sees exactly one window held", held == 1u, "%u", held);
|
|
ft_tt_test_check_fmt("...and names the child as its holder", info[2].pid == child,
|
|
"pid %ld vs child %ld", (long)info[2].pid, (long)child);
|
|
ft_tt_test_check_fmt("...with the reason it registered",
|
|
strcmp(info[2].reason, "child that dies") == 0, "%s", info[2].reason);
|
|
|
|
(void)kill(child, SIGKILL);
|
|
(void)waitpid(child, &status, 0);
|
|
|
|
/* Scan BEFORE re-acquiring. The first version of this stage scanned after,
|
|
* by which point release() had already unlinked the record and the sweep
|
|
* had nothing left to do — so a build with the sweep DELETED passed. The
|
|
* order is the check. */
|
|
(void)ft_tt_udrv_claim_scan(&ns, info, TEST_WINDOW_COUNT, &held);
|
|
ft_tt_test_check_fmt("nothing is held once the holder is dead", held == 0u, "%u", held);
|
|
ft_tt_test_check_fmt("the orphaned record was swept by the scan", info[2].pid == 0,
|
|
"pid %ld", (long)info[2].pid);
|
|
|
|
ft_tt_test_check("SIGKILL released the claim — the kernel does it, not our code",
|
|
ft_tt_udrv_claim_acquire(&ns, 2u, "after the death", &claim) ==
|
|
FT_TT_UDRV_OK,
|
|
"");
|
|
(void)ft_tt_udrv_claim_release(claim);
|
|
}
|
|
|
|
static void stage_scan_reporting(void)
|
|
{
|
|
ft_tt_udrv_claim_namespace ns;
|
|
ft_tt_udrv_claim *claim = NULL;
|
|
ft_tt_udrv_claim_info info[TEST_WINDOW_COUNT];
|
|
uint32_t held = 99u;
|
|
|
|
ft_tt_test_step("Stage 5 — the scan reports the LOCK, not the record");
|
|
|
|
(void)ft_tt_udrv_claim_namespace_init(&ns, scratch_root, "0000:12:00.0", "scan",
|
|
TEST_WINDOW_COUNT);
|
|
|
|
(void)ft_tt_udrv_claim_scan(&ns, info, TEST_WINDOW_COUNT, &held);
|
|
ft_tt_test_check_fmt("a fresh namespace has nothing held", held == 0u, "%u", held);
|
|
|
|
(void)ft_tt_udrv_claim_acquire(&ns, 5u, "scan subject", &claim);
|
|
(void)ft_tt_udrv_claim_scan(&ns, info, TEST_WINDOW_COUNT, &held);
|
|
|
|
ft_tt_test_check_fmt("one window held", held == 1u, "%u", held);
|
|
ft_tt_test_check("...and it is the right one", info[5].held && !info[4].held, "index 5");
|
|
ft_tt_test_check_fmt("...owned by this process", info[5].pid == getpid() && info[5].pid_alive,
|
|
"pid %ld", (long)info[5].pid);
|
|
|
|
ft_tt_test_check("a scan buffer that is too small is refused",
|
|
ft_tt_udrv_claim_scan(&ns, info, 2u, &held) == FT_TT_UDRV_EINVAL, "");
|
|
|
|
(void)ft_tt_udrv_claim_release(claim);
|
|
(void)ft_tt_udrv_claim_scan(&ns, info, TEST_WINDOW_COUNT, &held);
|
|
ft_tt_test_check_fmt("releasing clears both the lock and the record",
|
|
held == 0u && info[5].pid == 0, "held=%u pid=%ld", held,
|
|
(long)info[5].pid);
|
|
|
|
/* A record that names a LIVE process on a window nobody holds. This is the
|
|
* pid-reuse hazard the design rejects a bitmap over, and it is the only
|
|
* case that separates "the scan reports the LOCK" from "the scan reports
|
|
* the RECORD" — without it, a scan built entirely on records passes. */
|
|
{
|
|
char record_path[256];
|
|
FILE *forged;
|
|
|
|
(void)snprintf(record_path, sizeof(record_path), "%s/0000:12:00.0/scan/6.claim",
|
|
scratch_root);
|
|
forged = fopen(record_path, "w");
|
|
if (forged) {
|
|
(void)fprintf(forged, "pid %ld\nreason forged\nacquired 0\n", (long)getpid());
|
|
(void)fclose(forged);
|
|
}
|
|
|
|
(void)ft_tt_udrv_claim_scan(&ns, info, TEST_WINDOW_COUNT, &held);
|
|
ft_tt_test_check_fmt("a record naming a LIVE pid does not make a free window held",
|
|
!info[6].held && held == 0u, "held=%u index6=%d", held,
|
|
info[6].held ? 1 : 0);
|
|
ft_tt_test_check("...and it is NOT swept, because its pid is alive",
|
|
info[6].pid == getpid(), "sweeping it would delete a real claim");
|
|
}
|
|
}
|
|
|
|
static void stage_namespaces_are_separate(void)
|
|
{
|
|
ft_tt_udrv_claim_namespace windows;
|
|
ft_tt_udrv_claim_namespace cores;
|
|
ft_tt_udrv_claim *window_claim = NULL;
|
|
ft_tt_udrv_claim *core_claim = NULL;
|
|
|
|
ft_tt_test_step("Stage 6 — window 7 and core 7 are different resources");
|
|
|
|
(void)ft_tt_udrv_claim_namespace_init(&windows, scratch_root, "0000:12:00.0", "tlb2m", 8u);
|
|
(void)ft_tt_udrv_claim_namespace_init(&cores, scratch_root, "0000:12:00.0", "tensix", 8u);
|
|
|
|
ft_tt_test_check("window 7 claims",
|
|
ft_tt_udrv_claim_acquire(&windows, 7u, "w", &window_claim) == FT_TT_UDRV_OK,
|
|
"");
|
|
ft_tt_test_check("core 7 claims independently",
|
|
ft_tt_udrv_claim_acquire(&cores, 7u, "c", &core_claim) == FT_TT_UDRV_OK, "");
|
|
|
|
(void)ft_tt_udrv_claim_release(window_claim);
|
|
(void)ft_tt_udrv_claim_release(core_claim);
|
|
|
|
/* And a different card is a different namespace again — two cards in one
|
|
* box must not share a window pool. */
|
|
(void)ft_tt_udrv_claim_namespace_init(&cores, scratch_root, "0000:03:00.0", "tlb2m", 8u);
|
|
ft_tt_test_check("window 7 of a SECOND card claims independently",
|
|
ft_tt_udrv_claim_acquire(&windows, 7u, "w", &window_claim) ==
|
|
FT_TT_UDRV_OK &&
|
|
ft_tt_udrv_claim_acquire(&cores, 7u, "w2", &core_claim) == FT_TT_UDRV_OK,
|
|
"");
|
|
|
|
(void)ft_tt_udrv_claim_release(window_claim);
|
|
(void)ft_tt_udrv_claim_release(core_claim);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
build_scratch_root();
|
|
|
|
stage_namespace_validation();
|
|
stage_basic_ownership();
|
|
stage_allocate_any();
|
|
stage_death_reclaims();
|
|
stage_scan_reporting();
|
|
stage_namespaces_are_separate();
|
|
|
|
remove_scratch_root();
|
|
return ft_tt_test_finish();
|
|
}
|