Foundry/tt/tests/test_udrv_card_tlb.c

1285 lines
55 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file test_udrv_card_tlb.c
* @brief ON-SILICON bring-up for the userspace driver's TLB path, sequenced so
* that tt-kmd is its own oracle and nothing is written until reading has
* proved the mapping, the offsets and the bit layout.
*
* Four rungs, each a strict superset of the one before:
*
* 1. discover the card through sysfs and mmap BAR0's TLB register file
* READ-ONLY. A read-only mapping is the safety property, not care: a
* stray store is a SIGSEGV on the host rather than a wrong aim on the
* card.
* 2. READ WINDOW 201 — the one tt-kmd keeps for its own NoC reads — and
* decode it. It must come out as the ARC tile (8,0), NoC 0, strict
* ordering, aimed at the ARC CSM page. That is a 96-bit prediction with
* zero risk attached: no window is claimed, nothing is written.
* 3. GATE THE ENCODER ON SILICON WITHOUT WRITING A REGISTER. Claim windows
* through ALLOCATE_TLB, aim them with CONFIGURE_TLB, and byte-compare
* the three words the DRIVER left in BAR0 against the three words our
* encoder says it should have written. tt-kmd becomes the oracle on real
* hardware, and any disagreement is found before we have written a bit.
* 4. ONLY THEN write the registers ourselves, read them back through a
* different path, and prove a window we aimed reaches the address we
* aimed it at. Requires the `write` argument — the default run cannot
* write to the card at all.
*
* THE SWEEP NEVER DEREFERENCES AN APERTURE. Configuring a window emits no NoC
* transaction; only touching its 2 MiB aperture does. That is what makes it
* safe to sweep coordinates at 0 and 63, multicast rectangles, and addresses
* at both ends of the 43-bit field: those windows are aimed at endpoints that
* would hang the chip if anyone read through them, and nothing here ever maps
* them. The one aperture this file maps is aimed at the ARC tile, at an
* address tt-kmd itself reads.
*
* WHY A WRONG AIM NEEDS A DELIBERATE DETECTOR. A window pointed at the wrong
* place does not fail — it silently reads a different address. So the
* functional check does not merely compare our readback against tt-kmd's; it
* also proves the comparison DISCRIMINATES, by re-aiming tt-kmd's window at a
* different page of the same tile and requiring the bytes to differ. If that
* arm ever passes, the equality arm above it was measuring nothing.
*
* @status Runs on the p150a at 0000:12:00.0, with tt-kmd
* loaded. Needs root (the BAR sysfs files are 0600 root:root).
* @issues
* - /dev/mem is NOT usable as a readback path on this box — a pread at the
* BAR's physical address returns EFAULT even as root. Rung 4a records that
* explicitly; the second reader is tt-kmd's own BAR0 mapping instead.
* @todo None
*/
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include "ft_tt_kmd_abi.h"
#include "ft_tt_test_report.h"
#include "ft_tt_udrv.h"
#include "ft_tt_udrv_pci_sysfs.h"
#include "ft_tt_udrv_tlb_encode.h"
#include "reference/ft_tt_tlb2m_reference_vectors.h"
/** tt-kmd's character device for card 0. */
#define CHARDEV_PATH "/dev/tenstorrent/0"
/** The window tt-kmd claims for itself: TLB_2M_WINDOW_COUNT - 1. */
#define KERNEL_WINDOW_ID (FT_TT_UDRV_TLB_2M_WINDOW_COUNT - 1u)
/** ARC tile, TRANSLATED coordinates. the kernel driver's value — the one
* coordinate in this backend that live hardware has confirmed. */
#define ARC_X 8u
#define ARC_Y 0u
/** ARC CSM, and the reset-unit scratch block. Both are addresses tt-kmd itself
* reads on this tile every time anything touches telemetry, which is why they
* are the only two the functional check is allowed to aim at. */
#define ARC_CSM_BASE 0x10000000ull
#define ARC_CSM_SIZE (1ull << 19)
#define ARC_SCRATCH_BASE 0x80030400ull
#define ARC_TELEMETRY_TABLE_SCRATCH 13u
#define ARC_TELEMETRY_VALUES_SCRATCH 12u
/** 2 MiB page containing an address. */
#define WINDOW_PAGE(addr) ((addr) & ~((uint64_t)FT_TT_UDRV_TLB_2M_WINDOW_SIZE - 1u))
/** How many windows the sweep claims. Enough to reach an id at or above
* FT_TT_UDRV_TLB_STRIDED_COUNT, plus the pair 0 and 12 that the strided-offset
* discriminator needs. */
#define CLAIMED_WINDOW_COUNT 40u
/** Bytes compared through an aperture in the functional check. */
#define FUNCTIONAL_BLOCK_BYTES 4096u
/** How much of an aperture the functional check maps. The WHOLE window, not
* the block it compares: the semantic arm reads the ARC reset-unit scratch at
* +0x30400 within the page, and a mapping sized to the comparison block would
* make that a segfault rather than a read. */
#define FUNCTIONAL_APERTURE_BYTES ((size_t)FT_TT_UDRV_TLB_2M_WINDOW_SIZE)
/* ------------------------------------------------------------------------ *
* Session state
* ------------------------------------------------------------------------ */
static ft_tt_udrv_pci_device card;
static int chardev = -1;
/** BAR0's TLB register file, mapped by US through sysfs. `read_registers` is
* the only mapping rungs 1-3 have, and it is PROT_READ. */
static const volatile uint32_t *read_registers;
static volatile uint32_t *write_registers;
static uint32_t claimed[CLAIMED_WINDOW_COUNT];
static uint64_t claimed_mmap_uc[CLAIMED_WINDOW_COUNT];
static size_t claimed_count;
/* ------------------------------------------------------------------------ *
* tt-kmd, used as an oracle and as an arbitrator
* ------------------------------------------------------------------------ */
static int allocate_window(uint32_t *out_id, uint64_t *out_mmap_uc)
{
struct ft_tt_kmd_allocate_window_request request;
memset(&request, 0, sizeof(request));
request.in.size = (uint64_t)FT_TT_UDRV_TLB_2M_WINDOW_SIZE;
if (ioctl(chardev, FT_TT_KMD_IOCTL_ALLOCATE_WINDOW, &request) != 0)
return FT_TT_UDRV_EIO;
*out_id = request.out.id;
*out_mmap_uc = request.out.mmap_offset_uc;
return FT_TT_UDRV_OK;
}
static int free_window(uint32_t id)
{
struct ft_tt_kmd_free_window_request request;
memset(&request, 0, sizeof(request));
request.in.id = id;
return (ioctl(chardev, FT_TT_KMD_IOCTL_FREE_WINDOW, &request) == 0) ? FT_TT_UDRV_OK
: FT_TT_UDRV_EIO;
}
/**
* Aim a window through the ioctl. The field mapping is tt-kmd's, not ours:
* `static_vc` in the ioctl feeds `use_static_vc` in the register, and the ioctl
* has no way to express `stream_header` or the 3-bit `static_vc` at all.
*/
static int configure_window_via_ioctl(uint32_t id, const ft_tt_udrv_tlb2m_config *config)
{
struct ft_tt_kmd_configure_window_request request;
memset(&request, 0, sizeof(request));
request.in.id = id;
request.in.config.addr = config->addr;
request.in.config.x_end = config->x_end;
request.in.config.y_end = config->y_end;
request.in.config.x_start = config->x_start;
request.in.config.y_start = config->y_start;
request.in.config.noc = config->noc;
request.in.config.mcast = config->multicast;
request.in.config.ordering = config->ordering;
request.in.config.linked = config->linked;
request.in.config.static_vc = config->use_static_vc;
return (ioctl(chardev, FT_TT_KMD_IOCTL_CONFIGURE_WINDOW, &request) == 0) ? FT_TT_UDRV_OK
: FT_TT_UDRV_EIO;
}
/* ------------------------------------------------------------------------ *
* Register access through OUR mapping
* ------------------------------------------------------------------------ */
/** Word index of window `id`'s register triple within the mapped file. */
static size_t register_word_index(uint32_t id)
{
return (size_t)id * (FT_TT_UDRV_TLB_REG_SIZE / 4u);
}
static void read_window_registers(uint32_t id, uint32_t out_words[3])
{
size_t base = register_word_index(id);
out_words[0] = read_registers[base + 0u];
out_words[1] = read_registers[base + 1u];
out_words[2] = read_registers[base + 2u];
}
/** Write a window's three registers, in tt-kmd's order: low, mid, high. */
static void write_window_registers(uint32_t id, const uint32_t words[3])
{
size_t base = register_word_index(id);
write_registers[base + 0u] = words[0];
write_registers[base + 1u] = words[1];
write_registers[base + 2u] = words[2];
}
/** Word index of window `id`'s strided-multicast register. */
static size_t strided_word_index(uint32_t id)
{
return (size_t)(FT_TT_UDRV_TLB_STRIDED_REGS_OFFSET +
id * FT_TT_UDRV_TLB_STRIDED_REG_SIZE) /
4u;
}
/* ------------------------------------------------------------------------ *
* The sweep
* ------------------------------------------------------------------------ */
/** One configuration the ioctl can express, and therefore one the driver can
* be asked to write for us. */
typedef struct sweep_case {
const char *name;
ft_tt_udrv_tlb2m_config config;
} sweep_case;
#define SWEEP_CAPACITY 160u
static sweep_case sweep[SWEEP_CAPACITY];
static size_t sweep_count;
static void add_sweep_case(const char *name, uint64_t addr, uint8_t x_end, uint8_t y_end,
uint8_t x_start, uint8_t y_start, uint8_t noc, uint8_t multicast,
uint8_t ordering, uint8_t linked, uint8_t use_static_vc)
{
sweep_case *entry;
if (sweep_count >= SWEEP_CAPACITY)
return;
entry = &sweep[sweep_count++];
memset(entry, 0, sizeof(*entry));
entry->name = name;
entry->config.addr = addr;
entry->config.x_end = x_end;
entry->config.y_end = y_end;
entry->config.x_start = x_start;
entry->config.y_start = y_start;
entry->config.noc = noc;
entry->config.multicast = multicast;
entry->config.ordering = ordering;
entry->config.linked = linked;
entry->config.use_static_vc = use_static_vc;
/* stream_header and static_vc_id stay zero: CONFIGURE_TLB cannot express
* them, so a sweep case that set them would be comparing our encoder
* against a driver that was never asked for the same thing. Those two
* fields are exactly what the userspace driver REGAINS, and they can only
* be gated in rung 4, where we do the writing. */
}
/**
* Boundary cases chosen against the field list, then every reference vector
* the ioctl can express.
*
* The address cases matter most: the register's address field is 43 bits at bit
* 0, so it fills word 0 and spills 11 bits into word 1. `addr < 2^53` and
* `addr >= 2^53` are the two sides of that boundary and they exercise different
* words.
*/
static void build_sweep(void)
{
size_t index;
add_sweep_case("zero", 0ull, 0, 0, 0, 0, 0, 0, 0, 0, 0);
add_sweep_case("arc_tile_strict", ARC_CSM_BASE, ARC_X, ARC_Y, 0, 0, 0, 0, 1, 0, 0);
add_sweep_case("arc_scratch_strict", WINDOW_PAGE(ARC_SCRATCH_BASE), ARC_X, ARC_Y, 0, 0, 0, 0,
1, 0, 0);
add_sweep_case("addr_one_window", 1ull << 21, 1, 1, 0, 0, 0, 0, 0, 0, 0);
add_sweep_case("addr_bit_31", 1ull << 31, 1, 1, 0, 0, 0, 0, 0, 0, 0);
add_sweep_case("addr_bit_32", 1ull << 32, 1, 1, 0, 0, 0, 0, 0, 0, 0);
add_sweep_case("addr_just_below_word_boundary", (1ull << 53) - (1ull << 21), 1, 1, 0, 0, 0, 0,
0, 0, 0);
add_sweep_case("addr_at_word_boundary", 1ull << 53, 1, 1, 0, 0, 0, 0, 0, 0, 0);
add_sweep_case("addr_just_above_word_boundary", (1ull << 53) + (1ull << 21), 1, 1, 0, 0, 0, 0,
0, 0, 0);
add_sweep_case("addr_bit_63", 1ull << 63, 1, 1, 0, 0, 0, 0, 0, 0, 0);
add_sweep_case("addr_field_all_ones", 0xFFFFFFFFFFE00000ull, 0, 0, 0, 0, 0, 0, 0, 0, 0);
add_sweep_case("coords_zero", 0ull, 0, 0, 0, 0, 0, 0, 0, 0, 0);
add_sweep_case("x_end_max", 0ull, 63, 0, 0, 0, 0, 0, 0, 0, 0);
add_sweep_case("y_end_max", 0ull, 0, 63, 0, 0, 0, 0, 0, 0, 0);
add_sweep_case("x_start_max", 0ull, 0, 0, 63, 0, 0, 0, 0, 0, 0);
add_sweep_case("y_start_max", 0ull, 0, 0, 0, 63, 0, 0, 0, 0, 0);
add_sweep_case("y_start_7_below_the_straddle", 0ull, 0, 0, 0, 7, 0, 0, 0, 0, 0);
add_sweep_case("y_start_8_across_the_straddle", 0ull, 0, 0, 0, 8, 0, 0, 0, 0, 0);
add_sweep_case("all_coords_max", 0ull, 63, 63, 63, 63, 0, 0, 0, 0, 0);
add_sweep_case("multicast_rectangle", 1ull << 21, 10, 9, 1, 2, 0, 1, 0, 0, 0);
add_sweep_case("multicast_full_grid", 0ull, 63, 63, 0, 0, 0, 1, 0, 0, 0);
add_sweep_case("noc_1", 0ull, 1, 1, 0, 0, 1, 0, 0, 0, 0);
add_sweep_case("noc_max", 0ull, 1, 1, 0, 0, 3, 0, 0, 0, 0);
add_sweep_case("ordering_relaxed", 0ull, 1, 1, 0, 0, 0, 0, 0, 0, 0);
add_sweep_case("ordering_strict", 0ull, 1, 1, 0, 0, 0, 0, 1, 0, 0);
add_sweep_case("ordering_posted", 0ull, 1, 1, 0, 0, 0, 0, 2, 0, 0);
add_sweep_case("ordering_posted_strict", 0ull, 1, 1, 0, 0, 0, 0, 3, 0, 0);
add_sweep_case("linked", 0ull, 1, 1, 0, 0, 0, 0, 0, 1, 0);
add_sweep_case("use_static_vc", 0ull, 1, 1, 0, 0, 0, 0, 0, 0, 1);
add_sweep_case("everything_the_ioctl_can_set", 0xFFFFFFFFFFE00000ull, 63, 63, 63, 63, 3, 1, 3,
1, 1);
for (index = 0u; index < FT_TT_TLB2M_REF_VECTOR_COUNT; index++) {
const ft_tt_tlb2m_reference_vector *vector = &ft_tt_tlb2m_reference_vectors[index];
if (vector->stream_header != 0u || vector->static_vc != 0u)
continue;
add_sweep_case(vector->name, vector->addr, vector->x_end, vector->y_end, vector->x_start,
vector->y_start, vector->noc, vector->multicast, vector->ordering,
vector->linked, vector->use_static_vc);
}
}
/* ------------------------------------------------------------------------ *
* Rung 1 — find the card, map BAR0 read-only
* ------------------------------------------------------------------------ */
static bool stage_open_the_card(void)
{
size_t count = 0u;
void *mapping = NULL;
ft_tt_test_step("Rung 1 — the card, and BAR0's register file mapped READ-ONLY");
ft_tt_test_check_fmt("running as root (the BAR files are 0600 root:root)", geteuid() == 0u,
"euid=%u", (unsigned)geteuid());
if (ft_tt_udrv_pci_find_devices(NULL, FT_TT_UDRV_PCI_VENDOR_TENSTORRENT,
FT_TT_UDRV_PCI_DEVICE_BLACKHOLE, &card, 1u,
&count) != FT_TT_UDRV_OK ||
count == 0u) {
ft_tt_test_check("a Blackhole is present in sysfs", false, "none found");
return false;
}
ft_tt_test_check_fmt("a Blackhole is present in sysfs", true, "%s (%zu found)", card.address,
count);
ft_tt_test_check_fmt("BAR0 is 512 MiB and prefetchable",
card.bar[0].length == (512ull << 20) && card.bar[0].prefetchable,
"0x%llx bytes at 0x%llx", (unsigned long long)card.bar[0].length,
(unsigned long long)card.bar[0].start);
ft_tt_test_check_fmt("BAR4 is absent — no 4 GiB windows on this board",
!card.bar[4].present, "len=%llu",
(unsigned long long)card.bar[4].length);
/* The register file is UNCACHED, through `resource0`, not `resource0_wc`.
* That is what tt-kmd's own pci_iomap_range() gives it,
* and write-combining a register write is how a store arrives late. */
if (ft_tt_udrv_pci_map_bar(&card, 0u, false, false, FT_TT_UDRV_TLB_REGS_START,
FT_TT_UDRV_TLB_REGS_LEN, &mapping) != FT_TT_UDRV_OK) {
ft_tt_test_check_fmt("BAR0's TLB register file maps read-only", false, "errno %d (%s)",
errno, strerror(errno));
return false;
}
read_registers = mapping;
ft_tt_test_check_fmt("BAR0's TLB register file maps read-only", true,
"resource0 + 0x%08X, %u bytes, PROT_READ", FT_TT_UDRV_TLB_REGS_START,
FT_TT_UDRV_TLB_REGS_LEN);
/* The write-combined companion file is the BULK UPLOAD path and nothing has
* ever used it. Mapping it read-only over the same registers costs nothing
* and settles half the open question: the file exists and maps. It does NOT
* settle that a WC STORE reaches the card, which is a separate item. */
{
void *wc_mapping = NULL;
if (ft_tt_udrv_pci_map_bar(&card, 0u, true, false, FT_TT_UDRV_TLB_REGS_START,
FT_TT_UDRV_TLB_REGS_LEN, &wc_mapping) != FT_TT_UDRV_OK) {
ft_tt_test_check_fmt("resource0_wc maps too", false, "errno %d (%s)", errno,
strerror(errno));
} else {
const volatile uint32_t *wc = wc_mapping;
size_t base = register_word_index(KERNEL_WINDOW_ID);
ft_tt_test_check_fmt("resource0_wc maps, and reads the same words as resource0",
wc[base + 0u] == read_registers[base + 0u] &&
wc[base + 1u] == read_registers[base + 1u] &&
wc[base + 2u] == read_registers[base + 2u],
"0x%08x 0x%08x 0x%08x", wc[base + 0u], wc[base + 1u],
wc[base + 2u]);
(void)ft_tt_udrv_pci_unmap_bar(wc_mapping, FT_TT_UDRV_TLB_REGS_LEN);
}
}
chardev = open(CHARDEV_PATH, O_RDWR | O_CLOEXEC);
ft_tt_test_check_fmt("tt-kmd is loaded and its chardev opens — it is the ORACLE for rung 3, "
"and the way back if anything wedges",
chardev >= 0, "%s fd=%d", CHARDEV_PATH, chardev);
return chardev >= 0;
}
/* ------------------------------------------------------------------------ *
* Rung 2 — the read-only rung
* ------------------------------------------------------------------------ */
/**
* Decode the window tt-kmd keeps for itself and check it against a full
* 96-bit prediction.
*
* tt-kmd re-aims window 201 on every NoC read it performs and every one of
* those reads is `noc_read32(bh, ARC_X, ARC_Y, addr, 0)` with
* `config.ordering = 1`. Telemetry lives in the CSM, so
* at rest the window sits on the CSM's 2 MiB page. Everything else must be
* zero.
*/
static void stage_read_the_kernels_window(void)
{
ft_tt_udrv_tlb2m_config decoded;
ft_tt_udrv_tlb2m_config neighbour;
uint32_t words[3] = {0u, 0u, 0u};
uint32_t shifted[3] = {0u, 0u, 0u};
ft_tt_udrv_tlb2m_config shifted_decode;
size_t all_ones = 0u;
size_t nonzero = 0u;
uint32_t id;
ft_tt_test_step("Rung 2 — READ ONLY. Window %u is tt-kmd's; it must decode to the ARC tile",
KERNEL_WINDOW_ID);
/* A dead BAR reads all-ones everywhere. Census first, so "it decoded" can
* never be a story about a mapping that returns garbage. */
for (id = 0u; id < FT_TT_UDRV_TLB_2M_WINDOW_COUNT; id++) {
uint32_t triple[3];
read_window_registers(id, triple);
if (triple[0] == 0xFFFFFFFFu && triple[1] == 0xFFFFFFFFu && triple[2] == 0xFFFFFFFFu)
all_ones++;
if (triple[0] != 0u || triple[1] != 0u || triple[2] != 0u)
nonzero++;
}
ft_tt_test_check_fmt("no window reads all-ones — the aperture is decoding, not floating",
all_ones == 0u, "%zu of %u", all_ones,
FT_TT_UDRV_TLB_2M_WINDOW_COUNT);
ft_tt_test_check_fmt("some windows hold live configuration — a freshly claimed window is NOT "
"blank",
nonzero > 0u, "%zu of %u non-zero", nonzero,
FT_TT_UDRV_TLB_2M_WINDOW_COUNT);
read_window_registers(KERNEL_WINDOW_ID, words);
ft_tt_test_check_fmt("the kernel window's three registers, raw", true,
"id %u: 0x%08x 0x%08x 0x%08x", KERNEL_WINDOW_ID, words[0], words[1],
words[2]);
(void)ft_tt_udrv_tlb2m_decode(words, &decoded);
ft_tt_test_check_fmt("...decodes to the ARC tile's X", decoded.x_end == ARC_X, "%u",
decoded.x_end);
ft_tt_test_check_fmt("...and the ARC tile's Y", decoded.y_end == ARC_Y, "%u", decoded.y_end);
ft_tt_test_check_fmt("...unicast, so the rectangle fields are zero",
decoded.multicast == 0u && decoded.x_start == 0u &&
decoded.y_start == 0u,
"mcast=%u start=(%u,%u)", decoded.multicast, decoded.x_start,
decoded.y_start);
ft_tt_test_check_fmt("...on NoC 0", decoded.noc == 0u, "%u", decoded.noc);
ft_tt_test_check_fmt("...strictly ordered, which is the only ordering tt-kmd ever asks for",
decoded.ordering == 1u, "%u", decoded.ordering);
ft_tt_test_check_fmt("...aimed at the ARC CSM page, where telemetry lives",
decoded.addr == WINDOW_PAGE(ARC_CSM_BASE), "0x%llx",
(unsigned long long)decoded.addr);
ft_tt_test_check_fmt("...with linked, use_static_vc and stream_header clear",
decoded.linked == 0u && decoded.use_static_vc == 0u &&
decoded.stream_header == 0u,
"linked=%u vc?=%u sh=%u", decoded.linked, decoded.use_static_vc,
decoded.stream_header);
/* THE ANTI-VACUITY ARM. Everything above would also pass if the decode
* were insensitive to where in BAR0 it read. Read the SAME field pattern
* one register triple lower, and four bytes off the triple boundary, and
* require both to come out different — otherwise "it decodes to (8,0)" is
* a statement about nothing. */
read_window_registers(KERNEL_WINDOW_ID - 1u, words);
(void)ft_tt_udrv_tlb2m_decode(words, &neighbour);
ft_tt_test_check_fmt("the NEIGHBOURING window does not also decode to (8,0) strict at the CSM "
"— the check is position-sensitive",
!(neighbour.x_end == ARC_X && neighbour.y_end == ARC_Y &&
neighbour.ordering == 1u &&
neighbour.addr == WINDOW_PAGE(ARC_CSM_BASE)),
"window %u: (%u,%u) ord %u addr 0x%llx", KERNEL_WINDOW_ID - 1u,
neighbour.x_end, neighbour.y_end, neighbour.ordering,
(unsigned long long)neighbour.addr);
shifted[0] = read_registers[register_word_index(KERNEL_WINDOW_ID) + 1u];
shifted[1] = read_registers[register_word_index(KERNEL_WINDOW_ID) + 2u];
shifted[2] = read_registers[register_word_index(KERNEL_WINDOW_ID) + 3u];
(void)ft_tt_udrv_tlb2m_decode(shifted, &shifted_decode);
ft_tt_test_check_fmt("a decode started one WORD late does not give (8,0) either — the register "
"offset is load-bearing and is being tested",
!(shifted_decode.x_end == ARC_X && shifted_decode.y_end == ARC_Y &&
shifted_decode.ordering == 1u),
"(%u,%u) ord %u", shifted_decode.x_end, shifted_decode.y_end,
shifted_decode.ordering);
}
/* ------------------------------------------------------------------------ *
* Rung 3 — tt-kmd as the on-silicon oracle
* ------------------------------------------------------------------------ */
static bool stage_claim_windows(void)
{
size_t index;
ft_tt_test_step("Rung 3a — claim %u windows through ALLOCATE_TLB (pure arbitration; nothing "
"reaches the card)",
CLAIMED_WINDOW_COUNT);
for (index = 0u; index < CLAIMED_WINDOW_COUNT; index++) {
uint32_t id = 0u;
uint64_t mmap_uc = 0u;
if (allocate_window(&id, &mmap_uc) != FT_TT_UDRV_OK)
break;
claimed[claimed_count] = id;
claimed_mmap_uc[claimed_count] = mmap_uc;
claimed_count++;
}
ft_tt_test_check_fmt("every window was granted", claimed_count == CLAIMED_WINDOW_COUNT,
"%zu of %u", claimed_count, CLAIMED_WINDOW_COUNT);
if (claimed_count == 0u)
return false;
ft_tt_test_check_fmt("the lowest id is below the strided threshold",
claimed[0] < FT_TT_UDRV_TLB_STRIDED_COUNT, "id %u < %u", claimed[0],
FT_TT_UDRV_TLB_STRIDED_COUNT);
ft_tt_test_check_fmt("the highest id is at or above it — both sides of the strided rule get "
"exercised",
claimed[claimed_count - 1u] >= FT_TT_UDRV_TLB_STRIDED_COUNT, "id %u",
claimed[claimed_count - 1u]);
ft_tt_test_check_fmt("none of them is tt-kmd's own window",
claimed[claimed_count - 1u] != KERNEL_WINDOW_ID, "highest is %u",
claimed[claimed_count - 1u]);
return true;
}
/**
* The step that makes the whole approach safe: ask the DRIVER to aim a window,
* then read the registers it wrote through OUR mapping and compare them, byte
* for byte, against what our encoder says they should be.
*
* Nothing here writes a register. If our encoder is wrong, this is where it is
* found — with the card in exactly the state tt-kmd left it.
*/
static void stage_encoder_against_silicon(uint32_t window, const char *label)
{
size_t disagreements = 0u;
size_t index;
ft_tt_test_step("Rung 3b — %s: %zu configurations, driver writes, we read and compare",
label, sweep_count);
for (index = 0u; index < sweep_count; index++) {
const sweep_case *entry = &sweep[index];
uint32_t expected[3] = {0u, 0u, 0u};
uint32_t observed[3] = {0u, 0u, 0u};
if (configure_window_via_ioctl(window, &entry->config) != FT_TT_UDRV_OK) {
ft_tt_test_check_fmt("CONFIGURE_TLB succeeds", false, "%s: errno %d", entry->name,
errno);
disagreements++;
continue;
}
(void)ft_tt_udrv_tlb2m_encode(&entry->config, expected);
read_window_registers(window, observed);
if (memcmp(expected, observed, sizeof(expected)) != 0) {
ft_tt_test_check_fmt("register words match tt-kmd's", false,
"%s: ours 0x%08x 0x%08x 0x%08x, card 0x%08x 0x%08x 0x%08x",
entry->name, expected[0], expected[1], expected[2], observed[0],
observed[1], observed[2]);
disagreements++;
}
}
ft_tt_test_check_fmt("every configuration agrees, byte for byte", disagreements == 0u,
"%zu of %zu disagreed on window %u", disagreements, sweep_count, window);
}
/**
* The ioctl takes 16-bit coordinates and drops them into 6-bit register fields,
* so a coordinate above 63 is silently truncated BY THE KERNEL. Our encoder
* truncates too, and the two have to truncate identically or a caller who
* passes 64 aims at tile 0 on one path and somewhere else on the other.
*/
static void stage_truncation_on_silicon(uint32_t window)
{
static const uint16_t wide[] = {64u, 65u, 127u, 255u, 4095u, 65535u};
size_t index;
ft_tt_test_step("Rung 3c — an out-of-range coordinate truncates the same way in the kernel "
"and in our encoder");
for (index = 0u; index < sizeof(wide) / sizeof(wide[0]); index++) {
struct ft_tt_kmd_configure_window_request request;
ft_tt_udrv_tlb2m_config config = {0};
uint32_t expected[3] = {0u, 0u, 0u};
uint32_t observed[3] = {0u, 0u, 0u};
memset(&request, 0, sizeof(request));
request.in.id = window;
request.in.config.x_end = wide[index];
request.in.config.y_end = wide[index];
if (ioctl(chardev, FT_TT_KMD_IOCTL_CONFIGURE_WINDOW, &request) != 0) {
ft_tt_test_check_fmt("CONFIGURE_TLB accepts a wide coordinate", false, "%u: errno %d",
wide[index], errno);
continue;
}
/* Our encoder is handed the same wide value, narrowed only by the C
* conversion the struct field forces — exactly what the kernel's
* bitfield assignment does. */
config.x_end = (uint8_t)(wide[index] & 0x3Fu);
config.y_end = (uint8_t)(wide[index] & 0x3Fu);
(void)ft_tt_udrv_tlb2m_encode(&config, expected);
read_window_registers(window, observed);
ft_tt_test_check_fmt("x_end/y_end truncate to 6 bits",
memcmp(expected, observed, sizeof(expected)) == 0, "%u -> %u",
wide[index], wide[index] & 0x3Fu);
}
}
/**
* Two windows, two different aims, read back at once.
*
* A register-offset bug that pointed every window at the same triple would pass
* every check above — each comparison would read back what was written most
* recently, which is what it expects. This is the arm that catches it.
*/
static void stage_windows_are_distinct(uint32_t low, uint32_t high)
{
ft_tt_udrv_tlb2m_config low_config = {0};
ft_tt_udrv_tlb2m_config high_config = {0};
uint32_t low_expected[3];
uint32_t high_expected[3];
uint32_t low_observed[3];
uint32_t high_observed[3];
ft_tt_test_step("Rung 3d — two windows hold two different aims at the same time");
low_config.addr = ARC_CSM_BASE;
low_config.x_end = ARC_X;
low_config.y_end = ARC_Y;
low_config.ordering = 1u;
high_config.addr = WINDOW_PAGE(ARC_SCRATCH_BASE);
high_config.x_end = ARC_X;
high_config.y_end = ARC_Y;
high_config.ordering = 1u;
(void)configure_window_via_ioctl(low, &low_config);
(void)configure_window_via_ioctl(high, &high_config);
(void)ft_tt_udrv_tlb2m_encode(&low_config, low_expected);
(void)ft_tt_udrv_tlb2m_encode(&high_config, high_expected);
read_window_registers(low, low_observed);
read_window_registers(high, high_observed);
ft_tt_test_check_fmt("the low window still holds its own aim",
memcmp(low_expected, low_observed, sizeof(low_expected)) == 0, "id %u",
low);
ft_tt_test_check_fmt("the high window holds the other",
memcmp(high_expected, high_observed, sizeof(high_expected)) == 0, "id %u",
high);
ft_tt_test_check("...and the two register triples are NOT the same memory",
memcmp(low_observed, high_observed, sizeof(low_observed)) != 0,
"id * 12 is really addressing distinct registers");
}
/**
* CONFIGURE_TLB zeroes a strided register for every window below 32. Reading
* that it is zero proves nothing on its own — it was probably already zero. So
* rung 3 only records the value, and rung 4 turns it into a real test by
* putting a marker there first.
*/
static void stage_strided_is_zero_after_aim(uint32_t low)
{
uint32_t value;
ft_tt_test_step("Rung 3e — the strided register after an aim (recorded, not yet proved)");
value = read_registers[strided_word_index(low)];
ft_tt_test_check_fmt("the low window's strided register reads zero after CONFIGURE_TLB",
value == 0u, "id %u: 0x%08x at BAR0 + 0x%08X", low, value,
(unsigned)(FT_TT_UDRV_TLB_REGS_START + FT_TT_UDRV_TLB_STRIDED_REGS_OFFSET +
low * FT_TT_UDRV_TLB_STRIDED_REG_SIZE));
}
/* ------------------------------------------------------------------------ *
* Rung 4 — our own writes
* ------------------------------------------------------------------------ */
/**
* THE INDEPENDENT READBACK PATH.
*
* Rung 4 writes registers through OUR sysfs mmap, so reading them back through
* the same mmap proves very little — it is the "do not instrument a mechanism
* with itself" rule. This is the other reader: tt-kmd's OWN BAR0 mapping,
* obtained from QUERY_MAPPINGS and mmap'd on the driver's character device. It
* is a different file, a different vma, a different kernel function
* (`map_pci_bar` -> `vm_iomap_memory`) and a different pgprot decision from
* the PCI core's `pci_mmap_resource`.
*
* /dev/mem was the first choice and it does NOT work on this box: a `pread` at
* the BAR's physical address returns EFAULT even as root, because x86's
* `xlate_dev_mem_ptr` cannot remap this high non-RAM range for the read path.
* That is recorded here rather than in a comment somewhere, because "we used
* /dev/mem" is exactly the kind of claim that gets repeated after it stopped
* being true.
*/
static const volatile uint32_t *driver_registers;
/** BAR0's UC mapping offset, as tt-kmd's QUERY_MAPPINGS reports it. */
static bool query_driver_bar0_mapping(uint64_t *out_base, uint64_t *out_size)
{
struct {
struct ft_tt_kmd_query_mappings_in in;
struct ft_tt_kmd_mapping mappings[8];
} query;
size_t index;
memset(&query, 0, sizeof(query));
query.in.count = 8u;
if (ioctl(chardev, FT_TT_KMD_IOCTL_QUERY_MAPPINGS, &query) != 0)
return false;
for (index = 0u; index < 8u; index++) {
if (query.mappings[index].id != FT_TT_KMD_MAPPING_RESOURCE0_UC)
continue;
*out_base = query.mappings[index].base;
*out_size = query.mappings[index].size;
return true;
}
return false;
}
static void read_triple_via_driver(uint32_t id, uint32_t out_words[3])
{
size_t base = register_word_index(id);
out_words[0] = driver_registers[base + 0u];
out_words[1] = driver_registers[base + 1u];
out_words[2] = driver_registers[base + 2u];
}
/** Does /dev/mem work here at all? Asked once, reported, never relied on.
* `out_errno` is captured before close(), which would otherwise overwrite it
* and make the transcript claim "Success" for a call that failed. */
static bool devmem_can_read_the_bar(int *out_errno)
{
uint32_t scratch[3] = {0u, 0u, 0u};
off_t physical = (off_t)(card.bar[0].start + FT_TT_UDRV_TLB_REGS_START);
ssize_t got;
int file = open("/dev/mem", O_RDONLY | O_CLOEXEC);
if (file < 0) {
*out_errno = errno;
return false;
}
errno = 0;
got = pread(file, scratch, sizeof(scratch), physical);
*out_errno = errno;
(void)close(file);
return got == (ssize_t)sizeof(scratch);
}
static bool stage_map_registers_writable(void)
{
uint64_t driver_base = 0u;
uint64_t driver_size = 0u;
void *mapping = NULL;
void *driver_mapping;
ft_tt_test_step("Rung 4a — a writable mapping of our own, and a SECOND READER that is not "
"ours at all");
if (ft_tt_udrv_pci_map_bar(&card, 0u, false, true, FT_TT_UDRV_TLB_REGS_START,
FT_TT_UDRV_TLB_REGS_LEN, &mapping) != FT_TT_UDRV_OK) {
ft_tt_test_check_fmt("the register file maps read-write through sysfs", false,
"errno %d (%s)", errno, strerror(errno));
return false;
}
write_registers = mapping;
ft_tt_test_check("the register file maps read-write through sysfs", true,
"the read-only mapping from rung 1 is still the one every read goes through");
if (!query_driver_bar0_mapping(&driver_base, &driver_size)) {
ft_tt_test_check("QUERY_MAPPINGS reports BAR0's UC mapping", false, "");
return false;
}
ft_tt_test_check_fmt("QUERY_MAPPINGS reports BAR0's UC mapping", driver_size == card.bar[0].length,
"base 0x%llx size 0x%llx", (unsigned long long)driver_base,
(unsigned long long)driver_size);
driver_mapping = mmap(NULL, FT_TT_UDRV_TLB_REGS_LEN, PROT_READ, MAP_SHARED, chardev,
(off_t)(driver_base + FT_TT_UDRV_TLB_REGS_START));
if (driver_mapping == MAP_FAILED) {
ft_tt_test_check_fmt("the same registers map through TT-KMD's own BAR0 path", false,
"errno %d (%s)", errno, strerror(errno));
return false;
}
driver_registers = driver_mapping;
ft_tt_test_check("the same registers map through TT-KMD's own BAR0 path", true,
"map_pci_bar/vm_iomap_memory, not pci_mmap_resource");
{
int devmem_errno = 0;
bool devmem_works = devmem_can_read_the_bar(&devmem_errno);
ft_tt_test_check_fmt("(informational) /dev/mem cannot read this BAR on this kernel, so it "
"is NOT the second reader",
!devmem_works, "pread errno %d (%s)", devmem_errno,
strerror(devmem_errno));
}
return true;
}
static void stage_our_writes_match(uint32_t window)
{
size_t sysfs_disagreements = 0u;
size_t driver_disagreements = 0u;
size_t index;
ft_tt_test_step("Rung 4b — WE write the registers; %zu configurations, read back two ways",
sweep_count);
for (index = 0u; index < sweep_count; index++) {
const sweep_case *entry = &sweep[index];
uint32_t written[3] = {0u, 0u, 0u};
uint32_t observed[3] = {0u, 0u, 0u};
uint32_t through_driver[3] = {0u, 0u, 0u};
(void)ft_tt_udrv_tlb2m_encode(&entry->config, written);
write_window_registers(window, written);
if (window < FT_TT_UDRV_TLB_STRIDED_COUNT)
write_registers[strided_word_index(window)] = 0u;
read_window_registers(window, observed);
if (memcmp(written, observed, sizeof(written)) != 0) {
ft_tt_test_check_fmt("what we wrote is what the register holds", false,
"%s: wrote 0x%08x 0x%08x 0x%08x, read 0x%08x 0x%08x 0x%08x",
entry->name, written[0], written[1], written[2], observed[0],
observed[1], observed[2]);
sysfs_disagreements++;
}
read_triple_via_driver(window, through_driver);
if (memcmp(written, through_driver, sizeof(written)) != 0) {
ft_tt_test_check_fmt("tt-kmd's own mapping sees the same register", false,
"%s: 0x%08x 0x%08x 0x%08x", entry->name, through_driver[0],
through_driver[1], through_driver[2]);
driver_disagreements++;
}
}
ft_tt_test_check_fmt("every write reads back through our sysfs mapping",
sysfs_disagreements == 0u, "%zu of %zu", sysfs_disagreements,
sweep_count);
ft_tt_test_check_fmt("...and through TT-KMD's BAR0 mapping, which is not the path that wrote "
"them",
driver_disagreements == 0u, "%zu of %zu", driver_disagreements,
sweep_count);
}
/**
* Then hand the same window to the DRIVER and let it overwrite what we wrote.
*
* This is the direction that matters for a machine with no tt-kmd on it: it
* proves the register we write and the register tt-kmd writes are the same
* register, not two things that merely look alike.
*/
static void stage_the_driver_overwrites_us(uint32_t window)
{
ft_tt_udrv_tlb2m_config ours = {0};
ft_tt_udrv_tlb2m_config theirs = {0};
uint32_t our_words[3];
uint32_t their_words[3];
uint32_t observed[3];
ft_tt_test_step("Rung 4c — the driver writes over our write, in the same register");
ours.addr = WINDOW_PAGE(ARC_SCRATCH_BASE);
ours.x_end = ARC_X;
ours.y_end = ARC_Y;
ours.ordering = 1u;
theirs.addr = ARC_CSM_BASE;
theirs.x_end = ARC_X;
theirs.y_end = ARC_Y;
theirs.ordering = 1u;
(void)ft_tt_udrv_tlb2m_encode(&ours, our_words);
(void)ft_tt_udrv_tlb2m_encode(&theirs, their_words);
write_window_registers(window, our_words);
read_window_registers(window, observed);
ft_tt_test_check("our write lands", memcmp(our_words, observed, sizeof(our_words)) == 0, "");
(void)configure_window_via_ioctl(window, &theirs);
read_window_registers(window, observed);
ft_tt_test_check("the driver's write replaces it",
memcmp(their_words, observed, sizeof(their_words)) == 0,
"same register, two writers");
ft_tt_test_check("...and the two configurations were actually different",
memcmp(our_words, their_words, sizeof(our_words)) != 0,
"otherwise the overwrite proved nothing");
}
/**
* 0x9D8 or 0xA08 — settled on silicon, not by reading source.
*
* The strided register block is sized from the TOTAL window count (202 2 MiB +
* 8 4 GiB = 210), so window N's strided register is at 0x9D8 + N*4. Sizing it
* from 214 gives 0xA08, which is what an earlier reading of this said, and
* 0xA08 + 0*4 is exactly 0x9D8 + 12*4 — window 12's register. So: put a marker
* in both, ask the driver to aim window 0, and see which one it clears.
*
* Both markers land in registers of windows THIS PROCESS owns, and neither
* window's aperture is ever mapped, so no transaction can be produced by
* either. The clean-up is the driver's own zeroing write, not ours.
*/
static void stage_the_strided_offset(uint32_t low, uint32_t low_plus_twelve)
{
ft_tt_udrv_tlb2m_config benign = {0};
const uint32_t marker = 0x5A5AA5A5u;
uint32_t at_9d8;
uint32_t at_a08;
ft_tt_test_step("Rung 4d — which offset does CONFIGURE_TLB zero, 0x9D8 or 0xA08?");
if (low != 0u || low_plus_twelve != 12u) {
ft_tt_test_check_fmt("windows 0 and 12 are the ones claimed", false, "got %u and %u", low,
low_plus_twelve);
return;
}
benign.addr = ARC_CSM_BASE;
benign.x_end = ARC_X;
benign.y_end = ARC_Y;
benign.ordering = 1u;
write_registers[strided_word_index(0u)] = marker;
write_registers[strided_word_index(12u)] = marker;
at_9d8 = read_registers[strided_word_index(0u)];
at_a08 = read_registers[strided_word_index(12u)];
ft_tt_test_check_fmt("both markers stuck — the strided registers are writable",
at_9d8 == marker && at_a08 == marker, "0x%08x / 0x%08x", at_9d8, at_a08);
(void)configure_window_via_ioctl(0u, &benign);
at_9d8 = read_registers[strided_word_index(0u)];
at_a08 = read_registers[strided_word_index(12u)];
ft_tt_test_check_fmt("aiming window 0 CLEARED 0x9D8 — that is window 0's strided register",
at_9d8 == 0u, "0x%08x", at_9d8);
ft_tt_test_check_fmt("...and left 0xA08 alone, so 0xA08 is NOT window 0's — the earlier "
"reading was wrong and this settles it on silicon",
at_a08 == marker, "0x%08x", at_a08);
/* Clean up the second marker the same way: it is window 12's register, so
* aiming window 12 is what zeroes it. */
(void)configure_window_via_ioctl(12u, &benign);
at_a08 = read_registers[strided_word_index(12u)];
ft_tt_test_check_fmt("aiming window 12 clears it, which is the same rule one window along",
at_a08 == 0u, "0x%08x", at_a08);
}
/**
* Did the aim we just wrote actually land, bit for bit?
*
* Checked through tt-kmd's BAR0 mapping, not through the one that wrote it, so
* a broken write path cannot confirm itself. A false return means the caller
* must NOT touch the aperture: the window is pointing somewhere nobody chose,
* and dereferencing it is the failure that costs a power cycle.
*/
static bool aim_landed(uint32_t id, const uint32_t intended[3])
{
uint32_t observed[3] = {0u, 0u, 0u};
read_triple_via_driver(id, observed);
if (memcmp(intended, observed, FT_TT_UDRV_TLB_REG_SIZE) == 0)
return true;
ft_tt_test_check_fmt("REFUSING to read through the aperture — the aim did not land", false,
"id %u: wanted 0x%08x 0x%08x 0x%08x, register holds 0x%08x 0x%08x 0x%08x",
id, intended[0], intended[1], intended[2], observed[0], observed[1],
observed[2]);
return false;
}
/**
* The functional check: a window WE aimed reaches the address we aimed it at.
*
* Register equality says the hardware sees the same 96 bits. It does not say
* the aperture goes anywhere. This does: read a block through a window we
* aimed with our own words through our own mapping, read the same block through
* a window tt-kmd aimed through its own mapping, and require byte equality.
*
* And then prove the comparison can say no — because a wrong aim is a wrong
* ADDRESS, not an error. tt-kmd's window is re-aimed at a different page of the
* same tile and the bytes must change. If they do not, the equality above was
* measuring nothing and the whole check is void.
*/
static void stage_a_window_we_aimed_reaches_it(uint32_t ours_id, uint64_t ours_aperture_offset,
uint32_t theirs_id, uint64_t theirs_mmap_offset)
{
ft_tt_udrv_tlb2m_config aim = {0};
ft_tt_udrv_tlb2m_config elsewhere = {0};
unsigned char ours[FUNCTIONAL_BLOCK_BYTES];
unsigned char theirs[FUNCTIONAL_BLOCK_BYTES];
unsigned char other_page[FUNCTIONAL_BLOCK_BYTES];
const volatile unsigned char *our_aperture = NULL;
volatile unsigned char *their_aperture = NULL;
void *mapping = NULL;
uint32_t words[3];
uint32_t table_pointer;
uint32_t values_pointer;
ft_tt_test_step("Rung 4e — a window WE aimed reads the address we aimed it at");
aim.addr = ARC_CSM_BASE;
aim.x_end = ARC_X;
aim.y_end = ARC_Y;
aim.ordering = 1u;
(void)ft_tt_udrv_tlb2m_encode(&aim, words);
write_window_registers(ours_id, words);
if (ours_id < FT_TT_UDRV_TLB_STRIDED_COUNT)
write_registers[strided_word_index(ours_id)] = 0u;
/* CONFIRM THE AIM BEFORE DEREFERENCING IT, through the reader that is not
* ours. This is the difference between "a wrong aim is unlikely" and "a
* wrong aim cannot be dereferenced": every other check in this file can
* fail and cost a red line, but reading through a window aimed at an
* endpoint nobody chose is what hangs the NoC and costs a walk to the box.
* If the write did not land exactly, refuse. */
if (!aim_landed(ours_id, words))
return;
/* Our aperture: our own mmap of resource0, at id * 2 MiB. Read-only,
* because this stage only reads. */
if (ft_tt_udrv_pci_map_bar(&card, 0u, false, false, ours_aperture_offset,
FUNCTIONAL_APERTURE_BYTES, &mapping) != FT_TT_UDRV_OK) {
ft_tt_test_check_fmt("our aperture maps", false, "errno %d", errno);
return;
}
our_aperture = mapping;
ft_tt_test_check_fmt("our aperture maps", true, "resource0 + 0x%llx",
(unsigned long long)ours_aperture_offset);
/* Their aperture: tt-kmd's mmap cookie on tt-kmd's fd. A different file, a
* different kernel path, a different vma. */
their_aperture = mmap(NULL, FUNCTIONAL_APERTURE_BYTES, PROT_READ, MAP_SHARED, chardev,
(off_t)theirs_mmap_offset);
if (their_aperture == MAP_FAILED) {
ft_tt_test_check_fmt("tt-kmd's aperture maps", false, "errno %d", errno);
(void)ft_tt_udrv_pci_unmap_bar(mapping, FUNCTIONAL_APERTURE_BYTES);
return;
}
(void)configure_window_via_ioctl(theirs_id, &aim);
ft_tt_test_check("tt-kmd's aperture maps, aimed at the same place through the ioctl", true,
"");
memcpy(ours, (const void *)our_aperture, sizeof(ours));
memcpy(theirs, (const void *)their_aperture, sizeof(theirs));
ft_tt_test_check_fmt("the two apertures return identical bytes",
memcmp(ours, theirs, sizeof(ours)) == 0, "%u bytes",
FUNCTIONAL_BLOCK_BYTES);
/* THE DISCRIMINATION ARM. Re-aim THEIR window at the scratch page — a
* different 2 MiB page of the same tile, and an address tt-kmd itself
* reads. The bytes must change. */
elsewhere.addr = WINDOW_PAGE(ARC_SCRATCH_BASE);
elsewhere.x_end = ARC_X;
elsewhere.y_end = ARC_Y;
elsewhere.ordering = 1u;
(void)configure_window_via_ioctl(theirs_id, &elsewhere);
memcpy(other_page, (const void *)their_aperture, sizeof(other_page));
ft_tt_test_check("a DIFFERENT page of the same tile returns DIFFERENT bytes — the comparison "
"above can say no",
memcmp(ours, other_page, sizeof(ours)) != 0,
"if this ever passes, the equality arm proved nothing");
ft_tt_test_check("two offsets within one page differ too — the readback is address-sensitive "
"below window granularity",
memcmp((const void *)our_aperture,
(const void *)(our_aperture + FUNCTIONAL_BLOCK_BYTES),
FUNCTIONAL_BLOCK_BYTES) != 0,
"");
/* And a SEMANTIC check on top of the byte one: aim our own window at the
* scratch page and read the two telemetry pointers ARC publishes. tt-kmd
* requires both to land inside the CSM before it will use them; so do we.
* Bytes from a wrong address would have to be inside a 512 KiB window of a
* 4 GiB space, twice, to fake this. */
(void)ft_tt_udrv_tlb2m_encode(&elsewhere, words);
write_window_registers(ours_id, words);
if (!aim_landed(ours_id, words))
goto unmap;
table_pointer = *(const volatile uint32_t *)(const void *)(our_aperture +
(ARC_SCRATCH_BASE -
WINDOW_PAGE(ARC_SCRATCH_BASE)) +
4u * ARC_TELEMETRY_TABLE_SCRATCH);
values_pointer = *(const volatile uint32_t *)(const void *)(our_aperture +
(ARC_SCRATCH_BASE -
WINDOW_PAGE(ARC_SCRATCH_BASE)) +
4u * ARC_TELEMETRY_VALUES_SCRATCH);
ft_tt_test_check_fmt("the telemetry table pointer read through OUR window is inside the ARC "
"CSM",
table_pointer >= ARC_CSM_BASE &&
table_pointer < ARC_CSM_BASE + ARC_CSM_SIZE,
"0x%08x", table_pointer);
ft_tt_test_check_fmt("...and so is the values pointer",
values_pointer >= ARC_CSM_BASE &&
values_pointer < ARC_CSM_BASE + ARC_CSM_SIZE,
"0x%08x", values_pointer);
unmap:
(void)munmap((void *)their_aperture, FUNCTIONAL_APERTURE_BYTES);
(void)ft_tt_udrv_pci_unmap_bar(mapping, FUNCTIONAL_APERTURE_BYTES);
}
/* ------------------------------------------------------------------------ *
* Release
* ------------------------------------------------------------------------ */
/**
* Leave every window we touched aimed somewhere BENIGN before giving it back.
*
* A freshly claimed window is not blank — that is a recorded hazard, and this
* run deliberately left windows aimed at coordinate (63,63) and at multicast
* rectangles because a configured window emits nothing. The next claimant is
* supposed to aim before it touches, but leaving 40 windows loaded with
* garbage coordinates is manufacturing the exact trap the hazard describes.
* So the release path re-aims each of them, through the ioctl, at the ARC tile.
*/
static void stage_release(void)
{
ft_tt_udrv_tlb2m_config benign = {0};
size_t index;
size_t failures = 0u;
ft_tt_test_step("Release — re-aim every claimed window somewhere harmless, then hand it back");
benign.addr = ARC_CSM_BASE;
benign.x_end = ARC_X;
benign.y_end = ARC_Y;
benign.ordering = 1u;
for (index = 0u; index < claimed_count; index++) {
if (configure_window_via_ioctl(claimed[index], &benign) != FT_TT_UDRV_OK)
failures++;
}
ft_tt_test_check_fmt("every window is aimed at the ARC tile, unicast, no multicast bit left "
"set anywhere",
failures == 0u, "%zu of %zu failed", failures, claimed_count);
failures = 0u;
for (index = 0u; index < claimed_count; index++) {
if (free_window(claimed[index]) != FT_TT_UDRV_OK)
failures++;
}
ft_tt_test_check_fmt("every window is released", failures == 0u, "%zu of %zu failed", failures,
claimed_count);
if (driver_registers) {
(void)munmap((void *)(uintptr_t)driver_registers, FT_TT_UDRV_TLB_REGS_LEN);
driver_registers = NULL;
}
if (write_registers) {
(void)ft_tt_udrv_pci_unmap_bar((void *)write_registers, FT_TT_UDRV_TLB_REGS_LEN);
write_registers = NULL;
}
if (read_registers) {
(void)ft_tt_udrv_pci_unmap_bar((void *)(uintptr_t)read_registers,
FT_TT_UDRV_TLB_REGS_LEN);
read_registers = NULL;
}
if (chardev >= 0) {
(void)close(chardev);
chardev = -1;
}
ft_tt_test_check("mappings unmapped and the chardev closed", true, "");
}
/* ------------------------------------------------------------------------ */
static void usage(const char *program)
{
(void)fprintf(stderr,
"usage: %s [read | write]\n"
" read rungs 1-3: map read-only, decode tt-kmd's window, and gate the\n"
" encoder against CONFIGURE_TLB. WRITES NOTHING TO THE CARD.\n"
" write rungs 1-4: the above, then write the registers ourselves.\n"
"Default is `read`.\n",
program);
}
int main(int argc, char **argv)
{
bool writing = false;
uint32_t low;
uint32_t high;
if (argc > 2) {
usage(argv[0]);
return 2;
}
if (argc == 2) {
if (strcmp(argv[1], "write") == 0) {
writing = true;
} else if (strcmp(argv[1], "read") != 0) {
usage(argv[0]);
return 2;
}
}
build_sweep();
if (!stage_open_the_card())
return ft_tt_test_finish();
stage_read_the_kernels_window();
if (!stage_claim_windows()) {
stage_release();
return ft_tt_test_finish();
}
low = claimed[0];
high = claimed[claimed_count - 1u];
stage_encoder_against_silicon(low, "a window below the strided threshold");
stage_encoder_against_silicon(high, "a window at or above it");
stage_truncation_on_silicon(low);
stage_windows_are_distinct(low, high);
stage_strided_is_zero_after_aim(low);
if (writing && stage_map_registers_writable()) {
stage_our_writes_match(low);
stage_our_writes_match(high);
stage_the_driver_overwrites_us(low);
stage_the_strided_offset(low, claimed_count > 12u ? claimed[12] : 0xFFFFFFFFu);
stage_a_window_we_aimed_reaches_it(low,
(uint64_t)low * FT_TT_UDRV_TLB_2M_WINDOW_SIZE, high,
claimed_mmap_uc[claimed_count - 1u]);
} else if (!writing) {
ft_tt_test_step("Rung 4 SKIPPED — run with `write` to let this process write a register");
}
stage_release();
return ft_tt_test_finish();
}