#include "netboot.h" #include "bba.h" #include #include #include #include #include #include #include #include /* ── 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]; /* How often the descriptor was found at each page offset. The assembler is * supposed to put it at +0 always; hardware shows it sometimes landing at +1, * varying frame to frame, so count both rather than assume either. */ static u32 s_desc_ok; /* ACKs where TI never set -- the frame did not reach the W5100. */ static u32 s_tx_timeouts; 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); } /* Our own view of RRP. Re-reading it from the device every poll cost an EXI * transaction for a value we had just written ourselves -- pure overhead in * the one loop whose speed decides whether the writer laps us. */ static u32 s_rrp; static int ring_next(u8 *buf, u32 cap) { u8 wp[2], hdr[8]; u32 rpage, wpage, dlen, flen, addr; u32 desc, nextp; if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1; rpage = s_rrp; 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, 8) < 0) return -1; memcpy(s_last_desc, hdr, 4); s_last_page = rpage; /* The descriptor is a PACKED 32-BIT LITTLE-ENDIAN WORD -- the format * libogc's own BBA driver reads, so this is what GC software expects: * * bits 0-11 next_packet_ptr : the PAGE the NEXT frame starts at * bits 12-23 packet_len : total bytes, INCLUDING these 4 * bits 24-31 status : 0x80 and 0x04 are errors * * See the note in exi_bba/rx_frame_assembler.py for the disassembly this * was taken from. The old reader assumed [00 00 len_hi len_lo] and * probed offsets +0 and +1 to work around the tx-FIFO-underrun bug that * shifted descriptors; that bug is fixed (confirmed on console, +1=0) and * the probe is gone -- under the packed format a +1 read is not a shifted * descriptor, just garbage. */ desc = (u32)hdr[0] | ((u32)hdr[1] << 8) | ((u32)hdr[2] << 16) | ((u32)hdr[3] << 24); dlen = (desc >> 12) & 0xFFF; nextp = desc & 0xFFF; if (((desc >> 24) & 0x84) || dlen <= 4 || dlen > 1518 + 4) { /* Only now is a hex dump worth an EXI transaction. Doing it for * EVERY frame (as an earlier version did) added a round trip to the * poll loop that is already losing the race against the writer -- * i.e. the diagnostic made the fault it was diagnosing worse. */ bba_stream((u16)addr, s_last_dump, sizeof(s_last_dump)); return -3; } s_desc_ok++; 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 to the descriptor's OWN next_packet_ptr, which is what * libogc does, rather than recomputing the page span here. The writer * put that value in the descriptor, so the two cannot disagree. */ { u32 next = nextp; if (next < 1 || next > 15) next = 1; if (bba_w8(REG_RRP_LO, (u8)next) < 0) return -1; s_rrp = next; } return (int)flen; } static int send_ack(u16 seq) { u8 f[NB_MIN]; int i, ir = 0; 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); /* THE FULL SEQUENCE, matching t_tx() which is proven on hardware. * bba_tx_bytes() only streams bytes into the TXDATA FIFO -- it does NOT * set the length or fire the transmit. An earlier version of this * function called it alone, so every ACK was written into the FIFO and * never put on the wire: the host retried block 0 twenty times, saw no * ACK and gave up, while the GC reported frames arriving normally * ("22 seen, 0 bad" against the sender's 20 retries). * * TWD must be written BEFORE streaming: that is what queues the length * so TXFrameDrain can consume concurrently with the EXI writes. Without * it the drain waits for a length that only arrives at the ST trigger and * the frame has to fit the 16-deep FIFO whole -- the truncate-at-16-bytes * bug. The NCRA ST write afterwards is the fallback trigger. */ bba_w8(REG_IR, 0xFF); /* clear TI/RI */ bba_w8(REG_TWD_LO, (u8)(sizeof(f) & 0xFF)); bba_w8(REG_TWD_HI, (u8)((sizeof(f) >> 8) & 0xFF)); if (bba_tx_bytes(f, sizeof(f)) < 0) return -1; bba_w8(REG_NCRA, NCRA_SR | NCRA_ST0); /* Poll for TI WITHOUT sleeping. usleep(500) x 20 meant every ACK cost * up to 10 ms even when the transmit succeeded immediately, and the * transfer is round-trip bound -- that is 10 ms on every one of ~292 * blocks. A bba_r8 is itself tens of microseconds, so a bare loop polls * often enough and finishes as soon as TI sets. * * Absence of TI is NOT treated as fatal: the frame may still have gone * out (TI only means TXFrameDrain handed it to the W5100), and if it did * not, the host's retransmit covers it. Blocking longer here would only * slow the common case to protect against the rare one. */ /* Poll a FEW times, not 200. Each bba_r8 is a whole EXI transaction * (lock, select, header, gap, read, deselect, unlock) at tens of * microseconds, so 200 of them is ~12 ms burned on the ONE path the host * is waiting on -- and it only burns them when TI is slow, which is * exactly when the host is closest to timing out. That turns a slow * transmit into a missed deadline instead of just a slow one. * * TI is not needed for correctness here: the host retransmits anything * it does not get an ACK for, and the GC re-ACKs duplicates. A handful * of polls is enough to count genuine failures in txto. */ for (i = 0; i < 8; i++) { ir = bba_r8(REG_IR); if (ir >= 0 && (ir & IR_TI)) return 0; } s_tx_timeouts++; return -1; } /* 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; /* Clear any stale interrupt flags, RBFI (bit 7) included: the gateware * now reports a ring-full drop there, and we would rather see a fresh * one than inherit a set bit from an earlier run. */ bba_w8(REG_IR, 0xFF); /* 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; s_rrp = wp[0]; 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; /* Keep this to ONE line. The console holds about 25 lines and the * failure diagnostics are what matter, so guidance text here just * pushes them off the top of the screen. */ if (status) status("waiting for host (send-dol.py)..."); 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]); s_rrp = 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]); s_rrp = 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 ok=%u, txto=%u)", got ? "timeout mid-transfer" : "timeout: no match", (unsigned)seen, (unsigned)bad, (unsigned)s_desc_ok, (unsigned)s_tx_timeouts); 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); /* txto distinguishes the two reasons an ACK can go * missing: TI never set (the frame did not reach the * W5100) versus sent-but-lost. The host reports the * retries; only the GC knows which kind they were. */ snprintf(line, sizeof(line), "desc ok=%u bad=%u lapped=%u txto=%u", (unsigned)s_desc_ok, (unsigned)bad, (unsigned)lapped, (unsigned)s_tx_timeouts); 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 ".tmp" and ".prev.dol" from ".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); }