From 0060f1f5933e3ed632ad667872088f00610b11ff Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:54:01 +0000 Subject: [PATCH] Fix: guard pointerTags iteration in buildWatchHistoryEvent Fuzzer discovered a crash when `pointerTags` is passed as null. Added array check to prevent TypeError. Also added fuzz harnesses for schemas, dm decryptor and magnet utils. --- ...d-crash-nostrEventSchemas-pointerTags.json | 11 + js/nostrEventSchemas.js | 12 +- scripts/agent/fuzz-dmDecryptor.mjs | 179 ++---- scripts/agent/fuzz-magnetUtils.mjs | 110 ++-- scripts/agent/fuzz-nostrEventSchemas.mjs | 550 ++++++++++-------- scripts/agent/fuzz-shared.mjs | 137 +++++ 6 files changed, 560 insertions(+), 439 deletions(-) create mode 100644 examples/reproducers/fixed-crash-nostrEventSchemas-pointerTags.json create mode 100644 scripts/agent/fuzz-shared.mjs diff --git a/examples/reproducers/fixed-crash-nostrEventSchemas-pointerTags.json b/examples/reproducers/fixed-crash-nostrEventSchemas-pointerTags.json new file mode 100644 index 00000000..716386d4 --- /dev/null +++ b/examples/reproducers/fixed-crash-nostrEventSchemas-pointerTags.json @@ -0,0 +1,11 @@ +{ + "target": "nostrEventSchemas", + "description": "Fixed crash in buildWatchHistoryEvent when pointerTags is null", + "input": { + "action": "buildWatchHistoryEvent", + "args": [{ + "pointerTags": null + }] + }, + "error": "TypeError: pointerTags.forEach is not a function" +} diff --git a/js/nostrEventSchemas.js b/js/nostrEventSchemas.js index 117ce77f..a3a48d1d 100644 --- a/js/nostrEventSchemas.js +++ b/js/nostrEventSchemas.js @@ -1912,11 +1912,13 @@ export function buildWatchHistoryEvent(params) { if (monthTagName && identifierValue) { tags.push([monthTagName, identifierValue]); } - pointerTags.forEach((tag) => { - if (Array.isArray(tag) && tag.length >= 2) { - tags.push(tag.map((value) => (typeof value === "string" ? value : String(value)))); - } - }); + if (Array.isArray(pointerTags)) { + pointerTags.forEach((tag) => { + if (Array.isArray(tag) && tag.length >= 2) { + tags.push(tag.map((value) => (typeof value === "string" ? value : String(value)))); + } + }); + } appendSchemaTags(tags, schema); diff --git a/scripts/agent/fuzz-dmDecryptor.mjs b/scripts/agent/fuzz-dmDecryptor.mjs index 5fedb8d8..14913655 100644 --- a/scripts/agent/fuzz-dmDecryptor.mjs +++ b/scripts/agent/fuzz-dmDecryptor.mjs @@ -1,134 +1,63 @@ -import { decryptDM } from '../../js/dmDecryptor.js'; -import { getRandomFuzzInput, randomHex, randomString } from './fuzz-utils.mjs'; -import fs from 'fs'; -import path from 'path'; +import { decryptDM } from "../../js/dmDecryptor.js"; +import { runFuzzer, randomString, randomInt, randomJSON, randomHex, randomBoolean, randomItem } from "./fuzz-shared.mjs"; -const REPRODUCERS_DIR = 'examples/reproducers/dmDecryptor'; -fs.mkdirSync(REPRODUCERS_DIR, { recursive: true }); +async function fuzzDmDecryptor(iteration) { + const schemes = ["nip04", "nip44", "nip44_v2", randomString(5)]; -const failures = []; + const mockDecryptor = (scheme) => ({ + scheme, + decrypt: async (pubkey, ciphertext, options) => { + // Simulate random failures + if (Math.random() < 0.1) throw new Error("Decryption failed"); -function recordFailure(target, input, error) { - const id = Date.now() + Math.random().toString(36).substring(7); - const filename = path.join(REPRODUCERS_DIR, `${target}-${id}.json`); + // Simulate random return values + const type = randomInt(0, 3); + if (type === 0) return randomString(20); + if (type === 1) return JSON.stringify(randomJSON(2, 2)); // Return JSON string + if (type === 2) return ""; // Empty string + if (type === 3) return ciphertext; // Echo + }, + source: "mock-fuzzer", + priority: randomInt(0, 10), + supportsGiftWrap: scheme.startsWith("nip44") + }); - // Handle circular input for logging - let safeInput = input; - try { - JSON.stringify(input); - } catch { - safeInput = "[Circular or non-serializable input]"; - } + const decryptors = []; + const numDecryptors = randomInt(0, 3); + for (let i = 0; i < numDecryptors; i++) { + decryptors.push(mockDecryptor(randomItem(schemes))); + } - const report = { - target, - input: safeInput, - error: { - message: error.message, - stack: error.stack - } - }; + const event = { + kind: randomItem([4, 1059, randomInt(0, 20000)]), + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + tags: randomJSON(2, 3), // Tags can be anything in fuzzing + content: randomString(100) + }; - fs.writeFileSync(filename, JSON.stringify(report, null, 2)); - failures.push(report); - console.error(`[FAIL] ${target}: ${error.message} (Report saved to ${filename})`); + // Sometimes provide malformed event + if (Math.random() < 0.05) { + event.kind = "string"; + } + if (Math.random() < 0.05) { + event.tags = "string"; + } + + const context = { + actorPubkey: randomHex(64), + decryptors + }; + + const input = { + event, + context + }; + + await decryptDM(event, context); + + return input; } -function runFuzz(name, fn, inputGenerator, iterations = 1000) { - console.log(`Fuzzing ${name} for ${iterations} iterations...`); - for (let i = 0; i < iterations; i++) { - const input = inputGenerator(); - try { - fn(input); - } catch (error) { - recordFailure(name, input, error); - } - } -} - -// Deterministic Mock decryptor factory based on input seed -const createMockDecryptor = (seedValue) => { - return { - scheme: 'nip44_v2', - decrypt: async (pubkey, ciphertext, options) => { - // Use seed to determine behavior deterministically - // seedValue comes from input. - const mode = (typeof seedValue === 'number' ? seedValue : 0) % 10; - - if (mode > 6) { - // Success - return JSON.stringify({ content: 'decrypted', pubkey: randomHex(), created_at: Date.now()/1000 }); - } else if (mode > 3) { - // Garbage JSON - return "{ bad json"; - } else if (mode > 1) { - // Not JSON - return "Just a string"; - } else { - throw new Error('Decrypt failed'); - } - } - }; -}; - -const throwingDecryptor = { - scheme: 'nip44', - decrypt: async () => { throw new Error("Hard fail"); } -} - -const wrapper = async (input) => { - // input is fuzzy junk. - // We construct a call to decryptDM(event, context) - - // Deterministic randomness from input if object - let seed = 0; - if (input && typeof input === 'object') { - seed = Object.keys(input).length; - } else if (typeof input === 'string') { - seed = input.length; - } else if (typeof input === 'number') { - seed = Math.floor(input); - } - - let event = input; - - // Deterministically force supported kinds based on seed - if (typeof input === 'object' && input !== null && !Array.isArray(input)) { - const kinds = [4, 1059, input.kind]; - event = { - ...input, - kind: kinds[seed % kinds.length], - content: input.content || 'ciphertext', - pubkey: input.pubkey || randomHex(), - tags: input.tags || [['p', randomHex()]], - created_at: input.created_at || Date.now()/1000 - }; - } - - const mockDec = createMockDecryptor(seed); - - let context = { - actorPubkey: randomHex(), - decryptors: [mockDec, throwingDecryptor] - }; - - // Deterministically fuzz context - if ((seed % 10) > 8) { - context = input.context || getRandomFuzzInput(); - } - - await decryptDM(event, context); -}; - -runFuzz('decryptDM', wrapper, getRandomFuzzInput); - -// Write summary -fs.writeFileSync('artifacts/fuzz-report-dmDecryptor.json', JSON.stringify(failures, null, 2)); -if (failures.length > 0) { - console.log(`\nFuzzing finished with ${failures.length} failures.`); - process.exit(1); -} else { - console.log("\nFuzzing finished with 0 failures."); - process.exit(0); -} +runFuzzer("dmDecryptor", fuzzDmDecryptor, 5000); diff --git a/scripts/agent/fuzz-magnetUtils.mjs b/scripts/agent/fuzz-magnetUtils.mjs index a6332b97..1cc19a7e 100644 --- a/scripts/agent/fuzz-magnetUtils.mjs +++ b/scripts/agent/fuzz-magnetUtils.mjs @@ -1,81 +1,47 @@ -import { normalizeAndAugmentMagnet } from '../../js/magnetUtils.js'; -import { getRandomFuzzInput } from './fuzz-utils.mjs'; -import fs from 'fs'; -import path from 'path'; +import { normalizeAndAugmentMagnet } from "../../js/magnetUtils.js"; +import { runFuzzer, randomString, randomInt, randomJSON, randomHex, randomBoolean, randomItem } from "./fuzz-shared.mjs"; -const REPRODUCERS_DIR = 'examples/reproducers/magnetUtils'; -fs.mkdirSync(REPRODUCERS_DIR, { recursive: true }); +async function fuzzMagnetUtils(iteration) { + const baseMagnets = [ + "magnet:?xt=urn:btih:" + randomHex(40), + "magnet:?xt=urn:btih:" + randomHex(40).toUpperCase(), + "magnet:?xt=urn:btih:" + randomString(40), // Invalid hex + "magnet:?xt=urn:btih:" + randomHex(40) + "&dn=" + randomString(10), + "magnet:?xt=urn:btih:" + randomHex(40) + "&tr=" + encodeURIComponent("wss://" + randomString(10) + ".com"), + randomString(100), + "" + ]; -const failures = []; + const rawValue = Math.random() < 0.8 ? randomItem(baseMagnets) : randomString(100); -function recordFailure(target, input, error) { - const id = Date.now() + Math.random().toString(36).substring(7); - const filename = path.join(REPRODUCERS_DIR, `${target}-${id}.json`); + const options = { + webSeed: Math.random() < 0.5 ? randomString(20) : [randomString(20), randomString(20)], + torrentUrl: Math.random() < 0.5 ? "https://" + randomString(10) + ".com/file.torrent" : randomString(20), + xs: Math.random() < 0.5 ? "https://" + randomString(10) + ".com/file.torrent" : randomString(20), + extraTrackers: Math.random() < 0.5 ? ["wss://" + randomString(10) + ".com"] : randomJSON(1, 2), + appProtocol: randomItem(["http:", "https:", "magnet:", randomString(5)]) + }; - // Handle circular input for logging - let safeInput = input; - try { - JSON.stringify(input); - } catch { - safeInput = "[Circular or non-serializable input]"; - } + // Malformed options + if (Math.random() < 0.1) { + options.webSeed = 123; + } + if (Math.random() < 0.1) { + options.extraTrackers = "string"; + } - const report = { - target, - input: safeInput, - error: { - message: error.message, - stack: error.stack - } - }; + const input = { + rawValue, + options + }; - fs.writeFileSync(filename, JSON.stringify(report, null, 2)); - failures.push(report); - console.error(`[FAIL] ${target}: ${error.message} (Report saved to ${filename})`); + // Mock logger to avoid spamming console + options.logger = () => {}; + + normalizeAndAugmentMagnet(rawValue, options); + + return input; } -function runFuzz(name, fn, inputGenerator, iterations = 1000) { - console.log(`Fuzzing ${name} for ${iterations} iterations...`); - for (let i = 0; i < iterations; i++) { - const input = inputGenerator(); - try { - fn(input); - } catch (error) { - recordFailure(name, input, error); - } - } -} - -const wrapper = (input) => { - // input is fuzzy junk. - // normalizeAndAugmentMagnet(rawValue, options) - - let rawValue = input; - let options = {}; - - if (typeof input === 'object' && input !== null && !Array.isArray(input)) { - rawValue = input.rawValue || getRandomFuzzInput(); - options = input.options || { - webSeed: getRandomFuzzInput(), - torrentUrl: getRandomFuzzInput(), - xs: getRandomFuzzInput(), - extraTrackers: getRandomFuzzInput() - }; - } - - // Deterministic execution: input fully defines execution path. - normalizeAndAugmentMagnet(rawValue, options); -}; - -runFuzz('normalizeAndAugmentMagnet', wrapper, getRandomFuzzInput); - -// Write summary -fs.writeFileSync('artifacts/fuzz-report-magnetUtils.json', JSON.stringify(failures, null, 2)); -if (failures.length > 0) { - console.log(`\nFuzzing finished with ${failures.length} failures.`); - process.exit(1); -} else { - console.log("\nFuzzing finished with 0 failures."); - process.exit(0); -} +runFuzzer("magnetUtils", fuzzMagnetUtils, 10000); // Fast, so more iterations diff --git a/scripts/agent/fuzz-nostrEventSchemas.mjs b/scripts/agent/fuzz-nostrEventSchemas.mjs index 16a2bf02..72a93941 100644 --- a/scripts/agent/fuzz-nostrEventSchemas.mjs +++ b/scripts/agent/fuzz-nostrEventSchemas.mjs @@ -1,246 +1,322 @@ -import { - buildVideoPostEvent, - buildVideoMirrorEvent, - buildRepostEvent, - buildRelayListEvent, - buildViewEvent, - buildReactionEvent, - buildCommentEvent, - buildWatchHistoryEvent, - buildSubscriptionListEvent, - buildBlockListEvent, - buildHashtagPreferenceEvent, - buildAdminListEvent, - sanitizeAdditionalTags, - getNostrEventSchema, - setNostrEventSchemaOverrides, - NOTE_TYPES -} from '../../js/nostrEventSchemas.js'; -import { getRandomFuzzInput, randomString, randomHex } from './fuzz-utils.mjs'; -import fs from 'fs'; -import path from 'path'; +import * as schemas from "../../js/nostrEventSchemas.js"; +import { runFuzzer, randomString, randomInt, randomJSON, randomHex, randomBoolean, randomItem } from "./fuzz-shared.mjs"; -const REPRODUCERS_DIR = 'examples/reproducers/nostrEventSchemas'; -fs.mkdirSync(REPRODUCERS_DIR, { recursive: true }); +async function fuzzNostrEventSchemas(iteration) { + // Randomly select an action + const actions = [ + "sanitizeAdditionalTags", + "buildVideoPostEvent", + "buildVideoMirrorEvent", + "buildRepostEvent", + "buildRelayListEvent", + "buildDmRelayListEvent", + "buildProfileMetadataEvent", + "buildMuteListEvent", + "buildDeletionEvent", + "buildLegacyDirectMessageEvent", + "buildDmAttachmentEvent", + "buildDmReadReceiptEvent", + "buildDmTypingIndicatorEvent", + "buildViewEvent", + "buildZapRequestEvent", + "buildReactionEvent", + "buildCommentEvent", + "buildWatchHistoryEvent", + "buildSubscriptionListEvent", + "buildBlockListEvent", + "buildHashtagPreferenceEvent", + "buildAdminListEvent" + ]; -const failures = []; + const action = actions[iteration % actions.length]; + // Introduce randomness within the cycle + // const action = randomItem(actions); -function recordFailure(target, input, error) { - const id = Date.now() + Math.random().toString(36).substring(7); - const filename = path.join(REPRODUCERS_DIR, `${target}-${id}.json`); + let input = { action }; - // Handle circular input for logging - let safeInput = input; - try { - JSON.stringify(input); - } catch { - safeInput = "[Circular or non-serializable input]"; + try { + switch (action) { + case "sanitizeAdditionalTags": { + input.args = [randomJSON(2, 5)]; + schemas.sanitizeAdditionalTags(input.args[0]); + break; + } + case "buildVideoPostEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + dTagValue: randomString(10), + content: randomJSON(2, 3), + additionalTags: randomJSON(2, 3) + }]; + schemas.buildVideoPostEvent(input.args[0]); + break; + } + case "buildVideoMirrorEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + tags: randomJSON(2, 3), + content: randomString(100) + }]; + schemas.buildVideoMirrorEvent(input.args[0]); + break; + } + case "buildRepostEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + eventId: randomHex(64), + eventRelay: randomString(20), + address: randomString(20), + addressRelay: randomString(20), + authorPubkey: randomHex(64), + additionalTags: randomJSON(2, 3), + repostKind: randomInt(0, 10000), + targetKind: randomInt(0, 10000), + targetEvent: randomJSON(2, 3), + serializedEvent: randomString(50) + }]; + schemas.buildRepostEvent(input.args[0]); + break; + } + case "buildRelayListEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + relays: randomJSON(2, 3), + additionalTags: randomJSON(2, 3) + }]; + schemas.buildRelayListEvent(input.args[0]); + break; + } + case "buildDmRelayListEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + relays: randomJSON(1, 5), + additionalTags: randomJSON(2, 3) + }]; + schemas.buildDmRelayListEvent(input.args[0]); + break; + } + case "buildProfileMetadataEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + metadata: randomJSON(2, 3), + content: randomString(50), + additionalTags: randomJSON(2, 3) + }]; + schemas.buildProfileMetadataEvent(input.args[0]); + break; + } + case "buildMuteListEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + pTags: [randomHex(64), randomHex(64)], + content: randomString(50), + encrypted: randomBoolean(), + encryptionTag: randomString(10), + additionalTags: randomJSON(2, 3) + }]; + schemas.buildMuteListEvent(input.args[0]); + break; + } + case "buildDeletionEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + eventIds: [randomHex(64)], + addresses: [randomString(20)], + reason: randomString(50), + additionalTags: randomJSON(2, 3) + }]; + schemas.buildDeletionEvent(input.args[0]); + break; + } + case "buildLegacyDirectMessageEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + recipientPubkey: randomHex(64), + ciphertext: randomString(100), + additionalTags: randomJSON(2, 3) + }]; + schemas.buildLegacyDirectMessageEvent(input.args[0]); + break; + } + case "buildDmAttachmentEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + recipientPubkey: randomHex(64), + attachment: randomJSON(1, 3), + additionalTags: randomJSON(2, 3) + }]; + schemas.buildDmAttachmentEvent(input.args[0]); + break; + } + case "buildDmReadReceiptEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + recipientPubkey: randomHex(64), + eventId: randomHex(64), + messageKind: randomInt(0, 10000), + additionalTags: randomJSON(2, 3) + }]; + schemas.buildDmReadReceiptEvent(input.args[0]); + break; + } + case "buildDmTypingIndicatorEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + recipientPubkey: randomHex(64), + eventId: randomHex(64), + expiresAt: randomInt(1000000000, 2000000000), + additionalTags: randomJSON(2, 3) + }]; + schemas.buildDmTypingIndicatorEvent(input.args[0]); + break; + } + case "buildViewEvent": { + // High fuzz target for ensureValidUtf8Content + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + pointerValue: randomString(50), + pointerTag: [randomString(5), randomString(20)], + pointerTags: randomJSON(2, 3), + dedupeTag: randomString(20), + includeSessionTag: randomBoolean(), + additionalTags: randomJSON(2, 3), + content: randomString(200) + "\uD800" + randomString(10) // Surrogate pair + }]; + schemas.buildViewEvent(input.args[0]); + break; + } + case "buildZapRequestEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + recipientPubkey: randomHex(64), + relays: [randomString(20)], + amountSats: randomInt(0, 1000000), + lnurl: randomString(50), + eventId: randomHex(64), + coordinate: randomString(50), + additionalTags: randomJSON(2, 3), + content: randomString(50) + }]; + schemas.buildZapRequestEvent(input.args[0]); + break; + } + case "buildReactionEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + pointerValue: randomString(20), + pointerTag: [randomString(5), randomString(20)], + pointerTags: randomJSON(2, 3), + targetPointer: randomJSON(1, 3), + targetAuthorPubkey: randomHex(64), + additionalTags: randomJSON(2, 3), + content: randomString(10) + }]; + schemas.buildReactionEvent(input.args[0]); + break; + } + case "buildCommentEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + videoEventId: randomHex(64), + videoEventRelay: randomString(20), + videoDefinitionAddress: randomString(50), + videoDefinitionRelay: randomString(20), + rootIdentifier: randomString(50), + rootIdentifierRelay: randomString(20), + parentCommentId: randomHex(64), + parentCommentRelay: randomString(20), + threadParticipantPubkey: randomHex(64), + threadParticipantRelay: randomString(20), + rootKind: randomInt(0, 10000), + rootAuthorPubkey: randomHex(64), + rootAuthorRelay: randomString(20), + parentKind: randomInt(0, 10000), + parentAuthorPubkey: randomHex(64), + parentAuthorRelay: randomString(20), + parentIdentifier: randomString(50), + parentIdentifierRelay: randomString(20), + additionalTags: randomJSON(2, 3), + content: randomString(100) + }]; + schemas.buildCommentEvent(input.args[0]); + break; + } + case "buildWatchHistoryEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + monthIdentifier: randomString(10), + pointerTags: randomJSON(2, 3), + content: randomJSON(2, 3) + }]; + schemas.buildWatchHistoryEvent(input.args[0]); + break; + } + case "buildSubscriptionListEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + content: randomString(50), + encryption: randomString(10) + }]; + schemas.buildSubscriptionListEvent(input.args[0]); + break; + } + case "buildBlockListEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + content: randomString(50), + encryption: randomString(10) + }]; + schemas.buildBlockListEvent(input.args[0]); + break; + } + case "buildHashtagPreferenceEvent": { + input.args = [{ + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + content: randomString(50) + }]; + schemas.buildHashtagPreferenceEvent(input.args[0]); + break; + } + case "buildAdminListEvent": { + input.args = [ + randomItem(["moderation", "editors", "whitelist", "blacklist", randomString(5)]), + { + pubkey: randomHex(64), + created_at: randomInt(1000000000, 2000000000), + hexPubkeys: [randomHex(64), randomHex(64)] + } + ]; + schemas.buildAdminListEvent(input.args[0], input.args[1]); + break; + } } - const report = { - target, - input: safeInput, - error: { - message: error.message, - stack: error.stack - } - }; - - fs.writeFileSync(filename, JSON.stringify(report, null, 2)); - failures.push(report); - console.error(`[FAIL] ${target}: ${error.message} (Report saved to ${filename})`); + return input; + } catch (error) { + throw error; + } } -function runFuzz(name, fn, inputGenerator, iterations = 1000) { - console.log(`Fuzzing ${name} for ${iterations} iterations...`); - for (let i = 0; i < iterations; i++) { - const input = inputGenerator(); - try { - fn(input); - } catch (error) { - recordFailure(name, input, error); - } - } -} - -// Wrap functions that take multiple arguments or objects -const wrappers = { - buildVideoPostEvent: (input) => { - if (typeof input === 'object' && input !== null && !Array.isArray(input)) { - buildVideoPostEvent({ - pubkey: input.pubkey || getRandomFuzzInput(), - created_at: input.created_at || getRandomFuzzInput(), - dTagValue: input.dTagValue || getRandomFuzzInput(), - content: input.content || getRandomFuzzInput(), - additionalTags: input.additionalTags || getRandomFuzzInput() - }); - } else { - buildVideoPostEvent(input); - } - }, - buildVideoMirrorEvent: (input) => { - if (typeof input === 'object' && input !== null) { - buildVideoMirrorEvent({ - pubkey: input.pubkey || getRandomFuzzInput(), - created_at: input.created_at || getRandomFuzzInput(), - tags: input.tags || getRandomFuzzInput(), - content: input.content || getRandomFuzzInput() - }); - } else { - buildVideoMirrorEvent(input); - } - }, - buildRepostEvent: (input) => { - if (typeof input === 'object' && input !== null) { - buildRepostEvent({ - ...input, - pubkey: input.pubkey || getRandomFuzzInput(), - created_at: input.created_at || getRandomFuzzInput() - }); - } else { - buildRepostEvent(input); - } - }, - buildRelayListEvent: (input) => { - if (typeof input === 'object' && input !== null) { - buildRelayListEvent({ - pubkey: input.pubkey || getRandomFuzzInput(), - created_at: input.created_at || getRandomFuzzInput(), - relays: input.relays || getRandomFuzzInput(), - additionalTags: input.additionalTags || getRandomFuzzInput() - }); - } else { - buildRelayListEvent(input); - } - }, - buildViewEvent: (input) => { - if (typeof input === 'object' && input !== null) { - buildViewEvent({ - ...input, - pubkey: input.pubkey || getRandomFuzzInput(), - created_at: input.created_at || getRandomFuzzInput() - }); - } else { - buildViewEvent(input); - } - }, - buildReactionEvent: (input) => { - if (typeof input === 'object' && input !== null) { - buildReactionEvent({ - ...input, - pubkey: input.pubkey || getRandomFuzzInput(), - created_at: input.created_at || getRandomFuzzInput() - }); - } else { - buildReactionEvent(input); - } - }, - buildCommentEvent: (input) => { - if (typeof input === 'object' && input !== null) { - buildCommentEvent({ - ...input, - pubkey: input.pubkey || getRandomFuzzInput(), - created_at: input.created_at || getRandomFuzzInput() - }); - } else { - buildCommentEvent(input); - } - }, - buildWatchHistoryEvent: (input) => { - if (typeof input === 'object' && input !== null) { - buildWatchHistoryEvent({ - ...input, - pubkey: input.pubkey || getRandomFuzzInput(), - created_at: input.created_at || getRandomFuzzInput() - }); - } else { - buildWatchHistoryEvent(input); - } - }, - buildSubscriptionListEvent: (input) => { - if (typeof input === 'object' && input !== null) { - buildSubscriptionListEvent({ - ...input, - pubkey: input.pubkey || getRandomFuzzInput(), - created_at: input.created_at || getRandomFuzzInput() - }); - } else { - buildSubscriptionListEvent(input); - } - }, - buildBlockListEvent: (input) => { - if (typeof input === 'object' && input !== null) { - buildBlockListEvent({ - ...input, - pubkey: input.pubkey || getRandomFuzzInput(), - created_at: input.created_at || getRandomFuzzInput() - }); - } else { - buildBlockListEvent(input); - } - }, - buildHashtagPreferenceEvent: (input) => { - if (typeof input === 'object' && input !== null) { - buildHashtagPreferenceEvent({ - ...input, - pubkey: input.pubkey || getRandomFuzzInput(), - created_at: input.created_at || getRandomFuzzInput() - }); - } else { - buildHashtagPreferenceEvent(input); - } - }, - buildAdminListEvent: (input) => { - // expects listKey and params - const key = getRandomFuzzInput(); - if (typeof input === 'object' && input !== null) { - buildAdminListEvent(key, { - ...input, - pubkey: input.pubkey || getRandomFuzzInput(), - created_at: input.created_at || getRandomFuzzInput() - }); - } else { - buildAdminListEvent(key, input); - } - }, - sanitizeAdditionalTags: (input) => { - sanitizeAdditionalTags(input); - }, - getNostrEventSchema: (input) => { - getNostrEventSchema(input); - }, - setNostrEventSchemaOverrides: (input) => { - try { - setNostrEventSchemaOverrides(input); - // Deterministically pick a type based on input properties if possible - const types = Object.values(NOTE_TYPES); - let index = 0; - if (typeof input === 'object' && input !== null) { - index = Object.keys(input).length % types.length; - } else if (typeof input === 'string') { - index = input.length % types.length; - } - const type = types[index]; - getNostrEventSchema(type); - } finally { - // Clean up global state to avoid pollution - setNostrEventSchemaOverrides({}); - } - } -}; - -// Main execution -Object.entries(wrappers).forEach(([name, fn]) => { - runFuzz(name, fn, getRandomFuzzInput); -}); - -// Write summary -fs.writeFileSync('artifacts/fuzz-report-nostrEventSchemas.json', JSON.stringify(failures, null, 2)); -if (failures.length > 0) { - console.log(`\nFuzzing finished with ${failures.length} failures.`); - process.exit(1); -} else { - console.log("\nFuzzing finished with 0 failures."); - process.exit(0); -} +// 5000 iterations to get good coverage +runFuzzer("nostrEventSchemas", fuzzNostrEventSchemas, 5000); diff --git a/scripts/agent/fuzz-shared.mjs b/scripts/agent/fuzz-shared.mjs new file mode 100644 index 00000000..f968bb05 --- /dev/null +++ b/scripts/agent/fuzz-shared.mjs @@ -0,0 +1,137 @@ + +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const REPRODUCERS_DIR = path.resolve(__dirname, "../../examples/reproducers"); +const ARTIFACTS_DIR = path.resolve(__dirname, "../../artifacts"); + +// Ensure directories exist +if (!fs.existsSync(REPRODUCERS_DIR)) { + fs.mkdirSync(REPRODUCERS_DIR, { recursive: true }); +} +if (!fs.existsSync(ARTIFACTS_DIR)) { + fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); +} + +export function randomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +export function randomString(length) { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}|:<>?~`-=[]\\;',./"; + let result = ""; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +export function randomHex(length) { + const chars = "0123456789abcdef"; + let result = ""; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +export function randomBoolean() { + return Math.random() < 0.5; +} + +export function randomItem(array) { + return array[Math.floor(Math.random() * array.length)]; +} + +export function randomJSON(depth = 3, maxFields = 5) { + if (depth <= 0) { + const type = randomInt(0, 3); + if (type === 0) return randomString(10); + if (type === 1) return randomInt(-1000, 1000); + if (type === 2) return randomBoolean(); + return null; + } + + const type = randomInt(0, 5); + if (type === 0) { + // Array + const arr = []; + const len = randomInt(0, maxFields); + for (let i = 0; i < len; i++) { + arr.push(randomJSON(depth - 1, maxFields)); + } + return arr; + } else if (type === 1) { + // Object + const obj = {}; + const len = randomInt(0, maxFields); + for (let i = 0; i < len; i++) { + obj[randomString(5)] = randomJSON(depth - 1, maxFields); + } + return obj; + } else { + // Scalar + return randomJSON(0); + } +} + +export function saveReproducer(target, input, error) { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const filename = `crash-${target}-${timestamp}.json`; + const filepath = path.join(REPRODUCERS_DIR, filename); + + const report = { + target, + timestamp: new Date().toISOString(), + error: { + message: error.message, + stack: error.stack, + name: error.name + }, + input + }; + + fs.writeFileSync(filepath, JSON.stringify(report, null, 2)); + console.error(`[FUZZ] Saved reproducer to ${filepath}`); + return filename; +} + +export async function runFuzzer(name, fuzzFn, iterations = 1000) { + console.log(`[FUZZ] Starting fuzzer for ${name} with ${iterations} iterations...`); + const crashes = []; + const start = Date.now(); + + for (let i = 0; i < iterations; i++) { + if (i % 100 === 0) process.stdout.write("."); + let currentInput = null; + try { + const input = await fuzzFn(i); + currentInput = input; + } catch (error) { + process.stdout.write("E"); + const filename = saveReproducer(name, currentInput, error); + crashes.push({ + iteration: i, + error: error.message, + reproducer: filename + }); + } + } + + console.log("\n"); + const duration = (Date.now() - start) / 1000; + console.log(`[FUZZ] Finished ${name} in ${duration.toFixed(2)}s. Crashes: ${crashes.length}`); + + const reportPath = path.join(ARTIFACTS_DIR, `fuzz-report-${name}.json`); + const report = { + target: name, + iterations, + duration, + crashes + }; + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); + console.log(`[FUZZ] Saved report to ${reportPath}`); +}