/* 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 #include #include #include #include #include #include #include #include #include #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(); }