Skip to content

Commit 12f906e

Browse files
committed
fix(usb-installer): harden live disk safety
1 parent 94d7a2c commit 12f906e

9 files changed

Lines changed: 269 additions & 25 deletions

File tree

packages/os/usb-installer/HANDOFF.md

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ Last updated: 2026-05-20
99
- Branch: `nubs/messylinux-cloud-e2e-hardening`
1010
- Previous PR #7803: https://github.com/elizaOS/eliza/pull/7803 (merged)
1111
- Follow-up PR #7825: https://github.com/elizaOS/eliza/pull/7825
12-
- Verified local base: `origin/develop@f6f16699fc66650318a66ba9278504e166ffd0eb`
13-
- Latest locally validated code head: `e9adb7fd02de92a0da70fcdb1164d64395cefb5d`
14-
- Latest local CI-fix validation: 2026-05-20 05:24 UTC
12+
- Latest rebased base for PR #7825: `origin/develop@51196656219dce9e8e6a13216c7c0e994bd40651`
13+
- Latest fetched `origin/develop`: `51196656219dce9e8e6a13216c7c0e994bd40651`
14+
- Latest locally validated code head: `6ab4cf964ba2d3b24addc2e21e6c10938ab467ab`
15+
(final handoff-only amend may change the commit hash without changing code)
16+
- Latest local USB/cloud validation: 2026-05-20 05:42 UTC
1517

1618
## What This Package Is
1719

@@ -27,7 +29,7 @@ behind the backend contract and future signed/elevated helpers.
2729
- CI has wiring for lint/typecheck/test/build/package in:
2830
- `.github/workflows/elizaos-os-release.yml`
2931
- `.github/workflows/release-usb-installer.yml`
30-
- PR #7803 is pushed and current with `origin/develop` as of the verified head.
32+
- PR #7825 is prepared on the latest fetched `origin/develop` listed above.
3133
- GitHub checks must still be treated as source of truth for mergeability; the
3234
last local pass below validates the USB installer and the root build path that
3335
previously failed in CI.
@@ -106,6 +108,17 @@ behind the backend contract and future signed/elevated helpers.
106108
- OS release CI and the Linux release-packaging path now run Playwright E2E
107109
and run the opt-in `scsi_debug` virtual block-device proof when the runner
108110
kernel provides that module.
111+
- Follow-up USB hardening added on 2026-05-20 after the read-only audit:
112+
- Linux drive enumeration now also reads `/proc/self/mountinfo` and resolves
113+
`/dev/*` mount sources through sysfs block-device ancestry, so a current
114+
root/live USB disk is blocked even when `lsblk` does not attach the system
115+
mountpoint to the candidate disk tree;
116+
- live-write plan expiry now has a deterministic clock hook for tests and
117+
expires at the TTL boundary instead of only after it;
118+
- backend step labels use `Finalize media` instead of overclaiming readback
119+
verification on platforms that currently flush/eject/finalize only;
120+
- completion copy is platform-specific, distinguishing macOS eject, Linux
121+
flushed writes, and Windows finalized disk state.
109122
- Additional cloud mock-stack E2E hardening added on 2026-05-20:
110123
- fixed the cloud E2E repo-root resolution so the PGlite TCP bridge script
111124
resolves from the repository root, not `packages/`;
@@ -181,6 +194,28 @@ behind the backend contract and future signed/elevated helpers.
181194
- `bun run --cwd packages/os/usb-installer test:linux-virtual-usb` passed
182195
against `scsi_debug`;
183196
- `git diff --check` passed.
197+
- Follow-up local USB validation on 2026-05-20 after mountinfo/sysfs root-disk
198+
hardening and honest finalize/eject copy:
199+
- `bun run --cwd packages/os/usb-installer typecheck` passed;
200+
- `bun run --cwd packages/os/usb-installer test` passed: 9 files passed, 1
201+
skipped, 81 tests passed, 1 skipped;
202+
- `bun run --cwd packages/os/usb-installer lint` passed;
203+
- `bun run --cwd packages/os/usb-installer build` passed;
204+
- `bun run --cwd packages/os/usb-installer test:e2e` passed: 6 Playwright
205+
tests;
206+
- `bun run --cwd packages/os/usb-installer test:linux-virtual-usb` passed
207+
against `scsi_debug`;
208+
- `git diff --check` passed.
209+
- Follow-up local cloud validation on 2026-05-20 after rebasing onto
210+
`origin/develop@5119665621`:
211+
- `bun install --frozen-lockfile` passed;
212+
- `bun run --cwd packages/cloud-shared typecheck` passed;
213+
- `bun run --cwd packages/cloud-shared lint` passed;
214+
- `bun run --cwd packages/cloud-api typecheck` passed;
215+
- `bun run --cwd packages/test/cloud-e2e typecheck` passed;
216+
- `bun run test:cloud` passed: 279 tests across 30 files;
217+
- `bun run cloud:e2e` passed: 4 Playwright tests covering onboarding,
218+
provision, deprovision, and stuck cleanup.
184219
- Disk cleanup on 2026-05-19:
185220
- removed ignored/generated stale ISO artifacts and root `dist/`;
186221
- removed inactive `/tmp/eliza-pr7803` temp checkout after confirming no
@@ -234,8 +269,8 @@ so the package needs hardening before we call it production-ready.
234269
now reject those placeholders; production needs an official signed manifest.
235270
- Tests still need broader UI component coverage and platform write-sequence
236271
coverage for macOS/Windows mocked subprocesses.
237-
- UI copy is still too macOS-specific in places and should adapt to the
238-
selected drive platform.
272+
- macOS and Windows need broader mocked subprocess write-sequence tests before
273+
being called production-proven on those platforms.
239274
- Keep visual branding white/blue and use official shared elizaOS logo assets.
240275
Avoid orange/black-heavy shell styling.
241276

