Skip to content

Commit 641ac90

Browse files
committed
bug detectors: track hooks of built-in modules
- in verbose mode, print module names with full path (for non built-in modules only) to make it easier to debug hooked functions
1 parent 606cc15 commit 641ac90

File tree

13 files changed

+230
-154
lines changed

13 files changed

+230
-154
lines changed

examples/bug-detectors/path-traversal/fuzz.js

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,20 @@ const { FuzzedDataProvider } = require("@jazzer.js/core");
2929
module.exports.fuzz = function (data) {
3030
// Parse the buffer into a JSZip object. The buffer might have been obtained from an http-request.
3131
// See https://stuk.github.io/jszip/documentation/howto/read_zip.html for some examples.
32-
JSZip.loadAsync(data)
33-
.then(function (zip) {
34-
for (const file in zip.files) {
35-
// We might want to extract the file from the zip archive and write it to disk.
36-
// The loadAsync function should have sanitized the path already.
37-
// Here we only construct the absolute path and trigger the path traversal bug.
38-
// This issue was fixed in jszip 3.8.0.
39-
path.join(__dirname, file);
40-
}
41-
})
42-
.catch(function (err) {
43-
// ignore broken zip files
44-
});
32+
return (
33+
JSZip.loadAsync(data)
34+
.then((zip) => {
35+
for (const file in zip.files) {
36+
// We might want to extract the file from the zip archive and write it to disk.
37+
// The loadAsync function should have sanitized the path already.
38+
// Here we only construct the absolute path and trigger the path traversal bug.
39+
// This issue was fixed in jszip 3.8.0.
40+
path.join(__dirname, file);
41+
}
42+
})
43+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
44+
.catch(() => {
45+
/* ignore broken zip files */
46+
})
47+
);
4548
};

