315 lines
15 KiB
C
315 lines
15 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_udrv_card_bank.c
|
|
* @brief ON THE CARD: prove that a window bank routes to the right endpoint, that a window
|
|
* pointed at the WRONG one is silent corruption, and that the audit catches it.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHY THIS EXISTS AND WHY THE MODEL RUN COULD NOT SETTLE IT
|
|
* ---------------------------------------------------------------------------
|
|
* `ft_tt_udrv_noc_bank.c` replaced a one-entry window cache with an N-entry one, and the
|
|
* failure it can introduce is the one this whole transport is built around: **a wrong window
|
|
* is a wrong ADDRESS, not an error.** The mis-aim red arm was run against a full decode and
|
|
* the answer was unreadable — the injection lands during ARC bring-up or core arming, the
|
|
* card refuses for its own reasons, the run falls back to the host forward, and the host
|
|
* forward produces the RIGHT text. A red arm whose consequence is "the run declined to use
|
|
* the card" cannot distinguish a detector that works from one that never looked.
|
|
*
|
|
* So the proof is here instead, on two endpoints nobody else is using, with the bytes read
|
|
* back and compared:
|
|
*
|
|
* GREEN write P to endpoint A and Q to endpoint B, read A, get P.
|
|
* RED poison the bank's belief so a window aimed at B claims to be aimed at A, read A,
|
|
* **get Q** — the read went to B and nothing said so.
|
|
* CAUGHT the same poisoned belief with `FT_TT_UDRV_AIM_AUDIT=1` REFUSES with EIO and
|
|
* names both endpoints, and nothing is dereferenced.
|
|
*
|
|
* IT USES DRAM, NOT L1, AND THAT IS DELIBERATE. Two DRAM banks on an idle card are two
|
|
* endpoints this process can write without touching a Tensix that might be running something.
|
|
* The mis-aimed read then lands in another DRAM bank, which is inert.
|
|
*
|
|
* @status None
|
|
* @issues
|
|
* - Needs root: the sysfs resource0 file is 0600 root:root.
|
|
* - `write` is required for anything that aims a window, same rule as the sibling suites.
|
|
* @todo None
|
|
*/
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "ft_tt_coord.h"
|
|
#include "ft_tt_hardware_session.h"
|
|
#include "ft_tt_udrv.h"
|
|
#include "ft_tt_udrv_claim.h"
|
|
#include "ft_tt_udrv_noc_bank.h"
|
|
#include "ft_tt_udrv_pci_sysfs.h"
|
|
|
|
/** Windows the bank under test claims. Two is the minimum that can hold two endpoints at
|
|
* once, which is the minimum that lets the red arm have somewhere wrong to point. */
|
|
#define BANK_WINDOWS 4u
|
|
|
|
/** Where in each DRAM bank the patterns go. High enough to be nowhere near anything a
|
|
* loader would put at the bottom, and 2 MiB aligned so one window covers it. */
|
|
#define TEST_DRAM_OFFSET 0x08000000ull
|
|
|
|
/** Bytes of pattern. Small: this is a correctness proof, not a bandwidth one. */
|
|
#define TEST_BYTES 256u
|
|
|
|
static unsigned checks;
|
|
static unsigned failures;
|
|
|
|
static void check(bool ok, const char *what)
|
|
{
|
|
checks++;
|
|
if (!ok)
|
|
failures++;
|
|
printf(" [%s] %s\n", ok ? "PASS" : "FAIL", what);
|
|
}
|
|
|
|
static void fill(unsigned char *buffer, size_t bytes, unsigned salt)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < bytes; i++)
|
|
buffer[i] = (unsigned char)((i * 31u + salt * 131u) & 0xFFu);
|
|
}
|
|
|
|
/**
|
|
* One arm: open a bank with the given environment, write both patterns, read endpoint A back.
|
|
*
|
|
* The bank is opened FRESH each time because the red and audit flags are read at open. The
|
|
* writes always go through the honest path — the red arm is read-only by construction, which
|
|
* is what keeps a stray store out of somebody else's memory.
|
|
*/
|
|
static int run_arm(ft_tt_udrv_noc_device *device, const ft_tt_udrv_claim_namespace *ns,
|
|
uint32_t reserved, uint16_t ax, uint16_t ay, uint16_t bx, uint16_t by,
|
|
const unsigned char *pattern_a, const unsigned char *pattern_b,
|
|
unsigned char *out_read, uint64_t *out_misaims)
|
|
{
|
|
ft_tt_udrv_noc_bank bank;
|
|
int rc = ft_tt_udrv_noc_bank_open(device, ns, reserved, false, BANK_WINDOWS, "test", &bank);
|
|
|
|
*out_misaims = 0u;
|
|
memset(out_read, 0, TEST_BYTES);
|
|
if (rc != FT_TT_UDRV_OK)
|
|
return rc;
|
|
|
|
rc = ft_tt_udrv_noc_bank_transfer(&bank, ax, ay, TEST_DRAM_OFFSET,
|
|
(void *)(uintptr_t)pattern_a, TEST_BYTES, false,
|
|
FT_TT_NOC_WIDTH_WORD32);
|
|
if (rc == FT_TT_UDRV_OK)
|
|
rc = ft_tt_udrv_noc_bank_transfer(&bank, bx, by, TEST_DRAM_OFFSET,
|
|
(void *)(uintptr_t)pattern_b, TEST_BYTES, false,
|
|
FT_TT_NOC_WIDTH_WORD32);
|
|
/* THE READ IS THE MEASUREMENT. Both windows are now aimed — A on one, B on another — so a
|
|
* red arm has somewhere wrong to send this and the bank has a hit to serve it from. */
|
|
if (rc == FT_TT_UDRV_OK)
|
|
rc = ft_tt_udrv_noc_bank_transfer(&bank, ax, ay, TEST_DRAM_OFFSET, out_read, TEST_BYTES,
|
|
true, FT_TT_NOC_WIDTH_WORD32);
|
|
*out_misaims = bank.misaims_injected;
|
|
(void)ft_tt_udrv_noc_bank_close(&bank);
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* THE `aimlow` ARM: one write through a bank whose every aim is perturbed in register word 0.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHY THIS IS A SEPARATE MODE AND NOT A FIFTH STAGE
|
|
* ---------------------------------------------------------------------------
|
|
* Both `FT_TT_UDRV_AIM_VERIFY` and `FT_TT_UDRV_AIM_RED` are parsed ONCE into a static, which
|
|
* is deliberate — they are on the hottest path in the driver. So `full` and `word` cannot
|
|
* both be exercised inside one process, and the two halves of this proof are two
|
|
* invocations of this binary with two environments. The runner prints both verdicts side by
|
|
* side; each alone says nothing.
|
|
*
|
|
* FT_TT_UDRV_AIM_VERIFY=full the readback disagrees with what was asked for and the aim
|
|
* REFUSES with EIO. Nothing is dereferenced.
|
|
* FT_TT_UDRV_AIM_VERIFY=word the readback AGREES, because word 1 does not carry a single
|
|
* significant address bit (proven off hardware in
|
|
* test_udrv_tlb_encode.c), and the write lands one 2 MiB page
|
|
* away — on DRAM, which is why this is DRAM and not L1.
|
|
*
|
|
* @return 0 when the verdict matched what the caller declared it expected.
|
|
*/
|
|
static int run_aimlow(ft_tt_udrv_noc_device *device, const ft_tt_udrv_claim_namespace *ns,
|
|
uint32_t reserved, uint16_t ax, uint16_t ay,
|
|
const unsigned char *pattern_a, bool arm, bool expect_refusal)
|
|
{
|
|
ft_tt_udrv_noc_bank bank;
|
|
const char *mode = getenv("FT_TT_UDRV_AIM_VERIFY");
|
|
uint64_t aim_failures;
|
|
bool armed;
|
|
int rc = ft_tt_udrv_noc_bank_open(device, ns, reserved, false, BANK_WINDOWS, "test", &bank);
|
|
|
|
if (rc != FT_TT_UDRV_OK) {
|
|
printf(" [FAIL] the bank would not open (rc=%d)\n", rc);
|
|
return 1;
|
|
}
|
|
/* THE ARM GOES LIVE HERE AND NOWHERE EARLIER. The card is already open, so the bring-up's
|
|
* own aims were honest; this perturbs exactly the transfer under test. An arm that could
|
|
* not fire returns false, so it reads as a NUMBER and not as an absence. */
|
|
armed = ft_tt_udrv_noc_aim_red_arm(arm);
|
|
printf(" the word-0 arm is %s\n", armed ? "ARMED" : "not armed");
|
|
if (arm != armed) {
|
|
check(false, "the arm did not reach the state this invocation asked for");
|
|
(void)ft_tt_udrv_noc_bank_close(&bank);
|
|
return 1;
|
|
}
|
|
|
|
rc = ft_tt_udrv_noc_bank_transfer(&bank, ax, ay, TEST_DRAM_OFFSET,
|
|
(void *)(uintptr_t)pattern_a, TEST_BYTES, false,
|
|
FT_TT_NOC_WIDTH_WORD32);
|
|
aim_failures = ft_tt_udrv_noc_bank_aim_failures(&bank);
|
|
(void)ft_tt_udrv_noc_aim_red_arm(false);
|
|
(void)ft_tt_udrv_noc_bank_close(&bank);
|
|
|
|
printf(" FT_TT_UDRV_AIM_VERIFY=%s, word 0 %s: rc=%d, %llu aim failures\n",
|
|
mode ? mode : "full(default)", armed ? "PERTURBED" : "untouched", rc,
|
|
(unsigned long long)aim_failures);
|
|
if (expect_refusal) {
|
|
check(rc == FT_TT_UDRV_EIO, "the aim was REFUSED — a wrong address is caught here");
|
|
check(aim_failures > 0u, "and the refusal is counted, not merely returned");
|
|
} else {
|
|
check(rc == FT_TT_UDRV_OK, "the aim was ACCEPTED");
|
|
check(aim_failures == 0u, "nothing was counted as a failure");
|
|
}
|
|
return failures == 0u ? 0 : 1;
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
ft_tt_hardware_session session;
|
|
ft_tt_udrv_pci_device found[4];
|
|
ft_tt_udrv_noc_device device;
|
|
ft_tt_udrv_claim_namespace ns;
|
|
ft_tt_noc_coord bank_a, bank_b;
|
|
unsigned char pattern_a[TEST_BYTES], pattern_b[TEST_BYTES], readback[TEST_BYTES];
|
|
uint64_t misaims = 0u;
|
|
size_t count = 0;
|
|
uint32_t reserved;
|
|
int rc;
|
|
|
|
if (argc < 2 || strcmp(argv[1], "write") != 0) {
|
|
printf("usage: %s write [clean-accept|aimlow-refuse|aimlow-accept]\n"
|
|
" (this aims windows and writes DRAM; root required)\n"
|
|
" the aimlow modes need FT_TT_UDRV_AIM_RED=aimlow plus its confirmation and\n"
|
|
" a FT_TT_UDRV_AIM_VERIFY setting; run them once each and read the pair\n",
|
|
argv[0]);
|
|
return 2;
|
|
}
|
|
|
|
/* The session is only used for the GRID — it names the DRAM endpoints. The bank under
|
|
* test claims its own windows through the same flock namespace the runtime uses. */
|
|
if (ft_tt_hardware_session_open(&session) != FT_TT_OK) {
|
|
printf("cannot open the card through the product seam — set FT_TT_DRIVER=udrv\n");
|
|
return 1;
|
|
}
|
|
if (ft_tt_coord_dram_bank(&session.grid, 0u, FT_TT_COORD_TRANSLATED, &bank_a) != FT_TT_OK ||
|
|
ft_tt_coord_dram_bank(&session.grid, 1u, FT_TT_COORD_TRANSLATED, &bank_b) != FT_TT_OK) {
|
|
printf("cannot derive two DRAM endpoints from the live grid\n");
|
|
ft_tt_hardware_session_close(&session);
|
|
return 1;
|
|
}
|
|
printf("endpoint A = DRAM bank 0 at (%u,%u), endpoint B = DRAM bank 1 at (%u,%u)\n",
|
|
(unsigned)bank_a.x, (unsigned)bank_a.y, (unsigned)bank_b.x, (unsigned)bank_b.y);
|
|
|
|
if (ft_tt_udrv_pci_find_devices(NULL, FT_TT_UDRV_PCI_VENDOR_TENSTORRENT, 0xFFFFu, found, 4u,
|
|
&count) != FT_TT_UDRV_OK ||
|
|
count == 0u) {
|
|
printf("no Tenstorrent device in sysfs\n");
|
|
ft_tt_hardware_session_close(&session);
|
|
return 1;
|
|
}
|
|
if (ft_tt_udrv_noc_device_open(&found[0], true, &device) != FT_TT_UDRV_OK) {
|
|
printf("cannot map BAR0 writable — this needs root\n");
|
|
ft_tt_hardware_session_close(&session);
|
|
return 1;
|
|
}
|
|
/* A SECOND namespace, so this test's windows can never collide with the ones the open
|
|
* session above is already holding through the runtime. */
|
|
if (ft_tt_udrv_claim_namespace_init(&ns, NULL, found[0].address, "tlb2m",
|
|
FT_TT_UDRV_TLB_2M_WINDOW_COUNT) != FT_TT_UDRV_OK) {
|
|
printf("cannot build the claim namespace\n");
|
|
return 1;
|
|
}
|
|
reserved = 201u;
|
|
|
|
fill(pattern_a, TEST_BYTES, 1u);
|
|
fill(pattern_b, TEST_BYTES, 2u);
|
|
|
|
/* THE `aimlow` MODE RUNS ALONE, because the verify knob is cached in a static and the
|
|
* proof needs two settings of it. `refuse` and `accept` declare what this invocation
|
|
* expects, so an invocation whose environment did not arm reads as a FAIL and not as a
|
|
* silent pass. */
|
|
if (argc >= 3 && (strcmp(argv[2], "aimlow-refuse") == 0 ||
|
|
strcmp(argv[2], "aimlow-accept") == 0 ||
|
|
strcmp(argv[2], "clean-accept") == 0)) {
|
|
bool arm = strncmp(argv[2], "aimlow", 6) == 0;
|
|
int one = run_aimlow(&device, &ns, reserved, (uint16_t)bank_a.x, (uint16_t)bank_a.y,
|
|
pattern_a, arm, strcmp(argv[2], "aimlow-refuse") == 0);
|
|
|
|
(void)ft_tt_udrv_noc_device_close(&device);
|
|
ft_tt_hardware_session_close(&session);
|
|
printf("\n%u checks / %u failed\n", checks, failures);
|
|
return one;
|
|
}
|
|
|
|
printf("\n1. GREEN — the bank routes to the endpoint it was asked for\n");
|
|
unsetenv("FT_TT_UDRV_RED");
|
|
unsetenv("FT_TT_UDRV_AIM_AUDIT");
|
|
rc = run_arm(&device, &ns, reserved, (uint16_t)bank_a.x, (uint16_t)bank_a.y,
|
|
(uint16_t)bank_b.x, (uint16_t)bank_b.y, pattern_a, pattern_b, readback,
|
|
&misaims);
|
|
check(rc == FT_TT_UDRV_OK, "the clean arm completed");
|
|
check(memcmp(readback, pattern_a, TEST_BYTES) == 0, "reading A returned A's bytes");
|
|
check(memcmp(readback, pattern_b, TEST_BYTES) != 0, "and they are NOT B's bytes");
|
|
check(misaims == 0u, "nothing was injected");
|
|
|
|
printf("\n2. GREEN — the audit refuses nothing on a correct bank\n");
|
|
setenv("FT_TT_UDRV_AIM_AUDIT", "1", 1);
|
|
rc = run_arm(&device, &ns, reserved, (uint16_t)bank_a.x, (uint16_t)bank_a.y,
|
|
(uint16_t)bank_b.x, (uint16_t)bank_b.y, pattern_a, pattern_b, readback,
|
|
&misaims);
|
|
check(rc == FT_TT_UDRV_OK, "the audited arm completed");
|
|
check(memcmp(readback, pattern_a, TEST_BYTES) == 0, "reading A still returned A's bytes");
|
|
|
|
printf("\n3. RED — a window that LIES about where it points is silent corruption\n");
|
|
unsetenv("FT_TT_UDRV_AIM_AUDIT");
|
|
setenv("FT_TT_UDRV_RED", "misaim", 1);
|
|
setenv("FT_TT_UDRV_RED_CONFIRM", "i-know-this-can-wedge-the-card", 1);
|
|
rc = run_arm(&device, &ns, reserved, (uint16_t)bank_a.x, (uint16_t)bank_a.y,
|
|
(uint16_t)bank_b.x, (uint16_t)bank_b.y, pattern_a, pattern_b, readback,
|
|
&misaims);
|
|
check(rc == FT_TT_UDRV_OK, "the arm returned SUCCESS — a wrong address is not an error");
|
|
check(misaims > 0u, "the arm actually fired (a red arm that could not fire is a zero)");
|
|
check(memcmp(readback, pattern_b, TEST_BYTES) == 0,
|
|
"reading A returned B'S BYTES — the read went to the wrong endpoint");
|
|
check(memcmp(readback, pattern_a, TEST_BYTES) != 0, "and NOT A's bytes");
|
|
|
|
printf("\n4. CAUGHT — the same lie with the audit watching\n");
|
|
setenv("FT_TT_UDRV_AIM_AUDIT", "1", 1);
|
|
rc = run_arm(&device, &ns, reserved, (uint16_t)bank_a.x, (uint16_t)bank_a.y,
|
|
(uint16_t)bank_b.x, (uint16_t)bank_b.y, pattern_a, pattern_b, readback,
|
|
&misaims);
|
|
check(rc == FT_TT_UDRV_EIO, "the transfer was REFUSED with EIO");
|
|
check(misaims > 0u, "the arm fired here too");
|
|
check(memcmp(readback, pattern_b, TEST_BYTES) != 0,
|
|
"and nothing was dereferenced — the buffer never got B's bytes");
|
|
|
|
unsetenv("FT_TT_UDRV_RED");
|
|
unsetenv("FT_TT_UDRV_RED_CONFIRM");
|
|
unsetenv("FT_TT_UDRV_AIM_AUDIT");
|
|
(void)ft_tt_udrv_noc_device_close(&device);
|
|
ft_tt_hardware_session_close(&session);
|
|
|
|
printf("\n%u checks / %u failed\n", checks, failures);
|
|
return failures == 0u ? 0 : 1;
|
|
}
|