packages/os/usb-installer/server.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const DEFAULT_ALLOWED_ORIGINS = [
2626
interface UsbInstallerHandlerOptions {
2727
allowedOrigins?: readonly string[];
2828
planTtlMs?: number;
29+
now?: () => number;
2930
}
3031

3132
interface StoredWritePlan {
@@ -142,11 +143,12 @@ export function createUsbInstallerHandler(
142143
const plans = new Map<string, StoredWritePlan>();
143144
const allowedOrigins = configuredAllowedOrigins(options);
144145
const planTtlMs = configuredPlanTtlMs(options);
146+
const now = options.now ?? Date.now;
145147

146148
function deleteExpiredPlans(): void {
147-
const now = Date.now();
149+
const timestamp = now();
148150
for (const [planId, stored] of plans) {
149-
if (now - stored.createdAt > planTtlMs) {
151+
if (timestamp - stored.createdAt >= planTtlMs) {
150152
plans.delete(planId);
151153
}
152154
}
@@ -166,7 +168,7 @@ export function createUsbInstallerHandler(
166168
const planId = randomUUID();
167169
plans.set(planId, {
168170
plan,
169-
createdAt: Date.now(),
171+
createdAt: now(),
170172
});
171173
return { ...plan, planId };
172174
}

packages/os/usb-installer/src/__tests__/server.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,11 @@ describe("USB installer server", () => {
203203
it("expires stored write plans before execution", async () => {
204204
process.env.ELIZAOS_USB_ENABLE_RAW_WRITE = "1";
205205
const backend = new FakeBackend();
206-
const handler = createUsbInstallerHandler(backend, { planTtlMs: -1 });
206+
let now = 1_000;
207+
const handler = createUsbInstallerHandler(backend, {
208+
now: () => now,
209+
planTtlMs: 100,
210+
});
207211

208212
const planRes = await handler(
209213
request("/plan", {
@@ -220,6 +224,7 @@ describe("USB installer server", () => {
220224
const plan = (await planRes.json()) as WritePlan;
221225
expect(plan.planId).toEqual(expect.any(String));
222226

227+
now = 1_101;
223228
const executeRes = await handler(
224229
request("/execute", {
225230
method: "POST",

packages/os/usb-installer/src/backend/__tests__/linux-backend.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,25 @@ describe("LinuxUsbInstallerBackend.listRemovableDrives", () => {
9898
});
9999
}
100100

101+
function makeBackendWithSystemDiskNames(
102+
stdout: string,
103+
currentSystemDiskNames: Set<string>,
104+
) {
105+
return new LinuxUsbInstallerBackend({
106+
execFile: async (
107+
command: string,
108+
args: readonly string[],
109+
): Promise<ExecFileResult> => {
110+
expect(command).toBe("lsblk");
111+
expect(args).toContain(
112+
"NAME,SIZE,TYPE,RM,MODEL,TRAN,HOTPLUG,MOUNTPOINTS",
113+
);
114+
return { stdout, stderr: "" };
115+
},
116+
currentSystemDiskNames: async () => currentSystemDiskNames,
117+
});
118+
}
119+
101120
it("blocks a removable disk when it is the current live/root disk", async () => {
102121
const backend = makeBackend(
103122
JSON.stringify({
@@ -133,6 +152,40 @@ describe("LinuxUsbInstallerBackend.listRemovableDrives", () => {
133152
});
134153
});
135154

155+
it("blocks a removable disk when mountinfo identifies it as the current system disk", async () => {
156+
const backend = makeBackendWithSystemDiskNames(
157+
JSON.stringify({
158+
blockdevices: [
159+
{
160+
name: "sdb",
161+
size: String(16 * 1024 ** 3),
162+
type: "disk",
163+
rm: true,
164+
model: "Live USB",
165+
tran: "usb",
166+
hotplug: true,
167+
children: [
168+
{
169+
name: "sdb1",
170+
type: "part",
171+
mountpoints: ["/media/amnesia/ELIZAOS"],
172+
},
173+
],
174+
},
175+
],
176+
}),
177+
new Set(["sdb"]),
178+
);
179+
180+
const [drive] = await backend.listRemovableDrives();
181+
182+
expect(drive).toMatchObject({
183+
id: "sdb",
184+
safety: "blocked-system",
185+
description: expect.stringContaining("current system device"),
186+
});
187+
});
188+
136189
it("allows a normal removable disk mounted under the user media path", async () => {
137190
const backend = makeBackend(
138191
JSON.stringify({

packages/os/usb-installer/src/backend/dry-run-backend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ const STEP_LABELS: Record<InstallerStep["id"], string> = {
109109
"resolve-image": "Resolve image",
110110
checksum: "Validate checksum",
111111
write: "Write image",
112-
verify: "Verify media",
112+
verify: "Finalize media",
113113
complete: "Complete",
114114
};
115115

0 commit comments

Comments
 (0)