THRIFT-5855: Add nodejs fuzzers
Add fuzzers for nodejs support, to improve the reliability/robustness of the implementation
diff --git a/FUZZING.md b/FUZZING.md
index 5cd4418..9ed7fb8 100644
--- a/FUZZING.md
+++ b/FUZZING.md
@@ -19,13 +19,13 @@
- c_glib (partially supported, needs round-trip support)
- C++
- Java/JVM (and other JVM languages)
+- JavaScript
We are working on adding fuzzers for the following languages:
- Rust
- Swift
- Python
-- JavaScript
- netstd
## Fuzzer Types
diff --git a/lib/nodejs/Makefile.am b/lib/nodejs/Makefile.am
index 5be8161..0933701 100644
--- a/lib/nodejs/Makefile.am
+++ b/lib/nodejs/Makefile.am
@@ -17,8 +17,11 @@
# We call npm twice to work around npm issues
-stubs: $(top_srcdir)/test/v0.16/ThriftTest.thrift
+stubs: $(top_srcdir)/test/v0.16/ThriftTest.thrift $(top_srcdir)/test/v0.16/FuzzTestNoUuid.thrift
$(THRIFT) --gen js:node -o test/ $(top_srcdir)/test/v0.16/ThriftTest.thrift
+ $(THRIFT) --gen js:node -o test/fuzz/ $(top_srcdir)/test/v0.16/FuzzTestNoUuid.thrift
+ sed -i "s/require('thrift')/require(\"..\/..\/..\/lib\/thrift\")/" test/fuzz/gen-nodejs/FuzzTestNoUuid_types.js
+
deps-root: $(top_srcdir)/package.json
$(NPM) install $(top_srcdir)/ || $(NPM) install $(top_srcdir)/
@@ -38,6 +41,7 @@
clean-local:
$(RM) -r test/gen-*
+ $(RM) -r test/fuzz/gen-*
$(RM) -r $(top_srcdir)/node_modules
$(RM) -r test/episodic-code-generation-test/gen*
$(RM) -r test/episodic-code-generation-test/node_modules
diff --git a/lib/nodejs/test/fuzz/README.md b/lib/nodejs/test/fuzz/README.md
new file mode 100644
index 0000000..aad48c0
--- /dev/null
+++ b/lib/nodejs/test/fuzz/README.md
@@ -0,0 +1,56 @@
+# Node.js Fuzzing README
+
+The Node.js Thrift implementation uses Jazzer.js for fuzzing. Jazzer.js is a coverage-guided, in-process fuzzer for JavaScript that integrates with libFuzzer.
+
+## Setup
+
+1. Install Jazzer.js:
+```bash
+npm install --save-dev @jazzer.js/core
+```
+
+## Available Fuzzers
+
+The Node.js Thrift implementation currently supports the following fuzz targets:
+
+* `fuzz_parse_TJSONProtocol.js` - fuzzes the deserialization of the JSON protocol
+* `fuzz_roundtrip_TJSONProtocol.js` - fuzzes the roundtrip of the JSON protocol (serialize -> deserialize -> compare)
+* `fuzz_parse_TBinaryProtocol.js` - fuzzes the deserialization of the Binary protocol
+* `fuzz_roundtrip_TBinaryProtocol.js` - fuzzes the roundtrip of the Binary protocol
+* `fuzz_parse_TCompactProtocol.js` - fuzzes the deserialization of the Compact protocol
+* `fuzz_roundtrip_TCompactProtocol.js` - fuzzes the roundtrip of the Compact protocol
+
+## Running Fuzzers
+
+To run a fuzzer, use the Jazzer.js CLI:
+
+```bash
+npx jazzer ./fuzz_parse_TJSONProtocol.js --corpus=./corpus
+```
+
+Where:
+- `--corpus` points to a directory containing seed inputs (optional)
+
+## Corpus Generation
+
+You can use the corpus generator from the Rust implementation to generate initial corpus files that can be used with these Node.js fuzzers. For JSON protocol fuzzers, ensure the corpus contains valid JSON data.
+
+## Adding New Fuzzers
+
+To add a new fuzzer:
+
+1. Create a new file in the `fuzz` directory
+2. Import the appropriate helper functions from `fuzz_common.js`
+3. Export a `fuzz` function that takes a Buffer parameter
+4. Use either `createParserFuzzer` or `createRoundtripFuzzer` with the appropriate protocol factory
+
+Example:
+```javascript
+const { createParserFuzzer } = require('./fuzz_common');
+
+module.exports.fuzz = createParserFuzzer((transport) => {
+ return new thrift.TJSONProtocol(transport);
+});
+```
+
+For more information about Jazzer.js and its options, see the [Jazzer.js documentation](https://github.com/CodeIntelligenceTesting/jazzer.js).
\ No newline at end of file
diff --git a/lib/nodejs/test/fuzz/fuzz_common.js b/lib/nodejs/test/fuzz/fuzz_common.js
new file mode 100644
index 0000000..1eb5f79
--- /dev/null
+++ b/lib/nodejs/test/fuzz/fuzz_common.js
@@ -0,0 +1,107 @@
+const thrift = require("../../lib/thrift");
+const FuzzTest = require("./gen-nodejs/FuzzTestNoUuid_types");
+// const { FuzzedDataProvider } = require("@jazzer.js/core");
+
+/**
+ * Creates a parser fuzzer function for a specific protocol
+ * @param {Function} protocolFactory - The Thrift protocol factory function
+ * @param {boolean} [readMessageBegin=false] - Whether to call readMessageBegin before reading the test instance
+ * This is needed for protocols that do not support parsing just a struct, such as TJSONProtocol.
+ * @returns {Function} A fuzzer function that can be used with Jazzer.js
+ */
+function createParserFuzzer(protocolFactory, readMessageBegin = false) {
+ return function fuzz(data) {
+ if (data.length < 2) {
+ return;
+ }
+
+ try {
+ // Set up transport with input data
+ const transport = new thrift.TFramedTransport(data);
+ const protocol = protocolFactory(transport);
+ const testInstance = new FuzzTest.FuzzTest();
+ if (readMessageBegin) {
+ protocol.readMessageBegin();
+ }
+ testInstance[Symbol.for("read")](protocol);
+ } catch (e) {
+ if (
+ !(
+ e.name === "InputBufferUnderrunError" ||
+ e.name === "TProtocolException"
+ )
+ ) {
+ // TODO: Are other exceptions expected?
+ // console.log(e);
+ }
+ }
+ };
+}
+
+/**
+ * Creates a roundtrip fuzzer function for a specific protocol
+ * @param {Function} protocolFactory - The Thrift protocol factory function
+ * @param {boolean} [readMessageBegin=false] - Whether to call readMessageBegin before reading the test instance
+ * This is needed for protocols that do not support parsing just a struct, such as TJSONProtocol.
+ * @returns {Function} A fuzzer function that can be used with Jazzer.js
+ */
+function createRoundtripFuzzer(protocolFactory, readMessageBegin = false) {
+ return function fuzz(data) {
+ if (data.length < 2) {
+ return;
+ }
+
+ try {
+ // First deserialize using framed transport for input
+ const transport1 = new thrift.TFramedTransport(data);
+ const protocol1 = protocolFactory(transport1);
+ const testInstance = new FuzzTest.FuzzTest();
+ if (readMessageBegin) {
+ protocol1.readMessageBegin();
+ }
+ testInstance[Symbol.for("read")](protocol1);
+
+ // Then serialize using buffered transport with callback
+ let serializedData;
+ const transport2 = new thrift.TBufferedTransport(null, function (buf) {
+ serializedData = buf;
+ });
+ const protocol2 = protocolFactory(transport2);
+ testInstance[Symbol.for("write")](protocol2);
+ protocol2.flush();
+
+ if (!serializedData) {
+ throw new Error("Serialization failed - no data produced");
+ }
+
+ // Finally deserialize again and compare using framed transport
+ const transport3 = new thrift.TFramedTransport(serializedData);
+ const protocol3 = protocolFactory(transport3);
+ const deserialized = new FuzzTest.FuzzTest();
+ if (readMessageBegin) {
+ protocol3.readMessageBegin();
+ }
+ deserialized[Symbol.for("read")](protocol3);
+
+ // Compare the objects
+ if (!testInstance.equals(deserialized)) {
+ throw new Error("Roundtrip comparison failed");
+ }
+ } catch (e) {
+ if (
+ !(
+ e.name === "InputBufferUnderrunError" ||
+ e.name === "TProtocolException"
+ )
+ ) {
+ // TODO: Are other exceptions expected?
+ // console.log(e);
+ }
+ }
+ };
+}
+
+module.exports = {
+ createParserFuzzer,
+ createRoundtripFuzzer,
+};
diff --git a/lib/nodejs/test/fuzz/fuzz_parse_TBinaryProtocol.js b/lib/nodejs/test/fuzz/fuzz_parse_TBinaryProtocol.js
new file mode 100644
index 0000000..b64a971
--- /dev/null
+++ b/lib/nodejs/test/fuzz/fuzz_parse_TBinaryProtocol.js
@@ -0,0 +1,6 @@
+const thrift = require("../../lib/thrift");
+const { createParserFuzzer } = require("./fuzz_common");
+
+module.exports.fuzz = createParserFuzzer((transport) => {
+ return new thrift.TBinaryProtocol(transport);
+});
diff --git a/lib/nodejs/test/fuzz/fuzz_parse_TCompactProtocol.js b/lib/nodejs/test/fuzz/fuzz_parse_TCompactProtocol.js
new file mode 100644
index 0000000..03209b4
--- /dev/null
+++ b/lib/nodejs/test/fuzz/fuzz_parse_TCompactProtocol.js
@@ -0,0 +1,6 @@
+const thrift = require("../../lib/thrift");
+const { createParserFuzzer } = require("./fuzz_common");
+
+module.exports.fuzz = createParserFuzzer((transport) => {
+ return new thrift.TCompactProtocol(transport);
+});
diff --git a/lib/nodejs/test/fuzz/fuzz_parse_TJSONProtocol.js b/lib/nodejs/test/fuzz/fuzz_parse_TJSONProtocol.js
new file mode 100644
index 0000000..cea20b5
--- /dev/null
+++ b/lib/nodejs/test/fuzz/fuzz_parse_TJSONProtocol.js
@@ -0,0 +1,6 @@
+const thrift = require("../../lib/thrift");
+const { createParserFuzzer } = require("./fuzz_common");
+
+module.exports.fuzz = createParserFuzzer((transport) => {
+ return new thrift.TJSONProtocol(transport);
+}, true);
diff --git a/lib/nodejs/test/fuzz/fuzz_roundtrip_TBinaryProtocol.js b/lib/nodejs/test/fuzz/fuzz_roundtrip_TBinaryProtocol.js
new file mode 100644
index 0000000..a88153c
--- /dev/null
+++ b/lib/nodejs/test/fuzz/fuzz_roundtrip_TBinaryProtocol.js
@@ -0,0 +1,6 @@
+const thrift = require("../../lib/thrift");
+const { createRoundtripFuzzer } = require("./fuzz_common");
+
+module.exports.fuzz = createRoundtripFuzzer((transport) => {
+ return new thrift.TBinaryProtocol(transport);
+});
diff --git a/lib/nodejs/test/fuzz/fuzz_roundtrip_TCompactProtocol.js b/lib/nodejs/test/fuzz/fuzz_roundtrip_TCompactProtocol.js
new file mode 100644
index 0000000..eee4457
--- /dev/null
+++ b/lib/nodejs/test/fuzz/fuzz_roundtrip_TCompactProtocol.js
@@ -0,0 +1,6 @@
+const thrift = require("../../lib/thrift");
+const { createRoundtripFuzzer } = require("./fuzz_common");
+
+module.exports.fuzz = createRoundtripFuzzer((transport) => {
+ return new thrift.TCompactProtocol(transport);
+});
diff --git a/lib/nodejs/test/fuzz/fuzz_roundtrip_TJSONProtocol.js b/lib/nodejs/test/fuzz/fuzz_roundtrip_TJSONProtocol.js
new file mode 100644
index 0000000..8d09d5f
--- /dev/null
+++ b/lib/nodejs/test/fuzz/fuzz_roundtrip_TJSONProtocol.js
@@ -0,0 +1,6 @@
+const thrift = require("../../lib/thrift");
+const { createRoundtripFuzzer } = require("./fuzz_common");
+
+module.exports.fuzz = createRoundtripFuzzer((transport) => {
+ return new thrift.TJSONProtocol(transport);
+}, true);