Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/lib/drivers/inl/inl-device.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from "vitest";
import { INLDevice } from "./inl-device";
import { NES } from "./inl-opcodes";

/**
* Build an INLDevice whose underlying USBDevice's controlTransferIn returns a
Expand Down Expand Up @@ -48,3 +49,51 @@ describe("INLDevice.payloadIn desync guard", () => {
await expect(inl.payloadIn(512)).rejects.toThrow(/254/);
});
});

describe("INLDevice flash-write guard (read-only dumper)", () => {
it("refuses MMC3_PRG_FLASH_WR at any address — the misused opcode", async () => {
// No device connected: the guard must fire before any transfer, so a
// flash command can never reach the cart — at a $5xxx register address
// (the one-off experiment's misuse) AND at a real $8000 flash address.
const inl = new INLDevice();
await expect(inl.nes(NES.MMC3_PRG_FLASH_WR, 0x5000, 0x40)).rejects.toThrow(
/flash-write opcode/i,
);
await expect(inl.nes(NES.MMC3_PRG_FLASH_WR, 0x8000, 0x40)).rejects.toThrow(
/flash-write opcode/i,
);
});

it("refuses the whole flash-program family, not just MMC3", async () => {
// Spot-check the contiguous block (CHR + other-mapper PRG writes) and
// the MMC3S stray at 0x26.
const inl = new INLDevice();
for (const op of [0x08, 0x0a, 0x0e, 0x14, 0x26]) {
await expect(inl.nes(op, 0x8000, 0x55)).rejects.toThrow(
/never programs/i,
);
}
});

it("refuses a flash opcode disguised in the high bits of an out-of-range value", async () => {
// controlIn() transmits only opcode & 0xff, so a caller passing 0x107
// would send 0x07 (MMC3_PRG_FLASH_WR). The guard normalizes first, so the
// disguised flash write is still refused — never sent as 0x07.
const inl = new INLDevice();
await expect(inl.nes(0x100 | NES.MMC3_PRG_FLASH_WR, 0x8000, 0x40)).rejects.toThrow(
/flash-write opcode/i,
);
});

it("does not interfere with the writes dumping needs", async () => {
// Register/serial writes used to select banks must still pass through to
// the transport (here they fail only because no device is connected).
const inl = new INLDevice();
await expect(inl.nes(NES.NES_CPU_WR, 0x5000, 0x40)).rejects.toThrow(
/not connected/i,
);
await expect(inl.nes(NES.NES_MMC1_WR, 0x8000, 0x01)).rejects.toThrow(
/not connected/i,
);
});
});
18 changes: 18 additions & 0 deletions src/lib/drivers/inl/inl-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import {
DICT,
NES_FLASH_WRITE_OPCODES,
BUFFER,
INL_DEVICE_FILTER,
RETURN,
Expand Down Expand Up @@ -116,6 +117,23 @@ export class INLDevice {
* GET_BANK_TABLE (0x86) which has RL=4.
*/
async nes(opcode: number, operand = 0, misc = 0): Promise<number> {
// controlIn() transmits only the low 8 bits (opcode & 0xff), so normalize
// up front: the flash-write guard and returnLength must both reflect the
// byte actually sent. Without this, nes(0x107) would slip 0x07 (a flash
// write) past the guard and mis-size its reply.
opcode &= 0xff;
// Hardware safety: nabu is a read-only dumper and must never program a
// cartridge's flash. Refuse every flash-PROGRAM opcode the firmware
// exposes (see NES_FLASH_WRITE_OPCODES) before any transfer goes out, so
// a stray flash command can never reach a cart that should only be read.
if (NES_FLASH_WRITE_OPCODES.has(opcode)) {
throw new Error(
`Refusing NES flash-write opcode 0x${opcode
.toString(16)
.padStart(2, "0")}: nabu only reads cartridges and never programs ` +
`their flash.`,
);
}
const returnLength = opcode >= 0x80 ? (opcode === 0x86 ? 4 : 3) : 1;
return this.controlIn(DICT.NES, opcode, operand, misc, returnLength);
}
Expand Down
34 changes: 27 additions & 7 deletions src/lib/drivers/inl/inl-opcodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,11 @@ export const NES = {
NES_DUALPORT_WR: 0x05,
// Flash-program a byte on an MMC3 board: three JEDEC unlock writes
// ($D555/$AAAA/$D555), then the (operand, misc) write, then $8000<-2 and
// a stability poll-read — ALL inside one USB transaction, each a full
// nes_cpu_wr M2 cycle microseconds apart. Caution: the tail poll re-reads
// the WRITTEN address until two consecutive reads agree, so a target
// whose reads flicker (e.g. $5xxx on the mapper-268 board) spins the
// firmware until a physical replug — which is exactly why it is NOT used
// as an M2-burst write primitive (see UNSUPPORTED_MAPPERS in
// ./unsupported-mappers).
// a stability poll-read, all inside one USB transaction. This is one of
// the firmware's flash-PROGRAM opcodes — nabu is a read-only dumper and
// never programs a cart, so `INLDevice.nes` hard-rejects the whole family
// (see NES_FLASH_WRITE_OPCODES below). Kept declared because it documents
// the device surface and is the one a one-off experiment once misused.
MMC3_PRG_FLASH_WR: 0x07,
SET_CUR_BANK: 0x20,
SET_BANK_TABLE: 0x21,
Expand All @@ -76,6 +74,28 @@ export const NES = {
GET_NUM_PRG_BANKS: 0x87,
} as const;

/**
* The INL firmware's NES-dictionary flash-PROGRAM opcodes
* (`shared_dict_nes.h`): per-mapper PRG/CHR flash byte-program commands,
* 0x07-0x14 contiguous (MMC3 / NROM / CNROM / CDREAM / UNROM / MMC1 / MMC4 /
* MAP30 / GTROM), plus MMC3S at 0x26. Each issues a JEDEC unlock+program
* sequence that writes the cart's flash chip.
*
* nabu is a read-only DUMPER — it drives mapper registers to select banks
* but never programs a cartridge's non-volatile storage — so `INLDevice.nes`
* refuses every opcode in this set, at any address. Listed by value rather
* than minting NES.* constants nabu otherwise never uses; only
* MMC3_PRG_FLASH_WR is named above (it documents the surface and was the one
* a one-off experiment misused). (The lower-level write *primitives* the
* firmware also uses for flashing — DISCRETE_EXP0_PRGROM_WR 0x00, M2_LOW_WR
* 0x22, FLASH_3V_WR/M2_HIGH_WR 0x25 — are dual-purpose bus-write variants, so
* they are deliberately NOT blanket-blocked here.)
*/
export const NES_FLASH_WRITE_OPCODES: ReadonlySet<number> = new Set([
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13,
0x14, 0x26,
]);

// ─── Buffer Dictionary ──────────────────────────────────────────────────────

export const BUFFER = {
Expand Down