examples/bug-detectors/path-traversal/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
"jszip": "3.7.1"
88
},
99
"scripts": {
10-
"bugDetectors": "jazzer fuzz -i fuzz.js -i jszip --timeout=100000000 corpus -- -runs=10000000 -print_final_stats=1 -use_value_profile=1 -max_len=600",
11-
"merge": "jazzer fuzz -i jszip --timeout=100000000 --corpus=new --corpus=corpus -- -runs=1 -use_value_profile=1 -merge=1",
10+
"fuzz": "jazzer fuzz -i fuzz.js -i jszip corpus -- -runs=10000000 -print_final_stats=1 -use_value_profile=1 -max_len=600 -seed=123456789",
1211
"dryRun": "jazzer fuzz --sync -x Error -- -runs=100000 -seed=123456789"
1312
},
1413
"devDependencies": {

package-lock.json

Lines changed: 2 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/bug-detectors/findings.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*
16-
* Examples showcasing the custom hooks API
1716
*/
1817

1918
export class Finding extends Error {}

packages/bug-detectors/internal/path-traversal.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { guideTowardsContainment } from "@jazzer.js/fuzzer";
1919
import { registerBeforeHook } from "@jazzer.js/hooking";
2020

2121
/**
22-
* Importing this file adds "before-hooks" for all functions in the built-in `child_process` module and guides
22+
* Importing this file adds "before-hooks" for all functions in the built-in `fs`, `fs/promises`, and `path` module and guides
2323
* the fuzzer towards the uniquely chosen `goal` string `"../../jaz_zer"`. If the goal is found in the first argument
2424
* of any hooked function, a `Finding` is reported.
2525
*/
@@ -36,16 +36,10 @@ const modulesToHook = [
3636
"chown",
3737
"chownSync",
3838
"chmodSync",
39-
"copyFile",
40-
"copyFileSync",
41-
"cp",
42-
"cpSync",
4339
"createReadStream",
4440
"createWriteStream",
4541
"exists",
4642
"existsSync",
47-
"link",
48-
"linkSync",
4943
"lchmod",
5044
"lchmodSync",
5145
"lchown",
@@ -69,8 +63,6 @@ const modulesToHook = [
6963
"readdirSync",
7064
"realpath",
7165
"realpathSync",
72-
"rename",
73-
"renameSync",
7466
"rm",
7567
"rmSync",
7668
"rmdir",
@@ -79,8 +71,6 @@ const modulesToHook = [
7971
"statfs",
8072
"statfsSync",
8173
"statSync",
82-
"symlink",
83-
"symlinkSync",
8474
"truncate",
8575
"truncateSync",
8676
"unlink",
@@ -101,11 +91,8 @@ const modulesToHook = [
10191
"appendFile",
10292
"chmod",
10393
"chown",
104-
"copyFile",
105-
"cp",
10694
"lchmod",
10795
"lchown",
108-
"link",
10996
"lstat",
11097
"lutimes",
11198
"mkdir",
@@ -115,12 +102,10 @@ const modulesToHook = [
115102
"readlink",
116103
"readdir",
117104
"realpath",
118-
"rename",
119105
"rm",
120106
"rmdir",
121107
"stat",
122108
"statfs",
123-
"symlink",
124109
"truncate",
125110
"unlink",
126111
"utimes",

packages/bug-detectors/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
"main": "dist/index.js",
1717
"types": "dist/index.d.js",
1818
"dependencies": {
19-
"@jazzer.js/fuzzer": "*",
20-
"@jazzer.js/instrumentor": "*"
19+
"@jazzer.js/fuzzer": "*"
2120
},
2221
"devDependencies": {},
2322
"engines": {

packages/core/core.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export interface Options {
6363
coverage: boolean; // Enables source code coverage report generation.
6464
coverageDirectory: string;
6565
coverageReporters: reports.ReportType[];
66-
disableBugDetectors: RegExp[];
66+
disableBugDetectors: string[];
6767
}
6868

6969
interface FuzzModule {
@@ -100,7 +100,9 @@ export async function initFuzzing(options: Options) {
100100
// above. However, the path the bug detectors must be the compiled path. For this reason we decided to load them
101101
// using this function, which loads each bug detector relative to the bug-detectors directory. E.g., in Jazzer
102102
// (without the .js) there is no distinction between custom hooks and bug detectors.
103-
await loadBugDetectors(options.disableBugDetectors);
103+
await loadBugDetectors(
104+
options.disableBugDetectors.map((pattern: string) => new RegExp(pattern))
105+
);
104106

105107
// Built-in functions cannot be hooked by the instrumentor, so we manually hook them here.
106108
await hookBuiltInFunctions(hooking.hookManager);
@@ -239,7 +241,7 @@ function stopFuzzing(
239241
) {
240242
const stopFuzzing = sync ? Fuzzer.stopFuzzing : Fuzzer.stopFuzzingAsync;
241243
if (process.env.JAZZER_DEBUG) {
242-
hooking.trackedHooks.categorizeUnknown(HookManager.hooks).print();
244+
hooking.hookTracker.categorizeUnknown(HookManager.hooks).print();
243245
}
244246
// Generate a coverage report in fuzzing mode (non-jest). The coverage report for our jest-runner is generated
245247
// by jest internally (as long as '--coverage' is set).

packages/hooking/hook.ts

Lines changed: 109 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,47 +14,136 @@
1414
* limitations under the License.
1515
*/
1616

17-
/*eslint @typescript-eslint/no-explicit-any: 0 */
17+
export interface TrackedHook {
18+
target: string;
19+
pkg: string;
20+
}
1821

19-
class hookTracker {
20-
public applied: Set<string> = new Set();
21-
public available: Set<string> = new Set();
22-
public notApplied: Set<string> = new Set();
22+
// HookTracker keeps track of hooks that were applied, are available, and were not applied.
23+
// This is helpful when debugging custom hooks and bug detectors.
24+
class HookTracker {
25+
private _applied = new HookTable();
26+
private _available = new HookTable();
27+
private _notApplied = new HookTable();
2328

2429
print() {
2530
console.log("DEBUG: [Hook] Summary:");
26-
console.log("DEBUG: [Hook] Not applied:");
27-
[...this.notApplied].sort().forEach((hook) => {
28-
console.log(`DEBUG: [Hook] ${hook}`);
31+
console.log("DEBUG: [Hook] Not applied: " + this._notApplied.length);
32+
this._notApplied.serialize().forEach((hook) => {
33+
console.log(`DEBUG: [Hook] not applied: ${hook.pkg} -> ${hook.target}`);
2934
});
30-
console.log("DEBUG: [Hook] Applied:");
31-
[...this.applied].sort().forEach((hook) => {
32-
console.log(`DEBUG: [Hook] ${hook}`);
35+
console.log("DEBUG: [Hook] Applied: " + this._applied.length);
36+
this._applied.serialize().forEach((hook) => {
37+
console.log(`DEBUG: [Hook] applied: ${hook.pkg} -> ${hook.target}`);
3338
});
34-
console.log("DEBUG: [Hook] Available:");
35-
[...this.available].sort().forEach((hook) => {
36-
console.log(`DEBUG: [Hook] ${hook}`);
39+
console.log("DEBUG: [Hook] Available: " + this._available.length);
40+
this._available.serialize().forEach((hook) => {
41+
console.log(`DEBUG: [Hook] available: ${hook.pkg} -> ${hook.target}`);
3742
});
3843
}
3944

4045
categorizeUnknown(requestedHooks: Hook[]): this {
4146
requestedHooks.forEach((hook) => {
42-
if (!this.applied.has(hook.target) && !this.available.has(hook.target)) {
43-
this.notApplied.add(hook.target);
47+
if (
48+
!this._applied.has(hook.pkg, hook.target) &&
49+
!this._available.has(hook.pkg, hook.target)
50+
) {
51+
this.addNotApplied(hook.pkg, hook.target);
4452
}
4553
});
4654
return this;
4755
}
4856

4957
clear() {
50-
this.applied.clear();
51-
this.notApplied.clear();
52-
this.available.clear();
58+
this._applied.clear();
59+
this._notApplied.clear();
60+
this._available.clear();
61+
}
62+
63+
addApplied(pkg: string, target: string) {
64+
this._applied.add(pkg, target);
65+
}
66+
67+
addAvailable(pkg: string, target: string) {
68+
this._available.add(pkg, target);
69+
}
70+
71+
addNotApplied(pkg: string, target: string) {
72+
this._notApplied.add(pkg, target);
73+
}
74+
75+
get applied(): TrackedHook[] {
76+
return this._applied.serialize();
77+
}
78+
79+
get available(): TrackedHook[] {
80+
return this._available.serialize();
81+
}
82+
83+
get notApplied(): TrackedHook[] {
84+
return this._notApplied.serialize();
5385
}
5486
}
5587

56-
export const trackedHooks = new hookTracker();
88+
// Stores package names and names of functions of interest (targets) from that package [packageName0 -> [target0, ...], ...].
89+
// This structure is used to keep track of all functions seen during instrumentation and execution of the fuzzing run,
90+
// to determine which hooks have been applied, are available, and have not been applied.
91+
class HookTable {
92+
hooks: Map<string, Set<string>> = new Map();
93+
94+
add(pkg: string, target: string) {
95+
if (!this.hooks.has(pkg)) {
96+
this.hooks.set(pkg, new Set());
97+
}
98+
this.hooks.get(pkg)?.add(target);
99+
}
57100

101+
has(pkg: string, target: string) {
102+
if (!this.hooks.has(pkg)) {
103+
return false;
104+
}
105+
return this.hooks.get(pkg)?.has(target);
106+
}
107+
108+
serialize(): TrackedHook[] {
109+
const result: TrackedHook[] = [];
110+
for (const [pkg, targets] of [...this.hooks].sort()) {
111+
for (const target of [...targets].sort()) {
112+
result.push({ pkg: pkg, target: target });
113+
}
114+
}
115+
return result;
116+
}
117+
118+
clear() {
119+
this.hooks.clear();
120+
}
121+
122+
get length() {
123+
let size = 0;
124+
for (const targets of this.hooks.values()) {
125+
size += targets.size;
126+
}
127+
return size;
128+
}
129+
}
130+
131+
export function logHooks(hooks: Hook[]) {
132+
hooks.forEach((hook) => {
133+
if (process.env.JAZZER_DEBUG) {
134+
console.log(
135+
`DEBUG: Applied %s-hook in %s#%s`,
136+
HookType[hook.type],
137+
hook.pkg,
138+
hook.target
139+
);
140+
}
141+
});
142+
}
143+
144+
export const hookTracker = new HookTracker();
145+
146+
/*eslint @typescript-eslint/no-explicit-any: 0 */
58147
export enum HookType {
59148
Before,
60149
After,

packages/hooking/manager.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
HookFn,
2222
HookType,
2323
ReplaceHookFn,
24+
logHooks,
25+
hookTracker,
2426
} from "./hook";
2527

2628
export class MatchingHooksResult {
@@ -247,5 +249,9 @@ export async function hookBuiltInFunction(hook: Hook): Promise<void> {
247249
const result: unknown = originalFn(...args);
248250
return (hook.hookFunction as AfterHookFn)(null, args, id, result);
249251
};
252+
} else {
253+
throw new Error(`Unknown hook type ${hook.type}`);
250254
}
255+
logHooks([hook]);
256+
hookTracker.addApplied(hook.pkg, hook.target);
251257
}

0 commit comments

Comments
 (0)