Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions projects/formatjs/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
################################################################################

FROM gcr.io/oss-fuzz-base/base-builder-javascript

COPY build.sh $SRC/

# formatjs is a Bazel + pnpm monorepo. Rather than reproduce that toolchain
# we set up a small fuzz workspace and install the published @formatjs/*
# packages from npm — these are the artifacts downstream users actually run.
RUN mkdir -p $SRC/formatjs

COPY package.json babel.config.json $SRC/formatjs/
COPY fuzz_icu_messageformat_parser.js \
fuzz_intl_messageformat.js \
fuzz_icu_skeleton_parser.js \
$SRC/formatjs/

WORKDIR $SRC/formatjs
4 changes: 4 additions & 0 deletions projects/formatjs/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"plugins": ["@babel/plugin-transform-modules-commonjs"],
"ignore": ["**/@jazzer.js", "**/@babel", "**/istanbul-reports"]
}
64 changes: 64 additions & 0 deletions projects/formatjs/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/bin/bash -eu
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
################################################################################

# The published @formatjs/* packages ship as ESM only (`"type": "module"`).
# Jazzer.js loads fuzz targets via CommonJS `require`, so we transpile the
# formatjs packages to CommonJS with Babel and rewrite their package.json's
# `type` field accordingly. Other packages in node_modules are already
# CommonJS and we leave them alone.
function change_type_to_commonjs() {
find "$1" -name "package.json" -type f | while read -r package_file; do
if grep -q '"type": "module"' "$package_file"; then
sed -i 's/"type": "module"/"type": "commonjs"/' "$package_file"
fi
done
}

function transform_dir_into_commonjs() {
babel "$1" --keep-file-extension --copy-files -D -d "$1"_commonjs
rm -r "$1"
mv "$1"_commonjs "$1"
}

# Install runtime dependencies (formatjs packages) and the build-time toolchain
# we need to convert ESM → CommonJS.
npm install
# Pin to Jazzer.js 2.x: Jazzer.js 4.0.0's prebuilt native addon requires
# GLIBC 2.32, which the OSS-Fuzz base images (Ubuntu 20.04 LTS, glibc 2.31)
# don't provide. Drop the pin once base images move past glibc 2.32.
npm install --save-dev '@jazzer.js/core@^2'
npm install --save-dev --global @babel/cli
npm install --save-dev @babel/core @babel/plugin-transform-modules-commonjs

