New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fuzz testing #1712
base: v6
Are you sure you want to change the base?
Fuzz testing #1712
Conversation
because it is not super useul
since coverage does not work as expected at all, we can use esm. So if jazzer at some point, add esm support for esm, we can easily add it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for setting this up! 🙌
I left some comments, most of them are minor, but I'm especially interested in the CI management part 😄
test/fuzz/readKeyBinary.js
Outdated
* @param { Buffer } inputData | ||
*/ | ||
export function fuzz (inputData) { | ||
const binaryKey = new Uint8Array(`-----BEGIN PGP PRIVATE KEY BLOCK-----\n ${inputData.toString('base64')} -----END PGP PRIVATE KEY BLOCK-----`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this test was finalised or missed by mistake?
I don't think this line does what would be expected: it just takes the string as "number" and uses it as size of the array 😄
BTW, a OpenPGP binary input does not correspond to an armored one in Uint8Array format. I think we could just pass the fuzzed/random input as binaryKey.
test/fuzz/readMessageText.js
Outdated
@@ -0,0 +1,27 @@ | |||
import { FuzzedDataProvider } from '@jazzer.js/core'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor, but for consistency, this file should be name readMessage**Armored**.js
🙂
test/fuzz/createCleartextMessage.js
Outdated
@@ -0,0 +1,15 @@ | |||
import { FuzzedDataProvider } from '@jazzer.js/core'; | |||
|
|||
import openpgp from '../initOpenpgp.js'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor, but that entrypoint is just for mocha tests, I think it's best to use import * as openpgp from 'openpgp';
(or just import { <method> } from 'openpgp'
) in all these fuzz tests instead, to ensure we test the released bundle as-is 🙂
test/fuzz/readMessageBinary.js
Outdated
@@ -0,0 +1,22 @@ | |||
import openpgp from '../initOpenpgp.js'; | |||
|
|||
const ignored = ['This message / key probably does not conform to a valid OpenPGP format']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very minor, but I'd consider renaming ignored
to expected
, which as far as I understand it's the purpose it serves..? 🙂
test/fuzz/reports/.gitkeep
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume this is needed as the fuzz lib expects it to exist?
Is there a way to have the reports output to stdout rather than creating files? Since we are not going to commit these reports (so this folder will always be empty), and we'd like to run some fuzz tests in the CI, I think that might be preferable.
Alternatively, if the folder is needed, I'd rather have the npm run fuzz
command create it before running the fuzzer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly, this directory needs to exist. And there is no way to print the output to stout. But the naming was confusing. These are no reports, more 'artifacts'. I renamed it, to avoid confusion. The directory will be created before the fuzzer runs.
test/fuzz/createMessageBinary.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more of a comment/question for @twiss , but I'd be for skipping fuzzing these create*
methods, as there is no parsing nor input processing being done, and I think some internal crypto functions (decryption in particular) would be a better use of "fuzzing time" ..? 😄
test/fuzz/generateKey.js
Outdated
const asciiString = data.consumeString(MAX_COMMENT_LENGTH); | ||
const utf8String = data.consumeString(MAX_NAME_LENGTH, 'utf-8'); | ||
|
||
return openpgp.generateKey({ userIDs: [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to me this is testing the UserID parsing part more than the key generation itself (which happens to be a not-so-cheap function to run)?
For the same purpose, I'd directly fuzz UserID.fromObject()
.
I'm not sure we need to fuzz generateKey
in general, for similar reasons as the create*
methods (cc @twiss).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a fuzz test for UserIDPacket.fromObject()
package.json
Outdated
@@ -47,6 +47,7 @@ | |||
"prepare": "npm run build", | |||
"test": "mocha --timeout 120000 test/unittests.js", | |||
"test-type-definitions": "node --loader ts-node/esm test/typescript/definitions.ts", | |||
"fuzz": "jazzer test/fuzz/$TARGET -- -artifact_prefix=test/fuzz/reports/", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With CI testing in mind, is there a way to have this run on all the possible targets automatically?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately not.
The jest integration allows to put several fuzz tests into one describe block https://github.com/CodeIntelligenceTesting/jazzer.js/blob/main/docs/jest-integration.md#executing-jest-fuzz-tests.
For the fuzz targets, there is theoretically the --includes
option, but it does not work with several fuzz targets.
To fuzz-test several functions, we could have one javascript file with all fuzz tests inside. This way is hard to debug when an error occurs. Especially when we have tests for one function with different parameter types.
Another way can be found in this example: https://github.com/CodeIntelligenceTesting/jazzer.js/tree/main/examples/bug-detectors
Each fuzz test has its package.json and on the root, a run-all
shell script is executed, which runs each npm run test
command for each package.json
The way this works today, we envision a long-running fuzz-testing run as a pre-release procedure, rather than fuzzing on each PR, which takes a long time, or if shortened, is not very useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation 🙂 The script approach sounds good, but I'd be more comfortable running the fuzzing CI on every commits to main (incl. PR merging), where we can allow it to run for longer compared to PR commits.
I wouldn't want to find out issues as I am about to release, as that might add a considerable delay to something else we have planned on top of it.
cc @twiss , for another opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC, properly fuzzing everything would take many hours, so running it on every commit to main might not really be feasible.
Also - AIUI fuzzing is normally not primarily to find regressions, but rather to find unknown issues. Obviously the latter can lead to the former, but e.g. if we find an issue using fuzzing, we can add a test case for it to make sure it doesn't regress (once fixed).
So I think running it occasionally, e.g. after large changes have been made, is fine 😊
test/fuzz/readMessageBinary.js
Outdated
* @param { Buffer } inputData | ||
*/ | ||
export function fuzz (inputData) { | ||
const binaryMessage = new Uint8Array(`-----BEGIN PGP MESSAGE-----\n ${inputData.toString('base64')} -----END PGP MESSAGE-----`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test/fuzz/readKeyArmored.js
Outdated
const fuzzedText = data.consumeString(MAX_MESSAGE_LENGTH, 'utf-8'); | ||
const armoredKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----\n ${fuzzedText} -----END PGP PRIVATE KEY BLOCK-----`; | ||
|
||
return openpgp.readKey({ armoredKey }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most (if not all) of the functions being tested in this PR are actually async. I just want to make sure that the fuzzing lib is await
ing them..? Since it's not being done explicitly in this code (nor is the function declared async
) 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does.
Whenever jazzer.js detects that the fuzz target returns a promise it will await the completion before it calls the fuzz target again.
This is explained here https://youtu.be/KyIhxEiNnfc?t=1571
7e80bb1
to
99a506a
Compare
99a506a
to
eadc867
Compare
9c6c094
to
08cda81
Compare
(Put this already as a comment, so you see the PR description edit) |
eeba908
to
afc0432
Compare
const bufferedMessage = new Uint8Array(Buffer.from(binaryMessage, 'utf8')); | ||
return readMessage({ binaryMessage: bufferedMessage }) | ||
.catch(error => { | ||
if (error.message && !ignoredError(error)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: This ignored error occurs each time because of this check:
(peekedBytes[0] & 0x80) === 0)
here https://github.com/openpgpjs/openpgpjs/blob/v6/src/packet/packet.js#L124
How can I avoid this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error means that the bytes are not a valid OpenPGP message, and should be rejected. So - for many byte arrays this error will be correct, and should indeed be ignored. In fact, I think there will be many other error messages that are (usually/hopefully) expected and correct, e.g. for malformed packets, etc.
I'm not super sure what's the easiest way to detect whether an error is "expected" or not; certainly things like TypeErrors will be unexpected, but those are also probably unlikely to happen purely based on the message contents (but perhaps not impossible).
Another option could be to check whether the message is "supposed" to be valid, e.g. with another OpenPGP implementation, though that would probably be a lot of work, and you might then run into many minor differences in parsing that may or may not be important (but might still be interesting).
afc0432
to
02adea3
Compare
b340608
to
b41298a
Compare
PR description
This PR is not meant as a comprehensive Fuzz suite, just the scaffolding with a few API entries that should be easy to extend.
Relates to #1689
Adds fuzz test for
Running a fuzz test
The
test/fuzz
directory contains fuzz targets like for examplecreateMessageBinary
.You can run one fuzz target:
Notice, that
TARGET
is the name of your fuzz target module.(Don't) running with coverage
Jazzer.js, unfortunately, does not support coverage for esm tests, yet.
We've implemented the tests also with coverage, turned everything into cjs, but removed it because it is not super useful.
Because the imported modules were not read correctly, the result always shows, that the fuzzing function is 100% covered but not the function we are trying to fuzz.
We decided to use esm-tests and wait until jazzer.js adds coverage support for esm.
Then it can be adapted easily, by adding the
--coverage
option to the jazzer command.