Skip to content

Commit baed359

Browse files
committed
Address PR review comments: security, validation, and code quality fixes
Major fixes: - Links.tsx: Add rel="noopener noreferrer" to external links for security - host-call-trace.ts: Add Zod validation for trace structure - debuggerSlice.ts: Add input validation in setHostCallsTrace reducer - workersSlice.ts: Fix auto-continue to advance host-call index and await resume - useDebuggerActions.ts: Add guard for undefined memoryRanges - findHostCallEntry: Prefer exact host call index matches with early return Minor fixes: - trace-loading.spec.ts: Use shared Playwright fixtures - Create tests/utils/fixtures.ts for shared test utilities
1 parent 7d13cfa commit baed359

File tree

7 files changed

+285
-26
lines changed

7 files changed

+285
-26
lines changed

src/components/ProgramLoader/Links.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ export const Links = () => {
55
<ul className="list-none sm:text-sm">
66
<li>
77
<div className="flex gap-2 text-[11px]">
8-
<a className="flex" href="https://github.com/w3f/jamtestvectors/pull/3/files" target="_blank">
8+
<a
9+
className="flex"
10+
href="https://github.com/w3f/jamtestvectors/pull/3/files"
11+
target="_blank"
12+
rel="noopener noreferrer"
13+
>
914
<ExternalLink className="inline w-4 mb-1 mr-2 text-xs text-brand-dark dark:text-brand" />
1015
</a>
1116
<div>
@@ -25,7 +30,7 @@ export const Links = () => {
2530

2631
<li>
2732
<div className="flex gap-2 text-[11px] mt-3">
28-
<a href="https://graypaper.fluffylabs.dev/#/5b732de/2a7e022a7e02" target="_blank">
33+
<a href="https://graypaper.fluffylabs.dev/#/5b732de/2a7e022a7e02" target="_blank" rel="noopener noreferrer">
2934
<ExternalLink className="inline w-4 mb-1 mr-2 text-brand-dark dark:text-brand" />
3035
</a>
3136
<div>
@@ -46,7 +51,7 @@ export const Links = () => {
4651
</li>
4752
<li>
4853
<div className="flex gap-2 text-[11px] mt-3">
49-
<a href="https://graypaper.fluffylabs.dev/#/5b732de/23c60023c600" target="_blank">
54+
<a href="https://graypaper.fluffylabs.dev/#/5b732de/23c60023c600" target="_blank" rel="noopener noreferrer">
5055
<ExternalLink className="inline w-4 mb-1 mr-2 text-brand-dark dark:text-brand" />
5156
</a>
5257
<div>

src/hooks/useDebuggerActions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ export const useDebuggerActions = () => {
7979
dispatch(resetHostCallIndex());
8080

8181
// Save memory ranges before destroying workers
82-
const memoryRanges = workers[0]?.memoryRanges ?? [];
82+
// Guard: ensure memoryRanges is always an array
83+
const memoryRanges = Array.isArray(workers[0]?.memoryRanges) ? workers[0].memoryRanges : [];
8384

8485
// Destroy all existing workers to clear message listeners
8586
await Promise.all(workers.map((worker: WorkerState) => dispatch(destroyWorker(worker.id)).unwrap()));
@@ -176,7 +177,8 @@ export const useDebuggerActions = () => {
176177
async (selectedPvms: SelectedPvmWithPayload[]) => {
177178
logger.debug("selectedPvms vs workers ", selectedPvms, workers);
178179

179-
const memoryRanges = workers[0]?.memoryRanges ?? [];
180+
// Guard: ensure memoryRanges is always an array
181+
const memoryRanges = Array.isArray(workers[0]?.memoryRanges) ? workers[0].memoryRanges : [];
180182

181183
// Destroy all existing workers
182184
await Promise.all(workers.map((worker: WorkerState) => dispatch(destroyWorker(worker.id)).unwrap()));

src/lib/host-call-trace.ts

Lines changed: 187 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* @see https://github.com/tomusdrw/JIPs/blob/td-jip6-ecalliloggin/JIP-6.md
99
*/
1010

11+
import { z } from "zod";
12+
1113
/** Register dump: mapping from register index to value */
1214
export type RegisterDump = Map<number, bigint>;
1315

@@ -121,6 +123,128 @@ export interface StateMismatch {
121123
details?: string;
122124
}
123125

126+
// ============================================================================
127+
// Zod Validation Schemas
128+
// ============================================================================
129+
130+
/** Zod schema for MemoryRead */
131+
const MemoryReadSchema = z.object({
132+
address: z.number(),
133+
length: z.number(),
134+
data: z.instanceof(Uint8Array),
135+
});
136+
137+
/** Zod schema for MemoryWrite */
138+
const MemoryWriteSchema = z.object({
139+
address: z.number(),
140+
length: z.number(),
141+
data: z.instanceof(Uint8Array),
142+
});
143+
144+
/** Zod schema for RegisterWrite */
145+
const RegisterWriteSchema = z.object({
146+
index: z.number(),
147+
value: z.bigint(),
148+
});
149+
150+
/** Zod schema for HostCallEntry */
151+
const HostCallEntrySchema = z.object({
152+
index: z.number(),
153+
pc: z.number(),
154+
gas: z.bigint(),
155+
registers: z.instanceof(Map),
156+
memoryReads: z.array(MemoryReadSchema),
157+
memoryWrites: z.array(MemoryWriteSchema),
158+
registerWrites: z.array(RegisterWriteSchema),
159+
gasAfter: z.bigint().nullable(),
160+
lineNumber: z.number(),
161+
});
162+
163+
/** Zod schema for StartEntry */
164+
const StartEntrySchema = z.object({
165+
pc: z.number(),
166+
gas: z.bigint(),
167+
registers: z.instanceof(Map),
168+
});
169+
170+
/** Zod schema for TracePrelude */
171+
const TracePreludeSchema = z.object({
172+
program: z.string().nullable(),
173+
start: StartEntrySchema.nullable(),
174+
initialMemoryWrites: z.array(MemoryWriteSchema),
175+
});
176+
177+
/** Zod schema for TerminationReason */
178+
const TerminationReasonSchema = z.union([
179+
z.object({ type: z.literal("HALT") }),
180+
z.object({ type: z.literal("PANIC"), argument: z.number() }),
181+
z.object({ type: z.literal("OOG") }),
182+
]);
183+
184+
/** Zod schema for TerminationEntry */
185+
const TerminationEntrySchema = z.object({
186+
reason: TerminationReasonSchema,
187+
pc: z.number(),
188+
gas: z.bigint(),
189+
registers: z.instanceof(Map),
190+
lineNumber: z.number(),
191+
});
192+
193+
/** Zod schema for TraceParseError */
194+
const TraceParseErrorSchema = z.object({
195+
lineNumber: z.number(),
196+
line: z.string(),
197+
message: z.string(),
198+
});
199+
200+
/** Zod schema for ParsedTrace */
201+
const ParsedTraceSchema = z.object({
202+
contextLines: z.array(z.string()),
203+
prelude: TracePreludeSchema,
204+
hostCalls: z.array(HostCallEntrySchema),
205+
termination: TerminationEntrySchema.nullable(),
206+
errors: z.array(TraceParseErrorSchema),
207+
});
208+
209+
/**
210+
* Validate a parsed trace structure using Zod
211+
* @param trace The parsed trace to validate
212+
* @returns Zod safe parse result
213+
*/
214+
export function validateParsedTrace(trace: unknown) {
215+
return ParsedTraceSchema.safeParse(trace);
216+
}
217+
218+
/**
219+
* Validate trace content string and return detailed validation result
220+
* @param content The trace content to validate
221+
* @returns Object containing validation result and any errors
222+
*/
223+
export function validateTraceContent(content: string): {
224+
success: boolean;
225+
errors: TraceParseError[];
226+
parseErrors?: z.ZodError;
227+
} {
228+
const parsed = parseTrace(content);
229+
230+
// Check for parse errors first
231+
if (parsed.errors.length > 0) {
232+
return { success: false, errors: parsed.errors };
233+
}
234+
235+
// Validate structure with Zod
236+
const validation = validateParsedTrace(parsed);
237+
if (!validation.success) {
238+
return {
239+
success: false,
240+
errors: parsed.errors,
241+
parseErrors: validation.error,
242+
};
243+
}
244+
245+
return { success: true, errors: [] };
246+
}
247+
124248
// ============================================================================
125249
// Parsing utilities
126250
// ============================================================================
@@ -646,17 +770,74 @@ export function findHostCallEntry(
646770
hostCallIndex: number,
647771
readMemory?: (address: number, length: number) => Uint8Array | null,
648772
): HostCallLookupResult {
649-
// Try to find an exact match by PC
650-
const matchingEntries = trace.hostCalls.slice(indexInTrace).filter((hc) => hc.pc === pc && hc.gas <= gas);
773+
// Get remaining entries from the current index
774+
const remainingEntries = trace.hostCalls.slice(indexInTrace);
775+
776+
// Try to find an exact match by PC and host call index
777+
const matchingEntries = remainingEntries.filter((hc) => hc.pc === pc && hc.gas <= gas);
651778

652779
if (matchingEntries.length === 0) {
653780
return { entry: null, mismatches: [] };
654781
}
655782

656-
// First try to find an entry whose hostCallIndex matches and has acceptable gas
657-
const exactMatch = matchingEntries.find((hc) => hc.index === hostCallIndex && hc.gas <= gas);
658-
// Use exact match if found, otherwise fall back to the first matching entry
659-
const entry = exactMatch ?? matchingEntries[0];
783+
// First try to find an entry whose hostCallIndex matches exactly
784+
// Prefer exact index match even if gas is slightly higher
785+
const exactIndexMatch = remainingEntries.find((hc) => hc.index === hostCallIndex && hc.pc === pc);
786+
787+
// If we have an exact index match with acceptable gas, use it
788+
if (exactIndexMatch && exactIndexMatch.gas <= gas) {
789+
const mismatches: StateMismatch[] = [];
790+
791+
// Check gas
792+
if (exactIndexMatch.gas !== gas) {
793+
mismatches.push({
794+
field: "gas",
795+
expected: exactIndexMatch.gas.toString(),
796+
actual: gas.toString(),
797+
});
798+
}
799+
800+
// Check registers
801+
for (const [idx, expectedValue] of exactIndexMatch.registers) {
802+
const actualValue = registers[idx] ?? 0n;
803+
if (expectedValue !== actualValue) {
804+
mismatches.push({
805+
field: "register",
806+
expected: `r${idx}=0x${expectedValue.toString(16)}`,
807+
actual: `r${idx}=0x${actualValue.toString(16)}`,
808+
details: `Register ${idx}`,
809+
});
810+
}
811+
}
812+
813+
// Check memory reads if readMemory function is provided
814+
if (readMemory) {
815+
for (const mr of exactIndexMatch.memoryReads) {
816+
const actualData = readMemory(mr.address, mr.length);
817+
if (actualData) {
818+
const expectedHex = Array.from(mr.data)
819+
.map((b) => b.toString(16).padStart(2, "0"))
820+
.join("");
821+
const actualHex = Array.from(actualData)
822+
.map((b) => b.toString(16).padStart(2, "0"))
823+
.join("");
824+
if (expectedHex !== actualHex) {
825+
mismatches.push({
826+
field: "memread",
827+
expected: `0x${expectedHex}`,
828+
actual: `0x${actualHex}`,
829+
details: `Memory at 0x${mr.address.toString(16)} (${mr.length} bytes)`,
830+
});
831+
}
832+
}
833+
}
834+
}
835+
836+
return { entry: exactIndexMatch, mismatches };
837+
}
838+
839+
// Fall back to first matching entry by PC and gas
840+
const entry = matchingEntries[0];
660841
const mismatches: StateMismatch[] = [];
661842

662843
// Check PC

src/store/debugger/debuggerSlice.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { RootState } from "@/store";
1313
import { SelectedPvmWithPayload } from "@/components/PvmSelect";
1414
import { PvmTypes } from "@/packages/web-worker/types.ts";
1515
import { logger } from "@/utils/loggerService.tsx";
16-
import { HostCallEntry, ParsedTrace, parseTrace, StateMismatch, validateTrace } from "@/lib/host-call-trace";
16+
import { HostCallEntry, ParsedTrace, parseTrace, StateMismatch, validateTraceContent } from "@/lib/host-call-trace";
1717

1818
export type UiRefreshMode = "instructions" | "block";
1919

@@ -229,11 +229,17 @@ const debuggerSlice = createSlice({
229229
state.nextHostCallIndex = 0;
230230
state.pendingHostCall = null;
231231
} else {
232-
// Validate trace before parsing
233-
const validationErrors = validateTrace(action.payload);
234-
if (validationErrors.length > 0) {
235-
console.error("Trace validation errors:", validationErrors);
232+
// Validate trace content (includes both parse errors and Zod structure validation)
233+
const validation = validateTraceContent(action.payload);
234+
if (!validation.success) {
235+
console.error("Trace validation errors:", validation.errors);
236+
if (validation.parseErrors) {
237+
console.error("Trace structure validation errors:", validation.parseErrors);
238+
}
236239
// Don't set invalid trace - keep existing state or set to null
240+
state.hostCallsTrace = null;
241+
state.nextHostCallIndex = 0;
242+
state.pendingHostCall = null;
237243
return;
238244
}
239245

src/store/workers/workersSlice.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,10 +358,16 @@ export const handleHostCall = createAsyncThunk(
358358
}),
359359
);
360360

361-
logger.info(" [handleHostCall] Done - returning without opening dialog");
362-
// Advance host call index for all workers and await resuming execution
361+
logger.info(" [handleHostCall] Done - auto-continuing without opening dialog");
362+
// Advance host call index before resuming execution
363+
dispatch(
364+
setPendingHostCall({
365+
pendingHostCall: null,
366+
nextHostCallIndex: nextHostCallIndex + 1,
367+
}),
368+
);
369+
// Resume execution
363370
await dispatch(runAllWorkers()).unwrap();
364-
return;
365371
},
366372
);
367373

tests/trace-loading.spec.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import { test, expect } from "@playwright/test";
2-
import * as path from "path";
3-
import { fileURLToPath } from "url";
4-
5-
const __filename = fileURLToPath(import.meta.url);
6-
const __dirname = path.dirname(__filename);
2+
import { FIXTURES, getFixturePath } from "./utils/fixtures";
73

84
test.describe("Trace file loading", () => {
95
test("should load io-trace-output.log without crashing", async ({ page }) => {
106
await page.goto("/", { waitUntil: "domcontentloaded" });
117
await page.evaluate(() => window.localStorage.clear());
128

139
const fileInput = page.locator('input[type="file"]');
14-
const tracePath = path.join(__dirname, "../io-trace-output.log");
10+
const tracePath = getFixturePath(FIXTURES.IO_TRACE_OUTPUT);
1511

1612
await fileInput.setInputFiles(tracePath);
1713

@@ -49,7 +45,7 @@ test.describe("Trace file loading", () => {
4945
await page.evaluate(() => window.localStorage.clear());
5046

5147
const fileInput = page.locator('input[type="file"]');
52-
const tracePath = path.join(__dirname, "../io-trace-output.log");
48+
const tracePath = getFixturePath(FIXTURES.IO_TRACE_OUTPUT);
5349

5450
await fileInput.setInputFiles(tracePath);
5551

0 commit comments

Comments
 (0)