From e63f86a603ebae0e7c66f94bfdea69905f36f80c Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 4 Sep 2026 19:04:41 -0700 Subject: pos: refuse to flash anything that is not a valid LittleFS image The flasher hex-encoded whatever the /flash endpoint returned, so a server error body was written to devices as the config partition. Check the response status, require a 128KB image with the LittleFS magic, and surface the error above the form instead. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014gVYGi29FjTeLdNTiutkZx --- src/routes/(app)/pos/+page.server.ts | 28 ++++++++++++++++++++++++++++ src/routes/(app)/pos/+page.svelte | 13 ++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/routes/(app)/pos/+page.server.ts b/src/routes/(app)/pos/+page.server.ts index 1df1bb33..1e3e1e99 100644 --- a/src/routes/(app)/pos/+page.server.ts +++ b/src/routes/(app)/pos/+page.server.ts @@ -1,6 +1,18 @@ import { PUBLIC_COINOS_URL } from "$env/static/public"; import { auth, fd, get } from "$lib/utils"; import { hex } from "@scure/base"; +import { fail } from "@sveltejs/kit"; + +const LITTLEFS_SIZE = 0x20000; +const LITTLEFS_MAGIC = "littlefs"; + +// A valid image is exactly one partition long and carries the LittleFS +// superblock magic at byte 8. Anything else (e.g. a JSON error body from +// the server) must never reach the flasher. +const isLittleFS = (buf: Uint8Array) => + buf.length === LITTLEFS_SIZE && + new TextDecoder().decode(buf.subarray(8, 8 + LITTLEFS_MAGIC.length)) === + LITTLEFS_MAGIC; export async function load({ cookies }) { let token; @@ -25,6 +37,22 @@ export const actions = { }); const buf = new Uint8Array(await res.arrayBuffer()); + + if (!res.ok) { + let message = `Config server returned ${res.status}`; + try { + message = JSON.parse(new TextDecoder().decode(buf)).message || message; + } catch (e) {} + return fail(res.status >= 400 && res.status < 600 ? res.status : 500, { + error: message, + }); + } + + if (!isLittleFS(buf)) + return fail(500, { + error: `Config server returned an invalid image (${buf.length} bytes)`, + }); + return { bytes: hex.encode(buf) }; }, }; diff --git a/src/routes/(app)/pos/+page.svelte b/src/routes/(app)/pos/+page.svelte index 871c355d..36869fba 100644 --- a/src/routes/(app)/pos/+page.svelte +++ b/src/routes/(app)/pos/+page.svelte @@ -30,7 +30,11 @@ let portInfo = $state(""); // --- Existing config path (generated server-side to hex in form.bytes) --- - let bytes = $derived(form ? hex.decode(form.bytes) : undefined); + let bytes = $derived(form?.bytes ? hex.decode(form.bytes) : undefined); + let configError = $state(""); + $effect(() => { + configError = form?.error || ""; + }); let littlefsAddress = $state(LITTLEFS_ADDRESS_DEFAULT); let configProgress = $state(0); let configDone = $state(false); @@ -68,6 +72,10 @@ let flashConfig = async () => { if (!esploader || !bytes) return; + if (bytes.length !== 0x20000) { + configError = `Refusing to flash: image is ${bytes.length} bytes, expected ${0x20000}`; + return; + } configDone = false; configProgress = 0; @@ -175,6 +183,9 @@ {#if configProgress > 0}
Progress: {configProgress}%
{/if} {/if} {:else} + {#if configError} +
{configError}
+ {/if}
-- cgit v1.2.3