diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml deleted file mode 100644 index d7368f39bd6..00000000000 --- a/.github/workflows/fuzz.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Fuzzing - -on: [push, pull_request] - -permissions: - contents: read - -jobs: - fuzzing: - name: Fuzz - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - with: - persist-credentials: false - - - name: Setup Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: lts/* - - - name: Install - run: | - npm install - - - name: Run fuzzing - timeout-minutes: 10 - run: | - npm run fuzz - - - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 - if: ${{ failure() }} - with: - name: undici-fuzz-results-${{ github.sha }} - path: | - corpus/ - crash-* - fuzz-results-*.json diff --git a/package.json b/package.json index 751c3c48f39..d32339c0536 100644 --- a/package.json +++ b/package.json @@ -92,8 +92,7 @@ "coverage:report:ci": "c8 report", "bench": "echo \"Error: Benchmarks have been moved to '/benchmarks'\" && exit 1", "serve:website": "echo \"Error: Documentation has been moved to '/docs'\" && exit 1", - "prepare": "husky install && node ./scripts/platform-shell.js", - "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" + "prepare": "husky install && node ./scripts/platform-shell.js" }, "devDependencies": { "@matteo.collina/tspl": "^0.1.1", @@ -104,13 +103,13 @@ "c8": "^9.1.0", "cross-env": "^7.0.3", "dns-packet": "^5.4.0", + "fast-check": "^3.17.1", "form-data": "^4.0.0", "formdata-node": "^6.0.3", "https-pem": "^3.0.0", "husky": "^9.0.7", "jest": "^29.0.2", "jsdom": "^24.0.0", - "jsfuzz": "^1.0.15", "node-forge": "^1.3.1", "pre-commit": "^1.2.2", "proxy": "^2.1.1", diff --git a/test/fuzzing/client/client-fuzz-body.js b/test/fuzzing/client/client-fuzz-body.js index 6643ddaf6f7..f9d3ac11aa2 100644 --- a/test/fuzzing/client/client-fuzz-body.js +++ b/test/fuzzing/client/client-fuzz-body.js @@ -6,12 +6,11 @@ const acceptableCodes = [ 'ERR_INVALID_ARG_TYPE' ] -// TODO: could make this a class with some inbuilt functionality that we can inherit -async function fuzz (netServer, results, buf) { +async function fuzz (address, results, buf) { const body = buf results.body = body try { - const data = await request(`http://localhost:${netServer.address().port}`, { body }) + const data = await request(address, { body }) data.body.destroy().on('error', () => {}) } catch (err) { results.err = err @@ -19,7 +18,6 @@ async function fuzz (netServer, results, buf) { if (Object.values(errors).some(undiciError => err instanceof undiciError)) { // Okay error } else if (!acceptableCodes.includes(err.code)) { - console.log(`=== Headers: ${JSON.stringify(body)} ===`) throw err } } diff --git a/test/fuzzing/client/client-fuzz-headers.js b/test/fuzzing/client/client-fuzz-headers.js index 84f3390709c..fb481068c11 100644 --- a/test/fuzzing/client/client-fuzz-headers.js +++ b/test/fuzzing/client/client-fuzz-headers.js @@ -6,11 +6,11 @@ const acceptableCodes = [ 'ERR_INVALID_ARG_TYPE' ] -async function fuzz (netServer, results, buf) { +async function fuzz (address, results, buf) { const headers = { buf: buf.toString() } results.body = headers try { - const data = await request(`http://localhost:${netServer.address().port}`, { headers }) + const data = await request(address, { headers }) data.body.destroy().on('error', () => {}) } catch (err) { results.err = err @@ -18,7 +18,6 @@ async function fuzz (netServer, results, buf) { if (Object.values(errors).some(undiciError => err instanceof undiciError)) { // Okay error } else if (!acceptableCodes.includes(err.code)) { - console.log(`=== Headers: ${JSON.stringify(headers)} ===`) throw err } } diff --git a/test/fuzzing/client/client-fuzz-options.js b/test/fuzzing/client/client-fuzz-options.js index 5be81b630c6..e28f08904e3 100644 --- a/test/fuzzing/client/client-fuzz-options.js +++ b/test/fuzzing/client/client-fuzz-options.js @@ -8,10 +8,9 @@ const acceptableCodes = [ 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED' - // ---- ] -async function fuzz (netServer, results, buf) { +async function fuzz (address, results, buf) { const optionKeys = ['body', 'path', 'method', 'opaque', 'upgrade', buf] const options = {} for (const optionKey of optionKeys) { @@ -21,7 +20,7 @@ async function fuzz (netServer, results, buf) { } results.options = options try { - const data = await request(`http://localhost:${netServer.address().port}`, options) + const data = await request(address, options) data.body.destroy().on('error', () => {}) } catch (err) { results.err = err @@ -29,7 +28,6 @@ async function fuzz (netServer, results, buf) { if (Object.values(errors).some(undiciError => err instanceof undiciError)) { // Okay error } else if (!acceptableCodes.includes(err.code)) { - console.log(`=== Options: ${JSON.stringify(options)} ===`) throw err } } diff --git a/test/fuzzing/fuzz.js b/test/fuzzing/fuzz.js deleted file mode 100644 index 0d938496309..00000000000 --- a/test/fuzzing/fuzz.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict' - -const net = require('node:net') -const fs = require('fs/promises') -const path = require('node:path') -const serverFuzzFnMap = require('./server') -const clientFuzzFnMap = require('./client') - -const port = process.env.PORT || 0 -const timeout = parseInt(process.env.TIMEOUT, 10) || 300_000 // 5 minutes by default - -const netServer = net.createServer((socket) => { - socket.on('data', (data) => { - // Select server fuzz fn - const serverFuzzFns = Object.values(serverFuzzFnMap) - const serverFuzzFn = serverFuzzFns[Math.floor(Math.random() * serverFuzzFns.length)] - - serverFuzzFn(socket, data) - }) -}) -const waitForNetServer = netServer.listen(port) - -// Set script to exit gracefully after a set period of time. -const timer = setTimeout(() => { - process.kill(process.pid, 'SIGINT') -}, timeout) - -async function writeResults (resultsPath, data) { - try { - await fs.writeFile(resultsPath, JSON.stringify(data, null, 2)) - console.log(`=== Written results to ${resultsPath} ===`) - } catch (err) { - console.log(`=== Unable to write results to ${resultsPath}`, err, '===') - } -} - -async function fuzz (buf) { - // Wait for net server to be ready - await waitForNetServer - - // Select client fuzz fn based on the buf input - await Promise.all( - Object.entries(clientFuzzFnMap).map(async ([clientFuzzFnName, clientFuzzFn]) => { - const results = {} - try { - await clientFuzzFn(netServer, results, buf) - } catch (err) { - clearTimeout(timer) - const output = { clientFuzzFnName, buf: { raw: buf, string: buf.toString() }, raw: JSON.stringify({ clientFuzzFnName, buf: { raw: buf, string: buf.toString() }, err, ...results }), err, ...results } - - console.log(`=== Failed fuzz ${clientFuzzFnName} with input '${buf}' ===`) - console.log('=== Fuzz results start ===') - console.log(output) - console.log('=== Fuzz results end ===') - - await writeResults(path.resolve(`fuzz-results-${Date.now()}.json`), output) - - throw err - } - }) - ) -} - -module.exports = { - fuzz -} diff --git a/test/fuzzing/fuzzing.test.js b/test/fuzzing/fuzzing.test.js new file mode 100644 index 00000000000..c800c1abd74 --- /dev/null +++ b/test/fuzzing/fuzzing.test.js @@ -0,0 +1,64 @@ +'use strict' + +const { once } = require('node:events') +const fc = require('fast-check') +const netServer = require('./server') +const { describe, before, after, test } = require('node:test') +const { + clientFuzzBody, + clientFuzzHeaders, + clientFuzzOptions +} = require('./client') + +// Detect if running in CI (here we use GitHub Workflows) +// https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables +const isCI = process.env.CI === 'true' + +fc.configureGlobal({ + interruptAfterTimeLimit: isCI ? 60_000 /* 1 minute */ : 10_000 /* 10 seconds */, + numRuns: Number.MAX_SAFE_INTEGER +}) + +describe('fuzzing', { timeout: 600_000 /* 10 minutes */ }, () => { + before(async () => { + netServer.listen(0) + await once(netServer, 'listening') + }) + + after(() => { + netServer.close() + }) + + test('body', async () => { + const address = `http://localhost:${netServer.address().port}` + await fc.assert( + fc.asyncProperty(fc.uint8Array(), async (body) => { + body = Buffer.from(body) + const results = {} + await clientFuzzBody(address, results, body) + }) + ) + }) + + test('headers', async () => { + const address = `http://localhost:${netServer.address().port}` + await fc.assert( + fc.asyncProperty(fc.uint8Array(), async (body) => { + body = Buffer.from(body) + const results = {} + await clientFuzzHeaders(address, results, body) + }) + ) + }) + + test('options', async () => { + const address = `http://localhost:${netServer.address().port}` + await fc.assert( + fc.asyncProperty(fc.uint8Array(), async (body) => { + body = Buffer.from(body) + const results = {} + await clientFuzzOptions(address, results, body) + }) + ) + }) +}) diff --git a/test/fuzzing/server/index.js b/test/fuzzing/server/index.js index 4bef5540534..dbd24f3cc98 100644 --- a/test/fuzzing/server/index.js +++ b/test/fuzzing/server/index.js @@ -1,6 +1,15 @@ 'use strict' -module.exports = { - splitData: require('./server-fuzz-split-data'), - appendData: require('./server-fuzz-append-data') -} +const net = require('node:net') +const serverFuzzFns = [ + require('./server-fuzz-append-data'), + require('./server-fuzz-split-data') +] + +const netServer = net.createServer(socket => { + socket.on('data', data => { + serverFuzzFns[(Math.random() * 2) | 0](socket, data) + }) +}) + +module.exports = netServer