Skip to content

Commit

Permalink
Merge pull request #176 from msgpack/jsfuzz
Browse files Browse the repository at this point in the history
Introduce Fuzzing with jsFuzz and fix issues found by fuzzing
  • Loading branch information
gfx authored May 4, 2021
2 parents 8b1e562 + ae09f48 commit c66d264
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 19 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/jsfuzz

name: Fuzz

on:
push:
branches:
- main
pull_request:

jobs:
fuzzing:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: "16"

- run: npm ci
- run: npm run test:fuzz
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ isolate-*.log

# flamebearer
flamegraph.html

# jsfuzz
corpus/
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"test:cover:purejs": "npx nyc --no-clean npm run test:purejs",
"test:cover:te": "npx nyc --no-clean npm run test:te",
"test:deno": "deno test test/deno_test.ts",
"test:fuzz": "npm exec -- jsfuzz@git+https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/jsfuzz.git --fuzzTime 60 --no-versifier test/decode.jsfuzz.js corpus",
"cover:clean": "rimraf .nyc_output coverage/",
"cover:report": "npx nyc report --reporter=text-summary --reporter=html --reporter=json",
"test:browser": "karma start --single-run",
Expand Down
16 changes: 16 additions & 0 deletions src/DecodeError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

export class DecodeError extends Error {
constructor(message: string) {
super(message);

// fix the prototype chain in a cross-platform way
const proto: typeof DecodeError.prototype = Object.create(DecodeError.prototype);
Object.setPrototypeOf(this, proto);

Object.defineProperty(this, "name", {
configurable: true,
enumerable: false,
value: DecodeError.name,
});
}
}
21 changes: 5 additions & 16 deletions src/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getInt64, getUint64 } from "./utils/int";
import { utf8DecodeJs, TEXT_DECODER_THRESHOLD, utf8DecodeTD } from "./utils/utf8";
import { createDataView, ensureUint8Array } from "./utils/typedArrays";
import { CachedKeyDecoder, KeyDecoder } from "./CachedKeyDecoder";
import { DecodeError } from "./DecodeError";

const enum State {
ARRAY,
Expand Down Expand Up @@ -60,22 +61,6 @@ const DEFAULT_MAX_LENGTH = 0xffff_ffff; // uint32_max

const sharedCachedKeyDecoder = new CachedKeyDecoder();

export class DecodeError extends Error {
constructor(message: string) {
super(message);

// fix the prototype chain in a cross-platform way
const proto: typeof DecodeError.prototype = Object.create(DecodeError.prototype);
Object.setPrototypeOf(this, proto);

Object.defineProperty(this, "name", {
configurable: true,
enumerable: false,
value: DecodeError.name,
});
}
}

export class Decoder<ContextType = undefined> {
private totalPos = 0;
private pos = 0;
Expand Down Expand Up @@ -133,6 +118,10 @@ export class Decoder<ContextType = undefined> {
return new RangeError(`Extra ${view.byteLength - pos} of ${view.byteLength} byte(s) found at buffer[${posToShow}]`);
}

/**
* @throws {DecodeError}
* @throws {RangeError}
*/
public decode(buffer: ArrayLike<number> | BufferSource): unknown {
this.reinitializeState();
this.setBuffer(buffer);
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ export { DecodeOptions };
import { decodeAsync, decodeArrayStream, decodeMultiStream, decodeStream } from "./decodeAsync";
export { decodeAsync, decodeArrayStream, decodeMultiStream, decodeStream };

import { Decoder, DecodeError } from "./Decoder";
export { Decoder, DecodeError };
import { Decoder, DataViewIndexOutOfBoundsError } from "./Decoder";
import { DecodeError } from "./DecodeError";
export { Decoder, DecodeError, DataViewIndexOutOfBoundsError };

import { Encoder } from "./Encoder";
export { Encoder };
Expand Down
3 changes: 2 additions & 1 deletion src/timestamp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type
import { DecodeError } from "./DecodeError";
import { getInt64, setInt64 } from "./utils/int";

export const EXT_TIMESTAMP = -1;
Expand Down Expand Up @@ -91,7 +92,7 @@ export function decodeTimestampToTimeSpec(data: Uint8Array): TimeSpec {
return { sec, nsec };
}
default:
throw new Error(`Unrecognized data size for timestamp: ${data.length}`);
throw new DecodeError(`Unrecognized data size for timestamp (expected 4, 8, or 12): ${data.length}`);
}
}

Expand Down
30 changes: 30 additions & 0 deletions test/decode.jsfuzz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable */
const assert = require("assert");
const { Decoder, encode, DecodeError } = require("../dist/index.js");

/**
* @param {Buffer} bytes
* @returns {void}
*/
module.exports.fuzz = function fuzz(bytes) {
const decoder = new Decoder();
try {
decoder.decode(bytes);
} catch (e) {
if (e instanceof DecodeError) {
// ok
} else if (e instanceof RangeError) {
// ok
} else {
throw e;
}
}

// make sure the decoder instance is not broken
const object = {
foo: 1,
bar: 2,
baz: ["one", "two", "three"],
};
assert.deepStrictEqual(decoder.decode(encode(object)), object);
}

0 comments on commit c66d264

Please sign in to comment.