1327f82719
The alignment bug is FIXED -- hardware now reports "frame 1 (342 B) etype=0800 at 12: FF FF FF FF FF FF 00 2B", i.e. broadcast dst at 0, host MAC at 6, ethertype at 12. Perfectly aligned, correctly received, correctly skipped as not ours. So 00 00 00 01 was never a dropped write. It is frame data being read as a descriptor, because the ring HAS NO OVERFLOW PROTECTION: nothing in the gateware consumes RRP, so RXFrameAssembler keeps writing and laps the reader (the real BBA has RBFI for exactly this). The poll loop costs ~5 EXI transactions per frame (~200 us) while a 100 Mbit link delivers a 342-byte frame every ~30 us, and the ring is 15 pages -- roughly 3 to 11 frames. Under continuous traffic pages are reused before they are read. That is also why it "worked once out of three tries": it is a race against the host's background traffic. ring_next() now returns -4 when the writer is more than two thirds of the ring ahead, and the caller resyncs and continues instead of parsing bytes that belong to a different frame. The timeout reports seen / bad-descriptor / lapped counts so the three cases stay distinguishable. Detection is not prevention, so the app and the README both now say to quiet the link first with gc-testlink.sh, which sets a static address and takes the interface out of NetworkManager -- stopping the DHCP client whose 342-byte broadcasts are the main offender. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
670 lines
22 KiB
C
670 lines
22 KiB
C
#include "netboot.h"
|
|
#include "bba.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <malloc.h>
|
|
#include <unistd.h>
|
|
#include <ogc/lwp_watchdog.h>
|
|
#include <ogc/cache.h>
|
|
#include <fat.h>
|
|
#include <sdcard/gcsd.h>
|
|
|
|
/* ── frame layout ────────────────────────────────────────────────────────
|
|
* 0 dst MAC (6)
|
|
* 6 src MAC (6)
|
|
* 12 ethertype (2) = NB_ETHERTYPE
|
|
* 14 magic (4) = "GCUP"
|
|
* 18 opcode (1)
|
|
* 19 reserved (1)
|
|
* 20 seq (2, big endian)
|
|
* 22 nblocks (2, big endian)
|
|
* 24 len (2, big endian)
|
|
* 26 data (len)
|
|
*/
|
|
#define NB_HDR 26u
|
|
#define NB_MIN 60u /* ethernet minimum, padded */
|
|
|
|
static u8 s_our_mac[6];
|
|
static u8 s_peer_mac[6];
|
|
/* Last descriptor ring_next() looked at, kept for the error path: when a
|
|
* length is rejected, the four raw bytes are the whole diagnosis. */
|
|
static u8 s_last_desc[4];
|
|
static u8 s_last_dump[32];
|
|
static u32 s_last_page;
|
|
|
|
static u16 rd16(const u8 *p) { return ((u16)p[0] << 8) | p[1]; }
|
|
static void wr16(u8 *p, u16 v) { p[0] = (u8)(v >> 8); p[1] = (u8)v; }
|
|
|
|
/* ── RX: pull one frame out of the SPRAM ring ────────────────────────────
|
|
* The ring is pages 1-15 with a 4-byte descriptor at each page start whose
|
|
* length field INCLUDES the descriptor. RRP is ours to advance; the
|
|
* gateware does not act on it. */
|
|
/* Pages the writer is ahead of us, over the 15-page ring. */
|
|
static u32 ring_lag(u32 rpage, u32 wpage)
|
|
{
|
|
return (wpage >= rpage) ? (wpage - rpage) : (wpage + 15u - rpage);
|
|
}
|
|
|
|
static int ring_next(u8 *buf, u32 cap)
|
|
{
|
|
u8 rp[2], wp[2], hdr[4];
|
|
u32 rpage, wpage, dlen, flen, addr;
|
|
|
|
if (bba_read(REG_RRP_LO, rp, 2) < 0) return -1;
|
|
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
|
|
rpage = ((u32)rp[1] << 8) | rp[0];
|
|
wpage = ((u32)wp[1] << 8) | wp[0];
|
|
if (rpage == wpage) return 0; /* empty */
|
|
if (rpage < 1 || rpage > 15) return -2; /* pointer corrupt */
|
|
|
|
/* THE RING HAS NO OVERFLOW PROTECTION. Nothing in the gateware consumes
|
|
* RRP, so RXFrameAssembler keeps writing and laps us: the real BBA has
|
|
* RBFI for this and we do not. Reading from a page the writer has
|
|
* already reused yields a "descriptor" that is really frame data -- which
|
|
* is where 00 00 00 01 came from, not a dropped write.
|
|
*
|
|
* Our poll loop costs ~5 EXI transactions per frame (~200 us) while a
|
|
* 100 Mbit link delivers a 342-byte frame every ~30 us, and the ring is
|
|
* only 15 pages, so under continuous traffic we lose that race. Bail out
|
|
* once the writer is more than two thirds of the ring ahead: the data we
|
|
* would read is not trustworthy, and resyncing is strictly better than
|
|
* parsing someone else's bytes. */
|
|
if (ring_lag(rpage, wpage) > 10) return -4;
|
|
|
|
addr = rpage << 8;
|
|
if (bba_stream((u16)addr, hdr, 4) < 0) return -1;
|
|
memcpy(s_last_desc, hdr, 4);
|
|
s_last_page = rpage;
|
|
/* Keep the head of the page for the error path. Guessing at a 4-byte
|
|
* descriptor has already cost several round trips; 32 bytes shows
|
|
* whether an ethernet header is present and AT WHAT OFFSET, which
|
|
* settles "bad length" versus "reading the wrong place" outright. */
|
|
if (bba_stream((u16)addr, s_last_dump, sizeof(s_last_dump)) < 0)
|
|
return -1;
|
|
dlen = ((u32)hdr[2] << 8) | hdr[3];
|
|
if (dlen <= 4 || dlen > 1518 + 4) return -3; /* descriptor implausible */
|
|
flen = dlen - 4;
|
|
if (flen > cap) flen = cap;
|
|
|
|
/* Descriptor + frame are contiguous from the page start. Read both and
|
|
* drop the descriptor rather than streaming from a mid-page address, so
|
|
* one DMA read covers it. */
|
|
{
|
|
static u8 tmp[1536 + 4];
|
|
u32 want = dlen;
|
|
if (want > sizeof(tmp)) want = sizeof(tmp);
|
|
if (bba_stream((u16)addr, tmp, want) < 0) return -1;
|
|
memcpy(buf, tmp + 4, flen);
|
|
}
|
|
|
|
/* Advance RRP by the pages this frame occupied, wrapping 1..15. */
|
|
{
|
|
u32 pages = (dlen + 255) >> 8;
|
|
u32 next = ((rpage - 1 + pages) % 15) + 1;
|
|
if (bba_w8(REG_RRP_LO, (u8)next) < 0) return -1;
|
|
}
|
|
return (int)flen;
|
|
}
|
|
|
|
static int send_ack(u16 seq)
|
|
{
|
|
u8 f[NB_MIN];
|
|
memset(f, 0, sizeof(f));
|
|
memcpy(f + 0, s_peer_mac, 6);
|
|
memcpy(f + 6, s_our_mac, 6);
|
|
wr16(f + 12, NB_ETHERTYPE);
|
|
f[14] = 'G'; f[15] = 'C'; f[16] = 'U'; f[17] = 'P';
|
|
f[18] = NB_OP_ACK;
|
|
wr16(f + 20, seq);
|
|
wr16(f + 22, 0);
|
|
wr16(f + 24, 0);
|
|
return bba_tx_bytes(f, sizeof(f));
|
|
}
|
|
|
|
/* Destination path carried in the DONE frame, so the host decides where the
|
|
* build lands rather than it being baked in here. */
|
|
static char s_dest[64] = "/gc_bba_test.dol";
|
|
|
|
const char *netboot_dest_path(void) { return s_dest; }
|
|
|
|
int netboot_receive(void *dst, u32 maxlen, void (*status)(const char *msg))
|
|
{
|
|
static u8 frame[1536];
|
|
u8 *out = (u8 *)dst;
|
|
u32 got = 0, nblocks = 0, bad = 0, seen = 0, lapped = 0;
|
|
u16 expect = 0;
|
|
char line[80];
|
|
u64 t0;
|
|
int have_peer = 0;
|
|
|
|
/* Bring the device up deterministically rather than hoping someone else
|
|
* already did. Pressing Z straight after launch is the normal case, and
|
|
* then NOTHING has issued the NCRA reset that triggers the W5100's own
|
|
* init -- MACRAW is not running, no frames are stored, and this would sit
|
|
* in "timeout: no frames" for a reason that has nothing to do with the
|
|
* transfer. Swiss may have initialised it, or an earlier test may have,
|
|
* or neither; a reset here makes all three cases identical.
|
|
*
|
|
* The reset also clears the SPRAM ring pointers, which is what we want:
|
|
* a clean ring with RRP == RWP == page 1, so the first frame we see is
|
|
* genuinely the host's first block. */
|
|
if (status) status("resetting the adapter...");
|
|
if (bba_w8(REG_NCRA, NCRA_RESET) < 0) return -1;
|
|
{
|
|
int i, v = -1;
|
|
for (i = 0; i < 50; i++) {
|
|
v = bba_r8(REG_NCRA);
|
|
if (v >= 0 && !(v & NCRA_RESET)) break;
|
|
usleep(5000);
|
|
}
|
|
if (i == 50) {
|
|
if (status) status("NCRA reset never self-cleared");
|
|
return -1;
|
|
}
|
|
/* The W5100 re-inits behind that: RSTn pulse plus a 60.3 ms settle,
|
|
* done twice. Polling it sooner reads all-zeros and looks dead. */
|
|
usleep(400000);
|
|
}
|
|
/* Start receive. Without the SR bit the assembler never writes the
|
|
* ring, so frames arrive at the W5100 and go nowhere. */
|
|
if (bba_w8(REG_NCRA, NCRA_SR) < 0) return -1;
|
|
|
|
/* Establish a KNOWN ring state instead of trusting what we inherited.
|
|
* Swiss initialises the BBA before launching us and writes RRP itself,
|
|
* and an NCRA reset does not restore the register file's copy of either
|
|
* pointer -- their init values only apply at FPGA configuration. So
|
|
* RRP can legitimately be anything, including 0, which is outside the
|
|
* ring (pages 1-15) and used to abort the whole transfer.
|
|
*
|
|
* Setting RRP = RWP declares the ring empty and consistent, whatever it
|
|
* was, so the first frame we read is genuinely the host's first block. */
|
|
{
|
|
u8 wp[2];
|
|
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
|
|
if (wp[0] < 1 || wp[0] > 15) {
|
|
/* RWP is the gateware's own pointer, so this should not
|
|
* happen -- say so loudly rather than papering over it. */
|
|
if (status) { snprintf(line, sizeof(line),
|
|
"RWP=%u outside ring pages 1-15", wp[0]);
|
|
status(line); }
|
|
return -5;
|
|
}
|
|
if (bba_w8(REG_RRP_LO, wp[0]) < 0) return -1;
|
|
if (status) { snprintf(line, sizeof(line),
|
|
"ring synced at page %u", wp[0]); status(line); }
|
|
}
|
|
|
|
/* PAR0-5 survives the reset (it is the gateware's power-on MAC), and it
|
|
* is read AFTER the reset so the source address we put in ACKs is the
|
|
* one the adapter is actually filtering on. */
|
|
if (bba_read(REG_PAR0, s_our_mac, 6) < 0) return -1;
|
|
|
|
if (status) {
|
|
status("waiting for host (send-dol.py)...");
|
|
status("NOTE: quiet the link first, or other traffic laps the ring:");
|
|
status(" sudo ./scripts/gc-testlink.sh up <iface>");
|
|
}
|
|
t0 = gettime();
|
|
|
|
for (;;) {
|
|
int n = ring_next(frame, sizeof(frame));
|
|
if (n == -4) {
|
|
/* Lapped. Skip to the writer and carry on; with stop-and-wait
|
|
* the host simply retransmits the block we just threw away. */
|
|
lapped++;
|
|
if (lapped <= 3 && status)
|
|
status("ring lapped (too much other traffic) - resyncing");
|
|
{
|
|
u8 wp[2];
|
|
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
|
|
if (wp[0] >= 1 && wp[0] <= 15)
|
|
bba_w8(REG_RRP_LO, wp[0]);
|
|
}
|
|
continue;
|
|
}
|
|
if (n == -3) {
|
|
/* A descriptor we cannot make sense of must NOT kill the
|
|
* transfer. The ring collects EVERYTHING on the wire --
|
|
* mDNS, IPv6 RAs, ARP from the host -- so pressing Z on a
|
|
* live network hits stray frames long before the sender
|
|
* starts. Log the first few, resync, and carry on. */
|
|
bad++;
|
|
if (bad <= 3 && status) {
|
|
int i;
|
|
snprintf(line, sizeof(line),
|
|
"skip bad descriptor at page %u: %02X %02X %02X %02X",
|
|
(unsigned)s_last_page, s_last_desc[0],
|
|
s_last_desc[1], s_last_desc[2], s_last_desc[3]);
|
|
status(line);
|
|
for (i = 0; i < 32; i += 16) {
|
|
snprintf(line, sizeof(line),
|
|
" +%02X %02X %02X %02X %02X %02X %02X %02X %02X "
|
|
"%02X %02X %02X %02X %02X %02X %02X %02X", i,
|
|
s_last_dump[i+0], s_last_dump[i+1], s_last_dump[i+2],
|
|
s_last_dump[i+3], s_last_dump[i+4], s_last_dump[i+5],
|
|
s_last_dump[i+6], s_last_dump[i+7], s_last_dump[i+8],
|
|
s_last_dump[i+9], s_last_dump[i+10], s_last_dump[i+11],
|
|
s_last_dump[i+12], s_last_dump[i+13], s_last_dump[i+14],
|
|
s_last_dump[i+15]);
|
|
status(line);
|
|
}
|
|
}
|
|
if (bad > 200) {
|
|
if (status) status("too many bad descriptors - giving up");
|
|
return -2;
|
|
}
|
|
/* Resync to the gateware's own pointer and keep waiting. */
|
|
{
|
|
u8 wp[2];
|
|
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
|
|
if (wp[0] >= 1 && wp[0] <= 15)
|
|
bba_w8(REG_RRP_LO, wp[0]);
|
|
}
|
|
continue;
|
|
}
|
|
if (n < 0) {
|
|
/* Report the pointers, not just the code: -1 is an EXI access
|
|
* failure, -2 an RRP outside the ring, -3 a descriptor whose
|
|
* length is implausible. Which one it is, and what the
|
|
* pointers read, is the whole diagnosis. */
|
|
u8 rp[2] = {0xFF, 0xFF}, wp[2] = {0xFF, 0xFF};
|
|
bba_read(REG_RRP_LO, rp, 2);
|
|
bba_read(REG_RWP_LO, wp, 2);
|
|
if (status) {
|
|
snprintf(line, sizeof(line),
|
|
"ring error %d (RRP=%u RWP=%u) %s", n, rp[0], wp[0],
|
|
n == -1 ? "EXI access failed" :
|
|
n == -2 ? "RRP outside pages 1-15" :
|
|
"descriptor length implausible");
|
|
status(line);
|
|
snprintf(line, sizeof(line),
|
|
" page %u descriptor = %02X %02X %02X %02X",
|
|
(unsigned)s_last_page, s_last_desc[0],
|
|
s_last_desc[1], s_last_desc[2], s_last_desc[3]);
|
|
status(line);
|
|
}
|
|
return -2;
|
|
}
|
|
if (n == 0) {
|
|
/* 30 s with nothing at all, or 5 s mid-transfer, is a bail-out
|
|
* rather than a hang -- a wedged update must not require a
|
|
* console power cycle. */
|
|
u32 idle = (u32)ticks_to_millisecs(diff_ticks(t0, gettime()));
|
|
if (idle > (got ? 5000u : 30000u)) {
|
|
if (status) {
|
|
snprintf(line, sizeof(line),
|
|
"%s (%u seen, %u bad desc, %u lapped)",
|
|
got ? "timeout mid-transfer" : "timeout: no match",
|
|
(unsigned)seen, (unsigned)bad, (unsigned)lapped);
|
|
status(line);
|
|
}
|
|
return -3;
|
|
}
|
|
continue;
|
|
}
|
|
seen++;
|
|
if ((u32)n < NB_HDR) continue;
|
|
if (rd16(frame + 12) != NB_ETHERTYPE) {
|
|
/* Dump the first non-matching frame. If the sender is running
|
|
* and nothing matches, the bytes show whether the body is
|
|
* offset -- the ethertype would appear at 11 or 13 instead of
|
|
* 12. */
|
|
if (seen == 1 && status) {
|
|
snprintf(line, sizeof(line),
|
|
"frame 1 (%d B) etype=%04X at 12; "
|
|
"%02X %02X %02X %02X %02X %02X %02X %02X",
|
|
n, rd16(frame + 12), frame[0], frame[1], frame[2],
|
|
frame[3], frame[4], frame[5], frame[6], frame[7]);
|
|
status(line);
|
|
}
|
|
continue;
|
|
}
|
|
if (frame[14] != 'G' || frame[15] != 'C' ||
|
|
frame[16] != 'U' || frame[17] != 'P') continue;
|
|
|
|
if (!have_peer) { memcpy(s_peer_mac, frame + 6, 6); have_peer = 1; }
|
|
t0 = gettime();
|
|
|
|
if (frame[18] == NB_OP_DONE) {
|
|
/* DONE may carry the destination path as its payload. */
|
|
u16 plen = rd16(frame + 24);
|
|
if (plen && plen < sizeof(s_dest) && NB_HDR + plen <= (u32)n) {
|
|
memcpy(s_dest, frame + NB_HDR, plen);
|
|
s_dest[plen] = 0;
|
|
}
|
|
if (status) { snprintf(line, sizeof(line),
|
|
"received %u bytes in %u blocks",
|
|
(unsigned)got, (unsigned)nblocks);
|
|
status(line); }
|
|
return (int)got;
|
|
}
|
|
if (frame[18] != NB_OP_DATA) continue;
|
|
|
|
{
|
|
u16 seq = rd16(frame + 20);
|
|
u16 len = rd16(frame + 24);
|
|
nblocks = rd16(frame + 22);
|
|
if (len > NB_BLOCK_MAX || NB_HDR + len > (u32)n) continue;
|
|
|
|
if (seq == expect) {
|
|
if (got + len > maxlen) {
|
|
if (status) status("image larger than buffer");
|
|
return -4;
|
|
}
|
|
memcpy(out + got, frame + NB_HDR, len);
|
|
got += len;
|
|
expect++;
|
|
if ((expect & 0x1F) == 0 && status) {
|
|
snprintf(line, sizeof(line), "block %u/%u (%u KB)",
|
|
(unsigned)expect, (unsigned)nblocks,
|
|
(unsigned)(got >> 10));
|
|
status(line);
|
|
}
|
|
}
|
|
/* ACK even a duplicate: it means our previous ACK was lost, and
|
|
* re-ACKing is what lets the host move on. */
|
|
send_ack(seq);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ── DOL loader ──────────────────────────────────────────────────────────
|
|
* 7 text + 11 data sections, each with a file offset, load address and size,
|
|
* then BSS and the entry point. All big endian. */
|
|
typedef struct {
|
|
u32 text_off[7];
|
|
u32 data_off[11];
|
|
u32 text_addr[7];
|
|
u32 data_addr[11];
|
|
u32 text_len[7];
|
|
u32 data_len[11];
|
|
u32 bss_addr;
|
|
u32 bss_len;
|
|
u32 entry;
|
|
u32 pad[7];
|
|
} dolhdr_t;
|
|
|
|
/* Reserved region at the top of MEM1:
|
|
*
|
|
* [ staged image .............. ][ stub ][ descriptor ][ new stack ]
|
|
* ^ s_stage ^ s_stub
|
|
*
|
|
* Nothing here may overlap any DOL section, and netboot_launch() checks that
|
|
* before it jumps rather than trusting it. */
|
|
#define NB_STAGE_MAX (6u << 20) /* generous: Swiss is ~2-3 MB */
|
|
#define NB_TAIL (64u << 10) /* stub + descriptor + stack */
|
|
|
|
extern void nb_stub(void *desc);
|
|
extern void nb_stub_end(void);
|
|
|
|
static u8 *s_stage;
|
|
static u8 *s_tail;
|
|
|
|
void *netboot_reserve(u32 *out_max)
|
|
{
|
|
u32 hi = (u32)SYS_GetArena1Hi();
|
|
u32 base = (hi - (NB_STAGE_MAX + NB_TAIL)) & ~31u;
|
|
|
|
if (base <= (u32)SYS_GetArena1Lo()) return NULL;
|
|
s_stage = (u8 *)base;
|
|
s_tail = (u8 *)(base + NB_STAGE_MAX);
|
|
/* Tell libogc this is not its memory any more. */
|
|
SYS_SetArena1Hi(s_stage);
|
|
if (out_max) *out_max = NB_STAGE_MAX;
|
|
return s_stage;
|
|
}
|
|
|
|
static int overlaps(u32 a, u32 alen, u32 b, u32 blen)
|
|
{
|
|
if (!alen || !blen) return 0;
|
|
return a < b + blen && b < a + alen;
|
|
}
|
|
|
|
int netboot_launch(const void *dol)
|
|
{
|
|
const dolhdr_t *h = (const dolhdr_t *)dol;
|
|
const u8 *base = (const u8 *)dol;
|
|
u32 reserved = (u32)s_stage;
|
|
u32 reserved_len = NB_STAGE_MAX + NB_TAIL;
|
|
u32 stub_len = (u32)((const u8 *)nb_stub_end - (const u8 *)nb_stub);
|
|
u32 *desc;
|
|
void (*stub)(void *);
|
|
int i, nsec = 0;
|
|
|
|
if (!s_stage) return -1;
|
|
|
|
/* Refuse a mis-parsed or truncated header rather than jumping into it:
|
|
* a bad jump hangs the console hard enough to need a power cycle. */
|
|
if ((h->entry >> 24) != 0x80 || h->text_len[0] == 0 ||
|
|
(h->text_addr[0] >> 24) != 0x80)
|
|
return -2;
|
|
|
|
/* Build the copy table in the reserved tail, where the image cannot
|
|
* reach it. While doing so, verify no section lands on the reserved
|
|
* region -- that would clobber the staged image, the stub we are about
|
|
* to run from, or this very table. */
|
|
desc = (u32 *)(s_tail + 0x1000);
|
|
for (i = 0; i < 7; i++) {
|
|
if (!h->text_len[i]) continue;
|
|
if (overlaps(h->text_addr[i], h->text_len[i], reserved, reserved_len))
|
|
return -3;
|
|
desc[5 + nsec * 3 + 0] = (u32)(base + h->text_off[i]);
|
|
desc[5 + nsec * 3 + 1] = h->text_addr[i];
|
|
desc[5 + nsec * 3 + 2] = h->text_len[i];
|
|
nsec++;
|
|
}
|
|
for (i = 0; i < 11; i++) {
|
|
if (!h->data_len[i]) continue;
|
|
if (overlaps(h->data_addr[i], h->data_len[i], reserved, reserved_len))
|
|
return -3;
|
|
desc[5 + nsec * 3 + 0] = (u32)(base + h->data_off[i]);
|
|
desc[5 + nsec * 3 + 1] = h->data_addr[i];
|
|
desc[5 + nsec * 3 + 2] = h->data_len[i];
|
|
nsec++;
|
|
}
|
|
if (h->bss_len &&
|
|
overlaps(h->bss_addr, h->bss_len, reserved, reserved_len))
|
|
return -3;
|
|
|
|
desc[0] = h->entry;
|
|
/* Hand the new image a stack inside the reserved tail, 64-byte aligned
|
|
* and clear of the descriptor. */
|
|
desc[1] = ((reserved + reserved_len) - 64) & ~63u;
|
|
desc[2] = (u32)nsec;
|
|
desc[3] = h->bss_addr;
|
|
desc[4] = h->bss_len;
|
|
|
|
/* Relocate the copier out of harm's way and make it executable there. */
|
|
stub = (void (*)(void *))s_tail;
|
|
memcpy(s_tail, (const void *)nb_stub, stub_len);
|
|
DCFlushRange(s_tail, NB_TAIL);
|
|
ICInvalidateRange(s_tail, NB_TAIL);
|
|
|
|
/* No interrupts from here on: we are about to overwrite the memory this
|
|
* program is running from, and the scheduler must not preempt us into
|
|
* code that no longer exists. */
|
|
IRQ_Disable();
|
|
stub(desc);
|
|
return 0; /* not reached */
|
|
}
|
|
|
|
/* ── SD card: save the new build, then boot a known-good image ───────────
|
|
* The update deliberately does NOT launch what it just received. It writes
|
|
* the image to the card and then boots /boot.dol (Swiss), so a broken test
|
|
* build is recoverable: Swiss comes up regardless and can load the previous
|
|
* one. The only image we ever jump into is one that was already known good. */
|
|
|
|
static int s_sd_mounted;
|
|
|
|
static int sd_mount(void (*status)(const char *msg))
|
|
{
|
|
/* Try every interface a GameCube might have. Auto-detecting beats
|
|
* asking: SD Gecko can be in either memory-card slot, and SD2SP2 is on
|
|
* its own EXI channel. */
|
|
static const struct { const char *name; const DISC_INTERFACE *io; } ifs[] = {
|
|
{ "SD2SP2", &__io_gcsd2 },
|
|
{ "SD Gecko slot B", &__io_gcsdb },
|
|
{ "SD Gecko slot A", &__io_gcsda },
|
|
};
|
|
unsigned i;
|
|
char line[64];
|
|
|
|
if (s_sd_mounted) return 0;
|
|
for (i = 0; i < sizeof(ifs) / sizeof(ifs[0]); i++) {
|
|
if (fatMountSimple("sd", ifs[i].io)) {
|
|
if (status) {
|
|
snprintf(line, sizeof(line), "SD mounted (%s)", ifs[i].name);
|
|
status(line);
|
|
}
|
|
s_sd_mounted = 1;
|
|
return 0;
|
|
}
|
|
}
|
|
if (status) status("no SD card found (SD2SP2 or SD Gecko)");
|
|
return -1;
|
|
}
|
|
|
|
/* Build "<base>.tmp" and "<base>.prev.dol" from "<base>.dol".
|
|
*
|
|
* The temp file deliberately does NOT end in .dol: Swiss lists .dol files, and
|
|
* a half-written image that Swiss offers to boot is exactly what we are trying
|
|
* to prevent. The backup DOES end in .dol, because booting it from Swiss is
|
|
* the whole point of keeping it. */
|
|
static void derive_names(const char *path, char *tmp, size_t ntmp,
|
|
char *prev, size_t nprev)
|
|
{
|
|
char base[80];
|
|
size_t n = strlen(path);
|
|
|
|
if (n >= sizeof(base)) n = sizeof(base) - 1;
|
|
memcpy(base, path, n);
|
|
base[n] = 0;
|
|
if (n > 4 && strcasecmp(base + n - 4, ".dol") == 0)
|
|
base[n - 4] = 0;
|
|
|
|
snprintf(tmp, ntmp, "sd:%s.tmp", base);
|
|
snprintf(prev, nprev, "sd:%s.prev.dol", base);
|
|
}
|
|
|
|
int netboot_sd_write(const char *path, const void *buf, u32 len,
|
|
void (*status)(const char *msg))
|
|
{
|
|
char final[96], tmp[112], prev[112], line[160];
|
|
FILE *f;
|
|
u32 done;
|
|
int rc = 0;
|
|
|
|
if (sd_mount(status) < 0) return -1;
|
|
snprintf(final, sizeof(final), "sd:%s", path);
|
|
derive_names(path, tmp, sizeof(tmp), prev, sizeof(prev));
|
|
|
|
/* Write to a TEMP file and verify it BEFORE touching the existing
|
|
* build. Ordering matters: if the transfer, the write or the verify
|
|
* fails, the copy you are running from is still on the card untouched,
|
|
* so you can always try the update again. Only once the new image is
|
|
* known good do we rotate. */
|
|
if (status) { snprintf(line, sizeof(line), "writing %s", tmp); status(line); }
|
|
f = fopen(tmp, "wb");
|
|
if (!f) { if (status) status("open for write failed"); return -2; }
|
|
done = (u32)fwrite(buf, 1, len, f);
|
|
fclose(f);
|
|
if (done != len) {
|
|
remove(tmp);
|
|
if (status) status("short write - existing build left alone");
|
|
return -3;
|
|
}
|
|
|
|
/* Read it back. A half-written .dol that still parses is the failure
|
|
* this design exists to avoid, and the card is the one component here
|
|
* with no other integrity check. */
|
|
if (status) status("verifying...");
|
|
f = fopen(tmp, "rb");
|
|
if (!f) { remove(tmp); if (status) status("reopen for verify failed"); return -4; }
|
|
{
|
|
static u8 chunk[4096];
|
|
const u8 *src = (const u8 *)buf;
|
|
u32 off = 0;
|
|
while (off < len) {
|
|
u32 want = len - off;
|
|
if (want > sizeof(chunk)) want = sizeof(chunk);
|
|
if (fread(chunk, 1, want, f) != want) { rc = -5; break; }
|
|
if (memcmp(chunk, src + off, want) != 0) { rc = -6; break; }
|
|
off += want;
|
|
}
|
|
}
|
|
fclose(f);
|
|
if (rc < 0) {
|
|
remove(tmp);
|
|
if (status) status("VERIFY FAILED - existing build left alone");
|
|
return rc;
|
|
}
|
|
|
|
/* Rotate: keep exactly one generation back. remove()/rename() of a
|
|
* file that is not there is fine and ignored -- that is just the first
|
|
* update on a fresh card. */
|
|
remove(prev);
|
|
rename(final, prev);
|
|
if (rename(tmp, final) != 0) {
|
|
/* Should not happen, but if it does the old build is sitting in
|
|
* .prev.dol and is still bootable from Swiss -- say so rather
|
|
* than leaving the user to guess. */
|
|
if (status) {
|
|
snprintf(line, sizeof(line),
|
|
"rename failed; previous build is at %s", prev);
|
|
status(line);
|
|
}
|
|
return -7;
|
|
}
|
|
|
|
if (status) {
|
|
snprintf(line, sizeof(line), "wrote %u bytes; previous build kept as %s",
|
|
(unsigned)len, prev);
|
|
status(line);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int netboot_boot_file(const char *path, void *dst, u32 maxlen,
|
|
void (*status)(const char *msg))
|
|
{
|
|
char full[96], line[128];
|
|
FILE *f;
|
|
long sz;
|
|
u32 got;
|
|
|
|
if (sd_mount(status) < 0) return -1;
|
|
snprintf(full, sizeof(full), "sd:%s", path);
|
|
|
|
f = fopen(full, "rb");
|
|
if (!f) {
|
|
if (status) { snprintf(line, sizeof(line), "%s not found", full);
|
|
status(line); }
|
|
return -2;
|
|
}
|
|
fseek(f, 0, SEEK_END);
|
|
sz = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
if (sz <= 0 || (u32)sz > maxlen) {
|
|
fclose(f);
|
|
if (status) status("boot image size implausible");
|
|
return -3;
|
|
}
|
|
got = (u32)fread(dst, 1, (size_t)sz, f);
|
|
fclose(f);
|
|
if (got != (u32)sz) {
|
|
if (status) status("short read of boot image");
|
|
return -4;
|
|
}
|
|
|
|
if (status) { snprintf(line, sizeof(line), "booting %s (%u bytes)",
|
|
full, (unsigned)got); status(line); }
|
|
|
|
/* Unmount before we disable interrupts and jump: leaving the card
|
|
* mounted hands the next image a filesystem that libfat still thinks it
|
|
* owns, on an EXI channel it is about to reinitialise. */
|
|
fatUnmount("sd");
|
|
s_sd_mounted = 0;
|
|
|
|
return netboot_launch(dst);
|
|
}
|