From 3fe3a9321d874ac42cda049fa53ace04244ece22 Mon Sep 17 00:00:00 2001 From: Tom Schuster Date: Fri, 8 Sep 2023 19:24:44 +0000 Subject: [PATCH] Bug 1846224 - Add a JavaScript per-realm locale override. r=jandem Differential Revision: https://phabricator.services.mozilla.com/D184944 --- js/public/RealmOptions.h | 13 ++++ js/src/builtin/TestingFunctions.cpp | 26 +------- js/src/builtin/TestingUtility.cpp | 34 +++++++++++ js/src/builtin/TestingUtility.h | 4 ++ .../tests/resist-fingerprinting/locale.js | 19 ++++++ js/src/jsapi.cpp | 18 ++++++ js/src/jsdate.cpp | 59 ++++++++++--------- js/src/shell/js.cpp | 16 ++++- js/src/vm/Realm.cpp | 8 +++ js/src/vm/Realm.h | 3 + js/src/vm/SelfHosting.cpp | 4 +- 11 files changed, 149 insertions(+), 55 deletions(-) create mode 100644 js/src/jit-test/tests/resist-fingerprinting/locale.js diff --git a/js/public/RealmOptions.h b/js/public/RealmOptions.h index 41885165ddd4..89d5bc4c24ec 100644 --- a/js/public/RealmOptions.h +++ b/js/public/RealmOptions.h @@ -18,6 +18,7 @@ #include "jstypes.h" // JS_PUBLIC_API #include "js/Class.h" // JSTraceOp +#include "js/RefCounted.h" struct JS_PUBLIC_API JSContext; class JS_PUBLIC_API JSObject; @@ -60,6 +61,14 @@ enum class WeakRefSpecifier { EnabledWithoutCleanupSome }; +struct LocaleString : js::RefCounted { + const char* chars_; + + explicit LocaleString(const char* chars) : chars_(chars) {} + + auto chars() const { return chars_; } +}; + /** * RealmCreationOptions specifies options relevant to creating a new realm, that * are either immutable characteristics of that realm or that are discarded @@ -259,6 +268,9 @@ class JS_PUBLIC_API RealmCreationOptions { return *this; } + RefPtr locale() const { return locale_; } + RealmCreationOptions& setLocaleCopyZ(const char* locale); + // Always use the fdlibm implementation of math functions instead of the // platform native libc implementations. Useful for fingerprinting protection // and cross-platform consistency. @@ -282,6 +294,7 @@ class JS_PUBLIC_API RealmCreationOptions { Zone* zone_; }; uint64_t profilerRealmID_ = 0; + RefPtr locale_; WeakRefSpecifier weakRefs_ = WeakRefSpecifier::Disabled; bool invisibleToDebugger_ = false; bool preserveJitCode_ = false; diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp index 28a382aa76f8..fad2bd7f1165 100644 --- a/js/src/builtin/TestingFunctions.cpp +++ b/js/src/builtin/TestingFunctions.cpp @@ -7813,34 +7813,12 @@ static bool SetDefaultLocale(JSContext* cx, unsigned argc, Value* vp) { } if (args[0].isString() && !args[0].toString()->empty()) { - Rooted str(cx, args[0].toString()->ensureLinear(cx)); - if (!str) { - return false; - } - - if (!StringIsAscii(str)) { - ReportUsageErrorASCII(cx, callee, - "First argument contains non-ASCII characters"); - return false; - } - - UniqueChars locale = JS_EncodeStringToASCII(cx, str); + RootedString str(cx, args[0].toString()); + UniqueChars locale = StringToLocale(cx, callee, str); if (!locale) { return false; } - bool containsOnlyValidBCP47Characters = - mozilla::IsAsciiAlpha(locale[0]) && - std::all_of(locale.get(), locale.get() + str->length(), [](auto c) { - return mozilla::IsAsciiAlphanumeric(c) || c == '-'; - }); - - if (!containsOnlyValidBCP47Characters) { - ReportUsageErrorASCII(cx, callee, - "First argument should be a BCP47 language tag"); - return false; - } - if (!JS_SetDefaultLocale(cx->runtime(), locale.get())) { ReportOutOfMemory(cx); return false; diff --git a/js/src/builtin/TestingUtility.cpp b/js/src/builtin/TestingUtility.cpp index 6dc82f4360e8..091dc3bcf641 100644 --- a/js/src/builtin/TestingUtility.cpp +++ b/js/src/builtin/TestingUtility.cpp @@ -18,6 +18,7 @@ #include "js/RootingAPI.h" // JS::Rooted, JS::Handle #include "js/Utility.h" // JS::UniqueChars #include "js/Value.h" // JS::Value, JS::StringValue +#include "vm/JSContext.h" // JS::ReportUsageErrorASCII #include "vm/JSScript.h" bool js::ParseCompileOptions(JSContext* cx, JS::CompileOptions& options, @@ -255,3 +256,36 @@ bool js::ParseDebugMetadata(JSContext* cx, JS::Handle opts, return true; } + +JS::UniqueChars js::StringToLocale(JSContext* cx, JS::Handle callee, + JS::Handle str_) { + Rooted str(cx, str_->ensureLinear(cx)); + if (!str) { + return nullptr; + } + + if (!StringIsAscii(str)) { + ReportUsageErrorASCII(cx, callee, + "First argument contains non-ASCII characters"); + return nullptr; + } + + UniqueChars locale = JS_EncodeStringToASCII(cx, str); + if (!locale) { + return nullptr; + } + + bool containsOnlyValidBCP47Characters = + mozilla::IsAsciiAlpha(locale[0]) && + std::all_of(locale.get(), locale.get() + str->length(), [](auto c) { + return mozilla::IsAsciiAlphanumeric(c) || c == '-'; + }); + + if (!containsOnlyValidBCP47Characters) { + ReportUsageErrorASCII(cx, callee, + "First argument should be a BCP47 language tag"); + return nullptr; + } + + return locale; +} diff --git a/js/src/builtin/TestingUtility.h b/js/src/builtin/TestingUtility.h index 8b6243e2b824..b0e252b0fac5 100644 --- a/js/src/builtin/TestingUtility.h +++ b/js/src/builtin/TestingUtility.h @@ -59,6 +59,10 @@ JSObject* CreateScriptPrivate(JSContext* cx, JS::MutableHandle privateValue, JS::MutableHandle elementAttributeName); +[[nodiscard]] JS::UniqueChars StringToLocale(JSContext* cx, + JS::Handle callee, + JS::Handle str_); + } /* namespace js */ #endif /* builtin_TestingUtility_h */ diff --git a/js/src/jit-test/tests/resist-fingerprinting/locale.js b/js/src/jit-test/tests/resist-fingerprinting/locale.js new file mode 100644 index 000000000000..6c15ca88ae1d --- /dev/null +++ b/js/src/jit-test/tests/resist-fingerprinting/locale.js @@ -0,0 +1,19 @@ +// |jit-test| skip-if: typeof Intl === 'undefined' + +function test(locale, timeZone) { + let global = newGlobal({locale, forceUTC: true}); + + const constructors = ["Collator", "DateTimeFormat", "ListFormat", + "NumberFormat", "PluralRules", "RelativeTimeFormat"]; + for (const constructor of constructors) { + let intl = new global.Intl[constructor]; + assertEq(intl.resolvedOptions().locale, locale); + } + + const date = new global.Date(2012, 0, 10); + let tzRE = /\(([^\)]+)\)/; + assertEq(tzRE.exec(date)[1], timeZone) +} + +test("de-CH", "Koordinierte Weltzeit"); +test("en", "Coordinated Universal Time"); diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 859a64fb6cfa..c78fc95388ee 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -1722,6 +1722,24 @@ JS::RealmCreationOptions& JS::RealmCreationOptions::setCoopAndCoepEnabled( return *this; } +JS::RealmCreationOptions& JS::RealmCreationOptions::setLocaleCopyZ( + const char* locale) { + const size_t size = strlen(locale) + 1; + + AutoEnterOOMUnsafeRegion oomUnsafe; + char* memoryPtr = js_pod_malloc(sizeof(LocaleString) + size); + if (!memoryPtr) { + oomUnsafe.crash("RealmCreationOptions::setLocaleCopyZ"); + } + + char* localePtr = memoryPtr + sizeof(LocaleString); + memcpy(localePtr, locale, size); + + locale_ = new (memoryPtr) LocaleString(localePtr); + + return *this; +} + const JS::RealmBehaviors& JS::RealmBehaviorsRef(JS::Realm* realm) { return realm->behaviors(); } diff --git a/js/src/jsdate.cpp b/js/src/jsdate.cpp index 130843bb9953..88b8ced7eaae 100644 --- a/js/src/jsdate.cpp +++ b/js/src/jsdate.cpp @@ -156,7 +156,8 @@ class DateTimeHelper { static double UTC(DateTimeInfo::ForceUTC forceUTC, double t); static JSString* timeZoneComment(JSContext* cx, DateTimeInfo::ForceUTC forceUTC, - double utcTime, double localTime); + const char* locale, double utcTime, + double localTime); #if !JS_HAS_INTL_API static size_t formatTime(DateTimeInfo::ForceUTC forceUTC, char* buf, size_t buflen, const char* fmt, double utcTime, @@ -2944,8 +2945,8 @@ static bool date_toJSON(JSContext* cx, unsigned argc, Value* vp) { #if JS_HAS_INTL_API JSString* DateTimeHelper::timeZoneComment(JSContext* cx, DateTimeInfo::ForceUTC forceUTC, - double utcTime, double localTime) { - const char* locale = cx->runtime()->getDefaultLocale(); + const char* locale, double utcTime, + double localTime) { if (!locale) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEFAULT_LOCALE_ERROR); @@ -3018,7 +3019,8 @@ size_t DateTimeHelper::formatTime(DateTimeInfo::ForceUTC forceUTC, char* buf, JSString* DateTimeHelper::timeZoneComment(JSContext* cx, DateTimeInfo::ForceUTC forceUTC, - double utcTime, double localTime) { + const char* locale, double utcTime, + double localTime) { char tzbuf[100]; size_t tzlen = @@ -3055,7 +3057,7 @@ JSString* DateTimeHelper::timeZoneComment(JSContext* cx, enum class FormatSpec { DateTime, Date, Time }; static bool FormatDate(JSContext* cx, DateTimeInfo::ForceUTC forceUTC, - double utcTime, FormatSpec format, + const char* locale, double utcTime, FormatSpec format, MutableHandleValue rval) { if (!std::isfinite(utcTime)) { rval.setString(cx->names().Invalid_Date_); @@ -3091,8 +3093,8 @@ static bool FormatDate(JSContext* cx, DateTimeInfo::ForceUTC forceUTC, // also means the time zone string may not fit into Latin-1. // Get a time zone string from the OS or ICU to include as a comment. - timeZoneComment = - DateTimeHelper::timeZoneComment(cx, forceUTC, utcTime, localTime); + timeZoneComment = DateTimeHelper::timeZoneComment(cx, forceUTC, locale, + utcTime, localTime); if (!timeZoneComment) { return false; } @@ -3142,9 +3144,12 @@ static bool FormatDate(JSContext* cx, DateTimeInfo::ForceUTC forceUTC, } #if !JS_HAS_INTL_API -static bool ToLocaleFormatHelper(JSContext* cx, DateTimeInfo::ForceUTC forceUTC, - double utcTime, const char* format, - MutableHandleValue rval) { +static bool ToLocaleFormatHelper(JSContext* cx, DateObject* unwrapped, + const char* format, MutableHandleValue rval) { + DateTimeInfo::ForceUTC forceUTC = unwrapped->forceUTC(); + const char* locale = unwrapped->realm()->getLocale(); + double utcTime = unwrapped->UTCTime().toNumber(); + char buf[100]; if (!std::isfinite(utcTime)) { strcpy(buf, "InvalidDate"); @@ -3157,7 +3162,8 @@ static bool ToLocaleFormatHelper(JSContext* cx, DateTimeInfo::ForceUTC forceUTC, /* If it failed, default to toString. */ if (result_len == 0) { - return FormatDate(cx, forceUTC, utcTime, FormatSpec::DateTime, rval); + return FormatDate(cx, forceUTC, locale, utcTime, FormatSpec::DateTime, + rval); } /* Hacked check against undesired 2-digit year 00/00/00 form. */ @@ -3212,9 +3218,7 @@ static bool date_toLocaleString(JSContext* cx, unsigned argc, Value* vp) { # endif ; - return ToLocaleFormatHelper(cx, unwrapped->forceUTC(), - unwrapped->UTCTime().toNumber(), format, - args.rval()); + return ToLocaleFormatHelper(cx, unwrapped, format, args.rval()); } static bool date_toLocaleDateString(JSContext* cx, unsigned argc, Value* vp) { @@ -3240,9 +3244,7 @@ static bool date_toLocaleDateString(JSContext* cx, unsigned argc, Value* vp) { # endif ; - return ToLocaleFormatHelper(cx, unwrapped->forceUTC(), - unwrapped->UTCTime().toNumber(), format, - args.rval()); + return ToLocaleFormatHelper(cx, unwrapped, format, args.rval()); } static bool date_toLocaleTimeString(JSContext* cx, unsigned argc, Value* vp) { @@ -3256,9 +3258,7 @@ static bool date_toLocaleTimeString(JSContext* cx, unsigned argc, Value* vp) { return false; } - return ToLocaleFormatHelper(cx, unwrapped->forceUTC(), - unwrapped->UTCTime().toNumber(), "%X", - args.rval()); + return ToLocaleFormatHelper(cx, unwrapped, "%X", args.rval()); } #endif /* !JS_HAS_INTL_API */ @@ -3272,8 +3272,9 @@ static bool date_toTimeString(JSContext* cx, unsigned argc, Value* vp) { return false; } - return FormatDate(cx, unwrapped->forceUTC(), unwrapped->UTCTime().toNumber(), - FormatSpec::Time, args.rval()); + return FormatDate(cx, unwrapped->forceUTC(), unwrapped->realm()->getLocale(), + unwrapped->UTCTime().toNumber(), FormatSpec::Time, + args.rval()); } static bool date_toDateString(JSContext* cx, unsigned argc, Value* vp) { @@ -3286,8 +3287,9 @@ static bool date_toDateString(JSContext* cx, unsigned argc, Value* vp) { return false; } - return FormatDate(cx, unwrapped->forceUTC(), unwrapped->UTCTime().toNumber(), - FormatSpec::Date, args.rval()); + return FormatDate(cx, unwrapped->forceUTC(), unwrapped->realm()->getLocale(), + unwrapped->UTCTime().toNumber(), FormatSpec::Date, + args.rval()); } static bool date_toSource(JSContext* cx, unsigned argc, Value* vp) { @@ -3323,8 +3325,9 @@ bool date_toString(JSContext* cx, unsigned argc, Value* vp) { return false; } - return FormatDate(cx, unwrapped->forceUTC(), unwrapped->UTCTime().toNumber(), - FormatSpec::DateTime, args.rval()); + return FormatDate(cx, unwrapped->forceUTC(), unwrapped->realm()->getLocale(), + unwrapped->UTCTime().toNumber(), FormatSpec::DateTime, + args.rval()); } bool js::date_valueOf(JSContext* cx, unsigned argc, Value* vp) { @@ -3480,8 +3483,8 @@ static bool NewDateObject(JSContext* cx, const CallArgs& args, ClippedTime t) { } static bool ToDateString(JSContext* cx, const CallArgs& args, ClippedTime t) { - return FormatDate(cx, ForceUTC(cx->realm()), t.toDouble(), - FormatSpec::DateTime, args.rval()); + return FormatDate(cx, ForceUTC(cx->realm()), cx->realm()->getLocale(), + t.toDouble(), FormatSpec::DateTime, args.rval()); } static bool DateNoArguments(JSContext* cx, const CallArgs& args) { diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index c8f3b3937cf0..d5d2d50f2a32 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -6776,6 +6776,9 @@ static bool WrapWithProto(JSContext* cx, unsigned argc, Value* vp) { } static bool NewGlobal(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + JS::RealmOptions options; JS::RealmCreationOptions& creationOptions = options.creationOptions(); JS::RealmBehaviors& behaviors = options.behaviors(); @@ -6794,7 +6797,6 @@ static bool NewGlobal(JSContext* cx, unsigned argc, Value* vp) { JS::AutoHoldPrincipals principals(cx); - CallArgs args = CallArgsFromVp(argc, vp); if (args.length() == 1 && args[0].isObject()) { RootedObject opts(cx, &args[0].toObject()); RootedValue v(cx); @@ -6914,6 +6916,18 @@ static bool NewGlobal(JSContext* cx, unsigned argc, Value* vp) { if (v.isBoolean()) { creationOptions.setAlwaysUseFdlibm(v.toBoolean()); } + + if (!JS_GetProperty(cx, opts, "locale", &v)) { + return false; + } + if (v.isString()) { + RootedString str(cx, v.toString()); + UniqueChars locale = StringToLocale(cx, callee, str); + if (!locale) { + return false; + } + creationOptions.setLocaleCopyZ(locale.get()); + } } if (!CheckRealmOptions(cx, options, principals.get())) { diff --git a/js/src/vm/Realm.cpp b/js/src/vm/Realm.cpp index 4242bb357659..c32018724186 100644 --- a/js/src/vm/Realm.cpp +++ b/js/src/vm/Realm.cpp @@ -488,6 +488,14 @@ void Realm::clearScriptCounts() { zone()->clearScriptCounts(this); } void Realm::clearScriptLCov() { zone()->clearScriptLCov(this); } +const char* Realm::getLocale() const { + if (RefPtr locale = creationOptions_.locale()) { + return locale->chars(); + } + + return runtime_->getDefaultLocale(); +} + void ObjectRealm::addSizeOfExcludingThis( mozilla::MallocSizeOf mallocSizeOf, size_t* innerViewsArg, size_t* objectMetadataTablesArg, diff --git a/js/src/vm/Realm.h b/js/src/vm/Realm.h index a41dcbbd9d18..65f5d7311d61 100644 --- a/js/src/vm/Realm.h +++ b/js/src/vm/Realm.h @@ -673,6 +673,9 @@ class JS::Realm : public JS::shadow::Realm { bool shouldCaptureStackForThrow(); + // Returns the locale for this realm. (Pointer must NOT be freed!) + const char* getLocale() const; + // Initializes randomNumberGenerator if needed. mozilla::non_crypto::XorShift128PlusRNG& getOrCreateRandomNumberGenerator(); diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index ade76c9b7a7e..b84ddbe54e79 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -1593,7 +1593,7 @@ static bool intrinsic_RuntimeDefaultLocale(JSContext* cx, unsigned argc, CallArgs args = CallArgsFromVp(argc, vp); MOZ_ASSERT(args.length() == 0); - const char* locale = cx->runtime()->getDefaultLocale(); + const char* locale = cx->realm()->getLocale(); if (!locale) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEFAULT_LOCALE_ERROR); @@ -1622,7 +1622,7 @@ static bool intrinsic_IsRuntimeDefaultLocale(JSContext* cx, unsigned argc, return true; } - const char* locale = cx->runtime()->getDefaultLocale(); + const char* locale = cx->realm()->getLocale(); if (!locale) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEFAULT_LOCALE_ERROR);