# Transform only the formatjs packages we fuzz (and the intl-messageformat
# dependency tree). Other packages in node_modules are CommonJS already and
# may use modern syntax that this minimal Babel config can't handle.
for pkg in node_modules/@formatjs/* node_modules/intl-messageformat; do
if [ -d "$pkg" ]; then
transform_dir_into_commonjs "$pkg"
change_type_to_commonjs "$pkg"
fi
done

# Build fuzzers. -i restricts Jazzer.js coverage instrumentation to formatjs
# code so we don't waste cycles on Node built-ins or transitive helpers.
compile_javascript_fuzzer formatjs fuzz_icu_messageformat_parser.js \
-i @formatjs -i intl-messageformat --sync
compile_javascript_fuzzer formatjs fuzz_intl_messageformat.js \
-i @formatjs -i intl-messageformat --sync
compile_javascript_fuzzer formatjs fuzz_icu_skeleton_parser.js \
-i @formatjs -i intl-messageformat --sync
45 changes: 45 additions & 0 deletions projects/formatjs/fuzz_icu_messageformat_parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

const { FuzzedDataProvider } = require('@jazzer.js/core');
const { parse } = require('@formatjs/icu-messageformat-parser');

module.exports.fuzz = function (data) {
const fdp = new FuzzedDataProvider(data);

const opts = {
ignoreTag: fdp.consumeBoolean(),
requiresOtherClause: fdp.consumeBoolean(),
shouldParseSkeletons: fdp.consumeBoolean(),
captureLocation: fdp.consumeBoolean(),
};

const message = fdp.consumeRemainingAsString();

try {
parse(message, opts);
} catch (e) {
if (!isExpectedParserError(e)) {
throw e;
}
}
};

function isExpectedParserError(e) {
// The parser throws SyntaxError on malformed ICU messages — that's the
// documented contract. Anything else is interesting.
return e instanceof SyntaxError;
}
68 changes: 68 additions & 0 deletions projects/formatjs/fuzz_icu_skeleton_parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

const { FuzzedDataProvider } = require('@jazzer.js/core');
const skeleton = require('@formatjs/icu-skeleton-parser');

module.exports.fuzz = function (data) {
const fdp = new FuzzedDataProvider(data);
const choice = fdp.consumeIntegralInRange(0, 2);
const input = fdp.consumeRemainingAsString();

try {
switch (choice) {
case 0:
skeleton.parseNumberSkeletonFromString(input);
break;
case 1:
skeleton.parseDateTimeSkeleton(input);
break;
case 2:
// parseNumberSkeleton expects pre-tokenized tokens; feed it a single
// synthetic token so the function body still gets exercised.
skeleton.parseNumberSkeleton([{ stem: input, options: [] }]);
break;
}
} catch (e) {
if (!isExpectedError(e)) throw e;
}
};

function isExpectedError(e) {
if (e instanceof SyntaxError) return true;
if (e instanceof RangeError) return true;
if (e instanceof TypeError) return true;

// The skeleton parsers throw plain Error subclasses for malformed input
// (e.g. "Number skeleton cannot be empty", "Invalid number skeleton").
// Treat any Error whose message looks like a parse failure as expected.
if (!(e instanceof Error)) return false;
const msg = e.message ? String(e.message).toLowerCase() : '';
return EXPECTED_MESSAGE_FRAGMENTS.some((f) => msg.includes(f));
}

const EXPECTED_MESSAGE_FRAGMENTS = [
'skeleton',
'invalid',
'unexpected',
'expected',
'unsupported',
'malformed',
'must be',
'cannot be empty',
'cannot read',
'undefined',
];
119 changes: 119 additions & 0 deletions projects/formatjs/fuzz_intl_messageformat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

const { FuzzedDataProvider } = require('@jazzer.js/core');
const { IntlMessageFormat } = require('intl-messageformat');

const LOCALES = ['en', 'en-US', 'fr', 'de', 'es', 'ja', 'zh', 'ar', 'pt-BR', 'ru'];

module.exports.fuzz = function (data) {
const fdp = new FuzzedDataProvider(data);

const locale = LOCALES[fdp.consumeIntegralInRange(0, LOCALES.length - 1)];
const ignoreTag = fdp.consumeBoolean();
const requiresOtherClause = fdp.consumeBoolean();
const shouldParseSkeletons = fdp.consumeBoolean();

const message = fdp.consumeString(fdp.consumeIntegralInRange(0, 4096));
const values = generateValues(fdp);

let fmt;
try {
fmt = new IntlMessageFormat(message, locale, undefined, {
ignoreTag,
requiresOtherClause,
shouldParseSkeletons,
});
} catch (e) {
if (!isExpectedError(e)) throw e;
return;
}

try {
fmt.format(values);
} catch (e) {
if (!isExpectedError(e)) throw e;
}

try {
fmt.formatToParts(values);
} catch (e) {
if (!isExpectedError(e)) throw e;
}
};

function generateValues(fdp) {
const numKeys = fdp.consumeIntegralInRange(0, 8);
const values = {};
for (let i = 0; i < numKeys; i++) {
const key = fdp.consumeString(fdp.consumeIntegralInRange(1, 16));
if (!key) continue;
switch (fdp.consumeIntegralInRange(0, 4)) {
case 0:
values[key] = fdp.consumeIntegralInRange(-1_000_000, 1_000_000);
break;
case 1:
values[key] = fdp.consumeString(fdp.consumeIntegralInRange(0, 64));
break;
case 2:
values[key] = fdp.consumeBoolean();
break;
case 3:
// Constrain to a sane Date range to avoid Invalid Date noise.
values[key] = new Date(fdp.consumeIntegralInRange(0, 4_102_444_800_000));
break;
default:
values[key] = null;
}
}
return values;
}

function isExpectedError(e) {
if (e instanceof SyntaxError) return true;
if (e instanceof TypeError) return true;
if (e instanceof RangeError) return true;

// intl-messageformat raises Error subclasses (FormatError, MissingValueError,
// InvalidValueTypeError, …) for malformed messages or values. Filter on the
// ICU/formatjs error code or message text.
const code = e && typeof e.code === 'string' ? e.code : '';
if (code.startsWith('FORMAT_ERROR') || code.startsWith('INVALID_') ||
code === 'MISSING_VALUE' || code === 'MISSING_INTL_API') {
return true;
}

const msg = e && e.message ? String(e.message).toLowerCase() : '';
return EXPECTED_MESSAGE_FRAGMENTS.some((f) => msg.includes(f));
}

const EXPECTED_MESSAGE_FRAGMENTS = [
'is not a function',
'cannot read',
'undefined',
'invalid',
'unexpected',
'expected',
'unsupported',
'must be',
'missing',
'no value',
'not found',
'malformed',
'no other clause',
'needs other clause',
'is not finite',
];
12 changes: 12 additions & 0 deletions projects/formatjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "formatjs-fuzz",
"version": "1.0.0",
"private": true,
"description": "OSS-Fuzz fuzz targets for formatjs (https://github.com/formatjs/formatjs).",
"license": "Apache-2.0",
"dependencies": {
"@formatjs/icu-messageformat-parser": "*",
"@formatjs/icu-skeleton-parser": "*",
"intl-messageformat": "*"
}
}
8 changes: 8 additions & 0 deletions projects/formatjs/project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
homepage: "https://formatjs.github.io/"
language: javascript
primary_contact: "formatjsproject@gmail.com"
main_repo: "https://github.com/formatjs/formatjs"
fuzzing_engines:
- libfuzzer
sanitizers:
- none