/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /** * @file ft_tt_core.c * @brief `ft_tt_core` — target one Tensix, a list, a range or all of them: report what * state they are in, prove whether they still compute, and climb a recovery ladder on * the ones that do not. * * --------------------------------------------------------------------------- * READ-ONLY BY DEFAULT * --------------------------------------------------------------------------- * `status` and `topology` write nothing. `probe` and `recover` write to the card and are * refused without `--write`. Rung 5 resets a PCIe bus and needs its own second flag on * top. A tool that can wedge a core by accident is worse than no tool. * * --------------------------------------------------------------------------- * WHY A READ IS NOT A HEALTH CHECK * --------------------------------------------------------------------------- * A wedged Tensix answers NoC reads exactly like a healthy one. `status` therefore * reports what the registers SAY and names its verdicts so that none of them can be read * as "this core works" — a core with all five RISC-Vs held comes back "held in reset", * which is what a cold core and a wedged core both look like. `probe` is the * discriminator: it boots firmware and runs a real vector-add triad through the * unpacker, FPU and packer, and compares the result byte-for-byte. * * --------------------------------------------------------------------------- * USAGE * --------------------------------------------------------------------------- * ft_tt_core status [--detail] * ft_tt_core probe --write [--program DIR] [--repeats N] [--force] * ft_tt_core recover --rung N --write [--then-probe] [--program DIR] [--force] * ft_tt_core topology [PCI-ADDRESS] * * 60 | 60,62,65 | 60-69 | 60-62,65 | all * * Rungs: 0 deassert the RISC-Vs (one core, one register write) * 1 reload firmware and boot (one core) * 2 poke what the core waits on (one core) * 3 ASIC reset (whole card) * 4 RESET_DEVICE ASIC_DMC_RESET (whole card) * 5 PCIe secondary bus reset (whole card; needs * --allow-pcie-secondary-bus-reset as well as --write) * * --------------------------------------------------------------------------- * THE RUNG IS THE BLAST RADIUS; `--driver` IS THE MECHANISM * --------------------------------------------------------------------------- * `--driver kmd` (the default) reaches rung 3 through tt-kmd's `RESET_DEVICE` ioctl and * completes it with a PCI remove + bridge rescan. `--driver udrv` does the SAME RUNG with no * vendor driver at all: three PCI config-space writes, our own config snapshot restored in * place, and a link retrain (`ft_tt_udrv_reset.h`). * * The ladder is NOT extended with new numbers for this. A rung number says what the operation * can destroy — that is what a caller has to reason about, and it is identical either way. * Numbering the userspace ASIC reset "6" would put it above the SBR, implying it is more * drastic than a bus reset, which is false. So the vocabulary fits, and `--driver` is * orthogonal to it. `--driver udrv` is only meaningful at rung 3: rung 4 needs two ARC * messages and there is no userspace ARC transport yet, and rung 5 is already pure sysfs. * * --------------------------------------------------------------------------- * WHY THE CARD RESET IS THE FLOOR — settled 2026-08-12, do not re-open * --------------------------------------------------------------------------- * Recovering ONE wedged core costs the whole card, and that is a property of the silicon, * not a missing feature here. Four things were checked, so nobody has to check them again: * * 1. Every wedge seen has been in the Tensix BACKEND — MATH stalled in the instruction * FIFO, dvalid never satisfied, a boot handshake that never finishes. None of it is * executing anything that could poll a flag, so rungs 0-2 are not incomplete, they are * structurally out of reach (`docs/tt_bringup_log.md` §21.2). * 2. **The per-tile reset register is a QUASAR feature.** `SMN_SLV_TILE_RESET` * (`TENSIX_RESET_N`, `NOC_RESET_N`, `OVERLAY_RESET_N`, `D2D_RESET_N`) exists only under * `hw/inc/internal/tt-2xx/quasar/`. Blackhole is tt-1xx and has no `TILE_RESET` at all. * 3. The Tensix backend soft-reset bits are not a substitute. Firmware boot never programs * the backend datapath — it sets reset PCs, and the stock sync init zeroes only * CB tile counters — so the 14 cores corrupted that day already got a full boot AND a * complete kernel LLK re-init, and still returned garbage until the machine was reset. * 4. PERST# alone does not do it: §21.4 measured a PCIe secondary bus reset leaving wedged * cores byte-identical. Only `RESET_DEVICE ASIC_RESET` has ever cleared one, and the ARC * exposes no per-tile granularity. * * So: check `status --detail` first, in case a wedge is ever NOT backend-resident and a * per-core rung can reach it. Otherwise rung 3 is the answer, and the advisory lock * (`ft_tt_card_lock.h`) is what makes it safe — it does not make the reset less * destructive, it makes it something other users see coming and refuse. Revisit on Quasar. * * @status Runs on the p150a test card. `probe all` is 140/140 on this card * (2026-08-17). Replaces tests/tt_resident_recover.c, which was written for the core-60 * wedge, never ran, and did not build against the current ft_tt_launch.h. * `--driver udrv` is on silicon as of 2026-08-17: it recovered core 101 from a deliberate * 0x00047000 wedge in place, no PCI remove and no rescan, and retrained the link from the * 2.5 GT/s the reset left behind. * * **AND THE WHOLE TOOL NOW RUNS WITH NO VENDOR MODULE, which is the board it was built * for (2026-08-17).** `--driver` selects the TRANSPORT for every command — status, probe * and all six rungs — and not just the mechanism for a whole-card rung, and `udrv` means * udrv even when a module happens to be loaded, so it can be compared against the oracle * on the card host that has one. With `tenstorrent` unloaded: `status all` read 140 cores, * `probe all --write` was **140 passed / 0 failed / 0 refused**, and * `recover 71 --rung 3 --write` did the ASIC reset and read both states. The same probe * over `--driver kmd` with no module exits 1, which is what makes the green mean * something. * * Three things were in the way and none of them was the driver. `--driver udrv` opened * the chardev to print the before-state; it checked the root-only PCI remove/rescan * permission that the in-place config restore never uses; and `probe` and rungs 0-2 took * their per-core claim through the tt-kmd session's wrapper, when a claim is a flock * under /tmp that has never needed a device handle. * @issues * - `topology` and `--driver udrv` read and write PCI config space through sysfs, which is * root-only, so both need sudo. The card lock and the per-core claim namespace live under * /tmp and a sudo run leaves them ROOT-OWNED, which then refuses every non-root card job * with FT_TT_EIO ("device I/O failed") — a message that reads like a card fault and is * not one. Fix the ownership, do not diagnose the card (DEV_NOTES §3). * @todo None */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include "ft_tt_card_lock.h" #include "ft_tt_core_probe.h" #include "ft_tt_core_readiness.h" #include "ft_tt_tensix_claim.h" #include "ft_tt_udrv.h" #include "ft_tt_udrv_claim.h" #include "ft_tt_udrv_reset.h" #include "ft_tt_core_recover.h" #include "ft_tt_core_status.h" #include "ft_tt_core_status_print.h" #include "ft_tt_device_reset.h" #include "ft_tt_hardware_session.h" #include "ft_tt_runtime.h" /** More than any p150 has cores; the real bound is the LIVE core count, checked below. */ #define TOOL_MAX_TARGETS 256u #define TOOL_CHARDEV_PATH "/dev/tenstorrent/0" /** Where sysfs publishes PCI devices. Both the card and its bridge are read from here. */ #define TOOL_PCI_DEVICES_DIR "/sys/bus/pci/devices" /** Which mechanism a whole-card rung uses. The rung says WHAT is destroyed; this says HOW. */ typedef enum tool_reset_driver { TOOL_RESET_DRIVER_KMD = 0, /**< tt-kmd's RESET_DEVICE ioctl, then remove + rescan */ TOOL_RESET_DRIVER_UDRV = 1 /**< three config writes, our own snapshot restored in place */ } tool_reset_driver; typedef enum tool_command { TOOL_COMMAND_STATUS, TOOL_COMMAND_READINESS, TOOL_COMMAND_PROBE, TOOL_COMMAND_RECOVER, TOOL_COMMAND_TOPOLOGY, TOOL_COMMAND_CLAIM } tool_command; typedef struct tool_options { tool_command command; const char *target_text; bool allow_write; bool allow_secondary_bus_reset; bool detail; bool then_probe; bool force; unsigned rung; bool rung_given; tool_reset_driver reset_driver; bool udrv_reenumerate; /**< --driver udrv only: use remove + rescan, not in-place restore */ unsigned repeats; const char *program_directory; const char *save_directory; /* Card-lock coordination (ft_tt_card_lock.h) -- only rungs 3-5 and `claim` use these. */ const char *card_lock_reason; bool override_card_lock; /* Per-core coordination (ft_tt_tensix_claim.h) -- probe and rungs 0-2 use this. */ bool override_core_claims; unsigned hold_seconds; /**< `claim` only; 0 = hold until Ctrl-C */ } tool_options; typedef struct tool_targets { uint32_t index[TOOL_MAX_TARGETS]; unsigned count; bool all; } tool_targets; static void print_usage(const char *program_name) { fprintf(stderr, "usage:\n" " %s status [--detail]\n" " %s readiness may firmware be booted onto these cores RIGHT\n" " NOW? Read-only. The same reading Pantheon's\n" " arming path refuses on\n" " %s probe --write [--program DIR] [--repeats N] [--force]\n" " %s recover --rung N --write [--then-probe] [--program DIR]\n" " [--driver kmd|udrv] [--udrv-reenumerate]\n" " [--allow-pcie-secondary-bus-reset] [--save-dir DIR]\n" " [--reason TEXT] [--override-card-lock]\n" " %s topology [PCI-ADDRESS] (no address = this card; an address makes the\n" " rung-5 refusal demonstrable on another device)\n" " %s claim --reason TEXT [--hold-seconds N]\n" " register that this card is in use; holds a shared lock until\n" " Ctrl-C (or N seconds), so `recover --rung 3/4/5` refuses and\n" " names you instead of resetting the card out from under you\n" "\n" " 60 | 60,62,65 | 60-69 | 60-62,65 | all\n" " --force probe or reload a core whose kernel is still 'running' — only\n" " for a core already known wedged\n" " --driver which TRANSPORT reaches the card, and how a whole-card rung is\n" " performed. It applies to EVERY command: status, probe, and all\n" " six rungs. kmd (default) = tt-kmd's\n" " RESET_DEVICE ioctl then a PCI remove + rescan. udrv = NO VENDOR\n" " DRIVER: three PCI config writes, our own config snapshot restored\n" " in place, and a link retrain. Rung 3 only. The rung number is the\n" " blast radius and does not change with the mechanism. udrv works\n" " with NO tt-kmd loaded: the before/after state is then read over\n" " foundry's own driver, and the root-only PCI remove/rescan check\n" " is skipped because the in-place restore does not use it.\n" " --udrv-reenumerate with --driver udrv, finish with remove + rescan instead\n" " of the in-place config restore — the fallback, for comparison\n" " rungs 0 deassert RISC-Vs · 1 reload firmware · 2 poke the wait\n" " 3 ASIC reset · 4 RESET_DEVICE ASIC_DMC_RESET\n" " 5 PCIe secondary bus reset (also needs " "--allow-pcie-secondary-bus-reset)\n" " — rungs 3-5 reset the WHOLE CARD and take the card lock first;\n" " --override-card-lock proceeds anyway and says loudly who it\n" " overrode. Rungs 0-2 and probe touch ONE CORE EACH: they take a\n" " per-core claim instead (ft_tt_tensix_claim.h) and refuse, naming\n" " the holder, rather than driving someone else's core.\n" " --override-core-claims probe/rungs 0-2 proceed on a core another process\n" " holds. It is a claim, not a fence — say why out loud first.\n", program_name, program_name, program_name, program_name, program_name, program_name); } /** Add one index, refusing a list longer than the array. */ static int add_target(tool_targets *targets, unsigned long value) { if (targets->count >= TOOL_MAX_TARGETS) { fprintf(stderr, "too many targets (max %u)\n", TOOL_MAX_TARGETS); return FT_TT_EINVAL; } targets->index[targets->count++] = (uint32_t)value; return FT_TT_OK; } /** Parse "all", "60", "60,62", "60-69" or any comma-separated mix of those. */ static int parse_targets(const char *text, tool_targets *out) { char buffer[512]; char *saved = NULL; char *token; memset(out, 0, sizeof(*out)); if (!text) return FT_TT_EINVAL; if (strcmp(text, "all") == 0) { out->all = true; return FT_TT_OK; } if (strlen(text) >= sizeof(buffer)) return FT_TT_EINVAL; snprintf(buffer, sizeof(buffer), "%s", text); for (token = strtok_r(buffer, ",", &saved); token; token = strtok_r(NULL, ",", &saved)) { char *dash = strchr(token, '-'); unsigned long first; unsigned long last; unsigned long value; if (dash) { *dash = '\0'; first = strtoul(token, NULL, 10); last = strtoul(dash + 1, NULL, 10); if (last < first) return FT_TT_EINVAL; for (value = first; value <= last; value++) if (add_target(out, value) != FT_TT_OK) return FT_TT_EINVAL; } else { if (token[0] == '\0') return FT_TT_EINVAL; if (add_target(out, strtoul(token, NULL, 10)) != FT_TT_OK) return FT_TT_EINVAL; } } return out->count == 0u ? FT_TT_EINVAL : FT_TT_OK; } /** Expand "all" and check every index against the LIVE core count, never a constant. */ static int resolve_targets(const ft_tt_grid_info *grid, tool_targets *targets) { uint32_t live = ft_tt_coord_tensix_core_count(grid); unsigned entry; if (live == 0u) { fprintf(stderr, "the grid reports no enabled Tensix cores\n"); return FT_TT_EIO; } if (targets->all) { uint32_t index; targets->count = 0; for (index = 0; index < live && targets->count < TOOL_MAX_TARGETS; index++) targets->index[targets->count++] = index; return FT_TT_OK; } for (entry = 0; entry < targets->count; entry++) { if (targets->index[entry] >= live) { fprintf(stderr, "Tensix %u is out of range: this card has %u enabled cores\n", targets->index[entry], live); return FT_TT_EINVAL; } } return FT_TT_OK; } static int parse_options(int argc, char **argv, tool_options *out) { int argument; /* Pre-scanned, not tracked through the loop, so the acknowledgement works whichever * side of --program it is written on. Order-dependent safety is not safety. */ bool foreign_program_acknowledged = false; memset(out, 0, sizeof(*out)); out->repeats = FT_TT_CORE_PROBE_DEFAULT_REPEATS; out->program_directory = ft_tt_firmware_directory(); if (argc < 2) return FT_TT_EINVAL; for (argument = 2; argument < argc; argument++) { if (strcmp(argv[argument], "--i-know-this-must-be-a-vector-add-triad") == 0) foreign_program_acknowledged = true; } /* THE ENVIRONMENT REACHES THE SAME CODE PATH AS --program, AND GATING ONLY THE FLAG * WAS AN INCOMPLETE FIX. ft_tt_firmware_directory() returns * FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT when it is set, so an exported * FT_TT_FIRMWARE_DIR silently pointed probe at another op's program directory -- * exactly the mixing the flag was gated to prevent. On 2026-08-13 a bisect script * exported it for its own builds, the shell then ran probe, and it wedged core 26 and * reported a false "boots but computes garbage". A safety check that covers one of two * routes to the same call is not a safety check. Other suites legitimately use this * variable, so the refusal belongs here in the tool rather than in the shared helper. */ if (getenv(FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT) != NULL && getenv(FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT)[0] != '\0' && !foreign_program_acknowledged) { fprintf(stderr, "refused: %s is set in the environment (%s).\n" " probe drives its OWN vector_add triad; pointing it at another op's\n" " directory mixes those kernels with vector_add's runtime args and WEDGES\n" " THE CORE -- and a wedged core needs a whole-card reset to recover.\n" " Unset it, or pass --i-know-this-must-be-a-vector-add-triad if that\n" " directory really is another build of the same triad.\n", FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT, getenv(FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT)); return FT_TT_EINVAL; } for (argument = 2; argument < argc; argument++) { if (strcmp(argv[argument], "--i-know-this-must-be-a-vector-add-triad") == 0) foreign_program_acknowledged = true; } if (strcmp(argv[1], "status") == 0) out->command = TOOL_COMMAND_STATUS; else if (strcmp(argv[1], "readiness") == 0) out->command = TOOL_COMMAND_READINESS; else if (strcmp(argv[1], "probe") == 0) out->command = TOOL_COMMAND_PROBE; else if (strcmp(argv[1], "recover") == 0) out->command = TOOL_COMMAND_RECOVER; else if (strcmp(argv[1], "topology") == 0) out->command = TOOL_COMMAND_TOPOLOGY; else if (strcmp(argv[1], "claim") == 0) out->command = TOOL_COMMAND_CLAIM; else return FT_TT_EINVAL; for (argument = 2; argument < argc; argument++) { const char *option = argv[argument]; bool has_value = argument + 1 < argc; if (option[0] != '-') { if (out->target_text) return FT_TT_EINVAL; out->target_text = option; } else if (strcmp(option, "--write") == 0) { out->allow_write = true; } else if (strcmp(option, "--allow-pcie-secondary-bus-reset") == 0) { out->allow_secondary_bus_reset = true; } else if (strcmp(option, "--detail") == 0) { out->detail = true; } else if (strcmp(option, "--force") == 0) { out->force = true; } else if (strcmp(option, "--then-probe") == 0) { out->then_probe = true; } else if (strcmp(option, "--rung") == 0 && has_value) { out->rung = (unsigned)strtoul(argv[++argument], NULL, 10); out->rung_given = true; } else if (strcmp(option, "--repeats") == 0 && has_value) { out->repeats = (unsigned)strtoul(argv[++argument], NULL, 10); } else if (strcmp(option, "--program") == 0 && has_value) { /* WEDGES CORES IF MISUSED, so it is gated. probe drives ITS OWN vector_add * triad — its runtime args, CB configuration and processor mask (0x1f, all * five). Pointing it at some other op's program directory boots that * directory's firmware and loads that directory's kernels, then drives them * with vector_add's expectations. The boot handshake never finishes and the * core is wedged until a card reset. It cost Tensix 30 and 31 on 2026-08-12. * * A file-existence check cannot catch this: every program directory holds the * same file names. The only valid --program is a DIFFERENT BUILD OF THE SAME * vector_add triad, which the tool cannot verify, so the caller asserts it. */ if (!foreign_program_acknowledged) { fprintf(stderr, "--program needs --i-know-this-must-be-a-vector-add-triad.\n" " probe drives its own vector_add triad; pointing it at another\n" " op's directory mixes those kernels with vector_add's runtime\n" " args and WEDGES THE CORE (cost Tensix 30 and 31, 2026-08-12).\n" " The only valid use is another build of the SAME triad.\n" " To health-check a core, run probe with no --program.\n"); return FT_TT_EINVAL; } out->program_directory = argv[++argument]; } else if (strcmp(option, "--i-know-this-must-be-a-vector-add-triad") == 0) { foreign_program_acknowledged = true; } else if (strcmp(option, "--save-dir") == 0 && has_value) { out->save_directory = argv[++argument]; } else if (strcmp(option, "--reason") == 0 && has_value) { out->card_lock_reason = argv[++argument]; } else if (strcmp(option, "--override-card-lock") == 0) { out->override_card_lock = true; } else if (strcmp(option, "--override-core-claims") == 0) { out->override_core_claims = true; } else if (strcmp(option, "--driver") == 0 && has_value) { const char *name = argv[++argument]; if (strcmp(name, "kmd") == 0) { out->reset_driver = TOOL_RESET_DRIVER_KMD; } else if (strcmp(name, "udrv") == 0) { out->reset_driver = TOOL_RESET_DRIVER_UDRV; } else { fprintf(stderr, "--driver must be kmd or udrv\n"); return FT_TT_EINVAL; } } else if (strcmp(option, "--udrv-reenumerate") == 0) { out->udrv_reenumerate = true; } else if (strcmp(option, "--hold-seconds") == 0 && has_value) { out->hold_seconds = (unsigned)strtoul(argv[++argument], NULL, 10); } else { fprintf(stderr, "unknown option: %s\n", option); return FT_TT_EINVAL; } } if (out->command != TOOL_COMMAND_TOPOLOGY && out->command != TOOL_COMMAND_CLAIM && !out->target_text) return FT_TT_EINVAL; if (out->repeats == 0u) return FT_TT_EINVAL; return FT_TT_OK; } /* ------------------------------------------------------------------------- */ /* readiness */ /* ------------------------------------------------------------------------- */ /** * May firmware be booted onto these cores RIGHT NOW? * * READ-ONLY, like `status`, and for the same reason: a NoC read of a wedged core is * indistinguishable from a read of a healthy one and changes neither. So it goes through the * transport seam rather than the tt-kmd session and needs no claim. * * This is the SAME reading Pantheon's arming path refuses on (`pn_tt_core_boot_guard.h`), so * running it here answers "why did that serve refuse core 90" without loading anything, and * — the reason it was added — it can be pointed at a core carrying a REAL leftover to see * whether the detector sees it, with nothing written to the card either way. */ static int run_readiness(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid, const tool_targets *targets) { static const char *const processor_name[FT_TT_PROC_COUNT] = {"BRISC", "NCRISC", "TRISC0", "TRISC1", "TRISC2"}; unsigned not_ready = 0; unsigned entry; for (entry = 0; entry < targets->count; entry++) { ft_tt_core_readiness readiness; unsigned processor; if (ft_tt_core_readiness_read(noc, grid, targets->index[entry], &readiness) != FT_TT_OK) { fprintf(stderr, "Tensix %u does not resolve to a coordinate\n", targets->index[entry]); continue; } printf("Tensix %u — translated NoC (%u, %u)\n", readiness.status.index, readiness.status.x, readiness.status.y); printf(" %-10s %s\n", readiness.ready ? "READY" : "NOT READY", ft_tt_core_readiness_string(readiness.state)); printf(" verdict '%s' reset 0x%08x instrn_buf 0x%08x go 0x%02x\n", ft_tt_core_verdict_string(readiness.status.verdict), readiness.status.state.reset_register, readiness.status.state.instrn_buf_status, readiness.status.state.go_messages[0].signal); /* The CONTROL heartbeat and the PER-PROCESSOR ones printed side by side, because * the whole reason this command exists rather than `status` alone is that they * disagree in exactly the failure being guarded against: the reader has exited, so * the control block is frozen while a TRISC is still spinning. */ printf(" resident queue %s, control heartbeat %s\n", readiness.status.queue_present ? "present" : "absent", readiness.status.queue_heartbeat_ticking ? "TICKING" : "frozen"); for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) { if (!readiness.processor[processor].slot_read) continue; printf(" %-7s state %s heartbeat %u %s\n", processor_name[processor], ft_tt_queue_state_string(readiness.processor[processor].state), readiness.processor[processor].heartbeat, readiness.processor[processor].ticking ? "TICKING" : "frozen"); } if (!readiness.ready) printf(" waiting %s help\n", readiness.waiting_can_help ? "CAN" : "cannot"); if (!readiness.ready) not_ready++; } printf("\n%u core%s, %u not ready\n", targets->count, targets->count == 1u ? "" : "s", not_ready); return FT_TT_OK; } /* ------------------------------------------------------------------------- */ /* status */ /* ------------------------------------------------------------------------- */ /** Every target's state, over whichever transport opened the card. * * Accessor + grid, not a session: `status` is READ-ONLY — a NoC read of a wedged core is * indistinguishable from a read of a healthy one and changes neither — so it is the one * command that can run on a board with no vendor module and no claim, and * `instrn_buf_status` (0x66 wedged / 0x77 healthy) read this way is the WHOLE health * instrument there. */ static int run_status(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid, const tool_targets *targets, bool detail) { unsigned verdict_total[FT_TT_CORE_VERDICT_LAST + 1u]; unsigned entry; unsigned verdict; memset(verdict_total, 0, sizeof(verdict_total)); if (!detail) ft_tt_core_status_print_header(); for (entry = 0; entry < targets->count; entry++) { ft_tt_core_status status; if (ft_tt_core_status_read(noc, grid, targets->index[entry], &status) != FT_TT_OK) { fprintf(stderr, "Tensix %u does not resolve to a coordinate\n", targets->index[entry]); continue; } if (detail) ft_tt_core_status_print_detail(&status); else ft_tt_core_status_print_line(&status); verdict_total[status.verdict]++; } printf("\n%u core%s:", targets->count, targets->count == 1u ? "" : "s"); for (verdict = 0; verdict <= (unsigned)FT_TT_CORE_VERDICT_LAST; verdict++) if (verdict_total[verdict] != 0u) printf(" %u %s", verdict_total[verdict], ft_tt_core_verdict_string((ft_tt_core_verdict)verdict)); printf("\n"); printf("(a read cannot tell a healthy parked core from a wedged one — that is what " "`probe` is for)\n"); return FT_TT_OK; } /* ------------------------------------------------------------------------- */ /* claim */ /* ------------------------------------------------------------------------- */ /** set by the signal handler, read by the wait loop; nothing else touches it, so a plain * sig_atomic_t is enough without needing signal-safe cleanup inside the handler itself. */ static volatile sig_atomic_t g_claim_stop_requested = 0; static void handle_claim_stop_signal(int signal_number) { (void)signal_number; g_claim_stop_requested = 1; } /** Register a shared claim and hold it until Ctrl-C (or --hold-seconds runs out). Touches * no card state -- this only ever writes to /tmp -- so it needs no --write and cannot be * refused by the read-only-by-default rule. */ static int run_claim(const tool_options *options) { struct sigaction action; const char *reason = options->card_lock_reason ? options->card_lock_reason : "(no reason given)"; int fd = -1; unsigned waited_seconds = 0; int rc = ft_tt_card_lock_claim_acquire(options->card_lock_reason, &fd); if (rc != FT_TT_OK) { fprintf(stderr, "claim: could not register (rc=%d)\n", rc); return rc; } printf("claim registered: pid %d, reason \"%s\"\n", (int)getpid(), reason); if (options->hold_seconds != 0u) printf(" held for up to %u second%s, or Ctrl-C to release early\n", options->hold_seconds, options->hold_seconds == 1u ? "" : "s"); else printf(" held until Ctrl-C -- a card-level reset (rung 3/4/5) will refuse and " "name this claim while it runs\n"); fflush(stdout); memset(&action, 0, sizeof(action)); action.sa_handler = handle_claim_stop_signal; sigemptyset(&action.sa_mask); sigaction(SIGINT, &action, NULL); sigaction(SIGTERM, &action, NULL); while (!g_claim_stop_requested) { if (options->hold_seconds != 0u && waited_seconds >= options->hold_seconds) break; sleep(1u); waited_seconds++; } ft_tt_card_lock_release(fd); printf("claim released\n"); return FT_TT_OK; } /* ------------------------------------------------------------------------- */ /* probe */ /* ------------------------------------------------------------------------- */ static int run_probe(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid, const tool_targets *targets, const tool_options *options) { ft_tt_core_probe_context context; unsigned passed = 0; unsigned failed = 0; unsigned refused = 0; unsigned entry; int rc; rc = ft_tt_core_probe_context_open(options->program_directory, &context); if (rc != FT_TT_OK) { fprintf(stderr, "cannot read firmware and kernels from %s (rc=%d)\n", options->program_directory, rc); return rc; } printf("probe: firmware and vector_add triad from %s, %u repeat%s per core\n", options->program_directory, options->repeats, options->repeats == 1u ? "" : "s"); for (entry = 0; entry < targets->count; entry++) { ft_tt_core_probe_result result; ft_tt_core_status status; if (ft_tt_core_status_read(noc, grid, targets->index[entry], &status) != FT_TT_OK) { fprintf(stderr, "Tensix %u does not resolve to a coordinate\n", targets->index[entry]); continue; } (void)ft_tt_core_probe_run(noc, grid, &context, &status, options->repeats, options->force, &result); ft_tt_core_probe_print_line(&result); fflush(stdout); if (result.refused) refused++; else if (ft_tt_core_probe_passed(&result)) passed++; else failed++; } ft_tt_core_probe_context_close(&context); printf("\nprobe: %u passed, %u failed, %u refused, of %u core%s\n", passed, failed, refused, targets->count, targets->count == 1u ? "" : "s"); return failed == 0u ? FT_TT_OK : FT_TT_ERROR; } /* ------------------------------------------------------------------------- */ /* recover */ /* ------------------------------------------------------------------------- */ /** One core through one of the core-level rungs, with a before and after status. */ static void recover_one_core(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid, uint32_t index, const tool_options *options, const ft_tt_firmware_set *firmware) { ft_tt_core_recovery_report report; ft_tt_core_status before; ft_tt_core_status after; if (ft_tt_core_status_read(noc, grid, index, &before) != FT_TT_OK) { fprintf(stderr, "Tensix %u does not resolve to a coordinate\n", index); return; } printf("--- before ---\n"); ft_tt_core_status_print_detail(&before); switch ((ft_tt_core_recovery_rung)options->rung) { case FT_TT_RECOVERY_RUNG_DEASSERT_RISCS: (void)ft_tt_core_recover_deassert_riscs(noc, &before, &report); break; case FT_TT_RECOVERY_RUNG_RELOAD_FIRMWARE: (void)ft_tt_core_recover_reload_firmware(noc, &before, firmware, options->force, &report); break; case FT_TT_RECOVERY_RUNG_POKE_WAIT: default: (void)ft_tt_core_recover_poke_wait(noc, &before, &report); break; } printf("--- %s ---\n", ft_tt_core_recovery_rung_string((ft_tt_core_recovery_rung)options->rung)); ft_tt_core_recovery_print(&report); printf("--- after ---\n"); if (ft_tt_core_status_read(noc, grid, index, &after) == FT_TT_OK) ft_tt_core_status_print_detail(&after); fflush(stdout); } /** Probe every target and print the result, used by `--then-probe`. * * Takes the ACCESSOR and the GRID rather than a session, because a card-level * rung may be driven on a machine where the vendor chardev does not exist and * the accessor came from foundry's own driver instead. Nothing in here cares * which — that is the whole point of the seam. */ static void probe_targets(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid, const tool_targets *targets, const tool_options *options) { ft_tt_core_probe_context context; unsigned entry; if (ft_tt_core_probe_context_open(options->program_directory, &context) != FT_TT_OK) { fprintf(stderr, "--then-probe: cannot read images from %s\n", options->program_directory); return; } printf("\n--- probe after the rung ---\n"); for (entry = 0; entry < targets->count; entry++) { ft_tt_core_probe_result result; ft_tt_core_status status; if (ft_tt_core_status_read(noc, grid, targets->index[entry], &status) != FT_TT_OK) continue; (void)ft_tt_core_probe_run(noc, grid, &context, &status, options->repeats, options->force, &result); ft_tt_core_probe_print_line(&result); fflush(stdout); } ft_tt_core_probe_context_close(&context); } /** Rungs 0-2: one core at a time, over the NoC. */ static int run_core_recovery(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid, const tool_targets *targets, const tool_options *options) { ft_tt_firmware_set firmware; bool firmware_loaded = false; unsigned entry; if (options->rung == FT_TT_RECOVERY_RUNG_RELOAD_FIRMWARE) { if (ft_tt_firmware_set_load(options->program_directory, &firmware) != FT_TT_OK) { fprintf(stderr, "rung 1 needs firmware images; %s did not provide them\n", options->program_directory); return FT_TT_EIO; } firmware_loaded = true; } for (entry = 0; entry < targets->count; entry++) recover_one_core(noc, grid, targets->index[entry], options, firmware_loaded ? &firmware : NULL); if (firmware_loaded) ft_tt_firmware_set_release(&firmware); if (options->then_probe) probe_targets(noc, grid, targets, options); return FT_TT_OK; } /** The state of every target, printed before a whole-card rung erases it. * * Accessor + grid, not a session, for the same reason probe_targets() is: with * tt-kmd unloaded `instrn_buf_status` read over a udrv NoC path is the WHOLE * health instrument (0x66 wedged / 0x77 healthy), and it is carried in exactly * this struct. */ static void print_targets_status(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid, const tool_targets *targets, const char *title) { unsigned entry; printf("--- %s ---\n", title); for (entry = 0; entry < targets->count; entry++) { ft_tt_core_status status; if (ft_tt_core_status_read(noc, grid, targets->index[entry], &status) == FT_TT_OK) ft_tt_core_status_print_detail(&status); } fflush(stdout); } /* ------------------------------------------------------------------------- */ /* main */ /* ------------------------------------------------------------------------- */ /** * The card as a whole-card rung sees it: EITHER a tt-kmd hardware session OR, * when there is no vendor module, an ft_tt_runtime over foundry's own driver. * * --------------------------------------------------------------------------- * WHY THIS EXISTS, AND WHY IT IS NOT A FORK * --------------------------------------------------------------------------- * `--driver udrv` was built for a board with NO tt-kmd, and until 2026-08-17 it * opened the chardev anyway — to print the before-state — so it could not run on * the one board it exists for. The reset library itself was always fine; the * tool around it was not. * * Everything a card-level rung does with the card is `ft_tt_core_status_read()` * and `ft_tt_core_probe_run()`, and both take an `ft_tt_noc_access` and a * `ft_tt_grid_info`. So this struct holds whichever thing produced those two * and hands out pointers to them; NOTHING below it is duplicated or branched. * With the module gone `instrn_buf_status` (0x66 wedged / 0x77 healthy) read * over a udrv path is the whole health instrument, and it arrives through * exactly this accessor. */ typedef struct tool_card { ft_tt_hardware_session kmd; bool kmd_open; ft_tt_runtime udrv; bool udrv_open; const ft_tt_noc_access *noc; /**< NULL when nothing is open */ const ft_tt_grid_info *grid; char address[FT_TT_PCI_ADDRESS_MAX]; /**< the claim namespace's card identity */ /** Per-core claims this invocation holds. Taken through ft_tt_tensix_claim * DIRECTLY rather than through the tt-kmd session's wrapper: a claim is a * flock under /tmp and has never needed a device handle, and routing it * through the session is what tied `probe` and rungs 0-2 to a chardev that * the target board will not have. */ ft_tt_tensix_claim_set *cores; } tool_card; /** Open, resolve targets, and hand back the card's PCI address for the device rungs. */ static int open_session(ft_tt_hardware_session *session, tool_targets *targets, char *out_card_address, size_t card_address_bytes) { int rc = ft_tt_hardware_session_open(session); if (rc != FT_TT_OK) return rc; rc = resolve_targets(&session->grid, targets); if (rc != FT_TT_OK) { ft_tt_hardware_session_close(session); return rc; } /* Through the runtime, which answers on either transport — the session no * longer necessarily holds a tt-kmd context, and the address is what the * per-core claim namespace is named after. */ if (out_card_address) (void)ft_tt_runtime_pci_address(&session->runtime, out_card_address, card_address_bytes); return FT_TT_OK; } /* ------------------------------------------------------------------------- */ /* per-core claims (ft_tt_tensix_claim.h) */ /* ------------------------------------------------------------------------- */ /** * Claim every target before touching any of them. * * `probe` and rungs 0-2 write to ONE CORE EACH -- that core's L1, mailbox and * soft-reset register -- so they belong on a per-core claim, not the whole-card * lock. The card lock stays where it was, on the rungs that reset the card. * * Straight to `ft_tt_tensix_claim`, NOT through the tt-kmd session's wrapper. A * claim has always been a flock under /tmp and has never needed a device handle; * going through the session is the only reason `probe` and rungs 0-2 required a * chardev, which is precisely what the board this driver exists for will not * have. * * @return FT_TT_OK (also under an override, where nothing was claimed), or the * refusal code with nothing written to the card */ static int claim_targets_or_explain(tool_card *card, const tool_targets *targets, const tool_options *options, const char *what) { const char *reason = options->card_lock_reason ? options->card_lock_reason : what; ft_tt_tensix_claim_request request; ft_tt_tensix_claim_refusal refusal; int rc; memset(&request, 0, sizeof(request)); /* NULL card reads the environment; ours is the address the open resolved, so * two invocations on one box cannot land in different namespaces. */ request.card = card->address[0] ? card->address : NULL; request.indices = targets->index; request.count = targets->count; request.reason = reason; rc = ft_tt_tensix_claim_acquire(&request, &card->cores, &refusal); if (rc == FT_TT_UDRV_OK) return FT_TT_OK; ft_tt_tensix_claim_refusal_print(&refusal); card->cores = NULL; /* FT_TT_UDRV_EAGAIN and FT_TT_EPERM are the same number by design * (ft_tt_udrv.h), but the caller's table is ft_tt_device.h's, so name it. */ rc = (rc == FT_TT_UDRV_EAGAIN) ? FT_TT_EPERM : rc; /* The refusal itself was already printed, naming the holder's pid and reason * (ft_tt_tensix_claim_refusal_print). All this adds is what to do next. */ if (!options->override_core_claims) { fprintf(stderr, " %s writes to each core it targets, so it will not drive a core somebody\n" " else owns. Nothing has been written. Re-run with --override-core-claims\n" " only if you know that process is finished with it.\n", what); return rc; } fprintf(stderr, "--override-core-claims: PROCEEDING on a core another process holds. If it is " "still driving that core, both of you will get wrong answers.\n"); return FT_TT_OK; } /** * Name every core-claim holder a whole-card reset is about to destroy. * * The card lock only ever saw processes that registered a SHARED CLAIM through * `ft_tt_core claim`. Now that ordinary work holds per-core claims instead, a * reset that consulted only the card lock would report "nobody is using the * card" while wiping eight live suites. The census closes that: the whole-card * operation reads the per-core namespace too. * * @return how many cores are held, or 0 when the namespace cannot be read */ static unsigned report_core_claim_holders(const char *card_address) { ft_tt_udrv_claim_namespace ns; ft_tt_udrv_claim_info *info; uint32_t held = 0u; uint32_t index; /* Resolved through the claim module, not with NULL and a literal, so this * census cannot end up reading a different directory than the claimants * write to -- that mismatch would report "nobody is using the card" and be * believed. */ if (ft_tt_udrv_claim_namespace_init(&ns, ft_tt_tensix_claim_resolve_root(NULL), ft_tt_tensix_claim_resolve_card(card_address), FT_TT_TENSIX_CLAIM_NAMESPACE, FT_TT_TENSIX_CLAIM_INDEX_COUNT) != FT_TT_UDRV_OK) return 0u; info = calloc((size_t)ns.count, sizeof(*info)); if (!info) return 0u; if (ft_tt_udrv_claim_scan(&ns, info, (size_t)ns.count, &held) != FT_TT_UDRV_OK || held == 0u) { free(info); return 0u; } fprintf(stderr, " %u Tensix core%s are claimed by live processes:\n", held, held == 1u ? "" : "s"); for (index = 0u; index < ns.count; index++) { if (!info[index].held) continue; fprintf(stderr, " core %-3u pid %-7ld %s\n", index, (long)info[index].pid, info[index].reason[0] != '\0' ? info[index].reason : "(no reason recorded)"); } free(info); return (unsigned)held; } /** Take the exclusive card lock, or explain the refusal / print the override banner. * @return FT_TT_OK with `*out_fd` holding the lock (may be -1 under a granted override -- * nothing was acquired, so there is nothing to release), or a refusal code with * `*out_fd` at -1 and nothing written to the card. */ static int acquire_card_lock_or_explain(const tool_options *options, int *out_fd) { ft_tt_card_lock_status lock_status; int rc = ft_tt_card_lock_exclusive_try(options->card_lock_reason, out_fd, &lock_status); if (rc == FT_TT_OK) return FT_TT_OK; /* CONTENDED and BROKEN are different failures and must not share a message. EPERM * means somebody holds it and lock_status names them; anything else means the lock * could not be consulted AT ALL, and claiming contention there sends the reader * hunting a holder that does not exist. MEASURED on the card host: fs.protected_regular=2 * stopped root open()ing the lock, and this branch reported a phantom holder. */ if (rc != FT_TT_EPERM) { fprintf(stderr, "refused: rung %u resets the whole card, and the card lock could NOT BE " "CONSULTED (%s).\n" " This is NOT contention. The lock file could not be opened, so whether\n" " anyone else is using the card is UNKNOWN -- refusing rather than\n" " guessing. Nothing has been reset. Re-run with --override-card-lock only\n" " if you are certain the card is yours alone.\n", options->rung, ft_tt_error_string(rc)); if (!options->override_card_lock) return rc; } else if (!options->override_card_lock) { fprintf(stderr, "refused: rung %u resets the whole card, and another process already " "holds the card lock:\n", options->rung); ft_tt_card_lock_status_print(&lock_status); fprintf(stderr, " Wait for it to finish or coordinate with whoever it is (`ft_tt_core " "claim` names them here). If you are certain it is safe to proceed " "anyway, re-run with --override-card-lock. Nothing has been reset.\n"); return FT_TT_EPERM; } fprintf(stderr, "--override-card-lock: PROCEEDING WITHOUT THE LOCK. Rung %u resets the whole " "card and will destroy the in-flight work of:\n", options->rung); ft_tt_card_lock_status_print(&lock_status); *out_fd = -1; /* nothing was acquired under an override; nothing to release later */ return FT_TT_OK; } /* ------------------------------------------------------------------------- */ /* rung 3 with NO VENDOR DRIVER (ft_tt_udrv_reset.h) */ /* ------------------------------------------------------------------------- */ /** "/sys/bus/pci/devices/0000:12:00.0" from "0000:12:00.0". */ static int pci_device_dir(const char *bdf, char *out, size_t out_bytes) { int written = snprintf(out, out_bytes, "%s/%s", TOOL_PCI_DEVICES_DIR, bdf); return (written > 0 && (size_t)written < out_bytes) ? FT_TT_OK : FT_TT_EINVAL; } /** One line naming a device's negotiated link, or why it could not be read. */ static void print_link_status(const char *label, const char *device_dir) { ft_tt_udrv_link_status status; if (ft_tt_udrv_link_status_read(device_dir, &status) != FT_TT_UDRV_OK) { printf(" %-8s link: could not be read\n", label); return; } printf(" %-8s link: %s x%u%s\n", label, ft_tt_udrv_link_speed_string(status.speed_code), status.width, status.training ? " (TRAINING)" : ""); } /** * The link comes back at Gen1 after every card-level reset, and every host-involved timing * then silently halves (DEV_NOTES §1). Retrain it on the BRIDGE, and only when the topology * check says the bridge has exactly one child and that child is this card — retraining a * bridge with K80s under it is not ours to do. */ static void retrain_link_if_downgraded(const char *card_dir, const char *card_bdf) { ft_tt_udrv_link_retrain_report retrain; ft_tt_udrv_link_status status; ft_tt_pcie_topology topology; char bridge_dir[FT_TT_UDRV_RESET_PATH_MAX]; if (ft_tt_udrv_link_status_read(card_dir, &status) != FT_TT_UDRV_OK) { printf("link: the card's LNKSTA could not be read; NOT retraining\n"); return; } if (status.speed_code >= FT_TT_UDRV_LINK_SPEED_5GT) { printf("link: already %s x%u — no retrain needed\n", ft_tt_udrv_link_speed_string(status.speed_code), status.width); return; } if (ft_tt_pcie_topology_resolve(card_bdf, &topology) != FT_TT_OK || !topology.safe) { printf("link: at %s but the bridge topology is not exclusively this card (%s) — " "NOT retraining\n", ft_tt_udrv_link_speed_string(status.speed_code), topology.refusal ? topology.refusal : "unresolved"); return; } if (pci_device_dir(topology.bridge, bridge_dir, sizeof(bridge_dir)) != FT_TT_OK) return; printf("link: %s — retraining on bridge %s\n", ft_tt_udrv_link_speed_string(status.speed_code), topology.bridge); (void)ft_tt_udrv_link_retrain(bridge_dir, 2000u, &retrain); ft_tt_udrv_link_retrain_print(&retrain); print_link_status("card", card_dir); print_link_status("bridge", bridge_dir); } /** * Rung 3 through the userspace driver: snapshot config, three config writes, wait for the * card to clear its own reset marker, write config back, retrain the link. * * The caller has already taken the card lock, censused the per-core claims, printed the * before state and CLOSED the session — this function must not be reached with an aperture * mapped, which is the discipline that replaces `tenstorrent_vma_zap`. * * @param card_bdf "0000:12:00.0" * @param options `udrv_reenumerate` selects the remove + rescan fallback instead * @return FT_TT_OK once the card is back and holds its config again */ static int run_udrv_asic_reset(const char *card_bdf, const tool_options *options) { ft_tt_udrv_config_restore_report restore; ft_tt_udrv_config_snapshot snapshot; ft_tt_udrv_reset_wait_report wait; ft_tt_udrv_reset_report reset; char card_dir[FT_TT_UDRV_RESET_PATH_MAX]; int rc = pci_device_dir(card_bdf, card_dir, sizeof(card_dir)); if (rc != FT_TT_OK) return rc; printf("\n--- rung 3, NO VENDOR DRIVER: %s ---\n", card_dir); print_link_status("card", card_dir); /* The snapshot is taken BEFORE the marker is set, so the saved command register has the * marker clear and restoring it cannot re-arm what the card just finished clearing. */ rc = ft_tt_udrv_config_snapshot_take(card_dir, &snapshot); if (rc != FT_TT_UDRV_OK) { fprintf(stderr, "refused: could not snapshot PCI config space (%d). Without it an in-place\n" " ASIC reset leaves the BARs and the command register at zero and the card\n" " undecodable. Nothing has been reset. Re-run under sudo if %s/config is\n" " not writable.\n", rc, card_dir); return FT_TT_EPERM; } printf("config snapshot: vendor 0x%04x device 0x%04x, PCI Express capability at 0x%02x " "version %u, BAR0 0x%08x, command 0x%04x\n", snapshot.vendor_id, snapshot.device_id, snapshot.pcie_capability_offset, snapshot.pcie_capability_version, snapshot.header[4], (unsigned)(snapshot.header[1] & 0xFFFFu)); rc = ft_tt_udrv_reset_asic(card_dir, &reset); if (rc != FT_TT_UDRV_OK) { ft_tt_udrv_reset_print(&reset, NULL); fprintf(stderr, "the ASIC reset did not complete its three writes\n"); return FT_TT_EIO; } /* WAIT FOR THE MARKER, then restore. Restoring while the marker is still set is the bug * that made tt-kmd's in-place path look impossible (ft_tt_udrv_reset.h @status). */ rc = ft_tt_udrv_reset_wait_ready(card_dir, snapshot.vendor_id, 15000u, &wait); ft_tt_udrv_reset_print(&reset, &wait); if (rc != FT_TT_UDRV_OK) { fprintf(stderr, "the card did not come back within 15 s. It is reset and NOT restored;\n" " `echo 1 > %s/remove` then `echo 1 > /sys/bus/pci/rescan` is the recovery.\n", card_dir); return FT_TT_EIO; } if (options->udrv_reenumerate) { ft_tt_pcie_reenumeration_report reenumeration; printf("--udrv-reenumerate: finishing with remove + rescan instead of the in-place " "restore\n"); rc = ft_tt_pcie_reenumerate(card_bdf, &reenumeration); ft_tt_pcie_reenumeration_print(&reenumeration); if (rc != FT_TT_OK) return rc; } else { rc = ft_tt_udrv_config_snapshot_restore(card_dir, &snapshot, &restore); ft_tt_udrv_config_restore_print(&restore); if (rc != FT_TT_UDRV_OK) { fprintf(stderr, "the config restore did not land; the card is reset and " "undecodable. Recover with remove + rescan.\n"); return FT_TT_EIO; } } retrain_link_if_downgraded(card_dir, card_bdf); return FT_TT_OK; } static void close_card(tool_card *card); /** * Open the card over the transport `--driver` names — and ONLY that one. * * `--driver kmd` (the default) is the vendor chardev, exactly as before. * `--driver udrv` is foundry's own driver and NEVER the chardev, whether or not * a module happens to be loaded. Two reasons, and the second is the important * one: * * - a flag that silently uses the other transport when the better one is * available is untestable on the machine that HAS the better one, and this * bank is exactly that machine — tt-kmd is the oracle and is deliberately * kept installed. `--driver udrv` has to mean udrv here or it can never be * compared against anything; * - a preference is a fallback wearing a different word, and a fallback that * produces a plausible answer from the wrong path is the defect this project * has shipped twice. * * What `--driver udrv` therefore does NOT get is `probe --write`, which needs * the tt-kmd backend. That is a named limit, not a surprise: `probe` keeps the * session (see main()). * * @param out_card zeroed and filled; `noc`/`grid` point into it, so it must * not be copied by value afterwards */ static int open_card(tool_card *card, tool_targets *targets, const tool_options *options, char *out_address, size_t address_bytes) { int rc; memset(card, 0, sizeof(*card)); if (options->reset_driver != TOOL_RESET_DRIVER_UDRV) { rc = open_session(&card->kmd, targets, card->address, sizeof(card->address)); if (rc != FT_TT_OK) { fprintf(stderr, "the vendor chardev did not open (rc=%d). --driver udrv reads " "the card without one.\n", rc); return rc; } card->kmd_open = true; card->noc = &card->kmd.noc; card->grid = &card->kmd.grid; if (out_address) (void)snprintf(out_address, address_bytes, "%s", card->address); return FT_TT_OK; } rc = ft_tt_runtime_open_with_driver(0, FT_TT_RUNTIME_DRIVER_UDRV, &card->udrv); if (rc != FT_TT_OK) { fprintf(stderr, "--driver udrv: the userspace driver could not open the card " "(rc=%d). REFUSING — it will not quietly use the chardev instead. " "Nothing has been done to the card.\n", rc); return rc; } card->udrv_open = true; card->noc = &card->udrv.noc; card->grid = &card->udrv.grid; rc = resolve_targets(card->grid, targets); if (rc != FT_TT_OK) { close_card(card); return rc; } (void)ft_tt_runtime_pci_address(&card->udrv, card->address, sizeof(card->address)); if (out_address) (void)snprintf(out_address, address_bytes, "%s", card->address); printf("card read over foundry's own driver (no vendor ioctl in this path)\n"); return FT_TT_OK; } /** Close whatever is open and blank the pointers. * * Called BEFORE every reset, not only at the end: a reset invalidates every * mapping the card had, and on the udrv path nothing zaps our VMAs for us * (ft_tt_udrv.h §5(b) — the replacement is this discipline, not a mechanism). */ static void close_card(tool_card *card) { if (!card) return; ft_tt_tensix_claim_release(card->cores); card->cores = NULL; if (card->kmd_open) ft_tt_hardware_session_close(&card->kmd); if (card->udrv_open) ft_tt_runtime_close(&card->udrv); card->kmd_open = false; card->udrv_open = false; card->noc = NULL; card->grid = NULL; } /** Rungs 3-5, which reset the whole card: capture, close, reset, reopen, report. * * Takes the exclusive card lock FIRST, before anything else -- the same property as the * PCIe reenumeration pre-flight below: acquire and check before anything destructive, and * release on every exit path via `cleanup`, including every failure path. A lock leaked by * a failed reset would be worse than no lock at all. */ static int run_device_recovery(tool_targets *targets, const tool_options *options) { char card_address[FT_TT_PCI_ADDRESS_MAX] = {0}; /* The userspace ASIC reset restores config space IN PLACE and needs no PCI * remove and no bus-wide rescan — that was measured, and `remove` + `rescan` * is kept only as the explicit `--udrv-reenumerate` fallback. So the * root-only reenumeration pre-flight applies to the kmd path and to that * fallback, and NOT to the default udrv path. Demanding it there would * refuse the one machine this driver exists for, for a permission it never * uses. */ bool needs_reenumeration = options->reset_driver != TOOL_RESET_DRIVER_UDRV || options->udrv_reenumerate; tool_card card; int lock_fd = -1; int rc = acquire_card_lock_or_explain(options, &lock_fd); memset(&card, 0, sizeof(card)); if (rc != FT_TT_OK) return rc; rc = open_card(&card, targets, options, card_address, sizeof(card_address)); if (rc != FT_TT_OK) goto cleanup; printf("card is at PCI %s\n", card_address); /* SECOND PRE-FLIGHT: the card lock above only sees processes that registered * a shared claim. Ordinary work now holds PER-CORE claims, and a reset wipes * those too, so consult that namespace before anything destructive. */ { unsigned core_holders = report_core_claim_holders(card_address); if (core_holders != 0u && !options->override_card_lock) { fprintf(stderr, "refused: rung %u resets the whole card, and %u core%s are claimed by " "live processes (listed above).\n" " A card reset returns every Tensix to power-on default, so it destroys " "their work as surely as it would a shared claim's.\n" " Wait for them, or re-run with --override-card-lock. Nothing has been " "reset.\n", options->rung, core_holders, core_holders == 1u ? " is" : "s are"); rc = FT_TT_EPERM; goto cleanup; } if (core_holders != 0u) fprintf(stderr, "--override-card-lock: those %u core claim%s will be destroyed.\n", core_holders, core_holders == 1u ? "" : "s"); } /* PRE-FLIGHT, BEFORE ANYTHING DESTRUCTIVE. Every card-level rung leaves the card * reset-but-not-re-enumerated (§21.3): memory decode off, BARs disabled, tt-kmd * unbound, /dev/tenstorrent gone. Only the PCI remove + rescan finishes the job, and * those sysfs nodes are root-only. Discovering that AFTER the reset leaves the card * dead until someone runs the rescan by hand -- which is exactly what happened on * 2026-08-12 (errno 13 at "removing the card from the PCI bus", card down until it was * re-enumerated manually). So refuse up front rather than half-finish. */ rc = needs_reenumeration ? ft_tt_pcie_reenumerate_is_permitted(card_address) : FT_TT_OK; if (rc != FT_TT_OK) { close_card(&card); fprintf(stderr, "refused: rung %u resets the whole card, and completing it needs write\n" " access to /sys/bus/pci/devices/%s/remove and /sys/bus/pci/rescan,\n" " which this process does not have. Without them the card would be left\n" " reset-but-not-re-enumerated: BARs disabled, tt-kmd unbound, no chardev.\n" " Re-run under sudo. Nothing has been reset.\n", options->rung, card_address); rc = FT_TT_EPERM; goto cleanup; } print_targets_status(card.noc, card.grid, targets, "before (captured now: a successful reset erases it)"); /* Every handle must be closed: the reset paths zap VMAs and reclaim TLB windows, so a * live session's mapped windows are invalid the moment they return. On the udrv path * nothing zaps them for us, which makes closing here the mechanism rather than a tidy-up. */ close_card(&card); if (options->reset_driver == TOOL_RESET_DRIVER_UDRV) { rc = run_udrv_asic_reset(card_address, options); if (rc != FT_TT_OK) goto cleanup; } else if (options->rung == 5u) { ft_tt_pcie_sbr_report report; rc = ft_tt_pcie_secondary_bus_reset(card_address, options->save_directory, &report); ft_tt_pcie_sbr_print(&report); if (rc == FT_TT_EPERM) { fprintf(stderr, "refused: %s\n", report.topology.refusal ? report.topology.refusal : "topology check"); goto cleanup; } } else { ft_tt_device_reset_report report; ft_tt_pcie_reenumeration_report reenumeration; ft_tt_device_reset_flag flag = options->rung == 3u ? FT_TT_RESET_DEVICE_ASIC_RESET : FT_TT_RESET_DEVICE_ASIC_DMC_RESET; rc = ft_tt_device_reset_issue(TOOL_CHARDEV_PATH, flag, &report); ft_tt_device_reset_print(&report); if (rc != FT_TT_OK) goto cleanup; /* The driver's in-place completion does not finish the job on this host: the card * comes back with memory decode disabled and an ARC that answers 0xffffffff. See * ft_tt_pcie_reenumerate()'s header — this is measured, not defensive. */ rc = ft_tt_pcie_reenumerate(card_address, &reenumeration); ft_tt_pcie_reenumeration_print(&reenumeration); if (rc != FT_TT_OK) goto cleanup; } rc = open_card(&card, targets, options, NULL, 0); if (rc != FT_TT_OK) { fprintf(stderr, "the card did not reopen after the reset (rc=%d)\n", rc); goto cleanup; } print_targets_status(card.noc, card.grid, targets, "after"); if (options->then_probe) probe_targets(card.noc, card.grid, targets, options); rc = FT_TT_OK; cleanup: close_card(&card); ft_tt_card_lock_release(lock_fd); return rc; } /** The refusals that keep a read-only tool read-only. */ static int check_permissions(const tool_options *options) { if (options->command == TOOL_COMMAND_PROBE && !options->allow_write) { fprintf(stderr, "probe writes firmware, kernels and tiles to the card: pass --write\n"); return FT_TT_EPERM; } if (options->command != TOOL_COMMAND_RECOVER) return FT_TT_OK; if (!options->rung_given || options->rung > 5u) { fprintf(stderr, "recover needs --rung 0..5\n"); return FT_TT_EINVAL; } if (!options->allow_write) { fprintf(stderr, "recover writes to the card: pass --write\n"); return FT_TT_EPERM; } if (options->rung == 5u && !options->allow_secondary_bus_reset) { fprintf(stderr, "rung 5 resets a PCIe bus: pass --allow-pcie-secondary-bus-reset " "as well as --write\n"); return FT_TT_EPERM; } /* `--driver` selects the TRANSPORT for every command, and additionally the MECHANISM * for a whole-card rung. Only rung 3 has a userspace mechanism, so those are the only * two rungs it can be refused for — and the refusal names why rather than silently * doing the kmd thing. Rung 4 is two ARC messages and there is no userspace ARC * transport wired into the reset path yet; rung 5 is already pure sysfs and has no * vendor-driver dependency to remove. * * `status`, `probe` and rungs 0-2 are NOT restricted: they reach the card only through * the ft_tt_noc_access vtable and a per-core flock, and both exist on either transport. * That is what makes the whole tool usable on a board with no vendor module. */ if (options->command == TOOL_COMMAND_RECOVER && options->reset_driver == TOOL_RESET_DRIVER_UDRV && options->rung >= 4u) { fprintf(stderr, "--driver udrv has no mechanism for rung %u. It is %s.\n", options->rung, options->rung == 4u ? "ASIC+DMC, which needs two ARC messages and there is no userspace ARC " "transport wired into the reset path yet" : "already pure sysfs and uses no vendor driver"); return FT_TT_EINVAL; } if (options->udrv_reenumerate && options->reset_driver != TOOL_RESET_DRIVER_UDRV) { fprintf(stderr, "--udrv-reenumerate only means something with --driver udrv\n"); return FT_TT_EINVAL; } return FT_TT_OK; } int main(int argc, char **argv) { tool_card card; tool_options options; tool_targets targets; char card_address[FT_TT_PCI_ADDRESS_MAX] = {0}; int rc; setvbuf(stdout, NULL, _IOLBF, 0); if (parse_options(argc, argv, &options) != FT_TT_OK) { print_usage(argv[0]); return 2; } if (check_permissions(&options) != FT_TT_OK) return 2; if (options.command == TOOL_COMMAND_CLAIM) return run_claim(&options) == FT_TT_OK ? 0 : 1; if (options.command == TOOL_COMMAND_TOPOLOGY) { ft_tt_pcie_topology topology; /* An optional PCI address makes the REFUSAL demonstrable: point it at a device * that is not the card — a K80 behind a bridge with two children — and watch it * refuse. Read-only, and it does not change what rung 5 targets: that address is * always derived from the open device, never taken from a command line. */ if (options.target_text) { snprintf(card_address, sizeof(card_address), "%s", options.target_text); } else { memset(&targets, 0, sizeof(targets)); targets.all = true; rc = open_card(&card, &targets, &options, card_address, sizeof(card_address)); if (rc != FT_TT_OK) return 1; close_card(&card); } if (ft_tt_pcie_topology_resolve(card_address, &topology) != FT_TT_OK) fprintf(stderr, "could not read the PCIe topology for %s\n", card_address); ft_tt_pcie_topology_print(&topology); /* The negotiated link, at BOTH ends. A reset leaves it at Gen1 and every * host-involved number then silently halves, so it belongs in the read-only * command that gets run at the start and end of a round (DEV_NOTES §1). */ { char device_dir[FT_TT_UDRV_RESET_PATH_MAX]; if (pci_device_dir(topology.card[0] ? topology.card : card_address, device_dir, sizeof(device_dir)) == FT_TT_OK) print_link_status("card", device_dir); if (topology.bridge[0] && pci_device_dir(topology.bridge, device_dir, sizeof(device_dir)) == FT_TT_OK) print_link_status("bridge", device_dir); } return topology.safe ? 0 : 1; } if (parse_targets(options.target_text, &targets) != FT_TT_OK) { fprintf(stderr, "cannot parse targets: %s\n", options.target_text); return 2; } if (options.command == TOOL_COMMAND_RECOVER && options.rung >= 3u) return run_device_recovery(&targets, &options) == FT_TT_OK ? 0 : 1; /* STATUS goes through the transport seam and NOT through the tt-kmd session, because it * is the one command that has to work on a board with no vendor module: it reads only, * so it needs no claim, and `instrn_buf_status` read this way is the whole health * instrument once the module is gone. PROBE and rungs 0-2 WRITE to each core they * target, so they keep the session that carries the per-core claim. */ if (options.command == TOOL_COMMAND_STATUS) { rc = open_card(&card, &targets, &options, NULL, 0); if (rc != FT_TT_OK) return 1; rc = run_status(card.noc, card.grid, &targets, options.detail); close_card(&card); return rc == FT_TT_OK ? 0 : 1; } /* READINESS travels with STATUS for the same reason: reads only, so no claim and no * session, and it must work on a board with no vendor module. */ if (options.command == TOOL_COMMAND_READINESS) { rc = open_card(&card, &targets, &options, NULL, 0); if (rc != FT_TT_OK) return 1; rc = run_readiness(card.noc, card.grid, &targets); close_card(&card); return rc == FT_TT_OK ? 0 : 1; } rc = open_card(&card, &targets, &options, NULL, 0); if (rc != FT_TT_OK) return 1; switch (options.command) { case TOOL_COMMAND_PROBE: rc = claim_targets_or_explain(&card, &targets, &options, "probe"); if (rc == FT_TT_OK) rc = run_probe(card.noc, card.grid, &targets, &options); break; case TOOL_COMMAND_STATUS: case TOOL_COMMAND_READINESS: case TOOL_COMMAND_RECOVER: default: rc = claim_targets_or_explain(&card, &targets, &options, "recover"); if (rc == FT_TT_OK) rc = run_core_recovery(card.noc, card.grid, &targets, &options); break; } close_card(&card); return rc == FT_TT_OK ? 0 : 1; }