Skip to content

Commit cf14e41

Browse files
committed
[MERGE #5174 @jackhorton] OS#17513493 (OSS-Fuzz 7950): add workaround for redefined option getters
Merge pull request #5174 from jackhorton:intl/os17513493 See tc39/ecma402#237
2 parents 6bbd218 + 1dd2d2d commit cf14e41

File tree

2 files changed

+55
-5
lines changed

2 files changed

+55
-5
lines changed

lib/Runtime/Library/IntlEngineInterfaceExtensionObject.cpp

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,12 +330,12 @@ namespace Js
330330
typedef FinalizableICUObject<UPluralRules *, uplrules_close> FinalizableUPluralRules;
331331

332332
template<typename TExecutor>
333-
static void EnsureBuffer(_In_ TExecutor executor, _In_ Recycler *recycler, _Outptr_result_buffer_(returnLength) char16 **ret, _Out_ int *returnLength, _In_ int firstTryLength = 8)
333+
static void EnsureBuffer(_In_ TExecutor executor, _In_ Recycler *recycler, _Outptr_result_buffer_(returnLength) char16 **ret, _Out_ int *returnLength, _In_ bool allowZeroLengthStrings = false, _In_ int firstTryLength = 8)
334334
{
335335
UErrorCode status = U_ZERO_ERROR;
336336
*ret = RecyclerNewArrayLeaf(recycler, char16, firstTryLength);
337337
*returnLength = executor(reinterpret_cast<UChar *>(*ret), firstTryLength, &status);
338-
AssertOrFailFastMsg(*returnLength > 0, "Executor reported needing negative bytes");
338+
AssertOrFailFast(allowZeroLengthStrings ? *returnLength >= 0 : *returnLength > 0);
339339
if (ICU_BUFFER_FAILURE(status))
340340
{
341341
AssertOrFailFastMsg(*returnLength >= firstTryLength, "Executor reported buffer failure but did not require additional space");
@@ -2665,6 +2665,9 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
26652665
);
26662666
}
26672667

2668+
// We intentionally special-case the following two calls to EnsureBuffer to allow zero-length strings.
2669+
// See comment in GetPatternForSkeleton.
2670+
26682671
char16 *formatted = nullptr;
26692672
int formattedLen = 0;
26702673
if (!toParts)
@@ -2673,7 +2676,7 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
26732676
EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status)
26742677
{
26752678
return udat_format(*dtf, date, buf, bufLen, nullptr, status);
2676-
}, scriptContext->GetRecycler(), &formatted, &formattedLen);
2679+
}, scriptContext->GetRecycler(), &formatted, &formattedLen, /* allowZeroLengthStrings */ true);
26772680
return JavascriptString::NewWithBuffer(formatted, formattedLen, scriptContext);
26782681
}
26792682

@@ -2683,7 +2686,7 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
26832686
EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status)
26842687
{
26852688
return udat_formatForFields(*dtf, date, buf, bufLen, fpi, status);
2686-
}, scriptContext->GetRecycler(), &formatted, &formattedLen);
2689+
}, scriptContext->GetRecycler(), &formatted, &formattedLen, /* allowZeroLengthStrings */ true);
26872690

26882691
JavascriptLibrary *library = scriptContext->GetLibrary();
26892692
JavascriptArray* ret = library->CreateArray(0);
@@ -2807,6 +2810,14 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
28072810

28082811
char16 *formatted = nullptr;
28092812
int formattedLen = 0;
2813+
2814+
// OS#17513493 (OSS-Fuzz 7950): It is possible for the skeleton to be a zero-length string
2815+
// because [[Get]] operations are performed on the options object twice, according to spec.
2816+
// Follow-up spec discussion here: https://github.com/tc39/ecma402/issues/237.
2817+
// We need to special-case this because calling udatpg_getBestPatternWithOptions on an empty skeleton
2818+
// will produce an empty pattern, which causes an assert in EnsureBuffer by default.
2819+
// As a result, we pass a final optional parameter to EnsureBuffer to say that zero-length results are OK.
2820+
// TODO(jahorto): re-visit this workaround and the one in FormatDateTime upon resolution of the spec issue.
28102821
EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status)
28112822
{
28122823
return udatpg_getBestPatternWithOptions(
@@ -2818,7 +2829,7 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
28182829
bufLen,
28192830
status
28202831
);
2821-
}, scriptContext->GetRecycler(), &formatted, &formattedLen);
2832+
}, scriptContext->GetRecycler(), &formatted, &formattedLen, /* allowZeroLengthStrings */ true);
28222833

28232834
INTL_TRACE("Best pattern '%s' will be used for skeleton '%s' and langtag '%s'", formatted, skeleton->GetSz(), langtag->GetSz());
28242835

test/Intl/common.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,44 @@ testRunner.runTests([
4040
testWithVariants(["invalid", "INVALID", "invaLID"]);
4141
}
4242
}
43+
},
44+
{
45+
name: "OSS-Fuzz #7950: Have option getters redefine themselves",
46+
body() {
47+
if (WScript.Platform.INTL_LIBRARY === "winglob") {
48+
return;
49+
}
50+
51+
function test(callback, optionName, optionValue, shouldCallSecondGetter) {
52+
const options = {};
53+
let calledSecondGetter = false;
54+
Object.defineProperty(options, optionName, {
55+
get() {
56+
Object.defineProperty(options, optionName, {
57+
get() {
58+
calledSecondGetter = true;
59+
return undefined;
60+
}
61+
});
62+
63+
return optionValue;
64+
},
65+
configurable: true
66+
});
67+
68+
callback(options);
69+
assert.areEqual(shouldCallSecondGetter, calledSecondGetter, "Second getter behavior was incorrect");
70+
}
71+
72+
test((options) => assert.areEqual(1, new Intl.Collator("en-US", options).compare("A", "a")), "sensitivity", "case", false);
73+
test((options) => assert.areEqual(-1, new Intl.Collator("en-US", options).compare("A", "B")), "sensitivity", "case", false);
74+
test((options) => assert.areEqual(0, new Intl.Collator("en-US", options).compare("a", "\u00E2")), "sensitivity", "case", false);
75+
test((options) => assert.areEqual("1000", new Intl.NumberFormat("en-US", options).format(1000)), "useGrouping", false, false);
76+
test((options) => assert.areEqual("$1.50", new Intl.NumberFormat("en-US", Object.assign(options, { currency: "USD" })).format(1.5)), "style", "currency", false);
77+
78+
// This was the original bug - at present, all browsers format the string as "" because the value returned by the second getter dictates format selection
79+
test((options) => assert.areEqual("", new Intl.DateTimeFormat("en-US", options).format()), "year", "numeric", true);
80+
test((options) => assert.areEqual("", new Intl.DateTimeFormat("en-US", options).format()), "minute", "numeric", true);
81+
}
4382
}
4483
], { verbose: false });

0 commit comments

Comments
 (0)