Skip to content

Commit c66d264

Browse files
authored
Merge pull request msgpack#176 from msgpack/jsfuzz
Introduce Fuzzing with jsFuzz and fix issues found by fuzzing
2 parents 8b1e562 + ae09f48 commit c66d264

File tree

8 files changed

+83
-19
lines changed

8 files changed

+83
-19
lines changed

.github/workflows/fuzz.yml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/jsfuzz
2+
3+
name: Fuzz
4+
5+
on:
6+
push:
7+
branches:
8+
- main
9+
pull_request:
10+
11+
jobs:
12+
fuzzing:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v2
17+
- name: Setup Node.js
18+
uses: actions/setup-node@v1
19+
with:
20+
node-version: "16"
21+
22+
- run: npm ci
23+
- run: npm run test:fuzz

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ isolate-*.log
1111

1212
# flamebearer
1313
flamegraph.html
14+
15+
# jsfuzz
16+
corpus/

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"test:cover:purejs": "npx nyc --no-clean npm run test:purejs",
2424
"test:cover:te": "npx nyc --no-clean npm run test:te",
2525
"test:deno": "deno test test/deno_test.ts",
26+
"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",
2627
"cover:clean": "rimraf .nyc_output coverage/",
2728
"cover:report": "npx nyc report --reporter=text-summary --reporter=html --reporter=json",
2829
"test:browser": "karma start --single-run",

src/DecodeError.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
export class DecodeError extends Error {
3+
constructor(message: string) {
4+
super(message);
5+
6+
// fix the prototype chain in a cross-platform way
7+
const proto: typeof DecodeError.prototype = Object.create(DecodeError.prototype);
8+
Object.setPrototypeOf(this, proto);
9+
10+
Object.defineProperty(this, "name", {
11+
configurable: true,
12+
enumerable: false,
13+
value: DecodeError.name,
14+
});
15+
}
16+
}

src/Decoder.ts

+5-16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getInt64, getUint64 } from "./utils/int";
44
import { utf8DecodeJs, TEXT_DECODER_THRESHOLD, utf8DecodeTD } from "./utils/utf8";
55
import { createDataView, ensureUint8Array } from "./utils/typedArrays";
66
import { CachedKeyDecoder, KeyDecoder } from "./CachedKeyDecoder";
7+
import { DecodeError } from "./DecodeError";
78

89
const enum State {
910
ARRAY,
@@ -60,22 +61,6 @@ const DEFAULT_MAX_LENGTH = 0xffff_ffff; // uint32_max
6061

6162
const sharedCachedKeyDecoder = new CachedKeyDecoder();
6263

63-
export class DecodeError extends Error {
64-
constructor(message: string) {
65-
super(message);
66-
67-
// fix the prototype chain in a cross-platform way
68-
const proto: typeof DecodeError.prototype = Object.create(DecodeError.prototype);
69-
Object.setPrototypeOf(this, proto);
70-
71-
Object.defineProperty(this, "name", {
72-
configurable: true,
73-
enumerable: false,
74-
value: DecodeError.name,
75-
});
76-
}
77-
}
78-
7964
export class Decoder<ContextType = undefined> {
8065
private totalPos = 0;
8166
private pos = 0;
@@ -133,6 +118,10 @@ export class Decoder<ContextType = undefined> {
133118
return new RangeError(`Extra ${view.byteLength - pos} of ${view.byteLength} byte(s) found at buffer[${posToShow}]`);
134119
}
135120

121+
/**
122+
* @throws {DecodeError}
123+
* @throws {RangeError}
124+
*/
136125
public decode(buffer: ArrayLike<number> | BufferSource): unknown {
137126
this.reinitializeState();
138127
this.setBuffer(buffer);

src/index.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ export { DecodeOptions };
1313
import { decodeAsync, decodeArrayStream, decodeMultiStream, decodeStream } from "./decodeAsync";
1414
export { decodeAsync, decodeArrayStream, decodeMultiStream, decodeStream };
1515

16-
import { Decoder, DecodeError } from "./Decoder";
17-
export { Decoder, DecodeError };
16+
import { Decoder, DataViewIndexOutOfBoundsError } from "./Decoder";
17+
import { DecodeError } from "./DecodeError";
18+
export { Decoder, DecodeError, DataViewIndexOutOfBoundsError };
1819

1920
import { Encoder } from "./Encoder";
2021
export { Encoder };

src/timestamp.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type
2+
import { DecodeError } from "./DecodeError";
23
import { getInt64, setInt64 } from "./utils/int";
34

45
export const EXT_TIMESTAMP = -1;
@@ -91,7 +92,7 @@ export function decodeTimestampToTimeSpec(data: Uint8Array): TimeSpec {
9192
return { sec, nsec };
9293
}
9394
default:
94-
throw new Error(`Unrecognized data size for timestamp: ${data.length}`);
95+
throw new DecodeError(`Unrecognized data size for timestamp (expected 4, 8, or 12): ${data.length}`);
9596
}
9697
}
9798

test/decode.jsfuzz.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* eslint-disable */
2+
const assert = require("assert");
3+
const { Decoder, encode, DecodeError } = require("../dist/index.js");
4+
5+
/**
6+
* @param {Buffer} bytes
7+
* @returns {void}
8+
*/
9+
module.exports.fuzz = function fuzz(bytes) {
10+
const decoder = new Decoder();
11+
try {
12+
decoder.decode(bytes);
13+
} catch (e) {
14+
if (e instanceof DecodeError) {
15+
// ok
16+
} else if (e instanceof RangeError) {
17+
// ok
18+
} else {
19+
throw e;
20+
}
21+
}
22+
23+
// make sure the decoder instance is not broken
24+
const object = {
25+
foo: 1,
26+
bar: 2,
27+
baz: ["one", "two", "three"],
28+
};
29+
assert.deepStrictEqual(decoder.decode(encode(object)), object);
30+
}

0 commit comments

Comments
 (0)