diff --git a/devtools/client/netmonitor/test/browser.ini b/devtools/client/netmonitor/test/browser.ini index 24eeea44d218..14ecefef3df3 100644 --- a/devtools/client/netmonitor/test/browser.ini +++ b/devtools/client/netmonitor/test/browser.ini @@ -121,6 +121,7 @@ skip-if = (os == 'linux' && debug && bits == 32) # Bug 1303439 [browser_net_filter-flags.js] [browser_net_footer-summary.js] [browser_net_headers-alignment.js] +[browser_net_headers_filter.js] [browser_net_headers_sorted.js] [browser_net_image-tooltip.js] [browser_net_json-b64.js] diff --git a/devtools/client/netmonitor/test/browser_net_headers_filter.js b/devtools/client/netmonitor/test/browser_net_headers_filter.js new file mode 100644 index 000000000000..b230f9e6615a --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_headers_filter.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if Request-Headers and Response-Headers are correctly filtered in Headers tab. + */ +add_task(async function () { + let { tab, monitor } = await initNetMonitor(SIMPLE_SJS); + info("Starting test... "); + + let { document, store, windowRequire } = monitor.panelWin; + let Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + let { + getSortedRequests, + } = windowRequire("devtools/client/netmonitor/src/selectors/index"); + + store.dispatch(Actions.batchEnable(false)); + + let wait = waitForNetworkEvents(monitor, 1); + tab.linkedBrowser.reload(); + await wait; + + wait = waitUntil(() => document.querySelector(".headers-overview")); + EventUtils.sendMouseEvent({ type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0]); + await wait; + + await waitUntil(() => { + let request = getSortedRequests(store.getState()).get(0); + return request.requestHeaders && request.responseHeaders; + }); + + document.querySelectorAll(".devtools-filterinput")[1].focus(); + EventUtils.synthesizeKey("con", {}); + await waitUntil(() => document.querySelector(".treeRow.hidden")); + + info("Check if Headers are filtered correctly"); + + let totalResponseHeaders = ["cache-control", "connection", "content-length", + "content-type", "date", "expires", "foo-bar", + "foo-bar", "foo-bar", "pragma", "server", "set-cookie", + "set-cookie"]; + let expectedResponseHeaders = ["cache-control", "connection", "content-length", + "content-type"]; + let expectedRequestHeaders = ["Cache-Control", "Connection"]; + + let labelCells = document.querySelectorAll(".treeLabelCell"); + let filteredResponseHeaders = []; + let filteredRequestHeaders = []; + + let responseHeadersLength = totalResponseHeaders.length; + for (let i = 1; i < responseHeadersLength + 1; i++) { + if (labelCells[i].offsetHeight > 0) { + filteredResponseHeaders.push(labelCells[i].innerText); + } + } + + for (let i = responseHeadersLength + 2; i < labelCells.length; i++) { + if (labelCells[i].offsetHeight > 0) { + filteredRequestHeaders.push(labelCells[i].innerText); + } + } + + is(filteredResponseHeaders.toString(), expectedResponseHeaders.toString(), + "Response Headers are filtered"); + is(filteredRequestHeaders.toString(), expectedRequestHeaders.toString(), + "Request Headers are filtered"); + + await teardown(monitor); +}); diff --git a/devtools/client/shared/components/tree/TreeView.css b/devtools/client/shared/components/tree/TreeView.css index 230d8c969801..1f89a1299d27 100644 --- a/devtools/client/shared/components/tree/TreeView.css +++ b/devtools/client/shared/components/tree/TreeView.css @@ -82,7 +82,7 @@ /* Filtering */ .treeTable .treeRow.hidden { - display: none; + display: none !important; } .treeTable .treeValueCellDivider { diff --git a/dom/credentialmanagement/CredentialsContainer.cpp b/dom/credentialmanagement/CredentialsContainer.cpp index a782daa0ae1b..6e724efbf5f7 100644 --- a/dom/credentialmanagement/CredentialsContainer.cpp +++ b/dom/credentialmanagement/CredentialsContainer.cpp @@ -143,5 +143,23 @@ CredentialsContainer::Store(const Credential& aCredential, ErrorResult& aRv) return mManager->Store(aCredential); } +already_AddRefed +CredentialsContainer::PreventSilentAccess(ErrorResult& aRv) +{ + nsCOMPtr global = do_QueryInterface(mParent); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr promise = Promise::Create(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + promise->MaybeResolveWithUndefined(); + return promise.forget(); +} + } // namespace dom } // namespace mozilla diff --git a/dom/credentialmanagement/CredentialsContainer.h b/dom/credentialmanagement/CredentialsContainer.h index f187c5d4c2d6..6279c2c8ec12 100644 --- a/dom/credentialmanagement/CredentialsContainer.h +++ b/dom/credentialmanagement/CredentialsContainer.h @@ -41,6 +41,9 @@ public: already_AddRefed Store(const Credential& aCredential, ErrorResult& aRv); + already_AddRefed + PreventSilentAccess(ErrorResult& aRv); + private: ~CredentialsContainer(); diff --git a/dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html b/dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html index a63b1185914d..dd34df4d2668 100644 --- a/dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html +++ b/dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html @@ -42,6 +42,15 @@ function testSameOrigin() { .catch(function sameOriginCatch(aResult) { local_ok(false, "Should not have failed " + aResult); }) + .then(function sameOriginPreventSilentAccess() { + return navigator.credentials.preventSilentAccess(); + }) + .then(function sameOriginPreventSilentAccessThen(aResult) { + local_ok(aResult == undefined, "PreventSilentAccess worked " + aResult); + }) + .catch(function sameOriginPreventSilentAccessCatch(aResult) { + local_ok(false, "Should not have failed " + aResult); + }) .then(function() { local_finished(); }); @@ -58,6 +67,15 @@ function testCrossOrigin() { local_ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError, received " + aResult); }) + .then(function crossOriginPreventSilentAccess() { + return navigator.credentials.preventSilentAccess(); + }) + .then(function crossOriginPreventSilentAccessThen(aResult) { + local_ok(aResult == undefined, "PreventSilentAccess worked " + aResult); + }) + .catch(function crossOriginPreventSilentAccessCatch(aResult) { + local_ok(false, "Should not have failed " + aResult); + }) .then(function() { local_finished(); }); diff --git a/dom/u2f/U2F.cpp b/dom/u2f/U2F.cpp index 9758b7cf5efb..0af194a5d431 100644 --- a/dom/u2f/U2F.cpp +++ b/dom/u2f/U2F.cpp @@ -9,6 +9,7 @@ #include "mozilla/ipc/PBackgroundChild.h" #include "mozilla/ipc/BackgroundChild.h" #include "mozilla/dom/WebAuthnTransactionChild.h" +#include "mozilla/dom/WebAuthnUtil.h" #include "nsContentUtils.h" #include "nsICryptoHash.h" #include "nsIEffectiveTLDService.h" @@ -128,110 +129,6 @@ RegisteredKeysToScopedCredentialList(const nsAString& aAppId, } } -enum class U2FOperation -{ - Register, - Sign -}; - -static ErrorCode -EvaluateAppID(nsPIDOMWindowInner* aParent, const nsString& aOrigin, - const U2FOperation& aOp, /* in/out */ nsString& aAppId) -{ - // Facet is the specification's way of referring to the web origin. - nsAutoCString facetString = NS_ConvertUTF16toUTF8(aOrigin); - nsCOMPtr facetUri; - if (NS_FAILED(NS_NewURI(getter_AddRefs(facetUri), facetString))) { - return ErrorCode::BAD_REQUEST; - } - - // If the facetId (origin) is not HTTPS, reject - bool facetIsHttps = false; - if (NS_FAILED(facetUri->SchemeIs("https", &facetIsHttps)) || !facetIsHttps) { - return ErrorCode::BAD_REQUEST; - } - - // If the appId is empty or null, overwrite it with the facetId and accept - if (aAppId.IsEmpty() || aAppId.EqualsLiteral("null")) { - aAppId.Assign(aOrigin); - return ErrorCode::OK; - } - - // AppID is user-supplied. It's quite possible for this parse to fail. - nsAutoCString appIdString = NS_ConvertUTF16toUTF8(aAppId); - nsCOMPtr appIdUri; - if (NS_FAILED(NS_NewURI(getter_AddRefs(appIdUri), appIdString))) { - return ErrorCode::BAD_REQUEST; - } - - // if the appId URL is not HTTPS, reject. - bool appIdIsHttps = false; - if (NS_FAILED(appIdUri->SchemeIs("https", &appIdIsHttps)) || !appIdIsHttps) { - return ErrorCode::BAD_REQUEST; - } - - nsAutoCString appIdHost; - if (NS_FAILED(appIdUri->GetAsciiHost(appIdHost))) { - return ErrorCode::BAD_REQUEST; - } - - // Allow localhost. - if (appIdHost.EqualsLiteral("localhost")) { - nsAutoCString facetHost; - if (NS_FAILED(facetUri->GetAsciiHost(facetHost))) { - return ErrorCode::BAD_REQUEST; - } - - if (facetHost.EqualsLiteral("localhost")) { - return ErrorCode::OK; - } - } - - // Run the HTML5 algorithm to relax the same-origin policy, copied from W3C - // Web Authentication. See Bug 1244959 comment #8 for context on why we are - // doing this instead of implementing the external-fetch FacetID logic. - nsCOMPtr document = aParent->GetDoc(); - if (!document || !document->IsHTMLDocument()) { - return ErrorCode::BAD_REQUEST; - } - nsHTMLDocument* html = document->AsHTMLDocument(); - if (NS_WARN_IF(!html)) { - return ErrorCode::BAD_REQUEST; - } - - // Use the base domain as the facet for evaluation. This lets this algorithm - // relax the whole eTLD+1. - nsCOMPtr tldService = - do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); - if (!tldService) { - return ErrorCode::BAD_REQUEST; - } - - nsAutoCString lowestFacetHost; - if (NS_FAILED(tldService->GetBaseDomain(facetUri, 0, lowestFacetHost))) { - return ErrorCode::BAD_REQUEST; - } - - MOZ_LOG(gU2FLog, LogLevel::Debug, - ("AppId %s Facet %s", appIdHost.get(), lowestFacetHost.get())); - - if (html->IsRegistrableDomainSuffixOfOrEqualTo(NS_ConvertUTF8toUTF16(lowestFacetHost), - appIdHost)) { - return ErrorCode::OK; - } - - // Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023. - if (aOp == U2FOperation::Sign && lowestFacetHost.EqualsLiteral("google.com") && - (aAppId.Equals(kGoogleAccountsAppId1) || - aAppId.Equals(kGoogleAccountsAppId2))) { - MOZ_LOG(gU2FLog, LogLevel::Debug, - ("U2F permitted for Google Accounts via Bug #1436085")); - return ErrorCode::OK; - } - - return ErrorCode::BAD_REQUEST; -} - static nsresult BuildTransactionHashes(const nsCString& aRpId, const nsCString& aClientDataJSON, @@ -375,13 +272,10 @@ U2F::Register(const nsAString& aAppId, } // Evaluate the AppID - nsString adjustedAppId; - adjustedAppId.Assign(aAppId); - ErrorCode appIdResult = EvaluateAppID(mParent, mOrigin, U2FOperation::Register, - adjustedAppId); - if (appIdResult != ErrorCode::OK) { + nsString adjustedAppId(aAppId); + if (!EvaluateAppID(mParent, mOrigin, U2FOperation::Register, adjustedAppId)) { RegisterResponse response; - response.mErrorCode.Construct(static_cast(appIdResult)); + response.mErrorCode.Construct(static_cast(ErrorCode::BAD_REQUEST)); ExecuteCallback(response, callback); return; } @@ -538,13 +432,10 @@ U2F::Sign(const nsAString& aAppId, } // Evaluate the AppID - nsString adjustedAppId; - adjustedAppId.Assign(aAppId); - ErrorCode appIdResult = EvaluateAppID(mParent, mOrigin, U2FOperation::Sign, - adjustedAppId); - if (appIdResult != ErrorCode::OK) { + nsString adjustedAppId(aAppId); + if (!EvaluateAppID(mParent, mOrigin, U2FOperation::Sign, adjustedAppId)) { SignResponse response; - response.mErrorCode.Construct(static_cast(appIdResult)); + response.mErrorCode.Construct(static_cast(ErrorCode::BAD_REQUEST)); ExecuteCallback(response, callback); return; } diff --git a/dom/webauthn/PWebAuthnTransaction.ipdl b/dom/webauthn/PWebAuthnTransaction.ipdl index e18336169749..194d2f1fea95 100644 --- a/dom/webauthn/PWebAuthnTransaction.ipdl +++ b/dom/webauthn/PWebAuthnTransaction.ipdl @@ -30,8 +30,20 @@ struct WebAuthnScopedCredential { uint8_t transports; }; -struct WebAuthnExtension { - /* TODO Fill in with predefined extensions */ +struct WebAuthnExtensionAppId { + uint8_t[] AppId; +}; + +union WebAuthnExtension { + WebAuthnExtensionAppId; +}; + +struct WebAuthnExtensionResultAppId { + bool AppId; +}; + +union WebAuthnExtensionResult { + WebAuthnExtensionResultAppId; }; struct WebAuthnMakeCredentialInfo { @@ -57,8 +69,10 @@ struct WebAuthnGetAssertionInfo { }; struct WebAuthnGetAssertionResult { + uint8_t[] RpIdHash; uint8_t[] CredentialID; uint8_t[] SigBuffer; + WebAuthnExtensionResult[] Extensions; }; async protocol PWebAuthnTransaction { diff --git a/dom/webauthn/PublicKeyCredential.cpp b/dom/webauthn/PublicKeyCredential.cpp index e7da644b262a..23632fe3e567 100644 --- a/dom/webauthn/PublicKeyCredential.cpp +++ b/dom/webauthn/PublicKeyCredential.cpp @@ -115,6 +115,19 @@ PublicKeyCredential::IsUserVerifyingPlatformAuthenticatorAvailable(GlobalObject& return promise.forget(); } +void +PublicKeyCredential::GetClientExtensionResults(AuthenticationExtensionsClientOutputs& aResult) +{ + aResult = mClientExtensionOutputs; +} + +void +PublicKeyCredential::SetClientExtensionResultAppId(bool aResult) +{ + mClientExtensionOutputs.mAppid.Construct(); + mClientExtensionOutputs.mAppid.Value() = aResult; +} + } // namespace dom } // namespace mozilla diff --git a/dom/webauthn/PublicKeyCredential.h b/dom/webauthn/PublicKeyCredential.h index 5fe8b8acfa49..e29e2f980819 100644 --- a/dom/webauthn/PublicKeyCredential.h +++ b/dom/webauthn/PublicKeyCredential.h @@ -49,12 +49,17 @@ public: static already_AddRefed IsUserVerifyingPlatformAuthenticatorAvailable(GlobalObject& aGlobal); + void + GetClientExtensionResults(AuthenticationExtensionsClientOutputs& aResult); + + void + SetClientExtensionResultAppId(bool aResult); + private: CryptoBuffer mRawId; JS::Heap mRawIdCachedObj; RefPtr mResponse; - // Extensions are not supported yet. - // mClientExtensionResults; + AuthenticationExtensionsClientOutputs mClientExtensionOutputs; }; } // namespace dom diff --git a/dom/webauthn/U2FHIDTokenManager.cpp b/dom/webauthn/U2FHIDTokenManager.cpp index 3806cb819db9..c1db6ebdfdd6 100644 --- a/dom/webauthn/U2FHIDTokenManager.cpp +++ b/dom/webauthn/U2FHIDTokenManager.cpp @@ -127,6 +127,7 @@ U2FHIDTokenManager::Register(const nsTArray& aCredenti } ClearPromises(); + mCurrentAppId = aApplication; mTransactionId = rust_u2f_mgr_register(mU2FManager, registerFlags, (uint64_t)aTimeoutMS, @@ -164,6 +165,7 @@ RefPtr U2FHIDTokenManager::Sign(const nsTArray& aCredentials, const nsTArray& aApplication, const nsTArray& aChallenge, + const nsTArray& aExtensions, bool aRequireUserVerification, uint32_t aTimeoutMS) { @@ -176,15 +178,25 @@ U2FHIDTokenManager::Sign(const nsTArray& aCredentials, signFlags |= U2F_FLAG_REQUIRE_USER_VERIFICATION; } + nsTArray> appIds; + appIds.AppendElement(aApplication); + + // Process extensions. + for (const WebAuthnExtension& ext: aExtensions) { + if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) { + appIds.AppendElement(ext.get_WebAuthnExtensionAppId().AppId()); + } + } + ClearPromises(); + mCurrentAppId = aApplication; mTransactionId = rust_u2f_mgr_sign(mU2FManager, signFlags, (uint64_t)aTimeoutMS, u2f_sign_callback, aChallenge.Elements(), aChallenge.Length(), - aApplication.Elements(), - aApplication.Length(), + U2FAppIds(appIds).Get(), U2FKeyHandles(aCredentials).Get()); if (mTransactionId == 0) { return U2FSignPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); @@ -234,6 +246,12 @@ U2FHIDTokenManager::HandleSignResult(UniquePtr&& aResult) MOZ_ASSERT(!mSignPromise.IsEmpty()); + nsTArray appId; + if (!aResult->CopyAppId(appId)) { + mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); + return; + } + nsTArray keyHandle; if (!aResult->CopyKeyHandle(keyHandle)) { mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); @@ -246,7 +264,14 @@ U2FHIDTokenManager::HandleSignResult(UniquePtr&& aResult) return; } - WebAuthnGetAssertionResult result(keyHandle, signature); + nsTArray extensions; + + if (appId != mCurrentAppId) { + // Indicate to the RP that we used the FIDO appId. + extensions.AppendElement(WebAuthnExtensionResultAppId(true)); + } + + WebAuthnGetAssertionResult result(appId, keyHandle, signature, extensions); mSignPromise.Resolve(Move(result), __func__); } diff --git a/dom/webauthn/U2FHIDTokenManager.h b/dom/webauthn/U2FHIDTokenManager.h index 814a853b2b4a..f318d9b5c597 100644 --- a/dom/webauthn/U2FHIDTokenManager.h +++ b/dom/webauthn/U2FHIDTokenManager.h @@ -18,13 +18,32 @@ namespace mozilla { namespace dom { +class U2FAppIds { +public: + explicit U2FAppIds(const nsTArray>& aApplications) + { + mAppIds = rust_u2f_app_ids_new(); + + for (auto& app_id: aApplications) { + rust_u2f_app_ids_add(mAppIds, app_id.Elements(), app_id.Length()); + } + } + + rust_u2f_app_ids* Get() { return mAppIds; } + + ~U2FAppIds() { rust_u2f_app_ids_free(mAppIds); } + +private: + rust_u2f_app_ids* mAppIds; +}; + class U2FKeyHandles { public: explicit U2FKeyHandles(const nsTArray& aCredentials) { mKeyHandles = rust_u2f_khs_new(); - for (auto cred: aCredentials) { + for (auto& cred: aCredentials) { rust_u2f_khs_add(mKeyHandles, cred.id().Elements(), cred.id().Length(), @@ -66,6 +85,11 @@ public: return CopyBuffer(U2F_RESBUF_ID_SIGNATURE, aBuffer); } + bool CopyAppId(nsTArray& aBuffer) + { + return CopyBuffer(U2F_RESBUF_ID_APPID, aBuffer); + } + private: bool CopyBuffer(uint8_t aResBufID, nsTArray& aBuffer) { if (!mResult) { @@ -104,6 +128,7 @@ public: Sign(const nsTArray& aCredentials, const nsTArray& aApplication, const nsTArray& aChallenge, + const nsTArray& aExtensions, bool aRequireUserVerification, uint32_t aTimeoutMS) override; @@ -123,6 +148,7 @@ private: rust_u2f_manager* mU2FManager; uint64_t mTransactionId; + nsTArray mCurrentAppId; MozPromiseHolder mRegisterPromise; MozPromiseHolder mSignPromise; }; diff --git a/dom/webauthn/U2FSoftTokenManager.cpp b/dom/webauthn/U2FSoftTokenManager.cpp index c9d0e2abaa4b..7d64160b2620 100644 --- a/dom/webauthn/U2FSoftTokenManager.cpp +++ b/dom/webauthn/U2FSoftTokenManager.cpp @@ -693,6 +693,27 @@ U2FSoftTokenManager::Register(const nsTArray& aCredent return U2FRegisterPromise::CreateAndResolve(Move(result), __func__); } +bool +U2FSoftTokenManager::FindRegisteredKeyHandle(const nsTArray>& aAppIds, + const nsTArray& aCredentials, + /*out*/ nsTArray& aKeyHandle, + /*out*/ nsTArray& aAppId) +{ + for (const nsTArray& app_id: aAppIds) { + for (const WebAuthnScopedCredential& cred: aCredentials) { + bool isRegistered = false; + nsresult rv = IsRegistered(cred.id(), app_id, isRegistered); + if (NS_SUCCEEDED(rv) && isRegistered) { + aKeyHandle.Assign(cred.id()); + aAppId.Assign(app_id); + return true; + } + } + } + + return false; +} + // A U2F Sign operation creates a signature over the "param" arguments (plus // some other stuff) using the private key indicated in the key handle argument. // @@ -713,6 +734,7 @@ RefPtr U2FSoftTokenManager::Sign(const nsTArray& aCredentials, const nsTArray& aApplication, const nsTArray& aChallenge, + const nsTArray& aExtensions, bool aRequireUserVerification, uint32_t aTimeoutMS) { @@ -728,18 +750,21 @@ U2FSoftTokenManager::Sign(const nsTArray& aCredentials return U2FSignPromise::CreateAndReject(NS_ERROR_DOM_NOT_ALLOWED_ERR, __func__); } - nsTArray keyHandle; - for (auto cred: aCredentials) { - bool isRegistered = false; - nsresult rv = IsRegistered(cred.id(), aApplication, isRegistered); - if (NS_SUCCEEDED(rv) && isRegistered) { - keyHandle.Assign(cred.id()); - break; + nsTArray> appIds; + appIds.AppendElement(aApplication); + + // Process extensions. + for (const WebAuthnExtension& ext: aExtensions) { + if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) { + appIds.AppendElement(ext.get_WebAuthnExtensionAppId().AppId()); } } - // Fail if we didn't recognize a key id. - if (keyHandle.IsEmpty()) { + nsTArray chosenAppId(aApplication); + nsTArray keyHandle; + + // Fail if we can't find a valid key handle. + if (!FindRegisteredKeyHandle(appIds, aCredentials, keyHandle, chosenAppId)) { return U2FSignPromise::CreateAndReject(NS_ERROR_DOM_NOT_ALLOWED_ERR, __func__); } @@ -748,10 +773,10 @@ U2FSoftTokenManager::Sign(const nsTArray& aCredentials UniquePK11SlotInfo slot(PK11_GetInternalSlot()); MOZ_ASSERT(slot.get()); - if (NS_WARN_IF((aChallenge.Length() != kParamLen) || (aApplication.Length() != kParamLen))) { + if (NS_WARN_IF((aChallenge.Length() != kParamLen) || (chosenAppId.Length() != kParamLen))) { MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Parameter lengths are wrong! challenge=%d app=%d expected=%d", - (uint32_t)aChallenge.Length(), (uint32_t)aApplication.Length(), kParamLen)); + (uint32_t)aChallenge.Length(), (uint32_t)chosenAppId.Length(), kParamLen)); return U2FSignPromise::CreateAndReject(NS_ERROR_ILLEGAL_VALUE, __func__); } @@ -760,8 +785,8 @@ U2FSoftTokenManager::Sign(const nsTArray& aCredentials UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandle(slot, mWrappingKey, const_cast(keyHandle.Elements()), keyHandle.Length(), - const_cast(aApplication.Elements()), - aApplication.Length()); + const_cast(chosenAppId.Elements()), + chosenAppId.Length()); if (NS_WARN_IF(!privKey.get())) { MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Couldn't get the priv key!")); return U2FSignPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); @@ -790,7 +815,7 @@ U2FSoftTokenManager::Sign(const nsTArray& aCredentials // It's OK to ignore the return values here because we're writing into // pre-allocated space - signedDataBuf.AppendElements(aApplication.Elements(), aApplication.Length(), + signedDataBuf.AppendElements(chosenAppId.Elements(), chosenAppId.Length(), mozilla::fallible); signedDataBuf.AppendElement(0x01, mozilla::fallible); signedDataBuf.AppendSECItem(counterItem); @@ -832,7 +857,15 @@ U2FSoftTokenManager::Sign(const nsTArray& aCredentials signatureBuf.AppendSECItem(counterItem); signatureBuf.AppendSECItem(signatureItem); - WebAuthnGetAssertionResult result(keyHandle, nsTArray(signatureBuf)); + nsTArray signature(signatureBuf); + nsTArray extensions; + + if (chosenAppId != aApplication) { + // Indicate to the RP that we used the FIDO appId. + extensions.AppendElement(WebAuthnExtensionResultAppId(true)); + } + + WebAuthnGetAssertionResult result(chosenAppId, keyHandle, signature, extensions); return U2FSignPromise::CreateAndResolve(Move(result), __func__); } diff --git a/dom/webauthn/U2FSoftTokenManager.h b/dom/webauthn/U2FSoftTokenManager.h index 87bfa6d5c538..5f1271afc372 100644 --- a/dom/webauthn/U2FSoftTokenManager.h +++ b/dom/webauthn/U2FSoftTokenManager.h @@ -34,6 +34,7 @@ public: Sign(const nsTArray& aCredentials, const nsTArray& aApplication, const nsTArray& aChallenge, + const nsTArray& aExtensions, bool aRequireUserVerification, uint32_t aTimeoutMS) override; @@ -47,6 +48,12 @@ private: const nsTArray& aAppParam, bool& aResult); + bool + FindRegisteredKeyHandle(const nsTArray>& aAppIds, + const nsTArray& aCredentials, + /*out*/ nsTArray& aKeyHandle, + /*out*/ nsTArray& aAppId); + bool mInitialized; mozilla::UniquePK11SymKey mWrappingKey; diff --git a/dom/webauthn/U2FTokenManager.cpp b/dom/webauthn/U2FTokenManager.cpp index 51393245c837..d3b27a183d76 100644 --- a/dom/webauthn/U2FTokenManager.cpp +++ b/dom/webauthn/U2FTokenManager.cpp @@ -322,6 +322,7 @@ U2FTokenManager::Sign(PWebAuthnTransactionParent* aTransactionParent, mTokenManagerImpl->Sign(aTransactionInfo.AllowList(), aTransactionInfo.RpIdHash(), aTransactionInfo.ClientDataHash(), + aTransactionInfo.Extensions(), aTransactionInfo.RequireUserVerification(), aTransactionInfo.TimeoutMS()) ->Then(GetCurrentThreadSerialEventTarget(), __func__, diff --git a/dom/webauthn/U2FTokenTransport.h b/dom/webauthn/U2FTokenTransport.h index 8409abddcc2a..fde2dc1b21a4 100644 --- a/dom/webauthn/U2FTokenTransport.h +++ b/dom/webauthn/U2FTokenTransport.h @@ -38,6 +38,7 @@ public: Sign(const nsTArray& aCredentials, const nsTArray& aApplication, const nsTArray& aChallenge, + const nsTArray& aExtensions, bool aRequireUserVerification, uint32_t aTimeoutMS) = 0; diff --git a/dom/webauthn/WebAuthnManager.cpp b/dom/webauthn/WebAuthnManager.cpp index 6e601b672c30..14dbc43ad3a7 100644 --- a/dom/webauthn/WebAuthnManager.cpp +++ b/dom/webauthn/WebAuthnManager.cpp @@ -49,8 +49,11 @@ NS_IMPL_ISUPPORTS(WebAuthnManager, nsIDOMEventListener); **********************************************************************/ static nsresult -AssembleClientData(const nsAString& aOrigin, const CryptoBuffer& aChallenge, - const nsAString& aType, /* out */ nsACString& aJsonOut) +AssembleClientData(const nsAString& aOrigin, + const CryptoBuffer& aChallenge, + const nsAString& aType, + const AuthenticationExtensionsClientInputs& aExtensions, + /* out */ nsACString& aJsonOut) { MOZ_ASSERT(NS_IsMainThread()); @@ -65,6 +68,7 @@ AssembleClientData(const nsAString& aOrigin, const CryptoBuffer& aChallenge, clientDataObject.mChallenge.Assign(challengeBase64); clientDataObject.mOrigin.Assign(aOrigin); clientDataObject.mHashAlgorithm.AssignLiteral(u"SHA-256"); + clientDataObject.mClientExtensions = aExtensions; nsAutoString temp; if (NS_WARN_IF(!clientDataObject.ToJSON(temp))) { @@ -264,6 +268,12 @@ WebAuthnManager::MakeCredential(const PublicKeyCredentialCreationOptions& aOptio } } + // + if (aOptions.mExtensions.mAppid.WasPassed()) { + promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return promise.forget(); + } + CryptoBuffer rpIdHash; if (!rpIdHash.SetLength(SHA256_LENGTH, fallible)) { promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); @@ -345,7 +355,8 @@ WebAuthnManager::MakeCredential(const PublicKeyCredentialCreationOptions& aOptio nsAutoCString clientDataJSON; srv = AssembleClientData(origin, challenge, - NS_LITERAL_STRING("webauthn.create"), clientDataJSON); + NS_LITERAL_STRING("webauthn.create"), + aOptions.mExtensions, clientDataJSON); if (NS_WARN_IF(NS_FAILED(srv))) { promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); return promise.forget(); @@ -537,7 +548,7 @@ WebAuthnManager::GetAssertion(const PublicKeyCredentialRequestOptions& aOptions, nsAutoCString clientDataJSON; srv = AssembleClientData(origin, challenge, NS_LITERAL_STRING("webauthn.get"), - clientDataJSON); + aOptions.mExtensions, clientDataJSON); if (NS_WARN_IF(NS_FAILED(srv))) { promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); return promise.forget(); @@ -593,14 +604,40 @@ WebAuthnManager::GetAssertion(const PublicKeyCredentialRequestOptions& aOptions, bool requireUserVerification = aOptions.mUserVerification == UserVerificationRequirement::Required; - // TODO: Add extension list building - // If extensions was specified, process any extensions supported by this + // If extensions were specified, process any extensions supported by this // client platform, to produce the extension data that needs to be sent to the // authenticator. If an error is encountered while processing an extension, // skip that extension and do not produce any extension data for it. Call the // result of this processing clientExtensions. nsTArray extensions; + // + if (aOptions.mExtensions.mAppid.WasPassed()) { + nsString appId(aOptions.mExtensions.mAppid.Value()); + + // Check that the appId value is allowed. + if (!EvaluateAppID(mParent, origin, U2FOperation::Sign, appId)) { + promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return promise.forget(); + } + + CryptoBuffer appIdHash; + if (!appIdHash.SetLength(SHA256_LENGTH, fallible)) { + promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); + return promise.forget(); + } + + // We need the SHA-256 hash of the appId. + nsresult srv = HashCString(hashService, NS_ConvertUTF16toUTF8(appId), appIdHash); + if (NS_WARN_IF(NS_FAILED(srv))) { + promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return promise.forget(); + } + + // Append the hash and send it to the backend. + extensions.AppendElement(WebAuthnExtensionAppId(appIdHash)); + } + WebAuthnGetAssertionInfo info(rpIdHash, clientDataHash, adjustedTimeout, @@ -807,7 +844,7 @@ WebAuthnManager::FinishGetAssertion(const uint64_t& aTransactionId, } CryptoBuffer rpIdHashBuf; - if (!rpIdHashBuf.Assign(mTransaction.ref().mRpIdHash)) { + if (!rpIdHashBuf.Assign(aResult.RpIdHash())) { RejectTransaction(NS_ERROR_OUT_OF_MEMORY); return; } @@ -862,6 +899,14 @@ WebAuthnManager::FinishGetAssertion(const uint64_t& aTransactionId, credential->SetRawId(credentialBuf); credential->SetResponse(assertion); + // Forward client extension results. + for (auto& ext: aResult.Extensions()) { + if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultAppId) { + bool appid = ext.get_WebAuthnExtensionResultAppId().AppId(); + credential->SetClientExtensionResultAppId(appid); + } + } + mTransaction.ref().mPromise->MaybeResolve(credential); ClearTransaction(); } diff --git a/dom/webauthn/WebAuthnUtil.cpp b/dom/webauthn/WebAuthnUtil.cpp index bdcc454206d8..2d79d445e43c 100644 --- a/dom/webauthn/WebAuthnUtil.cpp +++ b/dom/webauthn/WebAuthnUtil.cpp @@ -5,11 +5,113 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/WebAuthnUtil.h" +#include "nsIEffectiveTLDService.h" +#include "nsNetUtil.h" #include "pkixutil.h" namespace mozilla { namespace dom { +// Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023. +NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId1, + "https://www.gstatic.com/securitykey/origins.json"); +NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId2, + "https://www.gstatic.com/securitykey/a/google.com/origins.json"); + +bool +EvaluateAppID(nsPIDOMWindowInner* aParent, const nsString& aOrigin, + const U2FOperation& aOp, /* in/out */ nsString& aAppId) +{ + // Facet is the specification's way of referring to the web origin. + nsAutoCString facetString = NS_ConvertUTF16toUTF8(aOrigin); + nsCOMPtr facetUri; + if (NS_FAILED(NS_NewURI(getter_AddRefs(facetUri), facetString))) { + return false; + } + + // If the facetId (origin) is not HTTPS, reject + bool facetIsHttps = false; + if (NS_FAILED(facetUri->SchemeIs("https", &facetIsHttps)) || !facetIsHttps) { + return false; + } + + // If the appId is empty or null, overwrite it with the facetId and accept + if (aAppId.IsEmpty() || aAppId.EqualsLiteral("null")) { + aAppId.Assign(aOrigin); + return true; + } + + // AppID is user-supplied. It's quite possible for this parse to fail. + nsAutoCString appIdString = NS_ConvertUTF16toUTF8(aAppId); + nsCOMPtr appIdUri; + if (NS_FAILED(NS_NewURI(getter_AddRefs(appIdUri), appIdString))) { + return false; + } + + // if the appId URL is not HTTPS, reject. + bool appIdIsHttps = false; + if (NS_FAILED(appIdUri->SchemeIs("https", &appIdIsHttps)) || !appIdIsHttps) { + return false; + } + + nsAutoCString appIdHost; + if (NS_FAILED(appIdUri->GetAsciiHost(appIdHost))) { + return false; + } + + // Allow localhost. + if (appIdHost.EqualsLiteral("localhost")) { + nsAutoCString facetHost; + if (NS_FAILED(facetUri->GetAsciiHost(facetHost))) { + return false; + } + + if (facetHost.EqualsLiteral("localhost")) { + return true; + } + } + + // Run the HTML5 algorithm to relax the same-origin policy, copied from W3C + // Web Authentication. See Bug 1244959 comment #8 for context on why we are + // doing this instead of implementing the external-fetch FacetID logic. + nsCOMPtr document = aParent->GetDoc(); + if (!document || !document->IsHTMLDocument()) { + return false; + } + nsHTMLDocument* html = document->AsHTMLDocument(); + if (NS_WARN_IF(!html)) { + return false; + } + + // Use the base domain as the facet for evaluation. This lets this algorithm + // relax the whole eTLD+1. + nsCOMPtr tldService = + do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); + if (!tldService) { + return false; + } + + nsAutoCString lowestFacetHost; + if (NS_FAILED(tldService->GetBaseDomain(facetUri, 0, lowestFacetHost))) { + return false; + } + + if (html->IsRegistrableDomainSuffixOfOrEqualTo(NS_ConvertUTF8toUTF16(lowestFacetHost), + appIdHost)) { + return true; + } + + // Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023. + if (aOp == U2FOperation::Sign && lowestFacetHost.EqualsLiteral("google.com") && + (aAppId.Equals(kGoogleAccountsAppId1) || + aAppId.Equals(kGoogleAccountsAppId2))) { + return true; + } + + return false; +} + + nsresult ReadToCryptoBuffer(pkix::Reader& aSrc, /* out */ CryptoBuffer& aDest, uint32_t aLen) diff --git a/dom/webauthn/WebAuthnUtil.h b/dom/webauthn/WebAuthnUtil.h index c8d56c54f322..6d9d11855945 100644 --- a/dom/webauthn/WebAuthnUtil.h +++ b/dom/webauthn/WebAuthnUtil.h @@ -16,6 +16,17 @@ namespace mozilla { namespace dom { + +enum class U2FOperation +{ + Register, + Sign +}; + +bool +EvaluateAppID(nsPIDOMWindowInner* aParent, const nsString& aOrigin, + const U2FOperation& aOp, /* in/out */ nsString& aAppId); + nsresult AssembleAuthenticatorData(const CryptoBuffer& rpIdHashBuf, const uint8_t flags, diff --git a/dom/webauthn/tests/browser/browser.ini b/dom/webauthn/tests/browser/browser.ini index a50cd7d0c07a..d62b4340ec32 100644 --- a/dom/webauthn/tests/browser/browser.ini +++ b/dom/webauthn/tests/browser/browser.ini @@ -1,5 +1,6 @@ [DEFAULT] support-files = + head.js tab_webauthn_result.html tab_webauthn_success.html ../cbor/* @@ -8,4 +9,5 @@ support-files = skip-if = !e10s [browser_abort_visibility.js] +[browser_fido_appid_extension.js] [browser_webauthn_telemetry.js] diff --git a/dom/webauthn/tests/browser/browser_fido_appid_extension.js b/dom/webauthn/tests/browser/browser_fido_appid_extension.js new file mode 100644 index 000000000000..7e2a4100f9d8 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_fido_appid_extension.js @@ -0,0 +1,153 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = "https://example.com/"; + +function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); +} + +function expectError(aType) { + let expected = `${aType}Error`; + return function (aResult) { + is(aResult.slice(0, expected.length), expected, `Expecting a ${aType}Error`); + }; +} + +let expectNotSupportedError = expectError("NotSupported"); +let expectNotAllowedError = expectError("NotAllowed"); +let expectSecurityError = expectError("Security"); + +function promiseU2FRegister(tab, app_id) { + let challenge = crypto.getRandomValues(new Uint8Array(16)); + challenge = bytesToBase64UrlSafe(challenge); + + return ContentTask.spawn(tab.linkedBrowser, [app_id, challenge], function ([app_id, challenge]) { + return new Promise(resolve => { + content.u2f.register(app_id, [{version: "U2F_V2", challenge}], [], resolve); + }); + }).then(res => { + is(res.errorCode, 0, "u2f.register() succeeded"); + let data = base64ToBytesUrlSafe(res.registrationData); + is(data[0], 0x05, "Reserved byte is correct"); + return data.slice(67, 67 + data[66]); + }); +} + +function promiseWebAuthnRegister(tab, appid) { + return ContentTask.spawn(tab.linkedBrowser, [appid], ([appid]) => { + const cose_alg_ECDSA_w_SHA256 = -7; + + let challenge = content.crypto.getRandomValues(new Uint8Array(16)); + + let pubKeyCredParams = [{ + type: "public-key", + alg: cose_alg_ECDSA_w_SHA256 + }]; + + let publicKey = { + rp: {id: content.document.domain, name: "none", icon: "none"}, + user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"}, + pubKeyCredParams, + extensions: {appid}, + challenge + }; + + return content.navigator.credentials.create({publicKey}) + .then(res => res.rawId); + }); +} + +function promiseWebAuthnSign(tab, key_handle, extensions = {}) { + return ContentTask.spawn(tab.linkedBrowser, [key_handle, extensions], + ([key_handle, extensions]) => { + let challenge = content.crypto.getRandomValues(new Uint8Array(16)); + + let credential = { + id: key_handle, + type: "public-key", + transports: ["usb"] + }; + + let publicKey = { + challenge, + extensions, + rpId: content.document.domain, + allowCredentials: [credential], + }; + + return content.navigator.credentials.get({publicKey}) + .then(credential => { + return { + authenticatorData: credential.response.authenticatorData, + clientDataJSON: credential.response.clientDataJSON, + extensions: credential.getClientExtensionResults() + }; + }) + }); +} + +add_task(function test_setup() { + Services.prefs.setBoolPref("security.webauth.u2f", true); + Services.prefs.setBoolPref("security.webauth.webauthn", true); + Services.prefs.setBoolPref("security.webauth.webauthn_enable_softtoken", true); + Services.prefs.setBoolPref("security.webauth.webauthn_enable_usbtoken", false); +}); + +add_task(async function test_appid() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Get a keyHandle for a FIDO AppId. + let appid = "https://example.com/appId"; + let keyHandle = await promiseU2FRegister(tab, appid); + + // The FIDO AppId extension can't be used for MakeCredential. + await promiseWebAuthnRegister(tab, appid) + .then(arrivingHereIsBad) + .catch(expectNotSupportedError); + + // Using the keyHandle shouldn't work without the FIDO AppId extension. + await promiseWebAuthnSign(tab, keyHandle) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + + // Invalid app IDs (for the current origin) must be rejected. + await promiseWebAuthnSign(tab, keyHandle, {appid: "https://bogus.com/appId"}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + + // Non-matching app IDs must be rejected. + await promiseWebAuthnSign(tab, keyHandle, {appid: appid + "2"}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + + let rpId = new TextEncoder("utf-8").encode(appid); + let rpIdHash = await crypto.subtle.digest("SHA-256", rpId); + + // Succeed with the right fallback rpId. + await promiseWebAuthnSign(tab, keyHandle, {appid}) + .then(({authenticatorData, clientDataJSON, extensions}) => { + is(extensions.appid, true, "appid extension was acted upon"); + + // Check that the correct rpIdHash is returned. + let rpIdHashSign = authenticatorData.slice(0, 32); + ok(memcmp(rpIdHash, rpIdHashSign), "rpIdHash is correct"); + + let clientData = JSON.parse(buffer2string(clientDataJSON)); + is(clientData.clientExtensions.appid, appid, "appid extension sent"); + }); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(function test_cleanup() { + Services.prefs.clearUserPref("security.webauth.u2f"); + Services.prefs.clearUserPref("security.webauth.webauthn"); + Services.prefs.clearUserPref("security.webauth.webauthn_enable_softtoken"); + Services.prefs.clearUserPref("security.webauth.webauthn_enable_usbtoken"); +}); diff --git a/dom/webauthn/tests/browser/head.js b/dom/webauthn/tests/browser/head.js new file mode 100644 index 000000000000..065ab3065067 --- /dev/null +++ b/dom/webauthn/tests/browser/head.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function bytesToBase64(u8a){ + let CHUNK_SZ = 0x8000; + let c = []; + for (let i = 0; i < u8a.length; i += CHUNK_SZ) { + c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ))); + } + return window.btoa(c.join("")); +} + +function bytesToBase64UrlSafe(buf) { + return bytesToBase64(buf) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); +} + +function base64ToBytes(b64encoded) { + return new Uint8Array(window.atob(b64encoded).split("").map(function(c) { + return c.charCodeAt(0); + })); +} + +function base64ToBytesUrlSafe(str) { + if (!str || str.length % 4 == 1) { + throw "Improper b64 string"; + } + + var b64 = str.replace(/\-/g, "+").replace(/\_/g, "/"); + while (b64.length % 4 != 0) { + b64 += "="; + } + return base64ToBytes(b64); +} + +function buffer2string(buf) { + let str = ""; + if (!(buf.constructor === Uint8Array)) { + buf = new Uint8Array(buf); + } + buf.map(function(x){ return str += String.fromCharCode(x) }); + return str; +} + +function memcmp(x, y) { + let xb = new Uint8Array(x); + let yb = new Uint8Array(y); + + if (x.byteLength != y.byteLength) { + return false; + } + + for (let i = 0; i < xb.byteLength; ++i) { + if (xb[i] != yb[i]) { + return false; + } + } + + return true; +} diff --git a/dom/webauthn/tests/test_webauthn_loopback.html b/dom/webauthn/tests/test_webauthn_loopback.html index 9551f429b307..bf8888505bcc 100644 --- a/dom/webauthn/tests/test_webauthn_loopback.html +++ b/dom/webauthn/tests/test_webauthn_loopback.html @@ -68,6 +68,10 @@ function() { is(clientData.hashAlgorithm, "SHA-256", "Hash algorithm is correct"); is(clientData.type, "webauthn.create", "Type is correct"); + let extensions = aCredInfo.getClientExtensionResults(); + is(extensions.appid, undefined, "appid extension wasn't used"); + is(clientData.clientExtensions.appid, undefined, "appid extension wasn't sent"); + return webAuthnDecodeCBORAttestation(aCredInfo.response.attestationObject) .then(function(aAttestationObj) { // Make sure the RP ID hash matches what we calculate. diff --git a/dom/webauthn/u2f-hid-rs/examples/main.rs b/dom/webauthn/u2f-hid-rs/examples/main.rs index 4b16563d71d1..9087d21bf84d 100644 --- a/dom/webauthn/u2f-hid-rs/examples/main.rs +++ b/dom/webauthn/u2f-hid-rs/examples/main.rs @@ -82,7 +82,7 @@ fn main() { flags, 15_000, chall_bytes, - app_bytes, + vec![app_bytes], vec![key_handle], move |rv| { tx.send(rv.unwrap()).unwrap(); }, ) diff --git a/dom/webauthn/u2f-hid-rs/src/capi.rs b/dom/webauthn/u2f-hid-rs/src/capi.rs index 119a10a3f03f..5ce897ad324c 100644 --- a/dom/webauthn/u2f-hid-rs/src/capi.rs +++ b/dom/webauthn/u2f-hid-rs/src/capi.rs @@ -9,6 +9,7 @@ use std::{ptr, slice}; use U2FManager; +type U2FAppIds = Vec<::AppId>; type U2FKeyHandles = Vec<::KeyHandle>; type U2FResult = HashMap>; type U2FCallback = extern "C" fn(u64, *mut U2FResult); @@ -16,6 +17,7 @@ type U2FCallback = extern "C" fn(u64, *mut U2FResult); const RESBUF_ID_REGISTRATION: u8 = 0; const RESBUF_ID_KEYHANDLE: u8 = 1; const RESBUF_ID_SIGNATURE: u8 = 2; +const RESBUF_ID_APPID: u8 = 3; // Generates a new 64-bit transaction id with collision probability 2^-32. fn new_tid() -> u64 { @@ -42,6 +44,27 @@ pub unsafe extern "C" fn rust_u2f_mgr_free(mgr: *mut U2FManager) { } } +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_app_ids_new() -> *mut U2FAppIds { + Box::into_raw(Box::new(vec![])) +} + +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_app_ids_add( + ids: *mut U2FAppIds, + id_ptr: *const u8, + id_len: usize +) { + (*ids).push(from_raw(id_ptr, id_len)); +} + +#[no_mangle] +pub unsafe extern "C" fn rust_u2f_app_ids_free(ids: *mut U2FAppIds) { + if !ids.is_null() { + Box::from_raw(ids); + } +} + #[no_mangle] pub unsafe extern "C" fn rust_u2f_khs_new() -> *mut U2FKeyHandles { Box::into_raw(Box::new(vec![])) @@ -165,8 +188,7 @@ pub unsafe extern "C" fn rust_u2f_mgr_sign( callback: U2FCallback, challenge_ptr: *const u8, challenge_len: usize, - application_ptr: *const u8, - application_len: usize, + app_ids: *const U2FAppIds, khs: *const U2FKeyHandles, ) -> u64 { if mgr.is_null() || khs.is_null() { @@ -174,13 +196,18 @@ pub unsafe extern "C" fn rust_u2f_mgr_sign( } // Check buffers. - if challenge_ptr.is_null() || application_ptr.is_null() { + if challenge_ptr.is_null() { + return 0; + } + + // Need at least one app_id. + if (*app_ids).len() < 1 { return 0; } let flags = ::SignFlags::from_bits_truncate(flags); let challenge = from_raw(challenge_ptr, challenge_len); - let application = from_raw(application_ptr, application_len); + let app_ids = (*app_ids).clone(); let key_handles = (*khs).clone(); let tid = new_tid(); @@ -188,13 +215,14 @@ pub unsafe extern "C" fn rust_u2f_mgr_sign( flags, timeout, challenge, - application, + app_ids, key_handles, move |rv| { - if let Ok((key_handle, signature)) = rv { + if let Ok((app_id, key_handle, signature)) = rv { let mut result = U2FResult::new(); result.insert(RESBUF_ID_KEYHANDLE, key_handle); result.insert(RESBUF_ID_SIGNATURE, signature); + result.insert(RESBUF_ID_APPID, app_id); callback(tid, Box::into_raw(Box::new(result))); } else { callback(tid, ptr::null_mut()); diff --git a/dom/webauthn/u2f-hid-rs/src/lib.rs b/dom/webauthn/u2f-hid-rs/src/lib.rs index 859a4b17afe6..c38d69cd1f17 100644 --- a/dom/webauthn/u2f-hid-rs/src/lib.rs +++ b/dom/webauthn/u2f-hid-rs/src/lib.rs @@ -75,6 +75,10 @@ pub struct KeyHandle { pub transports: AuthenticatorTransports, } +pub type AppId = Vec; +pub type RegisterResult = Vec; +pub type SignResult = (AppId, Vec, Vec); + #[cfg(fuzzing)] pub use u2fprotocol::*; #[cfg(fuzzing)] diff --git a/dom/webauthn/u2f-hid-rs/src/manager.rs b/dom/webauthn/u2f-hid-rs/src/manager.rs index 1c0f3712b704..385065b8c77c 100644 --- a/dom/webauthn/u2f-hid-rs/src/manager.rs +++ b/dom/webauthn/u2f-hid-rs/src/manager.rs @@ -16,17 +16,17 @@ enum QueueAction { flags: ::RegisterFlags, timeout: u64, challenge: Vec, - application: Vec, + application: ::AppId, key_handles: Vec<::KeyHandle>, - callback: OnceCallback>, + callback: OnceCallback<::RegisterResult>, }, Sign { flags: ::SignFlags, timeout: u64, challenge: Vec, - application: Vec, + app_ids: Vec<::AppId>, key_handles: Vec<::KeyHandle>, - callback: OnceCallback<(Vec, Vec)>, + callback: OnceCallback<::SignResult>, }, Cancel, } @@ -68,7 +68,7 @@ impl U2FManager { flags, timeout, challenge, - application, + app_ids, key_handles, callback, }) => { @@ -77,7 +77,7 @@ impl U2FManager { flags, timeout, challenge, - application, + app_ids, key_handles, callback, ); @@ -109,12 +109,12 @@ impl U2FManager { flags: ::RegisterFlags, timeout: u64, challenge: Vec, - application: Vec, + application: ::AppId, key_handles: Vec<::KeyHandle>, callback: F, ) -> io::Result<()> where - F: FnOnce(io::Result>), + F: FnOnce(io::Result<::RegisterResult>), F: Send + 'static, { if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { @@ -150,21 +150,37 @@ impl U2FManager { flags: ::SignFlags, timeout: u64, challenge: Vec, - application: Vec, + app_ids: Vec<::AppId>, key_handles: Vec<::KeyHandle>, callback: F, ) -> io::Result<()> where - F: FnOnce(io::Result<(Vec, Vec)>), + F: FnOnce(io::Result<::SignResult>), F: Send + 'static, { - if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE { + if challenge.len() != PARAMETER_SIZE { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Invalid parameter sizes", )); } + if app_ids.len() < 1 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "No app IDs given", + )); + } + + for app_id in &app_ids { + if app_id.len() != PARAMETER_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid app_id size", + )); + } + } + for key_handle in &key_handles { if key_handle.credential.len() > 256 { return Err(io::Error::new( @@ -179,7 +195,7 @@ impl U2FManager { flags, timeout, challenge, - application, + app_ids, key_handles, callback, }; diff --git a/dom/webauthn/u2f-hid-rs/src/statemachine.rs b/dom/webauthn/u2f-hid-rs/src/statemachine.rs index 7674a889a386..7756062db145 100644 --- a/dom/webauthn/u2f-hid-rs/src/statemachine.rs +++ b/dom/webauthn/u2f-hid-rs/src/statemachine.rs @@ -14,6 +14,31 @@ fn is_valid_transport(transports: ::AuthenticatorTransports) -> bool { transports.is_empty() || transports.contains(::AuthenticatorTransports::USB) } +fn find_valid_key_handles<'a, F>( + app_ids: &'a Vec<::AppId>, + key_handles: &'a Vec<::KeyHandle>, + mut is_valid: F, +) -> (&'a ::AppId, Vec<&'a ::KeyHandle>) +where + F: FnMut(&Vec, &::KeyHandle) -> bool, +{ + // Try all given app_ids in order. + for app_id in app_ids { + // Find all valid key handles for the current app_id. + let valid_handles = key_handles + .iter() + .filter(|key_handle| is_valid(app_id, key_handle)) + .collect::>(); + + // If there's at least one, stop. + if valid_handles.len() > 0 { + return (app_id, valid_handles); + } + } + + return (&app_ids[0], vec![]); +} + #[derive(Default)] pub struct StateMachine { transaction: Option, @@ -29,9 +54,9 @@ impl StateMachine { flags: ::RegisterFlags, timeout: u64, challenge: Vec, - application: Vec, + application: ::AppId, key_handles: Vec<::KeyHandle>, - callback: OnceCallback>, + callback: OnceCallback<::RegisterResult>, ) { // Abort any prior register/sign calls. self.cancel(); @@ -93,9 +118,9 @@ impl StateMachine { flags: ::SignFlags, timeout: u64, challenge: Vec, - application: Vec, + app_ids: Vec<::AppId>, key_handles: Vec<::KeyHandle>, - callback: OnceCallback<(Vec, Vec)>, + callback: OnceCallback<::SignResult>, ) { // Abort any prior register/sign calls. self.cancel(); @@ -125,14 +150,15 @@ impl StateMachine { return; } - // Find all matching key handles. - let key_handles = key_handles - .iter() - .filter(|key_handle| { - u2f_is_keyhandle_valid(dev, &challenge, &application, &key_handle.credential) - .unwrap_or(false) /* no match on failure */ - }) - .collect::>(); + // For each appId, try all key handles. If there's at least one + // valid key handle for an appId, we'll use that appId below. + let (app_id, valid_handles) = + find_valid_key_handles(&app_ids, &key_handles, + |app_id, key_handle| { + u2f_is_keyhandle_valid(dev, &challenge, app_id, + &key_handle.credential) + .unwrap_or(false) /* no match on failure */ + }); // Aggregate distinct transports from all given credentials. let transports = key_handles.iter().fold( @@ -149,7 +175,7 @@ impl StateMachine { while alive() { // If the device matches none of the given key handles // then just make it blink with bogus data. - if key_handles.is_empty() { + if valid_handles.is_empty() { let blank = vec![0u8; PARAMETER_SIZE]; if let Ok(_) = u2f_register(dev, &blank, &blank) { callback.call(Err(io_err("invalid key"))); @@ -157,15 +183,17 @@ impl StateMachine { } } else { // Otherwise, try to sign. - for key_handle in &key_handles { + for key_handle in &valid_handles { if let Ok(bytes) = u2f_sign( dev, &challenge, - &application, + app_id, &key_handle.credential, ) { - callback.call(Ok((key_handle.credential.clone(), bytes))); + callback.call(Ok((app_id.clone(), + key_handle.credential.clone(), + bytes))); break; } } diff --git a/dom/webauthn/u2f-hid-rs/src/u2fhid-capi.h b/dom/webauthn/u2f-hid-rs/src/u2fhid-capi.h index 3dc0bb0847ef..fd043e5f4242 100644 --- a/dom/webauthn/u2f-hid-rs/src/u2fhid-capi.h +++ b/dom/webauthn/u2f-hid-rs/src/u2fhid-capi.h @@ -14,6 +14,7 @@ extern "C" { const uint8_t U2F_RESBUF_ID_REGISTRATION = 0; const uint8_t U2F_RESBUF_ID_KEYHANDLE = 1; const uint8_t U2F_RESBUF_ID_SIGNATURE = 2; +const uint8_t U2F_RESBUF_ID_APPID = 3; const uint64_t U2F_FLAG_REQUIRE_RESIDENT_KEY = 1; const uint64_t U2F_FLAG_REQUIRE_USER_VERIFICATION = 2; @@ -34,6 +35,9 @@ const uint8_t U2F_AUTHENTICATOR_TRANSPORT_BLE = 4; // The `rust_u2f_mgr` opaque type is equivalent to the rust type `U2FManager` struct rust_u2f_manager; +// The `rust_u2f_app_ids` opaque type is equivalent to the rust type `U2FAppIds` +struct rust_u2f_app_ids; + // The `rust_u2f_key_handles` opaque type is equivalent to the rust type `U2FKeyHandles` struct rust_u2f_key_handles; @@ -65,13 +69,21 @@ uint64_t rust_u2f_mgr_sign(rust_u2f_manager* mgr, rust_u2f_callback, const uint8_t* challenge_ptr, size_t challenge_len, - const uint8_t* application_ptr, - size_t application_len, + const rust_u2f_app_ids* app_ids, const rust_u2f_key_handles* khs); uint64_t rust_u2f_mgr_cancel(rust_u2f_manager* mgr); +/// U2FAppIds functions. + +rust_u2f_app_ids* rust_u2f_app_ids_new(); +void rust_u2f_app_ids_add(rust_u2f_app_ids* ids, + const uint8_t* id, + size_t id_len); +/* unsafe */ void rust_u2f_app_ids_free(rust_u2f_app_ids* ids); + + /// U2FKeyHandles functions. rust_u2f_key_handles* rust_u2f_khs_new(); diff --git a/dom/webidl/CredentialManagement.webidl b/dom/webidl/CredentialManagement.webidl index baada3515f01..fcb525075aed 100644 --- a/dom/webidl/CredentialManagement.webidl +++ b/dom/webidl/CredentialManagement.webidl @@ -21,6 +21,8 @@ interface CredentialsContainer { Promise create(optional CredentialCreationOptions options); [Throws] Promise store(Credential credential); + [Throws] + Promise preventSilentAccess(); }; dictionary CredentialRequestOptions { diff --git a/dom/webidl/WebAuthentication.webidl b/dom/webidl/WebAuthentication.webidl index 525935c10354..8bc72f36425a 100644 --- a/dom/webidl/WebAuthentication.webidl +++ b/dom/webidl/WebAuthentication.webidl @@ -13,8 +13,7 @@ interface PublicKeyCredential : Credential { [SameObject] readonly attribute ArrayBuffer rawId; [SameObject] readonly attribute AuthenticatorResponse response; - // Extensions are not supported yet. - // [SameObject] readonly attribute AuthenticationExtensions clientExtensionResults; // Add in Bug 1406458 + AuthenticationExtensionsClientOutputs getClientExtensionResults(); }; [SecureContext] @@ -104,10 +103,18 @@ dictionary PublicKeyCredentialRequestOptions { AuthenticationExtensionsClientInputs extensions; }; +// TODO - Use partial dictionaries when bug 1436329 is fixed. dictionary AuthenticationExtensionsClientInputs { + // FIDO AppID Extension (appid) + // + USVString appid; }; +// TODO - Use partial dictionaries when bug 1436329 is fixed. dictionary AuthenticationExtensionsClientOutputs { + // FIDO AppID Extension (appid) + // + boolean appid; }; typedef record AuthenticationExtensionsAuthenticatorInputs; @@ -144,3 +151,16 @@ typedef sequence AuthenticatorSelectionList; typedef BufferSource AAGUID; +/* +// FIDO AppID Extension (appid) +// +partial dictionary AuthenticationExtensionsClientInputs { + USVString appid; +}; + +// FIDO AppID Extension (appid) +// +partial dictionary AuthenticationExtensionsClientOutputs { + boolean appid; +}; +*/ diff --git a/dom/xhr/XMLHttpRequestMainThread.cpp b/dom/xhr/XMLHttpRequestMainThread.cpp index 6c956db9b83d..58d543c862d6 100644 --- a/dom/xhr/XMLHttpRequestMainThread.cpp +++ b/dom/xhr/XMLHttpRequestMainThread.cpp @@ -235,7 +235,10 @@ XMLHttpRequestMainThread::~XMLHttpRequestMainThread() Abort(); } - mParseEndListener = nullptr; + if (mParseEndListener) { + mParseEndListener->SetIsStale(); + mParseEndListener = nullptr; + } MOZ_ASSERT(!mFlagSyncLooping, "we rather crash than hang"); mFlagSyncLooping = false; diff --git a/dom/xhr/XMLHttpRequestMainThread.h b/dom/xhr/XMLHttpRequestMainThread.h index be855243f621..e7196ee028fa 100644 --- a/dom/xhr/XMLHttpRequestMainThread.h +++ b/dom/xhr/XMLHttpRequestMainThread.h @@ -875,8 +875,13 @@ public: mXHR = nullptr; return NS_OK; } + explicit nsXHRParseEndListener(XMLHttpRequestMainThread* aXHR) : mXHR(aXHR) {} + + void SetIsStale() { + mXHR = nullptr; + } private: virtual ~nsXHRParseEndListener() {} diff --git a/js/src/jit-test/tests/SIMD/bug1435317.js b/js/src/jit-test/tests/SIMD/bug1435317.js new file mode 100644 index 000000000000..0c7f97883181 --- /dev/null +++ b/js/src/jit-test/tests/SIMD/bug1435317.js @@ -0,0 +1,24 @@ +var ab = new ArrayBuffer(64 * 1024); +var arr = new Uint8Array(ab); + +(function(glob, imp, b) { + "use asm"; + var arr = new glob.Uint8Array(b); + return {} +})(this, null, ab); + +function testSimdX4() { + for (var i = 10; i --> 0;) { + var caught; + try { + v = SIMD.Int32x4.load(arr, 65534); + } catch (e) { + caught = e; + } + assertEq(caught instanceof RangeError, true); + } +} + +setJitCompilerOption('ion.warmup.trigger', 0); +testSimdX4(); + diff --git a/js/src/jit/BaselineCompiler.cpp b/js/src/jit/BaselineCompiler.cpp index 960a0bcc377d..b7d8a6dd8b12 100644 --- a/js/src/jit/BaselineCompiler.cpp +++ b/js/src/jit/BaselineCompiler.cpp @@ -4676,7 +4676,7 @@ BaselineCompiler::emit_JSOP_RESUME() GeneratorObject::ResumeKind resumeKind = GeneratorObject::getResumeKind(pc); frame.syncStack(0); - masm.checkStackAlignment(); + masm.assertStackAlignment(sizeof(Value), 0); AllocatableGeneralRegisterSet regs(GeneralRegisterSet::All()); regs.take(BaselineFrameReg); @@ -4790,7 +4790,7 @@ BaselineCompiler::emit_JSOP_RESUME() masm.push(BaselineFrameReg); masm.moveStackPtrTo(BaselineFrameReg); masm.subFromStackPtr(Imm32(BaselineFrame::Size())); - masm.checkStackAlignment(); + masm.assertStackAlignment(sizeof(Value), 0); // Store flags and env chain. masm.store32(Imm32(BaselineFrame::HAS_INITIAL_ENV), frame.addressOfFlags()); diff --git a/js/src/jit/BaselineIC.cpp b/js/src/jit/BaselineIC.cpp index 244f6d3dff97..edf0326a5dfe 100644 --- a/js/src/jit/BaselineIC.cpp +++ b/js/src/jit/BaselineIC.cpp @@ -3449,7 +3449,7 @@ ICCall_ClassHook::Compiler::generateStubCode(MacroAssembler& masm) pushCallArguments(masm, regs, argcReg, /* isJitCall = */ false, isConstructing_); regs.take(scratch); - masm.checkStackAlignment(); + masm.assertStackAlignment(sizeof(Value), 0); // Native functions have the signature: // diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp index 825a2efe5e72..9102535a3b96 100644 --- a/js/src/jit/CodeGenerator.cpp +++ b/js/src/jit/CodeGenerator.cpp @@ -3833,10 +3833,12 @@ void CodeGenerator::visitTypeBarrierV(LTypeBarrierV* lir) { ValueOperand operand = ToValue(lir, LTypeBarrierV::Input); - Register scratch = ToTempRegisterOrInvalid(lir->temp()); + Register unboxScratch = ToTempRegisterOrInvalid(lir->unboxTemp()); + Register objScratch = ToTempRegisterOrInvalid(lir->objTemp()); Label miss; - masm.guardTypeSet(operand, lir->mir()->resultTypeSet(), lir->mir()->barrierKind(), scratch, &miss); + masm.guardTypeSet(operand, lir->mir()->resultTypeSet(), lir->mir()->barrierKind(), + unboxScratch, objScratch, &miss); bailoutFrom(&miss, lir->snapshot()); } @@ -3869,10 +3871,12 @@ void CodeGenerator::visitMonitorTypes(LMonitorTypes* lir) { ValueOperand operand = ToValue(lir, LMonitorTypes::Input); - Register scratch = ToTempUnboxRegister(lir->temp()); + Register unboxScratch = ToTempRegisterOrInvalid(lir->unboxTemp()); + Register objScratch = ToTempRegisterOrInvalid(lir->objTemp()); Label matched, miss; - masm.guardTypeSet(operand, lir->mir()->typeSet(), lir->mir()->barrierKind(), scratch, &miss); + masm.guardTypeSet(operand, lir->mir()->typeSet(), lir->mir()->barrierKind(), unboxScratch, + objScratch, &miss); bailoutFrom(&miss, lir->snapshot()); } @@ -5086,7 +5090,9 @@ CodeGenerator::generateArgumentsChecks(bool assert) MResumePoint* rp = mir.entryResumePoint(); // No registers are allocated yet, so it's safe to grab anything. - Register temp = AllocatableGeneralRegisterSet(GeneralRegisterSet::All()).getAny(); + AllocatableGeneralRegisterSet temps(GeneralRegisterSet::All()); + Register temp1 = temps.takeAny(); + Register temp2 = temps.takeAny(); const CompileInfo& info = gen->info(); @@ -5103,7 +5109,8 @@ CodeGenerator::generateArgumentsChecks(bool assert) // ... * sizeof(Value) - Scale by value size. // ArgToStackOffset(...) - Compute displacement within arg vector. int32_t offset = ArgToStackOffset((i - info.startArgSlot()) * sizeof(Value)); - masm.guardTypeSet(Address(masm.getStackPointer(), offset), types, BarrierKind::TypeSet, temp, &miss); + Address argAddr(masm.getStackPointer(), offset); + masm.guardTypeSet(argAddr, types, BarrierKind::TypeSet, temp1, temp2, &miss); } if (miss.used()) { @@ -5124,8 +5131,8 @@ CodeGenerator::generateArgumentsChecks(bool assert) Label skip; Address addr(masm.getStackPointer(), ArgToStackOffset((i - info.startArgSlot()) * sizeof(Value))); masm.branchTestObject(Assembler::NotEqual, addr, &skip); - Register obj = masm.extractObject(addr, temp); - masm.guardTypeSetMightBeIncomplete(types, obj, temp, &success); + Register obj = masm.extractObject(addr, temp1); + masm.guardTypeSetMightBeIncomplete(types, obj, temp1, &success); masm.bind(&skip); } @@ -5469,7 +5476,7 @@ CodeGenerator::emitAssertResultV(const ValueOperand input, const TemporaryTypeSe if (typeset && !typeset->unknown()) { // We have a result TypeSet, assert this value is in it. Label miss, ok; - masm.guardTypeSet(input, typeset, BarrierKind::TypeSet, temp1, &miss); + masm.guardTypeSet(input, typeset, BarrierKind::TypeSet, temp1, temp2, &miss); masm.jump(&ok); masm.bind(&miss); diff --git a/js/src/jit/IonCacheIRCompiler.cpp b/js/src/jit/IonCacheIRCompiler.cpp index abf1db46a85c..e0555e46cbd4 100644 --- a/js/src/jit/IonCacheIRCompiler.cpp +++ b/js/src/jit/IonCacheIRCompiler.cpp @@ -1439,6 +1439,22 @@ EmitCheckPropertyTypes(MacroAssembler& masm, const PropertyTypeCheckInfo* typeCh masm.Push(obj); Register scratch1 = obj; + // We may also need a scratch register for guardTypeSet. + Register objScratch = InvalidReg; + if (propTypes && !propTypes->unknownObject() && propTypes->getObjectCount() > 0) { + AllocatableGeneralRegisterSet regs(GeneralRegisterSet::All()); + if (!val.constant()) { + TypedOrValueRegister valReg = val.reg(); + if (valReg.hasValue()) + regs.take(valReg.valueReg()); + else if (!valReg.typedReg().isFloat()) + regs.take(valReg.typedReg().gpr()); + } + regs.take(scratch1); + objScratch = regs.takeAny(); + masm.Push(objScratch); + } + bool checkTypeSet = true; Label failedFastPath; @@ -1468,7 +1484,8 @@ EmitCheckPropertyTypes(MacroAssembler& masm, const PropertyTypeCheckInfo* typeCh if (propTypes) { // guardTypeSet can read from type sets without triggering read barriers. TypeSet::readBarrier(propTypes); - masm.guardTypeSet(valReg, propTypes, BarrierKind::TypeSet, scratch1, &failedFastPath); + masm.guardTypeSet(valReg, propTypes, BarrierKind::TypeSet, scratch1, objScratch, + &failedFastPath); masm.jump(&done); } else { masm.jump(&failedFastPath); @@ -1511,11 +1528,15 @@ EmitCheckPropertyTypes(MacroAssembler& masm, const PropertyTypeCheckInfo* typeCh masm.PopRegsInMaskIgnore(save, ignore); masm.branchIfTrueBool(scratch1, &done); + if (objScratch != InvalidReg) + masm.pop(objScratch); masm.pop(obj); masm.jump(failures); } masm.bind(&done); + if (objScratch != InvalidReg) + masm.Pop(objScratch); masm.Pop(obj); } diff --git a/js/src/jit/Lowering.cpp b/js/src/jit/Lowering.cpp index c31d851f45d9..5f61097084a2 100644 --- a/js/src/jit/Lowering.cpp +++ b/js/src/jit/Lowering.cpp @@ -2792,7 +2792,6 @@ LIRGenerator::visitTypeBarrier(MTypeBarrier* ins) // from inside a type barrier test. const TemporaryTypeSet* types = ins->resultTypeSet(); - bool needTemp = !types->unknownObject() && types->getObjectCount() > 0; MIRType inputType = ins->getOperand(0)->type(); MOZ_ASSERT(inputType == ins->type()); @@ -2807,10 +2806,13 @@ LIRGenerator::visitTypeBarrier(MTypeBarrier* ins) return; } + bool needObjTemp = !types->unknownObject() && types->getObjectCount() > 0; + // Handle typebarrier with Value as input. if (inputType == MIRType::Value) { - LDefinition tmp = needTemp ? temp() : tempToUnbox(); - LTypeBarrierV* barrier = new(alloc()) LTypeBarrierV(useBox(ins->input()), tmp); + LDefinition objTemp = needObjTemp ? temp() : LDefinition::BogusTemp(); + LTypeBarrierV* barrier = new(alloc()) LTypeBarrierV(useBox(ins->input()), tempToUnbox(), + objTemp); assignSnapshot(barrier, Bailout_TypeBarrierV); add(barrier, ins); redefine(ins, ins->input()); @@ -2829,7 +2831,7 @@ LIRGenerator::visitTypeBarrier(MTypeBarrier* ins) } if (needsObjectBarrier) { - LDefinition tmp = needTemp ? temp() : LDefinition::BogusTemp(); + LDefinition tmp = needObjTemp ? temp() : LDefinition::BogusTemp(); LTypeBarrierO* barrier = new(alloc()) LTypeBarrierO(useRegister(ins->getOperand(0)), tmp); assignSnapshot(barrier, Bailout_TypeBarrierO); add(barrier, ins); @@ -2848,10 +2850,11 @@ LIRGenerator::visitMonitorTypes(MMonitorTypes* ins) // from inside a type check. const TemporaryTypeSet* types = ins->typeSet(); - bool needTemp = !types->unknownObject() && types->getObjectCount() > 0; - LDefinition tmp = needTemp ? temp() : tempToUnbox(); - LMonitorTypes* lir = new(alloc()) LMonitorTypes(useBox(ins->input()), tmp); + bool needObjTemp = !types->unknownObject() && types->getObjectCount() > 0; + LDefinition objTemp = needObjTemp ? temp() : LDefinition::BogusTemp(); + + LMonitorTypes* lir = new(alloc()) LMonitorTypes(useBox(ins->input()), tempToUnbox(), objTemp); assignSnapshot(lir, Bailout_MonitorTypes); add(lir, ins); } @@ -3188,8 +3191,7 @@ LIRGenerator::visitSpectreMaskIndex(MSpectreMaskIndex* ins) MOZ_ASSERT(ins->length()->type() == MIRType::Int32); MOZ_ASSERT(ins->type() == MIRType::Int32); - LSpectreMaskIndex* lir = - new(alloc()) LSpectreMaskIndex(useRegister(ins->index()), useAny(ins->length())); + auto* lir = new(alloc()) LSpectreMaskIndex(useRegister(ins->index()), useAny(ins->length())); define(lir, ins); } diff --git a/js/src/jit/MCallOptimize.cpp b/js/src/jit/MCallOptimize.cpp index 643a24e5d291..3f4d4ba479ad 100644 --- a/js/src/jit/MCallOptimize.cpp +++ b/js/src/jit/MCallOptimize.cpp @@ -4306,9 +4306,9 @@ SimdTypeToArrayElementType(SimdType type) } bool -IonBuilder::prepareForSimdLoadStore(CallInfo& callInfo, Scalar::Type simdType, MInstruction** elements, - MDefinition** index, Scalar::Type* arrayType, - BoundsCheckKind boundsCheckKind) +IonBuilder::prepareForSimdLoadStore(CallInfo& callInfo, Scalar::Type simdType, + MInstruction** elements, MDefinition** index, + Scalar::Type* arrayType, BoundsCheckKind boundsCheckKind) { MDefinition* array = callInfo.getArg(0); *index = callInfo.getArg(1); @@ -4320,31 +4320,35 @@ IonBuilder::prepareForSimdLoadStore(CallInfo& callInfo, Scalar::Type simdType, M current->add(indexAsInt32); *index = indexAsInt32; - MDefinition* indexForBoundsCheck = *index; + MDefinition* indexLoadEnd = *index; - // Artificially make sure the index is in bounds by adding the difference - // number of slots needed (e.g. reading from Float32Array we need to make - // sure to be in bounds for 4 slots, so add 3, etc.). MOZ_ASSERT(Scalar::byteSize(simdType) % Scalar::byteSize(*arrayType) == 0); - int32_t suppSlotsNeeded = Scalar::byteSize(simdType) / Scalar::byteSize(*arrayType) - 1; - if (suppSlotsNeeded) { - MConstant* suppSlots = constant(Int32Value(suppSlotsNeeded)); - MAdd* addedIndex = MAdd::New(alloc(), *index, suppSlots); - // We're fine even with the add overflows, as long as the generated code - // for the bounds check uses an unsigned comparison. + int32_t byteLoadSize = Scalar::byteSize(simdType) / Scalar::byteSize(*arrayType); + if (byteLoadSize > 1) { + // Add the number of supplementary needed slots. Overflows are fine + // because the bounds check code uses an unsigned comparison. + MAdd* addedIndex = MAdd::New(alloc(), *index, constant(Int32Value(byteLoadSize - 1))); addedIndex->setInt32Specialization(); current->add(addedIndex); - indexForBoundsCheck = addedIndex; + indexLoadEnd = addedIndex; } MInstruction* length; addTypedArrayLengthAndData(array, SkipBoundsCheck, index, &length, elements, boundsCheckKind); - // It can be that the index is out of bounds, while the added index for the - // bounds check is in bounds, so we actually need two bounds checks here. + // If the index+size addition overflows, then indexLoadEnd might be + // in bounds while the actual index isn't, so we need two bounds checks + // here. + if (byteLoadSize > 1) { + indexLoadEnd = addBoundsCheck(indexLoadEnd, length, BoundsCheckKind::UnusedIndex); + auto* sub = MSub::New(alloc(), indexLoadEnd, constant(Int32Value(byteLoadSize - 1))); + sub->setInt32Specialization(); + current->add(sub); + *index = sub; + } + *index = addBoundsCheck(*index, length, boundsCheckKind); - addBoundsCheck(indexForBoundsCheck, length, BoundsCheckKind::UnusedIndex); return true; } diff --git a/js/src/jit/MacroAssembler.cpp b/js/src/jit/MacroAssembler.cpp index 475fd4934744..06cc50fbb1b3 100644 --- a/js/src/jit/MacroAssembler.cpp +++ b/js/src/jit/MacroAssembler.cpp @@ -80,8 +80,14 @@ EmitTypeCheck(MacroAssembler& masm, Assembler::Condition cond, const T& src, Typ template void MacroAssembler::guardTypeSet(const Source& address, const TypeSet* types, BarrierKind kind, - Register scratch, Label* miss) + Register unboxScratch, Register objScratch, Label* miss) { + // unboxScratch may be InvalidReg on 32-bit platforms. It should only be + // used for extracting the Value tag or payload. + // + // objScratch may be InvalidReg if the TypeSet does not contain specific + // objects to guard on. It should only be used for guardObjectType. + MOZ_ASSERT(kind == BarrierKind::TypeTagOnly || kind == BarrierKind::TypeSet); MOZ_ASSERT(!types->unknown()); @@ -119,7 +125,7 @@ MacroAssembler::guardTypeSet(const Source& address, const TypeSet* types, Barrie return; } - Register tag = extractTag(address, scratch); + Register tag = extractTag(address, unboxScratch); // Emit all typed tests. for (size_t i = 0; i < mozilla::ArrayLength(tests); i++) { @@ -140,26 +146,24 @@ MacroAssembler::guardTypeSet(const Source& address, const TypeSet* types, Barrie } // Test specific objects. - MOZ_ASSERT(scratch != InvalidReg); + MOZ_ASSERT(objScratch != InvalidReg); + MOZ_ASSERT(objScratch != unboxScratch); MOZ_ASSERT(numBranches == 1); branchTestObject(NotEqual, tag, miss); if (kind != BarrierKind::TypeTagOnly) { - Register obj = extractObject(address, scratch); - guardObjectType(obj, types, scratch, miss); + Register obj = extractObject(address, unboxScratch); + guardObjectType(obj, types, objScratch, miss); } else { #ifdef DEBUG Label fail; - Register obj = extractObject(address, scratch); - guardObjectType(obj, types, scratch, &fail); + Register obj = extractObject(address, unboxScratch); + guardObjectType(obj, types, objScratch, &fail); jump(&matched); + bind(&fail); - - if (obj == scratch) - extractObject(address, scratch); - guardTypeSetMightBeIncomplete(types, obj, scratch, &matched); - + guardTypeSetMightBeIncomplete(types, obj, objScratch, &matched); assumeUnreachable("Unexpected object type"); #endif } @@ -209,6 +213,7 @@ void MacroAssembler::guardObjectType(Register obj, const TypeSet* types, Register scratch, Label* miss) { + MOZ_ASSERT(obj != scratch); MOZ_ASSERT(!types->unknown()); MOZ_ASSERT(!types->hasType(TypeSet::AnyObjectType())); MOZ_ASSERT_IF(types->getObjectCount() > 0, scratch != InvalidReg); @@ -257,8 +262,6 @@ MacroAssembler::guardObjectType(Register obj, const TypeSet* types, if (hasObjectGroups) { comment("has object groups"); - // Note: Some platforms give the same register for obj and scratch. - // Make sure when writing to scratch, the obj register isn't used anymore! loadPtr(Address(obj, JSObject::offsetOfGroup()), scratch); for (unsigned i = 0; i < count; i++) { @@ -279,11 +282,14 @@ MacroAssembler::guardObjectType(Register obj, const TypeSet* types, } template void MacroAssembler::guardTypeSet(const Address& address, const TypeSet* types, - BarrierKind kind, Register scratch, Label* miss); + BarrierKind kind, Register unboxScratch, + Register objScratch, Label* miss); template void MacroAssembler::guardTypeSet(const ValueOperand& value, const TypeSet* types, - BarrierKind kind, Register scratch, Label* miss); + BarrierKind kind, Register unboxScratch, + Register objScratch, Label* miss); template void MacroAssembler::guardTypeSet(const TypedOrValueRegister& value, const TypeSet* types, - BarrierKind kind, Register scratch, Label* miss); + BarrierKind kind, Register unboxScratch, + Register objScratch, Label* miss); template static void diff --git a/js/src/jit/MacroAssembler.h b/js/src/jit/MacroAssembler.h index 0ceb966d7200..f517e5cfd248 100644 --- a/js/src/jit/MacroAssembler.h +++ b/js/src/jit/MacroAssembler.h @@ -1933,7 +1933,8 @@ class MacroAssembler : public MacroAssemblerSpecific // Emits a test of a value against all types in a TypeSet. A scratch // register is required. template - void guardTypeSet(const Source& address, const TypeSet* types, BarrierKind kind, Register scratch, Label* miss); + void guardTypeSet(const Source& address, const TypeSet* types, BarrierKind kind, + Register unboxScratch, Register objScratch, Label* miss); void guardObjectType(Register obj, const TypeSet* types, Register scratch, Label* miss); diff --git a/js/src/jit/mips-shared/Assembler-mips-shared.h b/js/src/jit/mips-shared/Assembler-mips-shared.h index 060dc3f7fa3c..f96b7d81eddc 100644 --- a/js/src/jit/mips-shared/Assembler-mips-shared.h +++ b/js/src/jit/mips-shared/Assembler-mips-shared.h @@ -833,6 +833,14 @@ class AssemblerMIPSShared : public AssemblerShared FCSR = 31 }; + enum FCSRBit { + CauseI = 12, + CauseU, + CauseO, + CauseZ, + CauseV + }; + enum FloatFormat { SingleFloat, DoubleFloat diff --git a/js/src/jit/mips-shared/CodeGenerator-mips-shared.cpp b/js/src/jit/mips-shared/CodeGenerator-mips-shared.cpp index 83447409ca23..7fff677ccebe 100644 --- a/js/src/jit/mips-shared/CodeGenerator-mips-shared.cpp +++ b/js/src/jit/mips-shared/CodeGenerator-mips-shared.cpp @@ -1481,17 +1481,19 @@ CodeGeneratorMIPSShared::visitWasmTruncateToInt32(LWasmTruncateToInt32* lir) MOZ_ASSERT(fromType == MIRType::Double || fromType == MIRType::Float32); - auto* ool = new (alloc()) OutOfLineWasmTruncateCheck(mir, input); + auto* ool = new (alloc()) OutOfLineWasmTruncateCheck(mir, input, output); addOutOfLineCode(ool, mir); Label* oolEntry = ool->entry(); if (mir->isUnsigned()) { if (fromType == MIRType::Double) - masm.wasmTruncateDoubleToUInt32(input, output, oolEntry); + masm.wasmTruncateDoubleToUInt32(input, output, mir->isSaturating(), oolEntry); else if (fromType == MIRType::Float32) - masm.wasmTruncateFloat32ToUInt32(input, output, oolEntry); + masm.wasmTruncateFloat32ToUInt32(input, output, mir->isSaturating(), oolEntry); else MOZ_CRASH("unexpected type"); + + masm.bind(ool->rejoin()); return; } @@ -1508,9 +1510,15 @@ CodeGeneratorMIPSShared::visitWasmTruncateToInt32(LWasmTruncateToInt32* lir) void CodeGeneratorMIPSShared::visitOutOfLineWasmTruncateCheck(OutOfLineWasmTruncateCheck* ool) { - masm.outOfLineWasmTruncateToIntCheck(ool->input(), ool->fromType(), ool->toType(), - ool->isUnsigned(), ool->rejoin(), - ool->bytecodeOffset()); + if(ool->toType() == MIRType::Int32) + { + masm.outOfLineWasmTruncateToInt32Check(ool->input(), ool->output(), ool->fromType(), + ool->flags(), ool->rejoin(), ool->bytecodeOffset()); + } else { + MOZ_ASSERT(ool->toType() == MIRType::Int64); + masm.outOfLineWasmTruncateToInt64Check(ool->input(), ool->output64(), ool->fromType(), + ool->flags(), ool->rejoin(), ool->bytecodeOffset()); + } } void diff --git a/js/src/jit/mips-shared/MacroAssembler-mips-shared-inl.h b/js/src/jit/mips-shared/MacroAssembler-mips-shared-inl.h index 10c41dd7c667..6a54ec1747e2 100644 --- a/js/src/jit/mips-shared/MacroAssembler-mips-shared-inl.h +++ b/js/src/jit/mips-shared/MacroAssembler-mips-shared-inl.h @@ -610,20 +610,10 @@ MacroAssembler::branchFloat(DoubleCondition cond, FloatRegister lhs, FloatRegist ma_bc1s(lhs, rhs, label, cond); } -void -MacroAssembler::branchTruncateFloat32MaybeModUint32(FloatRegister src, Register dest, Label* fail) -{ - Label test, success; - as_truncws(ScratchFloat32Reg, src); - as_mfc1(dest, ScratchFloat32Reg); - - ma_b(dest, Imm32(INT32_MAX), fail, Assembler::Equal); -} - void MacroAssembler::branchTruncateFloat32ToInt32(FloatRegister src, Register dest, Label* fail) { - convertFloat32ToInt32(src, dest, fail); + MOZ_CRASH(); } void @@ -633,25 +623,10 @@ MacroAssembler::branchDouble(DoubleCondition cond, FloatRegister lhs, FloatRegis ma_bc1d(lhs, rhs, label, cond); } -// Convert the floating point value to an integer, if it did not fit, then it -// was clamped to INT32_MIN/INT32_MAX, and we can test it. -// NOTE: if the value really was supposed to be INT32_MAX / INT32_MIN then it -// will be wrong. -void -MacroAssembler::branchTruncateDoubleMaybeModUint32(FloatRegister src, Register dest, Label* fail) -{ - Label test, success; - as_truncwd(ScratchDoubleReg, src); - as_mfc1(dest, ScratchDoubleReg); - - ma_b(dest, Imm32(INT32_MAX), fail, Assembler::Equal); - ma_b(dest, Imm32(INT32_MIN), fail, Assembler::Equal); -} - void MacroAssembler::branchTruncateDoubleToInt32(FloatRegister src, Register dest, Label* fail) { - convertDoubleToInt32(src, dest, fail); + MOZ_CRASH(); } template diff --git a/js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp b/js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp index aa4ada0d1538..06940c6379e0 100644 --- a/js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp +++ b/js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp @@ -1110,8 +1110,12 @@ MacroAssemblerMIPSShared::ma_lis(FloatRegister dest, float value) { Imm32 imm(mozilla::BitwiseCast(value)); - ma_li(ScratchRegister, imm); - moveToFloat32(ScratchRegister, dest); + if(imm.value != 0) { + ma_li(ScratchRegister, imm); + moveToFloat32(ScratchRegister, dest); + } else { + moveToFloat32(zero, dest); + } } void @@ -1654,7 +1658,7 @@ MacroAssembler::wasmTruncateDoubleToInt32(FloatRegister input, Register output, as_truncwd(ScratchFloat32Reg, input); as_cfc1(ScratchRegister, Assembler::FCSR); moveFromFloat32(ScratchFloat32Reg, output); - ma_ext(ScratchRegister, ScratchRegister, 6, 1); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseV, 1); ma_b(ScratchRegister, Imm32(0), oolEntry, Assembler::NotEqual); } @@ -1666,113 +1670,198 @@ MacroAssembler::wasmTruncateFloat32ToInt32(FloatRegister input, Register output, as_truncws(ScratchFloat32Reg, input); as_cfc1(ScratchRegister, Assembler::FCSR); moveFromFloat32(ScratchFloat32Reg, output); - ma_ext(ScratchRegister, ScratchRegister, 6, 1); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseV, 1); ma_b(ScratchRegister, Imm32(0), oolEntry, Assembler::NotEqual); } void -MacroAssembler::oolWasmTruncateCheckF32ToI32(FloatRegister input, Register, TruncFlags flags, - wasm::BytecodeOffset off, Label* rejoin) +MacroAssembler::oolWasmTruncateCheckF32ToI32(FloatRegister input, Register output, + TruncFlags flags, wasm::BytecodeOffset off, + Label* rejoin) { - outOfLineWasmTruncateToIntCheck(input, MIRType::Float32, MIRType::Int32, flags & TRUNC_UNSIGNED, - rejoin, off); + outOfLineWasmTruncateToInt32Check(input, output, MIRType::Float32, flags, rejoin, off); } void -MacroAssembler::oolWasmTruncateCheckF64ToI32(FloatRegister input, Register, TruncFlags flags, - wasm::BytecodeOffset off, Label* rejoin) +MacroAssembler::oolWasmTruncateCheckF64ToI32(FloatRegister input, Register output, + TruncFlags flags, wasm::BytecodeOffset off, + Label* rejoin) { - outOfLineWasmTruncateToIntCheck(input, MIRType::Double, MIRType::Int32, flags & TRUNC_UNSIGNED, - rejoin, off); + outOfLineWasmTruncateToInt32Check(input, output, MIRType::Double, flags, rejoin, off); } void -MacroAssembler::oolWasmTruncateCheckF32ToI64(FloatRegister input, Register64, TruncFlags flags, - wasm::BytecodeOffset off, Label* rejoin) +MacroAssembler::oolWasmTruncateCheckF32ToI64(FloatRegister input, Register64 output, + TruncFlags flags, wasm::BytecodeOffset off, + Label* rejoin) { - outOfLineWasmTruncateToIntCheck(input, MIRType::Float32, MIRType::Int64, flags & TRUNC_UNSIGNED, - rejoin, off); + outOfLineWasmTruncateToInt64Check(input, output, MIRType::Float32, flags, rejoin, off); } void -MacroAssembler::oolWasmTruncateCheckF64ToI64(FloatRegister input, Register64, TruncFlags flags, - wasm::BytecodeOffset off, Label* rejoin) +MacroAssembler::oolWasmTruncateCheckF64ToI64(FloatRegister input, Register64 output, + TruncFlags flags, wasm::BytecodeOffset off, + Label* rejoin) { - outOfLineWasmTruncateToIntCheck(input, MIRType::Double, MIRType::Int64, flags & TRUNC_UNSIGNED, - rejoin, off); + outOfLineWasmTruncateToInt64Check(input, output, MIRType::Double, flags, rejoin, off); } void -MacroAssemblerMIPSShared::outOfLineWasmTruncateToIntCheck(FloatRegister input, MIRType fromType, - MIRType toType, bool isUnsigned, - Label* rejoin, - wasm::BytecodeOffset trapOffset) +MacroAssemblerMIPSShared::outOfLineWasmTruncateToInt32Check(FloatRegister input, Register output, + MIRType fromType, TruncFlags flags, + Label* rejoin, + wasm::BytecodeOffset trapOffset) { - // Eagerly take care of NaNs. + bool isUnsigned = flags & TRUNC_UNSIGNED; + bool isSaturating = flags & TRUNC_SATURATING; + + if(isSaturating) { + + if(fromType == MIRType::Double) + asMasm().loadConstantDouble(0.0, ScratchDoubleReg); + else + asMasm().loadConstantFloat32(0.0f, ScratchFloat32Reg); + + if(isUnsigned) { + + ma_li(output, Imm32(UINT32_MAX)); + + FloatTestKind moveCondition; + compareFloatingPoint(fromType == MIRType::Double ? DoubleFloat : SingleFloat, + input, + fromType == MIRType::Double ? ScratchDoubleReg : ScratchFloat32Reg, + Assembler::DoubleLessThanOrUnordered, &moveCondition); + MOZ_ASSERT(moveCondition == TestForTrue); + + as_movt(output, zero); + } else { + + // Positive overflow is already saturated to INT32_MAX, so we only have + // to handle NaN and negative overflow here. + + FloatTestKind moveCondition; + compareFloatingPoint(fromType == MIRType::Double ? DoubleFloat : SingleFloat, + input, + input, + Assembler::DoubleUnordered, &moveCondition); + MOZ_ASSERT(moveCondition == TestForTrue); + + as_movt(output, zero); + + compareFloatingPoint(fromType == MIRType::Double ? DoubleFloat : SingleFloat, + input, + fromType == MIRType::Double ? ScratchDoubleReg : ScratchFloat32Reg, + Assembler::DoubleLessThan, &moveCondition); + MOZ_ASSERT(moveCondition == TestForTrue); + + ma_li(ScratchRegister, Imm32(INT32_MIN)); + as_movt(output, ScratchRegister); + } + + MOZ_ASSERT(rejoin->bound()); + asMasm().jump(rejoin); + return; + } + Label inputIsNaN; + if (fromType == MIRType::Double) asMasm().branchDouble(Assembler::DoubleUnordered, input, input, &inputIsNaN); else if (fromType == MIRType::Float32) asMasm().branchFloat(Assembler::DoubleUnordered, input, input, &inputIsNaN); - else - MOZ_CRASH("unexpected type in visitOutOfLineWasmTruncateCheck"); - // By default test for the following inputs and bail: - // signed: ] -Inf, INTXX_MIN - 1.0 ] and [ INTXX_MAX + 1.0 : +Inf [ - // unsigned: ] -Inf, -1.0 ] and [ UINTXX_MAX + 1.0 : +Inf [ - // Note: we cannot always represent those exact values. As a result - // this changes the actual comparison a bit. - double minValue, maxValue; - Assembler::DoubleCondition minCond = Assembler::DoubleLessThanOrEqual; - Assembler::DoubleCondition maxCond = Assembler::DoubleGreaterThanOrEqual; - if (toType == MIRType::Int64) { - if (isUnsigned) { - minValue = -1; - maxValue = double(UINT64_MAX) + 1.0; + asMasm().wasmTrap(wasm::Trap::IntegerOverflow, trapOffset); + asMasm().bind(&inputIsNaN); + asMasm().wasmTrap(wasm::Trap::InvalidConversionToInteger, trapOffset); +} + +void +MacroAssemblerMIPSShared::outOfLineWasmTruncateToInt64Check(FloatRegister input, Register64 output_, + MIRType fromType, TruncFlags flags, + Label* rejoin, + wasm::BytecodeOffset trapOffset) +{ + bool isUnsigned = flags & TRUNC_UNSIGNED; + bool isSaturating = flags & TRUNC_SATURATING; + + + if(isSaturating) { +#if defined(JS_CODEGEN_MIPS32) + // Saturating callouts don't use ool path. + return; +#else + Register output = output_.reg; + + if(fromType == MIRType::Double) + asMasm().loadConstantDouble(0.0, ScratchDoubleReg); + else + asMasm().loadConstantFloat32(0.0f, ScratchFloat32Reg); + + if(isUnsigned) { + + asMasm().ma_li(output, ImmWord(UINT64_MAX)); + + FloatTestKind moveCondition; + compareFloatingPoint(fromType == MIRType::Double ? DoubleFloat : SingleFloat, + input, + fromType == MIRType::Double ? ScratchDoubleReg : ScratchFloat32Reg, + Assembler::DoubleLessThanOrUnordered, &moveCondition); + MOZ_ASSERT(moveCondition == TestForTrue); + + as_movt(output, zero); } else { - // In the float32/double range there exists no value between - // INT64_MIN and INT64_MIN - 1.0. Making INT64_MIN the lower-bound. - minValue = double(INT64_MIN); - minCond = Assembler::DoubleLessThan; - maxValue = double(INT64_MAX) + 1.0; - } - } else { - if (isUnsigned) { - minValue = -1; - maxValue = double(UINT32_MAX) + 1.0; - } else { - if (fromType == MIRType::Float32) { - // In the float32 range there exists no value between - // INT32_MIN and INT32_MIN - 1.0. Making INT32_MIN the lower-bound. - minValue = double(INT32_MIN); - minCond = Assembler::DoubleLessThan; - } else { - minValue = double(INT32_MIN) - 1.0; - } - maxValue = double(INT32_MAX) + 1.0; + + // Positive overflow is already saturated to INT64_MAX, so we only have + // to handle NaN and negative overflow here. + + FloatTestKind moveCondition; + compareFloatingPoint(fromType == MIRType::Double ? DoubleFloat : SingleFloat, + input, + input, + Assembler::DoubleUnordered, &moveCondition); + MOZ_ASSERT(moveCondition == TestForTrue); + + as_movt(output, zero); + + compareFloatingPoint(fromType == MIRType::Double ? DoubleFloat : SingleFloat, + input, + fromType == MIRType::Double ? ScratchDoubleReg : ScratchFloat32Reg, + Assembler::DoubleLessThan, &moveCondition); + MOZ_ASSERT(moveCondition == TestForTrue); + + asMasm().ma_li(ScratchRegister, ImmWord(INT64_MIN)); + as_movt(output, ScratchRegister); } + + MOZ_ASSERT(rejoin->bound()); + asMasm().jump(rejoin); + return; +#endif + } - Label fail; + Label inputIsNaN; + + if (fromType == MIRType::Double) + asMasm().branchDouble(Assembler::DoubleUnordered, input, input, &inputIsNaN); + else if (fromType == MIRType::Float32) + asMasm().branchFloat(Assembler::DoubleUnordered, input, input, &inputIsNaN); + +#if defined(JS_CODEGEN_MIPS32) + + // Only possible valid input that produces INT64_MIN result. + double validInput = isUnsigned ? double(uint64_t(INT64_MIN)) : double(int64_t(INT64_MIN)); if (fromType == MIRType::Double) { - asMasm().loadConstantDouble(minValue, ScratchDoubleReg); - asMasm().branchDouble(minCond, input, ScratchDoubleReg, &fail); - - asMasm().loadConstantDouble(maxValue, ScratchDoubleReg); - asMasm().branchDouble(maxCond, input, ScratchDoubleReg, &fail); + asMasm().loadConstantDouble(validInput, ScratchDoubleReg); + asMasm().branchDouble(Assembler::DoubleEqual, input, ScratchDoubleReg, rejoin); } else { - asMasm().loadConstantFloat32(float(minValue), ScratchFloat32Reg); - asMasm().branchFloat(minCond, input, ScratchFloat32Reg, &fail); - - asMasm().loadConstantFloat32(float(maxValue), ScratchFloat32Reg); - asMasm().branchFloat(maxCond, input, ScratchFloat32Reg, &fail); + asMasm().loadConstantFloat32(float(validInput), ScratchFloat32Reg); + asMasm().branchFloat(Assembler::DoubleEqual, input, ScratchDoubleReg, rejoin); } - asMasm().jump(rejoin); +#endif - // Handle errors. - asMasm().bind(&fail); asMasm().wasmTrap(wasm::Trap::IntegerOverflow, trapOffset); asMasm().bind(&inputIsNaN); asMasm().wasmTrap(wasm::Trap::InvalidConversionToInteger, trapOffset); diff --git a/js/src/jit/mips-shared/MacroAssembler-mips-shared.h b/js/src/jit/mips-shared/MacroAssembler-mips-shared.h index 8d115f0cc6ef..b88695d54827 100644 --- a/js/src/jit/mips-shared/MacroAssembler-mips-shared.h +++ b/js/src/jit/mips-shared/MacroAssembler-mips-shared.h @@ -217,9 +217,12 @@ class MacroAssemblerMIPSShared : public Assembler void minMaxDouble(FloatRegister srcDest, FloatRegister other, bool handleNaN, bool isMax); void minMaxFloat32(FloatRegister srcDest, FloatRegister other, bool handleNaN, bool isMax); - void outOfLineWasmTruncateToIntCheck(FloatRegister input, MIRType fromType, - MIRType toType, bool isUnsigned, Label* rejoin, - wasm::BytecodeOffset trapOffset); + void outOfLineWasmTruncateToInt32Check(FloatRegister input, Register output, MIRType fromType, + TruncFlags flags, Label* rejoin, + wasm::BytecodeOffset trapOffset); + void outOfLineWasmTruncateToInt64Check(FloatRegister input, Register64 output, MIRType fromType, + TruncFlags flags, Label* rejoin, + wasm::BytecodeOffset trapOffset); protected: void wasmLoadImpl(const wasm::MemoryAccessDesc& access, Register memoryBase, Register ptr, diff --git a/js/src/jit/mips32/Architecture-mips32.h b/js/src/jit/mips32/Architecture-mips32.h index 7679a06c0baa..8bbec8fc1d9d 100644 --- a/js/src/jit/mips32/Architecture-mips32.h +++ b/js/src/jit/mips32/Architecture-mips32.h @@ -69,7 +69,7 @@ class FloatRegisters : public FloatRegistersMIPSShared static const uint32_t TotalDouble = 16; static const uint32_t TotalSingle = 16; - static const uint32_t Allocatable = 28; + static const uint32_t Allocatable = 30; static const SetType AllSingleMask = (1ULL << TotalSingle) - 1; static const SetType AllDoubleMask = ((1ULL << TotalDouble) - 1) << TotalSingle; @@ -95,8 +95,7 @@ class FloatRegisters : public FloatRegistersMIPSShared static const SetType WrapperMask = VolatileMask; static const SetType NonAllocatableMask = - ((SetType(1) << (FloatRegisters::f16 >> 1)) | - (SetType(1) << (FloatRegisters::f18 >> 1))) * ((1 << TotalSingle) + 1); + (SetType(1) << (FloatRegisters::f18 >> 1)) * ((1 << TotalSingle) + 1); static const SetType AllocatableMask = AllMask & ~NonAllocatableMask; }; diff --git a/js/src/jit/mips32/Assembler-mips32.h b/js/src/jit/mips32/Assembler-mips32.h index 7941744a7296..812d4accf97d 100644 --- a/js/src/jit/mips32/Assembler-mips32.h +++ b/js/src/jit/mips32/Assembler-mips32.h @@ -83,8 +83,6 @@ static constexpr FloatRegister ReturnFloat32Reg = { FloatRegisters::f0, FloatReg static constexpr FloatRegister ReturnDoubleReg = { FloatRegisters::f0, FloatRegister::Double }; static constexpr FloatRegister ScratchFloat32Reg = { FloatRegisters::f18, FloatRegister::Single }; static constexpr FloatRegister ScratchDoubleReg = { FloatRegisters::f18, FloatRegister::Double }; -static constexpr FloatRegister SecondScratchFloat32Reg = { FloatRegisters::f16, FloatRegister::Single }; -static constexpr FloatRegister SecondScratchDoubleReg = { FloatRegisters::f16, FloatRegister::Double }; struct ScratchFloat32Scope : public AutoFloatRegisterScope { diff --git a/js/src/jit/mips32/CodeGenerator-mips32.cpp b/js/src/jit/mips32/CodeGenerator-mips32.cpp index 3d39edef7ae7..696950b247e6 100644 --- a/js/src/jit/mips32/CodeGenerator-mips32.cpp +++ b/js/src/jit/mips32/CodeGenerator-mips32.cpp @@ -619,40 +619,45 @@ void CodeGeneratorMIPS::visitWasmTruncateToInt64(LWasmTruncateToInt64* lir) { FloatRegister input = ToFloatRegister(lir->input()); - FloatRegister scratch = input; + FloatRegister arg = input; Register64 output = ToOutRegister64(lir); MWasmTruncateToInt64* mir = lir->mir(); MIRType fromType = mir->input()->type(); - auto* ool = new(alloc()) OutOfLineWasmTruncateCheck(mir, input); + auto* ool = new(alloc()) OutOfLineWasmTruncateCheck(mir, input, Register64::Invalid()); addOutOfLineCode(ool, mir); - if (fromType == MIRType::Double) { - masm.branchDouble(Assembler::DoubleUnordered, input, input, ool->entry()); - } else if (fromType == MIRType::Float32) { - masm.branchFloat(Assembler::DoubleUnordered, input, input, ool->entry()); - scratch = ScratchDoubleReg; - masm.convertFloat32ToDouble(input, scratch); - } else { - MOZ_CRASH("unexpected type in visitOutOfLineWasmTruncateCheck"); + if (fromType == MIRType::Float32) { + arg = ScratchDoubleReg; + masm.convertFloat32ToDouble(input, arg); } - masm.Push(input); + if (!lir->mir()->isSaturating()) { + masm.Push(input); - masm.setupWasmABICall(); - masm.passABIArg(scratch, MoveOp::DOUBLE); - if (lir->mir()->isUnsigned()) - masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::TruncateDoubleToUint64); - else - masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::TruncateDoubleToInt64); + masm.setupWasmABICall(); + masm.passABIArg(arg, MoveOp::DOUBLE); - masm.Pop(input); + if (lir->mir()->isUnsigned()) + masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::TruncateDoubleToUint64); + else + masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::TruncateDoubleToInt64); - masm.ma_b(output.high, Imm32(0x80000000), ool->rejoin(), Assembler::NotEqual); - masm.ma_b(output.low, Imm32(0x00000000), ool->rejoin(), Assembler::NotEqual); - masm.ma_b(ool->entry()); + masm.Pop(input); - masm.bind(ool->rejoin()); + masm.ma_xor(ScratchRegister, output.high, Imm32(0x80000000)); + masm.ma_or(ScratchRegister, output.low); + masm.ma_b(ScratchRegister, Imm32(0), ool->entry(), Assembler::Equal); + + masm.bind(ool->rejoin()); + } else { + masm.setupWasmABICall(); + masm.passABIArg(arg, MoveOp::DOUBLE); + if (lir->mir()->isUnsigned()) + masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::SaturatingTruncateDoubleToUint64); + else + masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::SaturatingTruncateDoubleToInt64); + } MOZ_ASSERT(ReturnReg64 == output); } @@ -661,7 +666,7 @@ void CodeGeneratorMIPS::visitInt64ToFloatingPoint(LInt64ToFloatingPoint* lir) { Register64 input = ToRegister64(lir->getInt64Operand(0)); - FloatRegister output = ToFloatRegister(lir->output()); + mozilla::DebugOnly output = ToFloatRegister(lir->output()); MInt64ToFloatingPoint* mir = lir->mir(); MIRType toType = mir->type(); @@ -686,8 +691,8 @@ CodeGeneratorMIPS::visitInt64ToFloatingPoint(LInt64ToFloatingPoint* lir) else masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::Int64ToFloat32, MoveOp::FLOAT32); - MOZ_ASSERT_IF(toType == MIRType::Double, output == ReturnDoubleReg); - MOZ_ASSERT_IF(toType == MIRType::Float32, output == ReturnFloat32Reg); + MOZ_ASSERT_IF(toType == MIRType::Double, *(&output) == ReturnDoubleReg); + MOZ_ASSERT_IF(toType == MIRType::Float32, *(&output) == ReturnFloat32Reg); } void diff --git a/js/src/jit/mips32/MacroAssembler-mips32-inl.h b/js/src/jit/mips32/MacroAssembler-mips32-inl.h index 30b5c0e7de98..4007703aab1d 100644 --- a/js/src/jit/mips32/MacroAssembler-mips32-inl.h +++ b/js/src/jit/mips32/MacroAssembler-mips32-inl.h @@ -996,6 +996,26 @@ MacroAssembler::branchToComputedAddress(const BaseIndex& addr) as_nop(); } +void +MacroAssembler::branchTruncateDoubleMaybeModUint32(FloatRegister src, Register dest, Label* fail) +{ + as_truncwd(ScratchFloat32Reg, src); + as_cfc1(ScratchRegister, Assembler::FCSR); + moveFromFloat32(ScratchFloat32Reg, dest); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseV, 1); + ma_b(ScratchRegister, Imm32(0), fail, Assembler::NotEqual); +} + +void +MacroAssembler::branchTruncateFloat32MaybeModUint32(FloatRegister src, Register dest, Label* fail) +{ + as_truncws(ScratchFloat32Reg, src); + as_cfc1(ScratchRegister, Assembler::FCSR); + moveFromFloat32(ScratchFloat32Reg, dest); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseV, 1); + ma_b(ScratchRegister, Imm32(0), fail, Assembler::NotEqual); +} + // ======================================================================== // Memory access primitives. void diff --git a/js/src/jit/mips32/MacroAssembler-mips32.cpp b/js/src/jit/mips32/MacroAssembler-mips32.cpp index 161f7833f65d..8eaaa44c9687 100644 --- a/js/src/jit/mips32/MacroAssembler-mips32.cpp +++ b/js/src/jit/mips32/MacroAssembler-mips32.cpp @@ -62,20 +62,26 @@ MacroAssemblerMIPSCompat::convertInt32ToDouble(const BaseIndex& src, FloatRegist void MacroAssemblerMIPSCompat::convertUInt32ToDouble(Register src, FloatRegister dest) { - // We use SecondScratchDoubleReg because MacroAssembler::loadFromTypedArray - // calls with ScratchDoubleReg as dest. - MOZ_ASSERT(dest != SecondScratchDoubleReg); + Label positive, done; + ma_b(src, src, &positive, NotSigned, ShortJump); - // Subtract INT32_MIN to get a positive number - ma_subu(ScratchRegister, src, Imm32(INT32_MIN)); + const uint32_t kExponentShift = mozilla::FloatingPoint::kExponentShift - 32; + const uint32_t kExponent = (31 + mozilla::FloatingPoint::kExponentBias); - // Convert value - as_mtc1(ScratchRegister, dest); - as_cvtdw(dest, dest); + ma_ext(SecondScratchReg, src, 31 - kExponentShift, kExponentShift); + ma_li(ScratchRegister, Imm32(kExponent << kExponentShift)); + ma_or(SecondScratchReg, ScratchRegister); + ma_sll(ScratchRegister, src, Imm32(kExponentShift + 1)); + moveToDoubleHi(SecondScratchReg, dest); + moveToDoubleLo(ScratchRegister, dest); + + ma_b(&done, ShortJump); + + bind(&positive); + convertInt32ToDouble(src, dest); + + bind(&done); - // Add unsigned value of INT32_MIN - ma_lid(SecondScratchDoubleReg, 2147483648.0); - as_addd(dest, dest, SecondScratchDoubleReg); } void @@ -84,10 +90,19 @@ MacroAssemblerMIPSCompat::convertUInt32ToFloat32(Register src, FloatRegister des Label positive, done; ma_b(src, src, &positive, NotSigned, ShortJump); - // We cannot do the same as convertUInt32ToDouble because float32 doesn't - // have enough precision. - convertUInt32ToDouble(src, dest); - convertDoubleToFloat32(dest, dest); + const uint32_t kExponentShift = mozilla::FloatingPoint::kExponentShift - 32; + const uint32_t kExponent = (31 + mozilla::FloatingPoint::kExponentBias); + + ma_ext(SecondScratchReg, src, 31 - kExponentShift, kExponentShift); + ma_li(ScratchRegister, Imm32(kExponent << kExponentShift)); + ma_or(SecondScratchReg, ScratchRegister); + ma_sll(ScratchRegister, src, Imm32(kExponentShift + 1)); + FloatRegister destDouble = dest.asDouble(); + moveToDoubleHi(SecondScratchReg, destDouble); + moveToDoubleLo(ScratchRegister, destDouble); + + convertDoubleToFloat32(destDouble, dest); + ma_b(&done, ShortJump); bind(&positive); @@ -111,17 +126,18 @@ MacroAssemblerMIPSCompat::convertDoubleToInt32(FloatRegister src, Register dest, { if (negativeZeroCheck) { moveFromDoubleHi(src, dest); - moveFromDoubleLo(src, ScratchRegister); - as_movn(dest, zero, ScratchRegister); - ma_b(dest, Imm32(INT32_MIN), fail, Assembler::Equal); + moveFromDoubleLo(src, SecondScratchReg); + ma_xor(dest, Imm32(INT32_MIN)); + ma_or(dest, SecondScratchReg); + ma_b(dest, Imm32(0), fail, Assembler::Equal); } - // Convert double to int, then convert back and check if we have the - // same number. - as_cvtwd(ScratchDoubleReg, src); - as_mfc1(dest, ScratchDoubleReg); - as_cvtdw(ScratchDoubleReg, ScratchDoubleReg); - ma_bc1d(src, ScratchDoubleReg, fail, Assembler::DoubleNotEqualOrUnordered); + // Truncate double to int ; if result is inexact fail + as_truncwd(ScratchFloat32Reg, src); + as_cfc1(ScratchRegister, Assembler::FCSR); + moveFromFloat32(ScratchFloat32Reg, dest); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseI, 1); + ma_b(ScratchRegister, Imm32(0), fail, Assembler::NotEqual); } // Checks whether a float32 is representable as a 32-bit integer. If so, the @@ -136,18 +152,11 @@ MacroAssemblerMIPSCompat::convertFloat32ToInt32(FloatRegister src, Register dest ma_b(dest, Imm32(INT32_MIN), fail, Assembler::Equal); } - // Converting the floating point value to an integer and then converting it - // back to a float32 would not work, as float to int32 conversions are - // clamping (e.g. float(INT32_MAX + 1) would get converted into INT32_MAX - // and then back to float(INT32_MAX + 1)). If this ever happens, we just - // bail out. - as_cvtws(ScratchFloat32Reg, src); - as_mfc1(dest, ScratchFloat32Reg); - as_cvtsw(ScratchFloat32Reg, ScratchFloat32Reg); - ma_bc1s(src, ScratchFloat32Reg, fail, Assembler::DoubleNotEqualOrUnordered); - - // Bail out in the clamped cases. - ma_b(dest, Imm32(INT32_MAX), fail, Assembler::Equal); + as_truncws(ScratchFloat32Reg, src); + as_cfc1(ScratchRegister, Assembler::FCSR); + moveFromFloat32(ScratchFloat32Reg, dest); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseI, 1); + ma_b(ScratchRegister, Imm32(0), fail, Assembler::NotEqual); } void @@ -1377,49 +1386,19 @@ MacroAssemblerMIPSCompat::storeUnalignedDouble(const wasm::MemoryAccessDesc& acc append(access, store.getOffset(), framePushed); } -// Note: this function clobbers the input register. void MacroAssembler::clampDoubleToUint8(FloatRegister input, Register output) { - MOZ_ASSERT(input != ScratchDoubleReg); - Label positive, done; - - // <= 0 or NaN --> 0 - zeroDouble(ScratchDoubleReg); - branchDouble(DoubleGreaterThan, input, ScratchDoubleReg, &positive); - { - move32(Imm32(0), output); - jump(&done); - } - - bind(&positive); - - // Add 0.5 and truncate. - loadConstantDouble(0.5, ScratchDoubleReg); - addDouble(ScratchDoubleReg, input); - - Label outOfRange; - - branchTruncateDoubleMaybeModUint32(input, output, &outOfRange); - asMasm().branch32(Assembler::Above, output, Imm32(255), &outOfRange); - { - // Check if we had a tie. - convertInt32ToDouble(output, ScratchDoubleReg); - branchDouble(DoubleNotEqual, input, ScratchDoubleReg, &done); - - // It was a tie. Mask out the ones bit to get an even value. - // See also js_TypedArray_uint8_clamp_double. - and32(Imm32(~1), output); - jump(&done); - } - - // > 255 --> 255 - bind(&outOfRange); - { - move32(Imm32(255), output); - } - - bind(&done); + as_roundwd(ScratchDoubleReg, input); + ma_li(ScratchRegister, Imm32(255)); + as_mfc1(output, ScratchDoubleReg); + zeroDouble(ScratchDoubleReg); + as_sltiu(SecondScratchReg, output, 255); + as_colt(DoubleFloat, ScratchDoubleReg, input); + // if res > 255; res = 255; + as_movz(output, ScratchRegister, SecondScratchReg); + // if !(input > 0); res = 0; + as_movf(output, zero); } // higher level tag testing code @@ -2549,25 +2528,26 @@ void MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output, bool isSaturating, Label* oolEntry) { - MOZ_ASSERT(!isSaturating, "NYI"); + Label done; - loadConstantDouble(double(-1.0), ScratchDoubleReg); - branchDouble(Assembler::DoubleLessThanOrEqual, input, ScratchDoubleReg, oolEntry); + as_truncwd(ScratchFloat32Reg, input); + ma_li(ScratchRegister, Imm32(INT32_MAX)); + moveFromFloat32(ScratchFloat32Reg, output); - loadConstantDouble(double(UINT32_MAX) + 1.0, ScratchDoubleReg); - branchDouble(Assembler::DoubleGreaterThanOrEqualOrUnordered, input, ScratchDoubleReg, oolEntry); - Label done, simple; - loadConstantDouble(double(0x80000000UL), ScratchDoubleReg); - branchDouble(Assembler::DoubleLessThan, input, ScratchDoubleReg, &simple); + // For numbers in -1.[ : ]INT32_MAX range do nothing more + ma_b(output, ScratchRegister, &done, Assembler::Below, ShortJump); + + loadConstantDouble(double(INT32_MAX + 1ULL), ScratchDoubleReg); + ma_li(ScratchRegister, Imm32(INT32_MIN)); as_subd(ScratchDoubleReg, input, ScratchDoubleReg); - as_truncwd(ScratchDoubleReg, ScratchDoubleReg); - moveFromFloat32(ScratchDoubleReg, output); - ma_li(ScratchRegister, Imm32(0x80000000UL)); - ma_or(output, ScratchRegister); - ma_b(&done); - bind(&simple); - as_truncwd(ScratchDoubleReg, input); - moveFromFloat32(ScratchDoubleReg, output); + as_truncwd(ScratchFloat32Reg, ScratchDoubleReg); + as_cfc1(SecondScratchReg, Assembler::FCSR); + moveFromFloat32(ScratchFloat32Reg, output); + ma_ext(SecondScratchReg, SecondScratchReg, Assembler::CauseV, 1); + ma_addu(output, ScratchRegister); + + ma_b(SecondScratchReg, Imm32(0), oolEntry, Assembler::NotEqual); + bind(&done); } @@ -2575,25 +2555,29 @@ void MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, bool isSaturating, Label* oolEntry) { - MOZ_ASSERT(!isSaturating, "NYI"); + Label done; - loadConstantFloat32(double(-1.0), ScratchDoubleReg); - branchFloat(Assembler::DoubleLessThanOrEqualOrUnordered, input, ScratchDoubleReg, oolEntry); + as_truncws(ScratchFloat32Reg, input); + ma_li(ScratchRegister, Imm32(INT32_MAX)); + moveFromFloat32(ScratchFloat32Reg, output); + // For numbers in -1.[ : ]INT32_MAX range do nothing more + ma_b(output, ScratchRegister, &done, Assembler::Below, ShortJump); + + loadConstantFloat32(float(INT32_MAX + 1ULL), ScratchFloat32Reg); + ma_li(ScratchRegister, Imm32(INT32_MIN)); + as_subs(ScratchFloat32Reg, input, ScratchFloat32Reg); + as_truncws(ScratchFloat32Reg, ScratchFloat32Reg); + as_cfc1(SecondScratchReg, Assembler::FCSR); + moveFromFloat32(ScratchFloat32Reg, output); + ma_ext(SecondScratchReg, SecondScratchReg, Assembler::CauseV, 1); + ma_addu(output, ScratchRegister); + + // Guard against negative values that result in 0 due the precision loss. + as_sltiu(ScratchRegister, output, 1); + ma_or(SecondScratchReg, ScratchRegister); + + ma_b(SecondScratchReg, Imm32(0), oolEntry, Assembler::NotEqual); - loadConstantFloat32(double(UINT32_MAX) + 1.0, ScratchDoubleReg); - branchFloat(Assembler::DoubleGreaterThanOrEqualOrUnordered, input, ScratchDoubleReg, oolEntry); - Label done, simple; - loadConstantFloat32(double(0x80000000UL), ScratchDoubleReg); - branchFloat(Assembler::DoubleLessThan, input, ScratchDoubleReg, &simple); - as_subs(ScratchDoubleReg, input, ScratchDoubleReg); - as_truncws(ScratchDoubleReg, ScratchDoubleReg); - moveFromFloat32(ScratchDoubleReg, output); - ma_li(ScratchRegister, Imm32(0x80000000UL)); - ma_or(output, ScratchRegister); - ma_b(&done); - bind(&simple); - as_truncws(ScratchDoubleReg, input); - moveFromFloat32(ScratchDoubleReg, output); bind(&done); } diff --git a/js/src/jit/mips32/Simulator-mips32.cpp b/js/src/jit/mips32/Simulator-mips32.cpp index b666bc95c030..c3d86a009b69 100644 --- a/js/src/jit/mips32/Simulator-mips32.cpp +++ b/js/src/jit/mips32/Simulator-mips32.cpp @@ -1576,24 +1576,34 @@ Simulator::setFCSRRoundError(double original, double rounded) { bool ret = false; + setFCSRBit(kFCSRInexactCauseBit, false); + setFCSRBit(kFCSRUnderflowCauseBit, false); + setFCSRBit(kFCSROverflowCauseBit, false); + setFCSRBit(kFCSRInvalidOpCauseBit, false); + if (!std::isfinite(original) || !std::isfinite(rounded)) { setFCSRBit(kFCSRInvalidOpFlagBit, true); + setFCSRBit(kFCSRInvalidOpCauseBit, true); ret = true; } if (original != rounded) { setFCSRBit(kFCSRInexactFlagBit, true); + setFCSRBit(kFCSRInexactCauseBit, true); } if (rounded < DBL_MIN && rounded > -DBL_MIN && rounded != 0) { setFCSRBit(kFCSRUnderflowFlagBit, true); + setFCSRBit(kFCSRUnderflowCauseBit, true); ret = true; } if (rounded > INT_MAX || rounded < INT_MIN) { setFCSRBit(kFCSROverflowFlagBit, true); + setFCSRBit(kFCSROverflowCauseBit, true); // The reference is not really clear but it seems this is required: setFCSRBit(kFCSRInvalidOpFlagBit, true); + setFCSRBit(kFCSRInvalidOpCauseBit, true); ret = true; } @@ -1621,15 +1631,15 @@ Simulator::get_pc() const return registers_[pc]; } -void -Simulator::startInterrupt(JitActivation* activation) +JS::ProfilingFrameIterator::RegisterState +Simulator::registerState() { - JS::ProfilingFrameIterator::RegisterState state; + wasm::RegisterState state; state.pc = (void*) get_pc(); state.fp = (void*) getRegister(fp); state.sp = (void*) getRegister(sp); state.lr = (void*) getRegister(ra); - activation->startWasmInterrupt(state); + return state; } // The signal handler only redirects the PC to the interrupt stub when the PC is @@ -1651,7 +1661,9 @@ Simulator::handleWasmInterrupt() if (!segment || !segment->isModule() || !segment->containsCodePC(pc)) return; - startInterrupt(activation); + if (!activation->startWasmInterrupt(registerState())) + return; + set_pc(int32_t(segment->asModule()->interruptCode())); } @@ -1682,14 +1694,19 @@ Simulator::handleWasmFault(int32_t addr, unsigned numBytes) const wasm::ModuleSegment* moduleSegment = segment->asModule(); wasm::Instance* instance = wasm::LookupFaultingInstance(*moduleSegment, pc, fp); - if (!instance || !instance->memoryAccessInGuardRegion((uint8_t*)addr, numBytes)) + if (!instance) return false; + MOZ_RELEASE_ASSERT(&instance->code() == &moduleSegment->code()); + + if (!instance->memoryAccessInGuardRegion((uint8_t*)addr, numBytes)) + return false; + LLBit_ = false; const wasm::MemoryAccess* memoryAccess = instance->code().lookupMemoryAccess(pc); if (!memoryAccess) { - startInterrupt(act); + MOZ_ALWAYS_TRUE(act->startWasmInterrupt(registerState())); if (!instance->code().containsCodePC(pc)) MOZ_CRASH("Cannot map PC to trap handler"); set_pc(int32_t(moduleSegment->outOfBoundsCode())); @@ -1713,7 +1730,6 @@ Simulator::handleWasmTrapFault() JitActivation* act = cx->activation()->asJit(); void* pc = reinterpret_cast(get_pc()); - uint8_t* fp = reinterpret_cast(getRegister(Register::fp)); const wasm::CodeSegment* segment = wasm::LookupCodeSegment(pc); if (!segment || !segment->isModule()) @@ -1725,7 +1741,7 @@ Simulator::handleWasmTrapFault() if (!moduleSegment->code().lookupTrap(pc, &trap, &bytecode)) return false; - act->startWasmTrap(trap, bytecode.offset, pc, fp); + act->startWasmTrap(trap, bytecode.offset, registerState()); set_pc(int32_t(moduleSegment->trapCode())); return true; } diff --git a/js/src/jit/mips32/Simulator-mips32.h b/js/src/jit/mips32/Simulator-mips32.h index d0095f4cb7db..f84d6e8b938e 100644 --- a/js/src/jit/mips32/Simulator-mips32.h +++ b/js/src/jit/mips32/Simulator-mips32.h @@ -34,6 +34,7 @@ #include "mozilla/Atomics.h" #include "jit/IonTypes.h" +#include "js/ProfilingFrameIterator.h" #include "threading/Thread.h" #include "vm/MutexIDs.h" #include "wasm/WasmCode.h" @@ -77,6 +78,12 @@ const uint32_t kFCSROverflowFlagBit = 4; const uint32_t kFCSRDivideByZeroFlagBit = 5; const uint32_t kFCSRInvalidOpFlagBit = 6; +const uint32_t kFCSRInexactCauseBit = 12; +const uint32_t kFCSRUnderflowCauseBit = 13; +const uint32_t kFCSROverflowCauseBit = 14; +const uint32_t kFCSRDivideByZeroCauseBit = 15; +const uint32_t kFCSRInvalidOpCauseBit = 16; + const uint32_t kFCSRInexactFlagMask = 1 << kFCSRInexactFlagBit; const uint32_t kFCSRUnderflowFlagMask = 1 << kFCSRUnderflowFlagBit; const uint32_t kFCSROverflowFlagMask = 1 << kFCSROverflowFlagBit; @@ -299,7 +306,7 @@ class Simulator { // Handle a wasm interrupt triggered by an async signal handler. void handleWasmInterrupt(); - void startInterrupt(JitActivation* act); + JS::ProfilingFrameIterator::RegisterState registerState(); // Handle any wasm faults, returning true if the fault was handled. bool handleWasmFault(int32_t addr, unsigned numBytes); diff --git a/js/src/jit/mips64/Architecture-mips64.h b/js/src/jit/mips64/Architecture-mips64.h index 0b816553eb74..87a52fd00601 100644 --- a/js/src/jit/mips64/Architecture-mips64.h +++ b/js/src/jit/mips64/Architecture-mips64.h @@ -41,7 +41,7 @@ class FloatRegisters : public FloatRegistersMIPSShared static Encoding FromName(const char* name); static const uint32_t Total = 32 * NumTypes; - static const uint32_t Allocatable = 60; + static const uint32_t Allocatable = 62; // When saving all registers we only need to do is save double registers. static const uint32_t TotalPhys = 32; @@ -79,10 +79,7 @@ class FloatRegisters : public FloatRegistersMIPSShared static const SetType WrapperMask = VolatileMask; static const SetType NonAllocatableMask = - ( // f21 and f23 are MIPS scratch float registers. - (1U << FloatRegisters::f21) | - (1U << FloatRegisters::f23) - ) * Spread; + (1U << FloatRegisters::f23) * Spread; static const SetType AllocatableMask = AllMask & ~NonAllocatableMask; }; diff --git a/js/src/jit/mips64/Assembler-mips64.h b/js/src/jit/mips64/Assembler-mips64.h index 8a4e9551024b..34a392b6fba7 100644 --- a/js/src/jit/mips64/Assembler-mips64.h +++ b/js/src/jit/mips64/Assembler-mips64.h @@ -77,8 +77,6 @@ static constexpr FloatRegister ReturnFloat32Reg = { FloatRegisters::f0, FloatReg static constexpr FloatRegister ReturnDoubleReg = { FloatRegisters::f0, FloatRegisters::Double }; static constexpr FloatRegister ScratchFloat32Reg = { FloatRegisters::f23, FloatRegisters::Single }; static constexpr FloatRegister ScratchDoubleReg = { FloatRegisters::f23, FloatRegisters::Double }; -static constexpr FloatRegister SecondScratchFloat32Reg = { FloatRegisters::f21, FloatRegisters::Single }; -static constexpr FloatRegister SecondScratchDoubleReg = { FloatRegisters::f21, FloatRegisters::Double }; struct ScratchFloat32Scope : public AutoFloatRegisterScope { diff --git a/js/src/jit/mips64/CodeGenerator-mips64.cpp b/js/src/jit/mips64/CodeGenerator-mips64.cpp index 7f7bbe6dba6f..307d3355b8c9 100644 --- a/js/src/jit/mips64/CodeGenerator-mips64.cpp +++ b/js/src/jit/mips64/CodeGenerator-mips64.cpp @@ -594,18 +594,35 @@ void CodeGeneratorMIPS64::visitWasmTruncateToInt64(LWasmTruncateToInt64* lir) { FloatRegister input = ToFloatRegister(lir->input()); - Register output = ToRegister(lir->output()); + Register64 output = ToOutRegister64(lir); MWasmTruncateToInt64* mir = lir->mir(); MIRType fromType = mir->input()->type(); MOZ_ASSERT(fromType == MIRType::Double || fromType == MIRType::Float32); - auto* ool = new (alloc()) OutOfLineWasmTruncateCheck(mir, input); + auto* ool = new (alloc()) OutOfLineWasmTruncateCheck(mir, input, output); addOutOfLineCode(ool, mir); - masm.wasmTruncateToI64(input, output, fromType, mir->isUnsigned(), - ool->entry(), ool->rejoin()); + Label* oolEntry = ool->entry(); + Label* oolRejoin = ool->rejoin(); + bool isSaturating = mir->isSaturating(); + + if (fromType == MIRType::Double) { + if (mir->isUnsigned()) + masm.wasmTruncateDoubleToUInt64(input, output, isSaturating, oolEntry, oolRejoin, + InvalidFloatReg); + else + masm.wasmTruncateDoubleToInt64(input, output, isSaturating, oolEntry, oolRejoin, + InvalidFloatReg); + } else { + if (mir->isUnsigned()) + masm.wasmTruncateFloat32ToUInt64(input, output, isSaturating, oolEntry, oolRejoin, + InvalidFloatReg); + else + masm.wasmTruncateFloat32ToInt64(input, output, isSaturating, oolEntry, oolRejoin, + InvalidFloatReg); + } } void diff --git a/js/src/jit/mips64/MacroAssembler-mips64-inl.h b/js/src/jit/mips64/MacroAssembler-mips64-inl.h index 4127eadbb1cd..3efdf7a78d4a 100644 --- a/js/src/jit/mips64/MacroAssembler-mips64-inl.h +++ b/js/src/jit/mips64/MacroAssembler-mips64-inl.h @@ -744,6 +744,30 @@ MacroAssembler::branchToComputedAddress(const BaseIndex& addr) as_nop(); } +void +MacroAssembler::branchTruncateDoubleMaybeModUint32(FloatRegister src, Register dest, Label* fail) +{ + as_truncld(ScratchDoubleReg, src); + as_cfc1(ScratchRegister, Assembler::FCSR); + moveFromDouble(ScratchDoubleReg, dest); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseV, 1); + ma_b(ScratchRegister, Imm32(0), fail, Assembler::NotEqual); + + as_sll(dest, dest, 0); +} + +void +MacroAssembler::branchTruncateFloat32MaybeModUint32(FloatRegister src, Register dest, Label* fail) +{ + as_truncls(ScratchDoubleReg, src); + as_cfc1(ScratchRegister, Assembler::FCSR); + moveFromDouble(ScratchDoubleReg, dest); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseV, 1); + ma_b(ScratchRegister, Imm32(0), fail, Assembler::NotEqual); + + as_sll(dest, dest, 0); +} + // ======================================================================== // Memory access primitives. void diff --git a/js/src/jit/mips64/MacroAssembler-mips64.cpp b/js/src/jit/mips64/MacroAssembler-mips64.cpp index 2649795c1fc7..42ea89b8c171 100644 --- a/js/src/jit/mips64/MacroAssembler-mips64.cpp +++ b/js/src/jit/mips64/MacroAssembler-mips64.cpp @@ -58,20 +58,8 @@ MacroAssemblerMIPS64Compat::convertInt32ToDouble(const BaseIndex& src, FloatRegi void MacroAssemblerMIPS64Compat::convertUInt32ToDouble(Register src, FloatRegister dest) { - // We use SecondScratchDoubleReg because MacroAssembler::loadFromTypedArray - // calls with ScratchDoubleReg as dest. - MOZ_ASSERT(dest != SecondScratchDoubleReg); - - // Subtract INT32_MIN to get a positive number - ma_subu(ScratchRegister, src, Imm32(INT32_MIN)); - - // Convert value - as_mtc1(ScratchRegister, dest); - as_cvtdw(dest, dest); - - // Add unsigned value of INT32_MIN - ma_lid(SecondScratchDoubleReg, 2147483648.0); - as_addd(dest, dest, SecondScratchDoubleReg); + ma_dext(ScratchRegister, src, Imm32(0), Imm32(32)); + asMasm().convertInt64ToDouble(Register64(ScratchRegister), dest); } void @@ -101,19 +89,8 @@ MacroAssemblerMIPS64Compat::convertUInt64ToDouble(Register src, FloatRegister de void MacroAssemblerMIPS64Compat::convertUInt32ToFloat32(Register src, FloatRegister dest) { - Label positive, done; - ma_b(src, src, &positive, NotSigned, ShortJump); - - // We cannot do the same as convertUInt32ToDouble because float32 doesn't - // have enough precision. - convertUInt32ToDouble(src, dest); - convertDoubleToFloat32(dest, dest); - ma_b(&done, ShortJump); - - bind(&positive); - convertInt32ToFloat32(src, dest); - - bind(&done); + ma_dext(ScratchRegister, src, Imm32(0), Imm32(32)); + asMasm().convertInt64ToFloat32(Register64(ScratchRegister), dest); } void @@ -135,12 +112,12 @@ MacroAssemblerMIPS64Compat::convertDoubleToInt32(FloatRegister src, Register des ma_b(dest, Imm32(1), fail, Assembler::Equal); } - // Convert double to int, then convert back and check if we have the - // same number. - as_cvtwd(ScratchDoubleReg, src); - as_mfc1(dest, ScratchDoubleReg); - as_cvtdw(ScratchDoubleReg, ScratchDoubleReg); - ma_bc1d(src, ScratchDoubleReg, fail, Assembler::DoubleNotEqualOrUnordered); + // Truncate double to int ; if result is inexact fail + as_truncwd(ScratchFloat32Reg, src); + as_cfc1(ScratchRegister, Assembler::FCSR); + moveFromFloat32(ScratchFloat32Reg, dest); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseI, 1); + ma_b(ScratchRegister, Imm32(0), fail, Assembler::NotEqual); } // Checks whether a float32 is representable as a 32-bit integer. If so, the @@ -155,18 +132,11 @@ MacroAssemblerMIPS64Compat::convertFloat32ToInt32(FloatRegister src, Register de ma_b(dest, Imm32(INT32_MIN), fail, Assembler::Equal); } - // Converting the floating point value to an integer and then converting it - // back to a float32 would not work, as float to int32 conversions are - // clamping (e.g. float(INT32_MAX + 1) would get converted into INT32_MAX - // and then back to float(INT32_MAX + 1)). If this ever happens, we just - // bail out. - as_cvtws(ScratchFloat32Reg, src); - as_mfc1(dest, ScratchFloat32Reg); - as_cvtsw(ScratchFloat32Reg, ScratchFloat32Reg); - ma_bc1s(src, ScratchFloat32Reg, fail, Assembler::DoubleNotEqualOrUnordered); - - // Bail out in the clamped cases. - ma_b(dest, Imm32(INT32_MAX), fail, Assembler::Equal); + as_truncws(ScratchFloat32Reg, src); + as_cfc1(ScratchRegister, Assembler::FCSR); + moveFromFloat32(ScratchFloat32Reg, dest); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseI, 1); + ma_b(ScratchRegister, Imm32(0), fail, Assembler::NotEqual); } void @@ -855,8 +825,13 @@ MacroAssemblerMIPS64::ma_lid(FloatRegister dest, double value) { ImmWord imm(mozilla::BitwiseCast(value)); - ma_li(ScratchRegister, imm); - moveToDouble(ScratchRegister, dest); + if(imm.value != 0){ + ma_li(ScratchRegister, imm); + moveToDouble(ScratchRegister, dest); + } else { + moveToDouble(zero, dest); + } + } void @@ -1363,49 +1338,19 @@ MacroAssemblerMIPS64Compat::storeUnalignedDouble(const wasm::MemoryAccessDesc& a append(access, store.getOffset(), asMasm().framePushed()); } -// Note: this function clobbers the input register. void MacroAssembler::clampDoubleToUint8(FloatRegister input, Register output) { - MOZ_ASSERT(input != ScratchDoubleReg); - Label positive, done; - - // <= 0 or NaN --> 0 - zeroDouble(ScratchDoubleReg); - branchDouble(DoubleGreaterThan, input, ScratchDoubleReg, &positive); - { - move32(Imm32(0), output); - jump(&done); - } - - bind(&positive); - - // Add 0.5 and truncate. - loadConstantDouble(0.5, ScratchDoubleReg); - addDouble(ScratchDoubleReg, input); - - Label outOfRange; - - branchTruncateDoubleMaybeModUint32(input, output, &outOfRange); - asMasm().branch32(Assembler::Above, output, Imm32(255), &outOfRange); - { - // Check if we had a tie. - convertInt32ToDouble(output, ScratchDoubleReg); - branchDouble(DoubleNotEqual, input, ScratchDoubleReg, &done); - - // It was a tie. Mask out the ones bit to get an even value. - // See also js_TypedArray_uint8_clamp_double. - and32(Imm32(~1), output); - jump(&done); - } - - // > 255 --> 255 - bind(&outOfRange); - { - move32(Imm32(255), output); - } - - bind(&done); + as_roundwd(ScratchDoubleReg, input); + ma_li(ScratchRegister, Imm32(255)); + as_mfc1(output, ScratchDoubleReg); + zeroDouble(ScratchDoubleReg); + as_sltiu(SecondScratchReg, output, 255); + as_colt(DoubleFloat, ScratchDoubleReg, input); + // if res > 255; res = 255; + as_movz(output, ScratchRegister, SecondScratchReg); + // if !(input > 0); res = 0; + as_movf(output, zero); } void @@ -2441,33 +2386,22 @@ void MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output, bool isSaturating, Label* oolEntry) { - MOZ_ASSERT(!isSaturating, "NYI"); - as_truncld(ScratchDoubleReg, input); - moveFromDoubleHi(ScratchDoubleReg, output); - as_cfc1(ScratchRegister, Assembler::FCSR); - ma_ext(ScratchRegister, ScratchRegister, 6, 1); - ma_or(ScratchRegister, output); - moveFromFloat32(ScratchDoubleReg, output); + moveFromDouble(ScratchDoubleReg, output); + ma_dsrl(ScratchRegister, output, Imm32(32)); + as_sll(output, output, 0); ma_b(ScratchRegister, Imm32(0), oolEntry, Assembler::NotEqual); - - } void MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, bool isSaturating, Label* oolEntry) { - MOZ_ASSERT(!isSaturating, "NYI"); - as_truncls(ScratchDoubleReg, input); - moveFromDoubleHi(ScratchDoubleReg, output); - as_cfc1(ScratchRegister, Assembler::FCSR); - ma_ext(ScratchRegister, ScratchRegister, 6, 1); - ma_or(ScratchRegister, output); - moveFromFloat32(ScratchDoubleReg, output); + moveFromDouble(ScratchDoubleReg, output); + ma_dsrl(ScratchRegister, output, Imm32(32)); + as_sll(output, output, 0); ma_b(ScratchRegister, Imm32(0), oolEntry, Assembler::NotEqual); - } void @@ -2501,113 +2435,117 @@ MacroAssembler::wasmUnalignedStoreI64(const wasm::MemoryAccessDesc& access, Regi } void -MacroAssembler::wasmTruncateDoubleToInt64(FloatRegister input, Register64 output, bool, - Label* oolEntry, Label* oolRejoin, - FloatRegister tempDouble) +MacroAssembler::wasmTruncateDoubleToInt64(FloatRegister input, Register64 output, + bool isSaturating, Label* oolEntry, + Label* oolRejoin, FloatRegister tempDouble) { MOZ_ASSERT(tempDouble.isInvalid()); - wasmTruncateToI64(input, output.reg, MIRType::Double, false, oolEntry, oolRejoin); + + as_truncld(ScratchDoubleReg, input); + as_cfc1(ScratchRegister, Assembler::FCSR); + moveFromDouble(ScratchDoubleReg, output.reg); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseV, 1); + ma_b(ScratchRegister, Imm32(0), oolEntry, Assembler::NotEqual); + + if (isSaturating) + bind(oolRejoin); } void -MacroAssembler::wasmTruncateDoubleToUInt64(FloatRegister input, Register64 output, bool, - Label* oolEntry, Label* oolRejoin, - FloatRegister tempDouble) +MacroAssembler::wasmTruncateDoubleToUInt64(FloatRegister input, Register64 output_, + bool isSaturating, Label* oolEntry, + Label* oolRejoin, FloatRegister tempDouble) { MOZ_ASSERT(tempDouble.isInvalid()); - wasmTruncateToI64(input, output.reg, MIRType::Double, true, oolEntry, oolRejoin); + Register output = output_.reg; + + Label done; + + as_truncld(ScratchDoubleReg, input); + // ma_li INT64_MAX + ma_li(SecondScratchReg, Imm32(-1)); + ma_dext(SecondScratchReg, SecondScratchReg, Imm32(0), Imm32(63)); + moveFromDouble(ScratchDoubleReg, output); + // For numbers in -1.[ : ]INT64_MAX range do nothing more + ma_b(output, SecondScratchReg, &done, Assembler::Below, ShortJump); + + loadConstantDouble(double(INT64_MAX + 1ULL), ScratchDoubleReg); + // ma_li INT64_MIN + ma_daddu(SecondScratchReg, Imm32(1)); + as_subd(ScratchDoubleReg, input, ScratchDoubleReg); + as_truncld(ScratchDoubleReg, ScratchDoubleReg); + as_cfc1(ScratchRegister, Assembler::FCSR); + moveFromDouble(ScratchDoubleReg, output); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseV, 1); + ma_daddu(output, SecondScratchReg); + + // Guard against negative values that result in 0 due the precision loss. + as_sltiu(SecondScratchReg, output, 1); + ma_or(ScratchRegister, SecondScratchReg); + + ma_b(ScratchRegister, Imm32(0), oolEntry, Assembler::NotEqual); + + bind(&done); + + if (isSaturating) + bind(oolRejoin); } void -MacroAssembler::wasmTruncateFloat32ToInt64(FloatRegister input, Register64 output, bool, - Label* oolEntry, Label* oolRejoin, - FloatRegister tempFloat) +MacroAssembler::wasmTruncateFloat32ToInt64(FloatRegister input, Register64 output, + bool isSaturating, Label* oolEntry, + Label* oolRejoin, FloatRegister tempFloat) { MOZ_ASSERT(tempFloat.isInvalid()); - wasmTruncateToI64(input, output.reg, MIRType::Float32, false, oolEntry, oolRejoin); + + as_truncls(ScratchDoubleReg, input); + as_cfc1(ScratchRegister, Assembler::FCSR); + moveFromDouble(ScratchDoubleReg, output.reg); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseV, 1); + ma_b(ScratchRegister, Imm32(0), oolEntry, Assembler::NotEqual); + + if (isSaturating) + bind(oolRejoin); } void -MacroAssembler::wasmTruncateFloat32ToUInt64(FloatRegister input, Register64 output, bool, - Label* oolEntry, Label* oolRejoin, - FloatRegister tempFloat) +MacroAssembler::wasmTruncateFloat32ToUInt64(FloatRegister input, Register64 output_, + bool isSaturating, Label* oolEntry, + Label* oolRejoin, FloatRegister tempFloat) { MOZ_ASSERT(tempFloat.isInvalid()); - wasmTruncateToI64(input, output.reg, MIRType::Float32, true, oolEntry, oolRejoin); -} + Register output = output_.reg; -void -MacroAssemblerMIPS64Compat::wasmTruncateToI64(FloatRegister input, Register output, MIRType fromType, - bool isUnsigned, Label* oolEntry, Label* oolRejoin) -{ - if (isUnsigned) { - Label isLarge, done; + Label done; - if (fromType == MIRType::Double) { - asMasm().loadConstantDouble(double(INT64_MAX), ScratchDoubleReg); - asMasm().ma_bc1d(ScratchDoubleReg, input, &isLarge, - Assembler::DoubleLessThanOrEqual, ShortJump); + as_truncls(ScratchDoubleReg, input); + // ma_li INT64_MAX + ma_li(SecondScratchReg, Imm32(-1)); + ma_dext(SecondScratchReg, SecondScratchReg, Imm32(0), Imm32(63)); + moveFromDouble(ScratchDoubleReg, output); + // For numbers in -1.[ : ]INT64_MAX range do nothing more + ma_b(output, SecondScratchReg, &done, Assembler::Below, ShortJump); - asMasm().as_truncld(ScratchDoubleReg, input); - } else { - asMasm().loadConstantFloat32(float(INT64_MAX), ScratchFloat32Reg); - asMasm().ma_bc1s(ScratchFloat32Reg, input, &isLarge, - Assembler::DoubleLessThanOrEqual, ShortJump); + loadConstantFloat32(float(INT64_MAX + 1ULL), ScratchFloat32Reg); + // ma_li INT64_MIN + ma_daddu(SecondScratchReg, Imm32(1)); + as_subs(ScratchFloat32Reg, input, ScratchFloat32Reg); + as_truncls(ScratchDoubleReg, ScratchFloat32Reg); + as_cfc1(ScratchRegister, Assembler::FCSR); + moveFromDouble(ScratchDoubleReg, output); + ma_ext(ScratchRegister, ScratchRegister, Assembler::CauseV, 1); + ma_daddu(output, SecondScratchReg); - asMasm().as_truncls(ScratchDoubleReg, input); - } + // Guard against negative values that result in 0 due the precision loss. + as_sltiu(SecondScratchReg, output, 1); + ma_or(ScratchRegister, SecondScratchReg); - // Check that the result is in the uint64_t range. - asMasm().moveFromDouble(ScratchDoubleReg, output); - asMasm().as_cfc1(ScratchRegister, Assembler::FCSR); - // extract invalid operation flag (bit 6) from FCSR - asMasm().ma_ext(ScratchRegister, ScratchRegister, 16, 1); - asMasm().ma_dsrl(SecondScratchReg, output, Imm32(63)); - asMasm().ma_or(SecondScratchReg, ScratchRegister); - asMasm().ma_b(SecondScratchReg, Imm32(0), oolEntry, Assembler::NotEqual); + ma_b(ScratchRegister, Imm32(0), oolEntry, Assembler::NotEqual); - asMasm().ma_b(&done, ShortJump); + bind(&done); - // The input is greater than double(INT64_MAX). - asMasm().bind(&isLarge); - if (fromType == MIRType::Double) { - asMasm().as_subd(ScratchDoubleReg, input, ScratchDoubleReg); - asMasm().as_truncld(ScratchDoubleReg, ScratchDoubleReg); - } else { - asMasm().as_subs(ScratchDoubleReg, input, ScratchDoubleReg); - asMasm().as_truncls(ScratchDoubleReg, ScratchDoubleReg); - } - - // Check that the result is in the uint64_t range. - asMasm().moveFromDouble(ScratchDoubleReg, output); - asMasm().as_cfc1(ScratchRegister, Assembler::FCSR); - asMasm().ma_ext(ScratchRegister, ScratchRegister, 16, 1); - asMasm().ma_dsrl(SecondScratchReg, output, Imm32(63)); - asMasm().ma_or(SecondScratchReg, ScratchRegister); - asMasm().ma_b(SecondScratchReg, Imm32(0), oolEntry, Assembler::NotEqual); - - asMasm().ma_li(ScratchRegister, Imm32(1)); - asMasm().ma_dins(output, ScratchRegister, Imm32(63), Imm32(1)); - - asMasm().bind(&done); - asMasm().bind(oolRejoin); - return; - } - - // When the input value is Infinity, NaN, or rounds to an integer outside the - // range [INT64_MIN; INT64_MAX + 1[, the Invalid Operation flag is set in the FCSR. - if (fromType == MIRType::Double) - asMasm().as_truncld(ScratchDoubleReg, input); - else - asMasm().as_truncls(ScratchDoubleReg, input); - - // Check that the result is in the int64_t range. - asMasm().as_cfc1(output, Assembler::FCSR); - asMasm().ma_ext(output, output, 16, 1); - asMasm().ma_b(output, Imm32(0), oolEntry, Assembler::NotEqual); - - asMasm().bind(oolRejoin); - asMasm().moveFromDouble(ScratchDoubleReg, output); + if (isSaturating) + bind(oolRejoin); } void diff --git a/js/src/jit/mips64/MacroAssembler-mips64.h b/js/src/jit/mips64/MacroAssembler-mips64.h index 39e4cb28a4fe..98741f17f692 100644 --- a/js/src/jit/mips64/MacroAssembler-mips64.h +++ b/js/src/jit/mips64/MacroAssembler-mips64.h @@ -733,8 +733,6 @@ class MacroAssemblerMIPS64Compat : public MacroAssemblerMIPS64 void convertUInt64ToDouble(Register src, FloatRegister dest); - void wasmTruncateToI64(FloatRegister input, Register output, MIRType fromType, - bool isUnsigned, Label* oolEntry, Label* oolRejoin); void breakpoint(); diff --git a/js/src/jit/mips64/Simulator-mips64.cpp b/js/src/jit/mips64/Simulator-mips64.cpp index cb470d8df9ed..7fc7d103c3f5 100644 --- a/js/src/jit/mips64/Simulator-mips64.cpp +++ b/js/src/jit/mips64/Simulator-mips64.cpp @@ -1573,23 +1573,34 @@ Simulator::setFCSRRoundError(double original, double rounded) { bool ret = false; + setFCSRBit(kFCSRInexactCauseBit, false); + setFCSRBit(kFCSRUnderflowCauseBit, false); + setFCSRBit(kFCSROverflowCauseBit, false); + setFCSRBit(kFCSRInvalidOpCauseBit, false); + if (!std::isfinite(original) || !std::isfinite(rounded)) { setFCSRBit(kFCSRInvalidOpFlagBit, true); + setFCSRBit(kFCSRInvalidOpCauseBit, true); ret = true; } - if (original != rounded) + if (original != rounded) { setFCSRBit(kFCSRInexactFlagBit, true); + setFCSRBit(kFCSRInexactCauseBit, true); + } if (rounded < DBL_MIN && rounded > -DBL_MIN && rounded != 0) { setFCSRBit(kFCSRUnderflowFlagBit, true); + setFCSRBit(kFCSRUnderflowCauseBit, true); ret = true; } if (rounded > INT_MAX || rounded < INT_MIN) { setFCSRBit(kFCSROverflowFlagBit, true); + setFCSRBit(kFCSROverflowCauseBit, true); // The reference is not really clear but it seems this is required: setFCSRBit(kFCSRInvalidOpFlagBit, true); + setFCSRBit(kFCSRInvalidOpCauseBit, true); ret = true; } @@ -1617,15 +1628,15 @@ Simulator::get_pc() const return registers_[pc]; } -void -Simulator::startInterrupt(JitActivation* activation) +JS::ProfilingFrameIterator::RegisterState +Simulator::registerState() { - JS::ProfilingFrameIterator::RegisterState state; + wasm::RegisterState state; state.pc = (void*) get_pc(); state.fp = (void*) getRegister(fp); state.sp = (void*) getRegister(sp); state.lr = (void*) getRegister(ra); - activation->startWasmInterrupt(state); + return state; } // The signal handler only redirects the PC to the interrupt stub when the PC is @@ -1651,7 +1662,9 @@ Simulator::handleWasmInterrupt() if (!fp) return; - startInterrupt(activation); + if (!activation->startWasmInterrupt(registerState())) + return; + set_pc(int64_t(segment->asModule()->interruptCode())); } @@ -1681,14 +1694,19 @@ Simulator::handleWasmFault(uint64_t addr, unsigned numBytes) const wasm::ModuleSegment* moduleSegment = segment->asModule(); wasm::Instance* instance = wasm::LookupFaultingInstance(*moduleSegment, pc, fp); - if (!instance || !instance->memoryAccessInGuardRegion((uint8_t*)addr, numBytes)) + if (!instance) return false; + MOZ_RELEASE_ASSERT(&instance->code() == &moduleSegment->code()); + + if (!instance->memoryAccessInGuardRegion((uint8_t*)addr, numBytes)) + return false; + LLBit_ = false; const wasm::MemoryAccess* memoryAccess = instance->code().lookupMemoryAccess(pc); if (!memoryAccess) { - startInterrupt(act); + MOZ_ALWAYS_TRUE(act->startWasmInterrupt(registerState())); if (!instance->code().containsCodePC(pc)) MOZ_CRASH("Cannot map PC to trap handler"); set_pc(int64_t(moduleSegment->outOfBoundsCode())); @@ -1712,7 +1730,6 @@ Simulator::handleWasmTrapFault() JitActivation* act = cx->activation()->asJit(); void* pc = reinterpret_cast(get_pc()); - uint8_t* fp = reinterpret_cast(getRegister(Register::fp)); const wasm::CodeSegment* segment = wasm::LookupCodeSegment(pc); if (!segment || !segment->isModule()) @@ -1724,7 +1741,7 @@ Simulator::handleWasmTrapFault() if (!moduleSegment->code().lookupTrap(pc, &trap, &bytecode)) return false; - act->startWasmTrap(trap, bytecode.offset, pc, fp); + act->startWasmTrap(trap, bytecode.offset, registerState()); set_pc(int64_t(moduleSegment->trapCode())); return true; } @@ -4007,9 +4024,6 @@ Simulator::instructionDecode(SimInstruction* instr) void Simulator::branchDelayInstructionDecode(SimInstruction* instr) { - if (single_stepping_) - single_step_callback_(single_step_callback_arg_, this, (void*)instr); - if (instr->instructionBits() == NopInst) { // Short-cut generic nop instructions. They are always valid and they // never change the simulator state. diff --git a/js/src/jit/mips64/Simulator-mips64.h b/js/src/jit/mips64/Simulator-mips64.h index d2ef2c6a2f47..80105709d76f 100644 --- a/js/src/jit/mips64/Simulator-mips64.h +++ b/js/src/jit/mips64/Simulator-mips64.h @@ -35,6 +35,7 @@ #include "mozilla/Atomics.h" #include "jit/IonTypes.h" +#include "js/ProfilingFrameIterator.h" #include "threading/Thread.h" #include "vm/MutexIDs.h" @@ -82,6 +83,12 @@ const uint32_t kFCSROverflowFlagBit = 4; const uint32_t kFCSRDivideByZeroFlagBit = 5; const uint32_t kFCSRInvalidOpFlagBit = 6; +const uint32_t kFCSRInexactCauseBit = 12; +const uint32_t kFCSRUnderflowCauseBit = 13; +const uint32_t kFCSROverflowCauseBit = 14; +const uint32_t kFCSRDivideByZeroCauseBit = 15; +const uint32_t kFCSRInvalidOpCauseBit = 16; + const uint32_t kFCSRInexactFlagMask = 1 << kFCSRInexactFlagBit; const uint32_t kFCSRUnderflowFlagMask = 1 << kFCSRUnderflowFlagBit; const uint32_t kFCSROverflowFlagMask = 1 << kFCSROverflowFlagBit; @@ -314,7 +321,7 @@ class Simulator { // Handle a wasm interrupt triggered by an async signal handler. void handleWasmInterrupt(); - void startInterrupt(JitActivation* act); + JS::ProfilingFrameIterator::RegisterState registerState(); // Handle any wasm faults, returning true if the fault was handled. bool handleWasmFault(uint64_t addr, unsigned numBytes); diff --git a/js/src/jit/shared/LIR-shared.h b/js/src/jit/shared/LIR-shared.h index 4693dc4a9a28..02240d31a620 100644 --- a/js/src/jit/shared/LIR-shared.h +++ b/js/src/jit/shared/LIR-shared.h @@ -7788,14 +7788,16 @@ class LLoadUnboxedExpando : public LInstructionHelper<1, 1, 0> }; // Guard that a value is in a TypeSet. -class LTypeBarrierV : public LInstructionHelper<0, BOX_PIECES, 1> +class LTypeBarrierV : public LInstructionHelper<0, BOX_PIECES, 2> { public: LIR_HEADER(TypeBarrierV) - LTypeBarrierV(const LBoxAllocation& input, const LDefinition& temp) { + LTypeBarrierV(const LBoxAllocation& input, const LDefinition& unboxTemp, + const LDefinition& objTemp) { setBoxOperand(Input, input); - setTemp(0, temp); + setTemp(0, unboxTemp); + setTemp(1, objTemp); } static const size_t Input = 0; @@ -7803,9 +7805,12 @@ class LTypeBarrierV : public LInstructionHelper<0, BOX_PIECES, 1> const MTypeBarrier* mir() const { return mir_->toTypeBarrier(); } - const LDefinition* temp() { + const LDefinition* unboxTemp() { return getTemp(0); } + const LDefinition* objTemp() { + return getTemp(1); + } }; // Guard that a object is in a TypeSet. @@ -7830,14 +7835,16 @@ class LTypeBarrierO : public LInstructionHelper<0, 1, 1> }; // Guard that a value is in a TypeSet. -class LMonitorTypes : public LInstructionHelper<0, BOX_PIECES, 1> +class LMonitorTypes : public LInstructionHelper<0, BOX_PIECES, 2> { public: LIR_HEADER(MonitorTypes) - LMonitorTypes(const LBoxAllocation& input, const LDefinition& temp) { + LMonitorTypes(const LBoxAllocation& input, const LDefinition& unboxTemp, + const LDefinition& objTemp) { setBoxOperand(Input, input); - setTemp(0, temp); + setTemp(0, unboxTemp); + setTemp(1, objTemp); } static const size_t Input = 0; @@ -7845,9 +7852,12 @@ class LMonitorTypes : public LInstructionHelper<0, BOX_PIECES, 1> const MMonitorTypes* mir() const { return mir_->toMonitorTypes(); } - const LDefinition* temp() { + const LDefinition* unboxTemp() { return getTemp(0); } + const LDefinition* objTemp() { + return getTemp(1); + } }; // Generational write barrier used when writing an object to another object. diff --git a/js/src/wasm/WasmStubs.cpp b/js/src/wasm/WasmStubs.cpp index deb298645e57..940c0aca8620 100644 --- a/js/src/wasm/WasmStubs.cpp +++ b/js/src/wasm/WasmStubs.cpp @@ -1427,6 +1427,14 @@ static const LiveRegisterSet AllRegsExceptPCSP( (uint32_t(1) << Registers::pc))), FloatRegisterSet(FloatRegisters::AllDoubleMask)); static_assert(!SupportsSimd, "high lanes of SIMD registers need to be saved too."); +#elif defined(JS_CODEGEN_MIPS32) || defined(JS_CODEGEN_MIPS64) +static const LiveRegisterSet AllUserRegsExceptSP( + GeneralRegisterSet(Registers::AllMask & ~((uint32_t(1) << Registers::k0) | + (uint32_t(1) << Registers::k1) | + (uint32_t(1) << Registers::sp) | + (uint32_t(1) << Registers::zero))), + FloatRegisterSet(FloatRegisters::AllDoubleMask)); +static_assert(!SupportsSimd, "high lanes of SIMD registers need to be saved too."); #else static const LiveRegisterSet AllRegsExceptSP( GeneralRegisterSet(Registers::AllMask & ~(uint32_t(1) << Registers::StackPointer)), @@ -1487,14 +1495,16 @@ GenerateInterruptExit(MacroAssembler& masm, Label* throwLabel, Offsets* offsets) #elif defined(JS_CODEGEN_MIPS32) || defined(JS_CODEGEN_MIPS64) // Reserve space to store resumePC and HeapReg. masm.subFromStackPtr(Imm32(2 * sizeof(intptr_t))); - // set to zero so we can use masm.framePushed() below. + // Set to zero so we can use masm.framePushed() below. masm.setFramePushed(0); - static_assert(!SupportsSimd, "high lanes of SIMD registers need to be saved too."); - // save all registers,except sp. After this stack is alligned. - masm.PushRegsInMask(AllRegsExceptSP); - // Save the stack pointer in a non-volatile register. + // Save all registers, except sp. + masm.PushRegsInMask(AllUserRegsExceptSP); + + // Save the stack pointer and FCSR in a non-volatile registers. masm.moveStackPtrTo(s0); + masm.as_cfc1(s1, Assembler::FCSR); + // Align the stack. masm.ma_and(StackPointer, StackPointer, Imm32(~(ABIStackAlignment - 1))); @@ -1509,19 +1519,18 @@ GenerateInterruptExit(MacroAssembler& masm, Label* throwLabel, Offsets* offsets) masm.assertStackAlignment(ABIStackAlignment); masm.call(SymbolicAddress::HandleExecutionInterrupt); -# ifdef USES_O32_ABI - masm.addToStackPtr(Imm32(4 * sizeof(intptr_t))); -# endif - masm.branchTestPtr(Assembler::Zero, ReturnReg, ReturnReg, throwLabel); // This will restore stack to the address before the call. masm.moveToStackPtr(s0); + // Restore FCSR. + masm.as_ctc1(s1, Assembler::FCSR); + // Store resumePC into the reserved space. masm.storePtr(ReturnReg, Address(s0, masm.framePushed())); - masm.PopRegsInMask(AllRegsExceptSP); + masm.PopRegsInMask(AllUserRegsExceptSP); // Pop resumePC into PC. Clobber HeapReg to make the jump and restore it // during jump delay slot. diff --git a/layout/tools/reftest/mach_commands.py b/layout/tools/reftest/mach_commands.py index abac9c88d067..461f3e9229bf 100644 --- a/layout/tools/reftest/mach_commands.py +++ b/layout/tools/reftest/mach_commands.py @@ -137,7 +137,7 @@ class ReftestRunner(MozbuildObject): args.printDeviceInfo = False from mozrunner.devices.android_device import grant_runtime_permissions, get_adb_path - grant_runtime_permissions(self) + grant_runtime_permissions(self, args.app) if not args.adb_path: args.adb_path = get_adb_path(self) diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java index 1554ee071b69..982a43df7d80 100644 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java +++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java @@ -860,6 +860,10 @@ public abstract class GeckoApp extends GeckoActivity public void onFocusRequest(final GeckoSession session) { } + @Override // GeckoSession.ContentListener + public void onCloseRequest(final GeckoSession session) { + } + @Override // GeckoSession.ContentListener public void onFullScreen(final GeckoSession session, final boolean fullScreen) { if (fullScreen) { diff --git a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java index 8017b45427e1..04c985ca4e6c 100644 --- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java +++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java @@ -649,6 +649,13 @@ public class CustomTabsActivity extends AppCompatActivity return true; } + @Override + public void onNewSession(final GeckoSession session, final String uri, + final GeckoSession.Response response) { + // We should never get here because we abort loads that need a new session in onLoadUri() + throw new IllegalStateException("Unexpected new session"); + } + /* GeckoSession.ProgressListener */ @Override public void onPageStart(GeckoSession session, String url) { @@ -686,6 +693,11 @@ public class CustomTabsActivity extends AppCompatActivity startActivity(intent); } + @Override + public void onCloseRequest(GeckoSession session) { + // Ignore + } + @Override public void onFullScreen(GeckoSession session, boolean fullScreen) { ActivityUtils.setFullScreen(this, fullScreen); diff --git a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java index 010ea6f833e8..fdd0caf526ce 100644 --- a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java +++ b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java @@ -356,6 +356,11 @@ public class WebAppActivity extends AppCompatActivity startActivity(intent); } + @Override // GeckoSession.ContentListener + public void onCloseRequest(GeckoSession session) { + // Ignore + } + @Override // GeckoSession.ContentListener public void onContextMenu(GeckoSession session, int screenX, int screenY, String uri, String elementSrc) { @@ -422,6 +427,13 @@ public class WebAppActivity extends AppCompatActivity return true; } + @Override + public void onNewSession(final GeckoSession session, final String uri, + final GeckoSession.Response response) { + // We should never get here because we abort loads that need a new session in onLoadUri() + throw new IllegalStateException("Unexpected new session"); + } + private void updateFullScreen() { boolean fullScreen = mIsFullScreenContent || mIsFullScreenMode; if (ActivityUtils.isFullScreen(this) == fullScreen) { diff --git a/mobile/android/chrome/geckoview/GeckoViewContent.js b/mobile/android/chrome/geckoview/GeckoViewContent.js index b12f65abd73a..96179935bcda 100644 --- a/mobile/android/chrome/geckoview/GeckoViewContent.js +++ b/mobile/android/chrome/geckoview/GeckoViewContent.js @@ -24,6 +24,7 @@ class GeckoViewContent extends GeckoViewContentModule { addEventListener("DOMTitleChanged", this, false); addEventListener("DOMWindowFocus", this, false); + addEventListener("DOMWindowClose", this, false); addEventListener("MozDOMFullscreen:Entered", this, false); addEventListener("MozDOMFullscreen:Exit", this, false); addEventListener("MozDOMFullscreen:Exited", this, false); @@ -43,6 +44,7 @@ class GeckoViewContent extends GeckoViewContentModule { removeEventListener("DOMTitleChanged", this); removeEventListener("DOMWindowFocus", this); + removeEventListener("DOMWindowClose", this); removeEventListener("MozDOMFullscreen:Entered", this); removeEventListener("MozDOMFullscreen:Exit", this); removeEventListener("MozDOMFullscreen:Exited", this); @@ -179,6 +181,16 @@ class GeckoViewContent extends GeckoViewContentModule { type: "GeckoView:DOMWindowFocus" }); break; + case "DOMWindowClose": + if (!aEvent.isTrusted) { + return; + } + + aEvent.preventDefault(); + this.eventDispatcher.sendRequest({ + type: "GeckoView:DOMWindowClose" + }); + break; } } } diff --git a/mobile/android/chrome/geckoview/geckoview.js b/mobile/android/chrome/geckoview/geckoview.js index 86d0f6aa817d..52608f248c02 100644 --- a/mobile/android/chrome/geckoview/geckoview.js +++ b/mobile/android/chrome/geckoview/geckoview.js @@ -9,6 +9,8 @@ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); ChromeUtils.defineModuleGetter(this, "EventDispatcher", "resource://gre/modules/Messaging.jsm"); +ChromeUtils.defineModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyGetter(this, "WindowEventDispatcher", () => EventDispatcher.for(window)); @@ -23,8 +25,8 @@ XPCOMUtils.defineLazyGetter(this, "dump", () => // and remove by calling // remove() var ModuleManager = { - init: function() { - this.browser = document.getElementById("content"); + init: function(aBrowser) { + this.browser = aBrowser; this.modules = {}; }, @@ -45,8 +47,24 @@ var ModuleManager = { } }; +function createBrowser() { + const browser = window.browser = document.createElement("browser"); + browser.setAttribute("type", "content"); + browser.setAttribute("primary", "true"); + browser.setAttribute("flex", "1"); + + // There may be a GeckoViewNavigation module in another window waiting for us to + // create a browser so it can call presetOpenerWindow(), so allow them to do that now. + Services.obs.notifyObservers(window, "geckoview-window-created"); + window.document.getElementById("main-window").appendChild(browser); + + browser.stop(); + return browser; +} + function startup() { - ModuleManager.init(); + const browser = createBrowser(); + ModuleManager.init(browser); // GeckoViewNavigation needs to go first because nsIDOMBrowserWindow must set up // before the first remote browser. Bug 1365364. @@ -69,5 +87,5 @@ function startup() { // Move focus to the content window at the end of startup, // so things like text selection can work properly. - document.getElementById("content").focus(); + browser.focus(); } diff --git a/mobile/android/chrome/geckoview/geckoview.xul b/mobile/android/chrome/geckoview/geckoview.xul index 6d872b0c3285..711ae26ba662 100644 --- a/mobile/android/chrome/geckoview/geckoview.xul +++ b/mobile/android/chrome/geckoview/geckoview.xul @@ -3,14 +3,9 @@ - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - - - - - + + \ No newline at end of file diff --git a/mobile/android/geckoview/src/androidTest/assets/www/newSession_child.html b/mobile/android/geckoview/src/androidTest/assets/www/newSession_child.html new file mode 100644 index 000000000000..243cff3e6bad --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/www/newSession_child.html @@ -0,0 +1,11 @@ + + Hello, world! + +

I'm the child

+ + + \ No newline at end of file diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseGeckoViewTest.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseGeckoViewTest.java new file mode 100644 index 000000000000..a59a168ff5f2 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseGeckoViewTest.java @@ -0,0 +1,118 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.geckoview.test; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.mozilla.gecko.GeckoSession; +import org.mozilla.gecko.GeckoView; + +import android.net.Uri; +import android.os.SystemClock; +import android.support.test.espresso.Espresso; +import android.support.test.espresso.IdlingResource; +import android.support.test.espresso.assertion.ViewAssertions; +import android.support.test.espresso.matcher.ViewMatchers; +import android.support.test.rule.ActivityTestRule; +import android.view.MotionEvent; + +import static org.junit.Assert.assertTrue; + +public class BaseGeckoViewTest { + protected GeckoSession mSession; + protected GeckoView mView; + private ExplicitIdlingResource mIdlingResource; + + @Before + public void setup() { + mView = mActivityRule.getActivity().getGeckoView(); + mSession = mActivityRule.getActivity().getGeckoSession(); + + mIdlingResource = new ExplicitIdlingResource(); + Espresso.registerIdlingResources(mIdlingResource); + } + + @After + public void tearDown() { + Espresso.unregisterIdlingResources(mIdlingResource); + } + + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule<>( + TestRunnerActivity.class); + + protected void waitUntilDone() { + Espresso.onView(ViewMatchers.isRoot()).check(ViewAssertions.matches(ViewMatchers.isRoot())); + } + + protected void done() { + mIdlingResource.done(); + } + + protected String buildAssetUrl(String path) { + return "resource://android/assets/www/" + path; + } + + protected void loadTestPath(final String path, Runnable finished) { + loadTestPage(buildAssetUrl(path), finished); + } + + protected void loadTestPage(final String url, final Runnable finished) { + final String path = Uri.parse(url).getPath(); + mSession.setProgressListener(new GeckoSession.ProgressListener() { + @Override + public void onPageStart(GeckoSession session, String loadingUrl) { + assertTrue("Loaded url should end with " + path, loadingUrl.endsWith(path)); + } + + @Override + public void onPageStop(GeckoSession session, boolean success) { + assertTrue("Load should succeed", success); + mSession.setProgressListener(null); + finished.run(); + } + + @Override + public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) { + } + }); + + mSession.loadUri(url); + } + + protected void sendClick(int x, int y) { + mSession.getPanZoomController().onTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 5, 5, 0)); + mSession.getPanZoomController().onTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 5, 5, 0)); + } + + private class ExplicitIdlingResource implements IdlingResource { + private boolean mIsIdle; + private ResourceCallback mCallback; + + @Override + public String getName() { + return "Explicit Idling Resource"; + } + + @Override + public boolean isIdleNow() { + return mIsIdle; + } + + public void done() { + mIsIdle = true; + if (mCallback != null) { + mCallback.onTransitionToIdle(); + } + } + + @Override + public void registerIdleTransitionCallback(ResourceCallback callback) { + mCallback = callback; + } + } +} diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationTests.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationTests.java new file mode 100644 index 000000000000..32878a08defb --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationTests.java @@ -0,0 +1,298 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.geckoview.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.GeckoSession; + +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(AndroidJUnit4.class) +public class NavigationTests extends BaseGeckoViewTest { + @Test + public void testLoadUri() { + loadTestPath("hello.html", new Runnable() { + @Override public void run() { + done(); + } + }); + + waitUntilDone(); + } + + @Test + public void testGoBack() { + final String startPath = "hello.html"; + loadTestPath(startPath, new Runnable() { + @Override public void run() { + loadTestPath("hello2.html", new Runnable() { + @Override public void run() { + mSession.setNavigationListener(new GeckoSession.NavigationListener() { + @Override + public void onLocationChange(GeckoSession session, String url) { + assertTrue("URL should end with " + startPath + ", got " + url, url.endsWith(startPath)); + done(); + } + + @Override + public void onCanGoBack(GeckoSession session, boolean canGoBack) { + assertFalse("Should not be able to go back", canGoBack); + } + + @Override + public void onCanGoForward(GeckoSession session, boolean canGoForward) { + assertTrue("Should be able to go forward", canGoForward); + } + + @Override + public boolean onLoadUri(GeckoSession session, String uri, TargetWindow where) { + return false; + } + + @Override + public void onNewSession(GeckoSession session, String uri, GeckoSession.Response response) { + response.respond(null); + } + }); + + mSession.goBack(); + } + }); + } + }); + + waitUntilDone(); + } + + @Test + public void testReload() { + loadTestPath("hello.html", new Runnable() { + @Override public void run() { + mSession.setProgressListener(new GeckoSession.ProgressListener() { + @Override + public void onPageStart(GeckoSession session, String url) { + } + + @Override + public void onPageStop(GeckoSession session, boolean success) { + assertTrue(success); + done(); + } + + @Override + public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) { + + } + }); + + mSession.reload(); + } + }); + + waitUntilDone(); + } + + @Test + public void testExpiredCert() { + mSession.setProgressListener(new GeckoSession.ProgressListener() { + private boolean mNotBlank; + + @Override + public void onPageStart(GeckoSession session, String url) { + mNotBlank = !url.equals("about:blank"); + } + + @Override + public void onPageStop(GeckoSession session, boolean success) { + if (mNotBlank) { + assertFalse("Expected unsuccessful page load", success); + done(); + } + } + + @Override + public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) { + assertFalse(securityInfo.isSecure); + assertEquals(securityInfo.securityMode, SecurityInformation.SECURITY_MODE_UNKNOWN); + } + }); + + mSession.loadUri("https://expired.badssl.com/"); + waitUntilDone(); + } + + @Test + public void testValidTLS() { + mSession.setProgressListener(new GeckoSession.ProgressListener() { + private boolean mNotBlank; + + @Override + public void onPageStart(GeckoSession session, String url) { + mNotBlank = !url.equals("about:blank"); + } + + @Override + public void onPageStop(GeckoSession session, boolean success) { + if (mNotBlank) { + assertTrue("Expected successful page load", success); + done(); + } + } + + @Override + public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) { + assertTrue(securityInfo.isSecure); + assertEquals(securityInfo.securityMode, SecurityInformation.SECURITY_MODE_IDENTIFIED); + } + }); + + mSession.loadUri("https://mozilla-modern.badssl.com/"); + waitUntilDone(); + } + + @Test + public void testOnNewSession() { + mSession.setNavigationListener(new GeckoSession.NavigationListener() { + @Override + public void onLocationChange(GeckoSession session, String url) { + } + + @Override + public void onCanGoBack(GeckoSession session, boolean canGoBack) { + + } + + @Override + public void onCanGoForward(GeckoSession session, boolean canGoForward) { + + } + + @Override + public boolean onLoadUri(GeckoSession session, String uri, TargetWindow where) { + return false; + } + + @Override + public void onNewSession(GeckoSession session, String uri, GeckoSession.Response response) { + final GeckoSession newSession = new GeckoSession(session.getSettings()); + newSession.setContentListener(new GeckoSession.ContentListener() { + @Override + public void onTitleChange(GeckoSession session, String title) { + + } + + @Override + public void onFocusRequest(GeckoSession session) { + + } + + @Override + public void onCloseRequest(GeckoSession session) { + session.closeWindow(); + done(); + } + + @Override + public void onFullScreen(GeckoSession session, boolean fullScreen) { + + } + + @Override + public void onContextMenu(GeckoSession session, int screenX, int screenY, String uri, String elementSrc) { + + } + }); + + newSession.openWindow(InstrumentationRegistry.getTargetContext()); + response.respond(newSession); + } + }); + + mSession.setProgressListener(new GeckoSession.ProgressListener() { + @Override + public void onPageStart(GeckoSession session, String url) { + + } + + @Override + public void onPageStop(GeckoSession session, boolean success) { + // Send a click to open the window + sendClick(100, 100); + } + + @Override + public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) { + + } + }); + + + mSession.loadUri(buildAssetUrl("newSession.html")); + + waitUntilDone(); + } + + @Test(expected = IllegalArgumentException.class) + public void testOnNewSessionNoExisting() { + // This makes sure that we get an exception if you try to return + // an existing GeckoSession instance from the NavigationListener.onNewSession() + // implementation. + + mSession.setNavigationListener(new GeckoSession.NavigationListener() { + @Override + public void onLocationChange(GeckoSession session, String url) { + } + + @Override + public void onCanGoBack(GeckoSession session, boolean canGoBack) { + + } + + @Override + public void onCanGoForward(GeckoSession session, boolean canGoForward) { + + } + + @Override + public boolean onLoadUri(GeckoSession session, String uri, TargetWindow where) { + return false; + } + + @Override + public void onNewSession(GeckoSession session, String uri, GeckoSession.Response response) { + // This is where the throw should occur + response.respond(mSession); + } + }); + + mSession.setProgressListener(new GeckoSession.ProgressListener() { + @Override + public void onPageStart(GeckoSession session, String url) { + + } + + @Override + public void onPageStop(GeckoSession session, boolean success) { + sendClick(100, 100); + } + + @Override + public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) { + + } + }); + + + mSession.loadUri(buildAssetUrl("newSession.html")); + waitUntilDone(); + } +} diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java new file mode 100644 index 000000000000..da52dcf8f3d6 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java @@ -0,0 +1,139 @@ +package org.mozilla.geckoview.test; + +import org.mozilla.gecko.GeckoSession; +import org.mozilla.gecko.GeckoSessionSettings; +import org.mozilla.gecko.GeckoView; +import org.mozilla.gecko.mozglue.GeckoLoader; +import org.mozilla.gecko.mozglue.SafeIntent; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +public class TestRunnerActivity extends Activity { + private static final String LOGTAG = "TestRunnerActivity"; + + GeckoSession mSession; + GeckoView mView; + + private GeckoSession.NavigationListener mNavigationListener = new GeckoSession.NavigationListener() { + @Override + public void onLocationChange(GeckoSession session, String url) { + getActionBar().setSubtitle(url); + } + + @Override + public void onCanGoBack(GeckoSession session, boolean canGoBack) { + + } + + @Override + public void onCanGoForward(GeckoSession session, boolean canGoForward) { + + } + + @Override + public boolean onLoadUri(GeckoSession session, String uri, TargetWindow where) { + // Allow Gecko to load all URIs + return false; + } + + @Override + public void onNewSession(GeckoSession session, String uri, GeckoSession.Response response) { + response.respond(createSession(session.getSettings())); + } + }; + + private GeckoSession.ContentListener mContentListener = new GeckoSession.ContentListener() { + @Override + public void onTitleChange(GeckoSession session, String title) { + + } + + @Override + public void onFocusRequest(GeckoSession session) { + + } + + @Override + public void onCloseRequest(GeckoSession session) { + session.closeWindow(); + } + + @Override + public void onFullScreen(GeckoSession session, boolean fullScreen) { + + } + + @Override + public void onContextMenu(GeckoSession session, int screenX, int screenY, String uri, String elementSrc) { + + } + }; + + private GeckoSession createSession() { + return createSession(null); + } + + private GeckoSession createSession(GeckoSessionSettings settings) { + if (settings == null) { + settings = new GeckoSessionSettings(); + + // We can't use e10s because we get deadlocked when quickly creating and + // destroying sessions. Bug 1348361. + settings.setBoolean(GeckoSessionSettings.USE_MULTIPROCESS, false); + } + + final GeckoSession session = new GeckoSession(settings); + session.setNavigationListener(mNavigationListener); + session.openWindow(this); + return session; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + GeckoLoader.setLastIntent(new SafeIntent(getIntent())); + + final String intentArgs = intent.getStringExtra("args"); + final String args = intentArgs != null ? "-purgecaches " + intentArgs : "-purgecaches"; + GeckoSession.preload(this, args, false /* no multiprocess, see below */); + + // We can't use e10s because we get deadlocked when quickly creating and + // destroying sessions. Bug 1348361. + mSession = createSession(); + + // If we were passed a URI in the Intent, open it + final Uri uri = intent.getData(); + if (uri != null) { + mSession.loadUri(uri); + } + + mView = new GeckoView(this); + mView.setSession(mSession); + setContentView(mView); + } + + @Override + protected void onDestroy() { + mSession.closeWindow(); + super.onDestroy(); + } + + public GeckoView getGeckoView() { + return mView; + } + + public GeckoSession getGeckoSession() { + return mSession; + } +} diff --git a/mobile/android/geckoview/src/androidTest/res/drawable/ic_launcher_background.xml b/mobile/android/geckoview/src/androidTest/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000000..cd75f1434acb --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/res/drawable/ic_launcher_background.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/geckoview/src/androidTest/res/mipmap-hdpi/ic_launcher.png b/mobile/android/geckoview/src/androidTest/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..a2f5908281d0 Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/res/mipmap-hdpi/ic_launcher.png differ diff --git a/mobile/android/geckoview/src/androidTest/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/geckoview/src/androidTest/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000000..1b5239980811 Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/mobile/android/geckoview/src/androidTest/res/mipmap-mdpi/ic_launcher.png b/mobile/android/geckoview/src/androidTest/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..ff10afd6e182 Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/res/mipmap-mdpi/ic_launcher.png differ diff --git a/mobile/android/geckoview/src/androidTest/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/geckoview/src/androidTest/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000000..115a4c768a20 Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/mobile/android/geckoview/src/androidTest/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/geckoview/src/androidTest/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..dcd3cd808335 Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/mobile/android/geckoview/src/androidTest/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/geckoview/src/androidTest/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000000..459ca609d3ae Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/mobile/android/geckoview/src/androidTest/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/geckoview/src/androidTest/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..8ca12fe024be Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/mobile/android/geckoview/src/androidTest/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/geckoview/src/androidTest/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000000..8e19b410a1b1 Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/mobile/android/geckoview/src/androidTest/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/geckoview/src/androidTest/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..b824ebdd48db Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/mobile/android/geckoview/src/androidTest/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/geckoview/src/androidTest/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000000..4c19a13c239c Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/mobile/android/geckoview/src/androidTest/res/values/colors.xml b/mobile/android/geckoview/src/androidTest/res/values/colors.xml new file mode 100644 index 000000000000..3a96673022c0 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/res/values/colors.xml @@ -0,0 +1,9 @@ + + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/mobile/android/geckoview/src/androidTest/res/values/strings.xml b/mobile/android/geckoview/src/androidTest/res/values/strings.xml new file mode 100644 index 000000000000..7831a536eb65 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/res/values/strings.xml @@ -0,0 +1,7 @@ + + + + GeckoView Test Runner + diff --git a/mobile/android/geckoview/src/androidTest/res/values/styles.xml b/mobile/android/geckoview/src/androidTest/res/values/styles.xml new file mode 100644 index 000000000000..60abe4bf6306 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java index e1510995558b..a8ddf63a5045 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java @@ -83,6 +83,11 @@ public class GeckoThread extends Thread { } return false; } + + @Override + public String toString() { + return name(); + } } private static final NativeQueue sNativeQueue = @@ -495,13 +500,17 @@ public class GeckoThread extends Thread { @WrapForJNI(calledFrom = "gecko") private static void setState(final State newState) { - sNativeQueue.setState(newState); + checkAndSetState(null, newState); } @WrapForJNI(calledFrom = "gecko") private static boolean checkAndSetState(final State expectedState, final State newState) { - return sNativeQueue.checkAndSetState(expectedState, newState); + final boolean result = sNativeQueue.checkAndSetState(expectedState, newState); + if (result) { + Log.d(LOGTAG, "State changed to " + newState); + } + return result; } @WrapForJNI(stubName = "SpeculativeConnect") diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java index b668de2405e4..4c564e4f3c39 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java @@ -9,6 +9,7 @@ package org.mozilla.geckoview; import java.net.URLConnection; import java.util.ArrayList; import java.util.Arrays; +import java.util.UUID; import org.mozilla.gecko.annotation.WrapForJNI; import org.mozilla.gecko.EventDispatcher; @@ -73,6 +74,9 @@ public class GeckoSession extends LayerSession private final TextInputController mTextInput = new TextInputController(this, mNativeQueue); + private String mId = UUID.randomUUID().toString().replace("-", ""); + /* package */ String getId() { return mId; } + private final GeckoSessionHandler mContentHandler = new GeckoSessionHandler( "GeckoViewContent", this, @@ -80,6 +84,7 @@ public class GeckoSession extends LayerSession "GeckoView:ContextMenu", "GeckoView:DOMTitleChanged", "GeckoView:DOMWindowFocus", + "GeckoView:DOMWindowClose", "GeckoView:FullScreenEnter", "GeckoView:FullScreenExit" } @@ -101,6 +106,8 @@ public class GeckoSession extends LayerSession message.getString("title")); } else if ("GeckoView:DOMWindowFocus".equals(event)) { listener.onFocusRequest(GeckoSession.this); + } else if ("GeckoView:DOMWindowClose".equals(event)) { + listener.onCloseRequest(GeckoSession.this); } else if ("GeckoView:FullScreenEnter".equals(event)) { listener.onFullScreen(GeckoSession.this, true); } else if ("GeckoView:FullScreenExit".equals(event)) { @@ -114,7 +121,8 @@ public class GeckoSession extends LayerSession "GeckoViewNavigation", this, new String[]{ "GeckoView:LocationChange", - "GeckoView:OnLoadUri" + "GeckoView:OnLoadUri", + "GeckoView:OnNewSession" } ) { @Override @@ -137,6 +145,19 @@ public class GeckoSession extends LayerSession final boolean result = listener.onLoadUri(GeckoSession.this, uri, where); callback.sendSuccess(result); + } else if ("GeckoView:OnNewSession".equals(event)) { + final String uri = message.getString("uri"); + listener.onNewSession(GeckoSession.this, uri, + new Response() { + @Override + public void respond(GeckoSession session) { + if (session != null && session.isOpen() && session.isReady()) { + throw new IllegalArgumentException("Must use a new GeckoSession instance"); + } + + callback.sendSuccess(session != null ? session.getId() : null); + } + }); } } }; @@ -353,7 +374,7 @@ public class GeckoSession extends LayerSession public static native void open(Window instance, Compositor compositor, EventDispatcher dispatcher, GeckoBundle settings, String chromeUri, - int screenId, boolean privateMode); + int screenId, boolean privateMode, String id); @Override // JNIObject protected void disposeNative() { @@ -434,7 +455,7 @@ public class GeckoSession extends LayerSession private GeckoSessionSettings mSettings; public GeckoSession() { - this(/* settings */ null); + this(null); } public GeckoSession(final GeckoSessionSettings settings) { @@ -447,13 +468,15 @@ public class GeckoSession extends LayerSession mListener.registerListeners(); } - private void transferFrom(final Window window, final GeckoSessionSettings settings) { + private void transferFrom(final Window window, final GeckoSessionSettings settings, + final String id) { if (isOpen()) { throw new IllegalStateException("Session is open"); } mWindow = window; mSettings = new GeckoSessionSettings(settings, this); + mId = id; if (mWindow != null) { if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { @@ -471,7 +494,7 @@ public class GeckoSession extends LayerSession } /* package */ void transferFrom(final GeckoSession session) { - transferFrom(session.mWindow, session.mSettings); + transferFrom(session.mWindow, session.mSettings, session.mId); session.mWindow = null; session.onWindowChanged(); } @@ -485,6 +508,7 @@ public class GeckoSession extends LayerSession public void writeToParcel(Parcel out, int flags) { out.writeStrongInterface(mWindow); out.writeParcelable(mSettings, flags); + out.writeString(mId); } // AIDL code may call readFromParcel even though it's not part of Parcelable. @@ -495,7 +519,8 @@ public class GeckoSession extends LayerSession final Window window = (ifce instanceof Window) ? (Window) ifce : null; final GeckoSessionSettings settings = source.readParcelable(getClass().getClassLoader()); - transferFrom(window, settings); + final String id = source.readString(); + transferFrom(window, settings, id); } public static final Creator CREATOR = new Creator() { @@ -546,6 +571,10 @@ public class GeckoSession extends LayerSession return mWindow != null; } + /* package */ boolean isReady() { + return mNativeQueue.isReady(); + } + public void openWindow(final Context appContext) { ThreadUtils.assertOnUiThread(); @@ -567,7 +596,7 @@ public class GeckoSession extends LayerSession if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { Window.open(mWindow, mCompositor, mEventDispatcher, - mSettings.asBundle(), chromeUri, screenId, isPrivate); + mSettings.asBundle(), chromeUri, screenId, isPrivate, mId); } else { GeckoThread.queueNativeCallUntil( GeckoThread.State.PROFILE_READY, @@ -577,7 +606,7 @@ public class GeckoSession extends LayerSession EventDispatcher.class, mEventDispatcher, GeckoBundle.class, mSettings.asBundle(), String.class, chromeUri, - screenId, isPrivate); + screenId, isPrivate, mId); } onWindowChanged(); @@ -762,7 +791,7 @@ public class GeckoSession extends LayerSession /** * Set the tracking protection callback handler. * This will replace the current handler. - * @param listener An implementation of TrackingProtectionDelegate. + * @param delegate An implementation of TrackingProtectionDelegate. */ public void setTrackingProtectionDelegate(TrackingProtectionDelegate delegate) { mTrackingProtectionHandler.setListener(delegate, this); @@ -1278,6 +1307,12 @@ public class GeckoSession extends LayerSession */ void onFocusRequest(GeckoSession session); + /** + * A page has requested to close + * @param session The GeckoSession that initiated the callback. + */ + void onCloseRequest(GeckoSession session); + /** * A page has entered or exited full screen mode. Typically, the implementation * would set the Activity containing the GeckoSession to full screen when the page is @@ -1306,6 +1341,16 @@ public class GeckoSession extends LayerSession String uri, String elementSrc); } + /** + * This is used to send responses in delegate methods that have asynchronous responses. + */ + public interface Response { + /** + * @param val The value contained in the response + */ + void respond(T val); + } + public interface NavigationListener { /** * A view has started loading content from the network. @@ -1378,10 +1423,22 @@ public class GeckoSession extends LayerSession * @param uri The URI to be loaded. * @param where The target window. * - * @return True if the URI loading has been handled, false if Gecko - * should handle the loading. + * @return Whether or not the load was handled. Returning false will allow Gecko + * to continue the load as normal. */ boolean onLoadUri(GeckoSession session, String uri, TargetWindow where); + + /** + * A request has been made to open a new session. The URI is provided only for + * informational purposes. Do not call GeckoSession.loadUri() here. Additionally, the + * returned GeckoSession must be a newly-created one. + * + * @param session The GeckoSession that initiated the callback. + * @param uri The URI to be loaded. + * + * @param response A Response which will hold the returned GeckoSession + */ + void onNewSession(GeckoSession session, String uri, Response response); } /** @@ -1877,7 +1934,7 @@ public class GeckoSession extends LayerSession * @param session The GeckoSession that initiated the callback. * @param uri The URI of the blocked element. * @param categories The tracker categories of the blocked element. - * One or more of the {@link #CATEGORY_AD CATEGORY_*} + * One or more of the {@link TrackingProtectionDelegate#CATEGORY_AD} * flags. */ void onTrackerBlocked(GeckoSession session, String uri, int categories); @@ -1886,7 +1943,7 @@ public class GeckoSession extends LayerSession /** * Enable tracking protection. * @param categories The categories of trackers that should be blocked. - * Use one or more of the {@link #CATEGORY_AD CATEGORY_*} + * Use one or more of the {@link TrackingProtectionDelegate#CATEGORY_AD} * flags. **/ public void enableTrackingProtection(int categories) { diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSessionHandler.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSessionHandler.java index 41be897c05f9..2970fee1a700 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSessionHandler.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSessionHandler.java @@ -89,6 +89,8 @@ import android.util.Log; if (mListener != null) { handleMessage(mListener, event, message, callback); + } else { + callback.sendError("No listener registered"); } } diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java index 587e4209a68f..38c2300a3fa6 100644 --- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java +++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java @@ -23,6 +23,7 @@ import org.mozilla.gecko.util.GeckoBundle; import org.mozilla.geckoview.GeckoSession; import org.mozilla.geckoview.GeckoSessionSettings; import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource; +import org.mozilla.geckoview.GeckoSession.Response; import org.mozilla.geckoview.GeckoSession.TrackingProtectionDelegate; import org.mozilla.geckoview.GeckoView; @@ -87,8 +88,8 @@ public class GeckoViewActivity extends Activity { permission.androidPermissionRequestCode = REQUEST_PERMISSIONS; mGeckoSession.setPermissionDelegate(permission); - mGeckoView.getSettings().setBoolean(GeckoSessionSettings.USE_MULTIPROCESS, - useMultiprocess); + mGeckoSession.getSettings().setBoolean(GeckoSessionSettings.USE_MULTIPROCESS, + useMultiprocess); mGeckoSession.enableTrackingProtection( TrackingProtectionDelegate.CATEGORY_AD | @@ -173,11 +174,6 @@ public class GeckoViewActivity extends Activity { Log.i(LOGTAG, "Content title changed to " + title); } - @Override - public void onFocusRequest(GeckoSession session) { - Log.i(LOGTAG, "Content requesting focus"); - } - @Override public void onFullScreen(final GeckoSession session, final boolean fullScreen) { getWindow().setFlags(fullScreen ? WindowManager.LayoutParams.FLAG_FULLSCREEN : 0, @@ -189,6 +185,18 @@ public class GeckoViewActivity extends Activity { } } + @Override + public void onFocusRequest(final GeckoSession session) { + Log.i(LOGTAG, "Content requesting focus"); + } + + @Override + public void onCloseRequest(final GeckoSession session) { + if (session != mGeckoSession) { + session.closeWindow(); + } + } + @Override public void onContextMenu(GeckoSession session, int screenX, int screenY, String uri, String elementSrc) { @@ -346,13 +354,13 @@ public class GeckoViewActivity extends Activity { @Override public boolean onLoadUri(final GeckoSession session, final String uri, final TargetWindow where) { - Log.d(LOGTAG, "onLoadUri=" + uri + - " where=" + where); - if (where != TargetWindow.NEW) { - return false; - } - session.loadUri(uri); - return true; + Log.d(LOGTAG, "onLoadUri=" + uri + " where=" + where); + return false; + } + + @Override + public void onNewSession(final GeckoSession session, final String uri, Response response) { + response.respond(null); } } diff --git a/mobile/android/gradle/with_gecko_binaries.gradle b/mobile/android/gradle/with_gecko_binaries.gradle index 3128aa8f274f..6d5b479e5a3a 100644 --- a/mobile/android/gradle/with_gecko_binaries.gradle +++ b/mobile/android/gradle/with_gecko_binaries.gradle @@ -43,10 +43,11 @@ ext.configureVariantWithGeckoBinaries = { variant -> def distDir = "${topobjdir}/dist/${omnijar_dir}" def syncOmnijarFromDistDir = task("syncOmnijarFromDistDirFor${variant.name.capitalize()}", type: Sync) { - doFirst { + onlyIf { if (source.empty) { - throw new GradleException("Required omnijar not found in ${source.asPath}. Have you built and packaged?") + throw new StopExecutionException("Required omnijar not found in ${distDir}/{omni.ja,assets/omni.ja}. Have you built and packaged?") } + return true } into("${project.buildDir}/moz.build/src/${variant.name}/omnijar") @@ -58,10 +59,11 @@ ext.configureVariantWithGeckoBinaries = { variant -> } def syncLibsFromDistDir = task("syncLibsFromDistDirFor${variant.name.capitalize()}", type: Sync) { - doFirst { + onlyIf { if (source.empty) { - throw new GradleException("Required JNI libraries not found in ${source.asPath}. Have you built and packaged?") + throw new StopExecutionException("Required JNI libraries not found in ${distDir}/lib. Have you built and packaged?") } + return true } into("${project.buildDir}/moz.build/src/${variant.name}/jniLibs") @@ -69,10 +71,11 @@ ext.configureVariantWithGeckoBinaries = { variant -> } def syncAssetsFromDistDir = task("syncAssetsFromDistDirFor${variant.name.capitalize()}", type: Sync) { - doFirst { + onlyIf { if (source.empty) { - throw new GradleException("Required assets not found in ${source.asPath}. Have you built and packaged?") + throw new StopExecutionException("Required assets not found in ${distDir}/assets. Have you built and packaged?") } + return true } into("${project.buildDir}/moz.build/src/${variant.name}/assets") diff --git a/mobile/android/installer/package-manifest.in b/mobile/android/installer/package-manifest.in index c99437960cf8..ca0976ca9d3f 100644 --- a/mobile/android/installer/package-manifest.in +++ b/mobile/android/installer/package-manifest.in @@ -290,15 +290,16 @@ @BINPATH@/components/EditorUtils.manifest @BINPATH@/components/EditorUtils.js -#ifndef MOZ_GECKOVIEW_JAR @BINPATH@/components/extensions.manifest +@BINPATH@/components/addonManager.js +@BINPATH@/components/nsBlocklistService.js + +#ifndef MOZ_GECKOVIEW_JAR @BINPATH@/components/utils.manifest @BINPATH@/components/simpleServices.js -@BINPATH@/components/addonManager.js @BINPATH@/components/amContentHandler.js -@BINPATH@/components/amInstallTrigger.js @BINPATH@/components/amWebAPI.js -@BINPATH@/components/nsBlocklistService.js +@BINPATH@/components/amInstallTrigger.js #ifndef RELEASE_OR_BETA @BINPATH@/components/TabSource.js #endif @@ -314,9 +315,10 @@ @BINPATH@/components/nsUpdateService.js @BINPATH@/components/nsUpdateServiceStub.js #endif +#endif + @BINPATH@/components/nsUpdateTimerManager.manifest @BINPATH@/components/nsUpdateTimerManager.js -#endif @BINPATH@/components/pluginGlue.manifest @BINPATH@/components/ProcessSingleton.manifest diff --git a/mobile/android/modules/geckoview/GeckoViewNavigation.jsm b/mobile/android/modules/geckoview/GeckoViewNavigation.jsm index 5ff6aa203876..aa620f4660b2 100644 --- a/mobile/android/modules/geckoview/GeckoViewNavigation.jsm +++ b/mobile/android/modules/geckoview/GeckoViewNavigation.jsm @@ -92,19 +92,71 @@ class GeckoViewNavigation extends GeckoViewModule { let handled = undefined; this.eventDispatcher.sendRequestForResult(message).then(response => { handled = response; + }, () => { + // There was an error or listener was not registered in GeckoSession, treat as unhandled. + handled = false; }); Services.tm.spinEventLoopUntil(() => handled !== undefined); return handled; } + waitAndSetOpener(aSessionId, aOpener) { + if (!aSessionId) { + return Promise.resolve(null); + } + + return new Promise(resolve => { + const handler = { + observe(aSubject, aTopic, aData) { + if (aTopic === "geckoview-window-created" && aSubject.name === aSessionId) { + aSubject.browser.presetOpenerWindow(aOpener); + Services.obs.removeObserver(handler, "geckoview-window-created"); + resolve(aSubject); + } + } + }; + + // This event is emitted from createBrowser() in geckoview.js + Services.obs.addObserver(handler, "geckoview-window-created"); + }); + } + + handleNewSession(aUri, aOpener, aWhere, aFlags, aTriggeringPrincipal) { + debug("handleNewSession: aUri=" + (aUri && aUri.spec) + + " aWhere=" + aWhere + + " aFlags=" + aFlags); + + if (!this.isRegistered) { + return null; + } + + const message = { + type: "GeckoView:OnNewSession", + uri: aUri ? aUri.displaySpec : "" + }; + + let browser = undefined; + this.eventDispatcher.sendRequestForResult(message).then(sessionId => { + return this.waitAndSetOpener(sessionId, aOpener); + }).then(window => { + browser = (window && window.browser); + }, () => { + browser = null; + }); + + // Wait indefinitely for app to respond with a browser or null + Services.tm.spinEventLoopUntil(() => browser !== undefined); + return browser; + } + // nsILoadURIDelegate. loadURI(aUri, aWhere, aFlags, aTriggeringPrincipal) { debug("loadURI " + aUri + " " + aWhere + " " + aFlags + " " + aTriggeringPrincipal); - let handled = this.handleLoadUri(aUri, null, aWhere, aFlags, - aTriggeringPrincipal); + const handled = this.handleLoadUri(aUri, null, aWhere, aFlags, + aTriggeringPrincipal); if (!handled) { throw Cr.NS_ERROR_ABORT; } @@ -116,15 +168,9 @@ class GeckoViewNavigation extends GeckoViewModule { " aWhere=" + aWhere + " aFlags=" + aFlags); - let handled = this.handleLoadUri(aUri, aOpener, aWhere, aFlags, - aTriggeringPrincipal); - if (!handled && - (aWhere === Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW || - aWhere === Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW)) { - return this.browser.contentWindow; - } - - throw Cr.NS_ERROR_ABORT; + const browser = this.handleNewSession(aUri, aOpener, aWhere, aFlags, + aTriggeringPrincipal); + return browser && browser.contentWindow; } // nsIBrowserDOMWindow. @@ -137,14 +183,12 @@ class GeckoViewNavigation extends GeckoViewModule { " aNextTabParentId=" + aNextTabParentId + " aName=" + aName); - let handled = this.handleLoadUri(aUri, null, aWhere, aFlags, null); - if (!handled && - (aWhere === Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW || - aWhere === Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW)) { - return this.browser; + const browser = this.handleNewSession(aUri, null, aWhere, aFlags, null); + if (browser) { + browser.setAttribute("nextTabParentId", aNextTabParentId); } - throw Cr.NS_ERROR_ABORT; + return browser; } // nsIBrowserDOMWindow. @@ -161,7 +205,6 @@ class GeckoViewNavigation extends GeckoViewModule { // nsIBrowserDOMWindow. isTabContentWindow(aWindow) { - debug("isTabContentWindow " + this.browser.contentWindow === aWindow); return this.browser.contentWindow === aWindow; } diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index a1c00282c467..1035e3b613e0 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -3756,11 +3756,7 @@ pref("font.name-list.sans-serif.ja", "Meiryo, Yu Gothic, MS PGothic, MS Gothic, pref("font.name-list.monospace.ja", "MS Gothic, MS Mincho, Meiryo, Yu Gothic, Yu Mincho, MS PGothic, MS PMincho"); pref("font.name-list.serif.ko", "Batang, Gulim"); -#ifdef EARLY_BETA_OR_EARLIER pref("font.name-list.sans-serif.ko", "Malgun Gothic, Gulim"); -#else -pref("font.name-list.sans-serif.ko", "Gulim, Malgun Gothic"); -#endif pref("font.name-list.monospace.ko", "GulimChe"); pref("font.name-list.cursive.ko", "Gungsuh"); diff --git a/netwerk/dns/nsHostResolver.cpp b/netwerk/dns/nsHostResolver.cpp index 35b6cb73d317..fe7b23ad7809 100644 --- a/netwerk/dns/nsHostResolver.cpp +++ b/netwerk/dns/nsHostResolver.cpp @@ -203,6 +203,7 @@ nsHostRecord::nsHostRecord(const nsHostKey& key) , mGetTtl(false) , mTrrAUsed(INIT) , mTrrAAAAUsed(INIT) + , mTrrLock("nsHostRecord.mTrrLock") , mBlacklistedCount(0) , mResolveAgain(false) { @@ -211,11 +212,14 @@ nsHostRecord::nsHostRecord(const nsHostKey& key) void nsHostRecord::Cancel() { + MutexAutoLock trrlock(mTrrLock); if (mTrrA) { mTrrA->Cancel(); + mTrrA = nullptr; } if (mTrrAAAA) { mTrrAAAA->Cancel(); + mTrrAAAA = nullptr; } } @@ -460,7 +464,6 @@ bool nsHostRecord::RemoveOrRefresh() { // no need to flush TRRed names, they're not resolved "locally" - Cancel(); MutexAutoLock lock(addr_info_lock); if (addr_info && addr_info->IsTRR()) { return false; @@ -1055,6 +1058,7 @@ nsHostResolver::ConditionallyCreateThread(nsHostRecord *rec) return NS_OK; } +// make sure the mTrrLock is held when this is used! #define TRROutstanding() ((rec->mTrrA || rec->mTrrAAAA)) nsresult @@ -1071,7 +1075,12 @@ nsHostResolver::TrrLookup(nsHostRecord *aRec, TRR *pushedTRR) { RefPtr rec(aRec); mLock.AssertCurrentThreadOwns(); - MOZ_ASSERT(!TRROutstanding()); +#ifdef DEBUG + { + MutexAutoLock trrlock(rec->mTrrLock); + MOZ_ASSERT(!TRROutstanding()); + } +#endif MOZ_ASSERT(!rec->mResolving); if (!gTRRService || !gTRRService->Enabled()) { @@ -1116,6 +1125,7 @@ nsHostResolver::TrrLookup(nsHostRecord *aRec, TRR *pushedTRR) sendAgain = false; LOG(("TRR Resolve %s type %d\n", rec->host.get(), (int)rectype)); RefPtr trr; + MutexAutoLock trrlock(rec->mTrrLock); trr = pushedTRR ? pushedTRR : new TRR(this, rec, rectype); if (pushedTRR || NS_SUCCEEDED(NS_DispatchToMainThread(trr))) { rec->mResolving++; @@ -1478,6 +1488,7 @@ nsHostResolver::CompleteLookup(nsHostRecord* rec, nsresult status, AddrInfo* aNe aNewRRSet ? aNewRRSet->IsTRR() : 0, rec->mResolving)); if (trrResult) { + MutexAutoLock trrlock(rec->mTrrLock); LOG(("TRR lookup Complete (%d) %s %s\n", newRRSet->IsTRR(), newRRSet->mHostName, NS_SUCCEEDED(status) ? "OK" : "FAILED")); diff --git a/netwerk/dns/nsHostResolver.h b/netwerk/dns/nsHostResolver.h index cffd81251f2e..4d8f27073cac 100644 --- a/netwerk/dns/nsHostResolver.h +++ b/netwerk/dns/nsHostResolver.h @@ -190,6 +190,7 @@ private: INIT, STARTED, OK, FAILED } mTrrAUsed, mTrrAAAAUsed; + Mutex mTrrLock; // lock when accessing the mTrrA[AAA] pointers RefPtr mTrrA; RefPtr mTrrAAAA; diff --git a/taskcluster/docker/periodic_updates/.eslintrc.js b/taskcluster/docker/periodic_updates/.eslintrc.js new file mode 100644 index 000000000000..02ea31b0deb7 --- /dev/null +++ b/taskcluster/docker/periodic_updates/.eslintrc.js @@ -0,0 +1,109 @@ +"use strict"; + +module.exports = { + "globals": { + // JS files in this folder are commonly xpcshell scripts where |arguments| + // is defined in the global scope. + "arguments": false + }, + "rules": { + // Enforce return statements in callbacks of array methods. + "array-callback-return": "error", + + // Braces only needed for multi-line arrow function blocks + "arrow-body-style": ["error", "as-needed"], + + // Verify calls of super() in constructors. + "constructor-super": "error", + + // Require braces around blocks that start a new line + "curly": ["error", "multi-line"], + + // Require default case in switch statements. + "default-case": "error", + + // Always require parenthesis for new calls + "new-parens": "error", + + // Disallow use of alert(), confirm(), and prompt(). + "no-alert": "error", + + // Disallow likely erroneous `switch` scoped lexical declarations in + // case/default clauses. + "no-case-declarations": "error", + + // Disallow use of the console API. + "no-console": "error", + + // Disallow constant expressions in conditions (except for loops). + "no-constant-condition": ["error", { "checkLoops": false }], + + // Disallow extending of native objects. + "no-extend-native": "error", + + // Disallow case statement fallthrough without explicit `// falls through` + // annotation. + "no-fallthrough": "error", + + // No reassigning native JS objects or read only globals. + "no-global-assign": "error", + + // Disallow use of assignment in return statement. + "no-return-assign": ["error", "always"], + + // Disallow use of the comma operator. + "no-sequences": "error", + + // Disallow template literal placeholder syntax in regular strings. + "no-template-curly-in-string": "error", + + // Disallow use of this/super before calling super() in constructors. + "no-this-before-super": "error", + + // Disallow throwing literals (eg. |throw "error"| instead of + // |throw new Error("error")|) + "no-throw-literal": "error", + + // Disallow unmodified loop conditions. + "no-unmodified-loop-condition": "error", + + // No expressions where a statement is expected + "no-unused-expressions": "error", + + // Disallow unnecessary escape usage in strings and regular expressions. + "no-useless-escape": "error", + + // Disallow blank line padding within blocks. + "padded-blocks": ["error", "never"], + + // Enforce spacing after semicolons. + "semi-spacing": ["error", { "before": false, "after": true }], + + // Never use spaces before named function parentheses, but always for async + // arrow functions. + "space-before-function-paren": ["error", { + "anonymous": "ignore", + "asyncArrow": "always", + "named": "never", + }], + + // No space padding in parentheses + "space-in-parens": ["error", "never"], + + // ++ and -- should not need spacing + "space-unary-ops": ["error", { "words": true, "nonwords": false }], + + // Require "use strict" to be defined globally in the script. + "strict": ["error", "global"], + + // Enforce valid JSDoc comments. + "valid-jsdoc": ["error", { + "requireParamDescription": false, + "requireReturn": false, + "requireReturnDescription": false, + }], + + // Disallow Yoda conditions. + "yoda": ["error", "never"], + } +}; diff --git a/taskcluster/docker/periodic_updates/Dockerfile b/taskcluster/docker/periodic_updates/Dockerfile new file mode 100644 index 000000000000..aa0524af84e8 --- /dev/null +++ b/taskcluster/docker/periodic_updates/Dockerfile @@ -0,0 +1,38 @@ +FROM ubuntu:bionic +MAINTAINER Simon Fraser + +# Required software +ENV DEBIAN_FRONTEND noninteractive + +# Chain apt-get commands with apt-get clean in a single docker RUN +# to make sure that files are removed within a single docker layer + +RUN apt-get update -q && \ + apt-get install -y --no-install-recommends \ + arcanist=0~git20170812-1 \ + bzip2=1.0.6-8.1 \ + ca-certificates=20170717 \ + curl=7.58.0-2ubuntu1 \ + jq=1.5+dfsg-2 \ + libdbus-glib-1-2=0.110-2 \ + libgtk-3-0=3.22.28-1ubuntu1 \ + libxml2-utils=2.9.4+dfsg1-6.1ubuntu1 \ + libxt6=1:1.1.5-1 \ + mercurial=4.3.1-2 \ + python3=3.6.4-1 \ + shellcheck=0.4.6-1 \ + unzip=6.0-21ubuntu1 \ + wget=1.19.4-1ubuntu2 \ + && apt-get clean + +RUN useradd -d /home/worker -s /bin/bash -m worker + +COPY runme.sh / +COPY scripts/* /home/worker/scripts/ + +ENV HOME /home/worker +ENV SHELL /bin/bash +ENV USER worker +ENV LOGNAME worker + +CMD ["/runme.sh"] diff --git a/taskcluster/docker/periodic_updates/README.md b/taskcluster/docker/periodic_updates/README.md new file mode 100644 index 000000000000..493f000f1a60 --- /dev/null +++ b/taskcluster/docker/periodic_updates/README.md @@ -0,0 +1,93 @@ + +==Periodic File Updates== + +This docker image examines the in-tree files for HSTS preload data, HPKP pinning and blocklist.xml, and +will produce a diff for each necessary to update the in-tree files. + +If given a conduit API token, it will also use the arcanist client to submit the commits for review. + + +==Quick Start== + +```sh +docker build -t hsts-local --no-cache --rm . + +docker run -e DO_HSTS=1 -e DO_HPKP=1 -e DO_BLOCKLIST=1 -e PRODUCT="firefox" -e BRANCH="mozilla-central" -e USE_MOZILLA_CENTRAL=1 hsts-local +``` + +HSTS checks will only be run if the `DO_HSTS` environment variable is set. +Likewise for `DO_HPKP` and the HPKP checks, and `DO_BLOCKLIST` and the +blocklist checks. Environment variables are used rather than command line +arguments to make constructing taskcluster tasks easier. + +==Background== + +These scripts have been moved from `https://hg.mozilla.org/build/tools/scripts/periodic_file_updates/` and +`security/manager/tools/` in the main repos, as part of the buildbot to taskcluster migration. + +==HSTS Checks== + +`scripts/getHSTSPreloadList.js` will examine the current contents of +nsSTSPreloadList.inc from whichever `BRANCH` is specified, add in the mandatory +hosts, and those from the Chromium source, and check them all to see if their +SSL configuration is valid, and whether or not they have the +Strict-Transport-Security header set with an appropriate `max-age`. + +This javascript has been modified to use async calls to improve performance. + +==HPKP Checks== + +`scripts/genHPKPStaticPins.js` will ensure the list of pinned public keys are +up to date. + +==Example Taskcluster Task== + +https://tools.taskcluster.net/tasks/create + +```yaml +provisionerId: aws-provisioner-v1 +workerType: gecko-1-b-linux +retries: 0 +created: '2018-02-07T14:45:57.347Z' +deadline: '2018-02-07T17:45:57.348Z' +expires: '2019-02-07T17:45:57.348Z' +scopes: [] +payload: + image: srfraser/hsts1 + maxRunTime: 1800 + artifacts: + public/build/nsSTSPreloadList.diff: + path: /home/worker/artifacts/nsSTSPreloadList.diff + expires: '2019-02-07T13:57:35.448Z' + type: file + public/build/StaticHPKPins.h.diff: + path: /home/worker/artifacts/StaticHPKPins.h.diff + expires: '2019-02-07T13:57:35.448Z' + type: file + public/build/blocklist.diff: + path: /home/worker/artifacts/blocklist.diff + expires: '2019-02-07T13:57:35.448Z' + type: file + env: + DO_HSTS: 1 + DO_HPKP: 1 + DO_BLOCKLIST: 1 + PRODUCT: firefox + BRANCH: mozilla-central + USE_MOZILLA_CENTRAL: 1 + REVIEWERS: catlee +metadata: + name: Periodic updates testing + description: Produce diffs for HSTS and HPKP in-tree files. + owner: sfraser@mozilla.com + source: 'https://tools.taskcluster.net/task-creator/' +tags: {} +extra: + treeherder: + jobKind: test + machine: + platform: linux64 + tier: 1 + symbol: 'hsts' + +``` diff --git a/taskcluster/docker/periodic_updates/runme.sh b/taskcluster/docker/periodic_updates/runme.sh new file mode 100755 index 000000000000..45f4197a7c08 --- /dev/null +++ b/taskcluster/docker/periodic_updates/runme.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +set -xe + +# Things to be set by task definition. +# --pinset --hsts --hpkp --blocklist +# -b branch +# --use-mozilla-central +# -p firefox +# Artifact directory +# Artifact names. + + +test "${BRANCH}" +test "${PRODUCT}" + +PARAMS="" + +if [ ! -z "${USE_MOZILLA_CENTRAL}" ] +then + PARAMS="${PARAMS} --use-mozilla-central" +fi + +# TODO change these, so that they're run if the artifact location is specified? +if [ ! -z "${DO_HSTS}" ] +then + PARAMS="${PARAMS} --hsts" +fi + +if [ ! -z "${DO_HPKP}" ] +then + PARAMS="${PARAMS} --hpkp" +fi + +if [ ! -z "${DO_BLOCKLIST}" ] +then + PARAMS="${PARAMS} --blocklist" +fi + +export ARTIFACTS_DIR="/home/worker/artifacts" +mkdir -p "$ARTIFACTS_DIR" + +# Get Arcanist API token + +if [ -n "${TASK_ID}" ] +then + curl --location --retry 10 --retry-delay 10 -o /home/worker/task.json \ + "https://queue.taskcluster.net/v1/task/$TASK_ID" + ARC_SECRET=$(jq -r '.scopes[] | select(contains ("arc-phabricator-token"))' /home/worker/task.json | awk -F: '{print $3}') +fi +if [ -n "${ARC_SECRET}" ] && getent hosts taskcluster +then + set +x # Don't echo these + secrets_url="http://taskcluster/secrets/v1/secret/${ARC_SECRET}" + SECRET=$(curl "${secrets_url}") + TOKEN=$(echo "${SECRET}" | jq -r '.secret.token') +elif [ -n "${ARC_TOKEN}" ] # Allow for local testing. +then + TOKEN="${ARC_TOKEN}" +fi + +if [ -n "${TOKEN}" ] +then + cat >"${HOME}/.arcrc" < " + + ""); +} + +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm", {}); +var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm", {}); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {}); + +Cu.importGlobalProperties(["XMLHttpRequest"]); + +var gCertDB = Cc["@mozilla.org/security/x509certdb;1"] + .getService(Ci.nsIX509CertDB); + +const SHA256_PREFIX = "sha256/"; +const GOOGLE_PIN_PREFIX = "GOOGLE_PIN_"; + +// Pins expire in 14 weeks (6 weeks on Beta + 8 weeks on stable) +const PINNING_MINIMUM_REQUIRED_MAX_AGE = 60 * 60 * 24 * 7 * 14; + +const FILE_HEADER = "/* This Source Code Form is subject to the terms of the Mozilla Public\n" + +" * License, v. 2.0. If a copy of the MPL was not distributed with this\n" + +" * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n" + +"\n" + +"/*****************************************************************************/\n" + +"/* This is an automatically generated file. If you're not */\n" + +"/* PublicKeyPinningService.cpp, you shouldn't be #including it. */\n" + +"/*****************************************************************************/\n" + +"#include " + +"\n"; + +const DOMAINHEADER = "/* Domainlist */\n" + + "struct TransportSecurityPreload {\n" + + " // See bug 1338873 about making these fields const.\n" + + " const char* mHost;\n" + + " bool mIncludeSubdomains;\n" + + " bool mTestMode;\n" + + " bool mIsMoz;\n" + + " int32_t mId;\n" + + " const StaticFingerprints* pinset;\n" + + "};\n\n"; + +const PINSETDEF = "/* Pinsets are each an ordered list by the actual value of the fingerprint */\n" + + "struct StaticFingerprints {\n" + + " // See bug 1338873 about making these fields const.\n" + + " size_t size;\n" + + " const char* const* data;\n" + + "};\n\n"; + +// Command-line arguments +var gStaticPins = parseJson(arguments[0]); + +// Open the output file. +var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); +file.initWithPath(arguments[1]); +var gFileOutputStream = FileUtils.openSafeFileOutputStream(file); + +function writeString(string) { + gFileOutputStream.write(string, string.length); +} + +function readFileToString(filename) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(filename); + let stream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + stream.init(file, -1, 0, 0); + let buf = NetUtil.readInputStreamToString(stream, stream.available()); + return buf; +} + +function stripComments(buf) { + let lines = buf.split("\n"); + let entryRegex = /^\s*\/\//; + let data = ""; + for (let i = 0; i < lines.length; ++i) { + let match = entryRegex.exec(lines[i]); + if (!match) { + data = data + lines[i]; + } + } + return data; +} + +function download(filename) { + let req = new XMLHttpRequest(); + req.open("GET", filename, false); // doing the request synchronously + try { + req.send(); + } catch (e) { + throw new Error(`ERROR: problem downloading '${filename}': ${e}`); + } + + if (req.status != 200) { + throw new Error("ERROR: problem downloading '" + filename + "': status " + + req.status); + } + + let resultDecoded; + try { + resultDecoded = atob(req.responseText); + } catch (e) { + throw new Error("ERROR: could not decode data as base64 from '" + filename + + "': " + e); + } + return resultDecoded; +} + +function downloadAsJson(filename) { + // we have to filter out '//' comments, while not mangling the json + let result = download(filename).replace(/^(\s*)?\/\/[^\n]*\n/mg, ""); + let data = null; + try { + data = JSON.parse(result); + } catch (e) { + throw new Error("ERROR: could not parse data from '" + filename + "': " + e); + } + return data; +} + +// Returns a Subject Public Key Digest from the given pem, if it exists. +function getSKDFromPem(pem) { + let cert = gCertDB.constructX509FromBase64(pem, pem.length); + return cert.sha256SubjectPublicKeyInfoDigest; +} + +/** + * Hashes |input| using the SHA-256 algorithm in the following manner: + * btoa(sha256(atob(input))) + * + * @argument {String} input Base64 string to decode and return the hash of. + * @returns {String} Base64 encoded SHA-256 hash. + */ +function sha256Base64(input) { + let decodedValue; + try { + decodedValue = atob(input); + } catch (e) { + throw new Error(`ERROR: could not decode as base64: '${input}': ${e}`); + } + + // Convert |decodedValue| to an array so that it can be hashed by the + // nsICryptoHash instance below. + // In most cases across the code base, convertToByteArray() of + // nsIScriptableUnicodeConverter is used to do this, but the method doesn't + // seem to work here. + let data = []; + for (let i = 0; i < decodedValue.length; i++) { + data[i] = decodedValue.charCodeAt(i); + } + + let hasher = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA256); + hasher.update(data, data.length); + + // true is passed so that the hasher returns a Base64 encoded string. + return hasher.finish(true); +} + +// Downloads the static certs file and tries to map Google Chrome nicknames +// to Mozilla nicknames, as well as storing any hashes for pins for which we +// don't have root PEMs. Each entry consists of a line containing the name of +// the pin followed either by a hash in the format "sha256/" + base64(hash), +// a PEM encoded public key, or a PEM encoded certificate. +// For certificates that we have in our database, +// return a map of Google's nickname to ours. For ones that aren't return a +// map of Google's nickname to SHA-256 values. This code is modeled after agl's +// https://github.com/agl/transport-security-state-generate, which doesn't +// live in the Chromium repo because go is not an official language in +// Chromium. +// For all of the entries in this file: +// - If the entry has a hash format, find the Mozilla pin name (cert nickname) +// and stick the hash into certSKDToName +// - If the entry has a PEM format, parse the PEM, find the Mozilla pin name +// and stick the hash in certSKDToName +// We MUST be able to find a corresponding cert nickname for the Chrome names, +// otherwise we skip all pinsets referring to that Chrome name. +function downloadAndParseChromeCerts(filename, certNameToSKD, certSKDToName) { + // Prefixes that we care about. + const BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + const END_CERT = "-----END CERTIFICATE-----"; + const BEGIN_PUB_KEY = "-----BEGIN PUBLIC KEY-----"; + const END_PUB_KEY = "-----END PUBLIC KEY-----"; + + // Parsing states. + const PRE_NAME = 0; + const POST_NAME = 1; + const IN_CERT = 2; + const IN_PUB_KEY = 3; + let state = PRE_NAME; + + let lines = download(filename).split("\n"); + let pemCert = ""; + let pemPubKey = ""; + let hash = ""; + let chromeNameToHash = {}; + let chromeNameToMozName = {}; + let chromeName; + for (let line of lines) { + // Skip comments and newlines. + if (line.length == 0 || line[0] == "#") { + continue; + } + switch (state) { + case PRE_NAME: + chromeName = line; + state = POST_NAME; + break; + case POST_NAME: + if (line.startsWith(SHA256_PREFIX)) { + hash = line.substring(SHA256_PREFIX.length); + chromeNameToHash[chromeName] = hash; + certNameToSKD[chromeName] = hash; + certSKDToName[hash] = chromeName; + state = PRE_NAME; + } else if (line.startsWith(BEGIN_CERT)) { + state = IN_CERT; + } else if (line.startsWith(BEGIN_PUB_KEY)) { + state = IN_PUB_KEY; + } else { + throw new Error("ERROR: couldn't parse Chrome certificate file " + + "line: " + line); + } + break; + case IN_CERT: + if (line.startsWith(END_CERT)) { + state = PRE_NAME; + hash = getSKDFromPem(pemCert); + pemCert = ""; + let mozName; + if (hash in certSKDToName) { + mozName = certSKDToName[hash]; + } else { + // Not one of our built-in certs. Prefix the name with + // GOOGLE_PIN_. + mozName = GOOGLE_PIN_PREFIX + chromeName; + dump("Can't find hash in builtin certs for Chrome nickname " + + chromeName + ", inserting " + mozName + "\n"); + certSKDToName[hash] = mozName; + certNameToSKD[mozName] = hash; + } + chromeNameToMozName[chromeName] = mozName; + } else { + pemCert += line; + } + break; + case IN_PUB_KEY: + if (line.startsWith(END_PUB_KEY)) { + state = PRE_NAME; + hash = sha256Base64(pemPubKey); + pemPubKey = ""; + chromeNameToHash[chromeName] = hash; + certNameToSKD[chromeName] = hash; + certSKDToName[hash] = chromeName; + } else { + pemPubKey += line; + } + break; + default: + throw new Error("ERROR: couldn't parse Chrome certificate file " + line); + } + } + return [ chromeNameToHash, chromeNameToMozName ]; +} + +// We can only import pinsets from chrome if for every name in the pinset: +// - We have a hash from Chrome's static certificate file +// - We have a builtin cert +// If the pinset meets these requirements, we store a map array of pinset +// objects: +// { +// pinset_name : { +// // Array of names with entries in certNameToSKD +// sha256_hashes: [] +// } +// } +// and an array of imported pinset entries: +// { name: string, include_subdomains: boolean, test_mode: boolean, +// pins: pinset_name } +function downloadAndParseChromePins(filename, + chromeNameToHash, + chromeNameToMozName, + certNameToSKD, + certSKDToName) { + let chromePreloads = downloadAsJson(filename); + let chromePins = chromePreloads.pinsets; + let chromeImportedPinsets = {}; + let chromeImportedEntries = []; + + chromePins.forEach(function(pin) { + let valid = true; + let pinset = { name: pin.name, sha256_hashes: [] }; + // Translate the Chrome pinset format to ours + pin.static_spki_hashes.forEach(function(name) { + if (name in chromeNameToHash) { + let hash = chromeNameToHash[name]; + pinset.sha256_hashes.push(certSKDToName[hash]); + + // We should have already added hashes for all of these when we + // imported the certificate file. + if (!certNameToSKD[name]) { + throw new Error("ERROR: No hash for name: " + name); + } + } else if (name in chromeNameToMozName) { + pinset.sha256_hashes.push(chromeNameToMozName[name]); + } else { + dump("Skipping Chrome pinset " + pinset.name + ", couldn't find " + + "builtin " + name + " from cert file\n"); + valid = false; + } + }); + if (valid) { + chromeImportedPinsets[pinset.name] = pinset; + } + }); + + // Grab the domain entry lists. Chrome's entry format is similar to + // ours, except theirs includes a HSTS mode. + const cData = gStaticPins.chromium_data; + let entries = chromePreloads.entries; + entries.forEach(function(entry) { + // HSTS entry only + if (!entry.pins) { + return; + } + let pinsetName = cData.substitute_pinsets[entry.pins]; + if (!pinsetName) { + pinsetName = entry.pins; + } + + // We trim the entry name here to avoid breaking hostname comparisons in the + // HPKP implementation. + entry.name = entry.name.trim(); + + let isProductionDomain = + (cData.production_domains.includes(entry.name)); + let isProductionPinset = + (cData.production_pinsets.includes(pinsetName)); + let excludeDomain = + (cData.exclude_domains.includes(entry.name)); + let isTestMode = !isProductionPinset && !isProductionDomain; + if (entry.pins && !excludeDomain && chromeImportedPinsets[entry.pins]) { + chromeImportedEntries.push({ + name: entry.name, + include_subdomains: entry.include_subdomains, + test_mode: isTestMode, + is_moz: false, + pins: pinsetName }); + } + }); + return [ chromeImportedPinsets, chromeImportedEntries ]; +} + +// Returns a pair of maps [certNameToSKD, certSKDToName] between cert +// nicknames and digests of the SPKInfo for the mozilla trust store +function loadNSSCertinfo(extraCertificates) { + let allCerts = gCertDB.getCerts(); + let enumerator = allCerts.getEnumerator(); + let certNameToSKD = {}; + let certSKDToName = {}; + while (enumerator.hasMoreElements()) { + let cert = enumerator.getNext().QueryInterface(Ci.nsIX509Cert); + if (!cert.isBuiltInRoot) { + continue; + } + let name = cert.displayName; + let SKD = cert.sha256SubjectPublicKeyInfoDigest; + certNameToSKD[name] = SKD; + certSKDToName[SKD] = name; + } + + for (let cert of extraCertificates) { + let name = cert.commonName; + let SKD = cert.sha256SubjectPublicKeyInfoDigest; + certNameToSKD[name] = SKD; + certSKDToName[SKD] = name; + } + + { + // This is the pinning test certificate. The key hash identifies the + // default RSA key from pykey. + let name = "End Entity Test Cert"; + let SKD = "VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8="; + certNameToSKD[name] = SKD; + certSKDToName[SKD] = name; + } + return [certNameToSKD, certSKDToName]; +} + +function parseJson(filename) { + let json = stripComments(readFileToString(filename)); + return JSON.parse(json); +} + +function nameToAlias(certName) { + // change the name to a string valid as a c identifier + // remove non-ascii characters + certName = certName.replace(/[^[:ascii:]]/g, "_"); + // replace non word characters + certName = certName.replace(/[^A-Za-z0-9]/g, "_"); + + return "k" + certName + "Fingerprint"; +} + +function compareByName(a, b) { + return a.name.localeCompare(b.name); +} + +function genExpirationTime() { + let now = new Date(); + let nowMillis = now.getTime(); + let expirationMillis = nowMillis + (PINNING_MINIMUM_REQUIRED_MAX_AGE * 1000); + let expirationMicros = expirationMillis * 1000; + return "static const PRTime kPreloadPKPinsExpirationTime = INT64_C(" + + expirationMicros + ");\n"; +} + +function writeFullPinset(certNameToSKD, certSKDToName, pinset) { + if (!pinset.sha256_hashes || pinset.sha256_hashes.length == 0) { + throw new Error(`ERROR: Pinset ${pinset.name} does not contain any hashes`); + } + writeFingerprints(certNameToSKD, certSKDToName, pinset.name, + pinset.sha256_hashes); +} + +function writeFingerprints(certNameToSKD, certSKDToName, name, hashes) { + let varPrefix = "kPinset_" + name; + writeString("static const char* const " + varPrefix + "_Data[] = {\n"); + let SKDList = []; + for (let certName of hashes) { + if (!(certName in certNameToSKD)) { + throw new Error(`ERROR: Can't find '${certName}' in certNameToSKD`); + } + SKDList.push(certNameToSKD[certName]); + } + for (let skd of SKDList.sort()) { + writeString(" " + nameToAlias(certSKDToName[skd]) + ",\n"); + } + if (hashes.length == 0) { + // ANSI C requires that an initialiser list be non-empty. + writeString(" 0\n"); + } + writeString("};\n"); + writeString("static const StaticFingerprints " + varPrefix + " = {\n " + + "sizeof(" + varPrefix + "_Data) / sizeof(const char*),\n " + varPrefix + + "_Data\n};\n\n"); +} + +function writeEntry(entry) { + let printVal = ` { "${entry.name}", `; + if (entry.include_subdomains) { + printVal += "true, "; + } else { + printVal += "false, "; + } + // Default to test mode if not specified. + let testMode = true; + if (entry.hasOwnProperty("test_mode")) { + testMode = entry.test_mode; + } + if (testMode) { + printVal += "true, "; + } else { + printVal += "false, "; + } + if (entry.is_moz || (entry.pins.includes("mozilla") && + entry.pins != "mozilla_test")) { + printVal += "true, "; + } else { + printVal += "false, "; + } + if ("id" in entry) { + if (entry.id >= 256) { + throw new Error("ERROR: Not enough buckets in histogram"); + } + if (entry.id >= 0) { + printVal += entry.id + ", "; + } + } else { + printVal += "-1, "; + } + printVal += "&kPinset_" + entry.pins; + printVal += " },\n"; + writeString(printVal); +} + +function writeDomainList(chromeImportedEntries) { + writeString("/* Sort hostnames for binary search. */\n"); + writeString("static const TransportSecurityPreload " + + "kPublicKeyPinningPreloadList[] = {\n"); + let count = 0; + let mozillaDomains = {}; + gStaticPins.entries.forEach(function(entry) { + mozillaDomains[entry.name] = true; + }); + // For any domain for which we have set pins, exclude them from + // chromeImportedEntries. + for (let i = chromeImportedEntries.length - 1; i >= 0; i--) { + if (mozillaDomains[chromeImportedEntries[i].name]) { + dump("Skipping duplicate pinset for domain " + + JSON.stringify(chromeImportedEntries[i], undefined, 2) + "\n"); + chromeImportedEntries.splice(i, 1); + } + } + let sortedEntries = gStaticPins.entries; + sortedEntries.push.apply(sortedEntries, chromeImportedEntries); + for (let entry of sortedEntries.sort(compareByName)) { + count++; + writeEntry(entry); + } + writeString("};\n"); + + writeString("\n// Pinning Preload List Length = " + count + ";\n"); + writeString("\nstatic const int32_t kUnknownId = -1;\n"); +} + +function writeFile(certNameToSKD, certSKDToName, + chromeImportedPinsets, chromeImportedEntries) { + // Compute used pins from both Chrome's and our pinsets, so we can output + // them later. + let usedFingerprints = {}; + let mozillaPins = {}; + gStaticPins.pinsets.forEach(function(pinset) { + mozillaPins[pinset.name] = true; + pinset.sha256_hashes.forEach(function (name) { + usedFingerprints[name] = true; + }); + }); + for (let key in chromeImportedPinsets) { + let pinset = chromeImportedPinsets[key]; + pinset.sha256_hashes.forEach(function(name) { + usedFingerprints[name] = true; + }); + } + + writeString(FILE_HEADER); + + // Write actual fingerprints. + Object.keys(usedFingerprints).sort().forEach(function(certName) { + if (certName) { + writeString("/* " + certName + " */\n"); + writeString("static const char " + nameToAlias(certName) + "[] =\n"); + writeString(" \"" + certNameToSKD[certName] + "\";\n"); + writeString("\n"); + } + }); + + // Write the pinsets + writeString(PINSETDEF); + writeString("/* PreloadedHPKPins.json pinsets */\n"); + gStaticPins.pinsets.sort(compareByName).forEach(function(pinset) { + writeFullPinset(certNameToSKD, certSKDToName, pinset); + }); + writeString("/* Chrome static pinsets */\n"); + for (let key in chromeImportedPinsets) { + if (mozillaPins[key]) { + dump("Skipping duplicate pinset " + key + "\n"); + } else { + dump("Writing pinset " + key + "\n"); + writeFullPinset(certNameToSKD, certSKDToName, chromeImportedPinsets[key]); + } + } + + // Write the domainlist entries. + writeString(DOMAINHEADER); + writeDomainList(chromeImportedEntries); + writeString("\n"); + writeString(genExpirationTime()); +} + +function loadExtraCertificates(certStringList) { + let constructedCerts = []; + for (let certString of certStringList) { + constructedCerts.push(gCertDB.constructX509FromBase64(certString)); + } + return constructedCerts; +} + +var extraCertificates = loadExtraCertificates(gStaticPins.extra_certificates); +var [ certNameToSKD, certSKDToName ] = loadNSSCertinfo(extraCertificates); +var [ chromeNameToHash, chromeNameToMozName ] = downloadAndParseChromeCerts( + gStaticPins.chromium_data.cert_file_url, certNameToSKD, certSKDToName); +var [ chromeImportedPinsets, chromeImportedEntries ] = + downloadAndParseChromePins(gStaticPins.chromium_data.json_file_url, + chromeNameToHash, chromeNameToMozName, certNameToSKD, certSKDToName); + +writeFile(certNameToSKD, certSKDToName, chromeImportedPinsets, + chromeImportedEntries); + +FileUtils.closeSafeFileOutputStream(gFileOutputStream); diff --git a/taskcluster/docker/periodic_updates/scripts/getHSTSPreloadList.js b/taskcluster/docker/periodic_updates/scripts/getHSTSPreloadList.js new file mode 100644 index 000000000000..c31aad784def --- /dev/null +++ b/taskcluster/docker/periodic_updates/scripts/getHSTSPreloadList.js @@ -0,0 +1,478 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// How to run this file: +// 1. [obtain firefox source code] +// 2. [build/obtain firefox binaries] +// 3. run `[path to]/run-mozilla.sh [path to]/xpcshell [path to]/getHSTSPreloadlist.js [absolute path to]/nsSTSPreloadlist.inc' +// Note: Running this file outputs a new nsSTSPreloadlist.inc in the current +// working directory. + +/* +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; +*/ +var gSSService = Cc["@mozilla.org/ssservice;1"].getService(Ci.nsISiteSecurityService); + +ChromeUtils.import("resource://gre/modules/Services.jsm"); +ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +Cu.importGlobalProperties(["XMLHttpRequest"]); + +const SOURCE = "https://chromium.googlesource.com/chromium/src/net/+/master/http/transport_security_state_static.json?format=TEXT"; +const OUTPUT = "nsSTSPreloadList.inc"; +const ERROR_OUTPUT = "nsSTSPreloadList.errors"; +const MINIMUM_REQUIRED_MAX_AGE = 60 * 60 * 24 * 7 * 18; +const MAX_CONCURRENT_REQUESTS = 500; +const MAX_RETRIES = 1; +const REQUEST_TIMEOUT = 30 * 1000; +const ERROR_NONE = "no error"; +const ERROR_CONNECTING_TO_HOST = "could not connect to host"; +const ERROR_NO_HSTS_HEADER = "did not receive HSTS header"; +const ERROR_MAX_AGE_TOO_LOW = "max-age too low: "; +const HEADER = `/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/*****************************************************************************/ +/* This is an automatically generated file. If you're not */ +/* nsSiteSecurityService.cpp, you shouldn't be #including it. */ +/*****************************************************************************/ + +#include +`; + +const GPERF_DELIM = "%%\n"; + +function download() { + let req = new XMLHttpRequest(); + req.open("GET", SOURCE, false); // doing the request synchronously + try { + req.send(); + } catch (e) { + throw new Error(`ERROR: problem downloading '${SOURCE}': ${e}`); + } + + if (req.status != 200) { + throw new Error("ERROR: problem downloading '" + SOURCE + "': status " + req.status); + } + + let resultDecoded; + try { + resultDecoded = atob(req.responseText); + } catch (e) { + throw new Error("ERROR: could not decode data as base64 from '" + SOURCE + "': " + e); + } + + // we have to filter out '//' comments, while not mangling the json + let result = resultDecoded.replace(/^(\s*)?\/\/[^\n]*\n/mg, ""); + let data = null; + try { + data = JSON.parse(result); + } catch (e) { + throw new Error(`ERROR: could not parse data from '${SOURCE}': ${e}`); + } + return data; +} + +function getHosts(rawdata) { + let hosts = []; + + if (!rawdata || !rawdata.entries) { + throw new Error("ERROR: source data not formatted correctly: 'entries' not found"); + } + + for (let entry of rawdata.entries) { + if (entry.mode && entry.mode == "force-https") { + if (entry.name) { + // We trim the entry name here to avoid malformed URI exceptions when we + // later try to connect to the domain. + entry.name = entry.name.trim(); + entry.retries = MAX_RETRIES; + // We prefer the camelCase variable to the JSON's snake case version + entry.includeSubdomains = entry.include_subdomains; + hosts.push(entry); + } else { + throw new Error("ERROR: entry not formatted correctly: no name found"); + } + } + } + + return hosts; +} + +function processStsHeader(host, header, status, securityInfo) { + let maxAge = { + value: 0 + }; + let includeSubdomains = { + value: false + }; + let error = ERROR_NONE; + if (header != null && securityInfo != null) { + try { + let uri = Services.io.newURI("https://" + host.name); + let sslStatus = securityInfo.QueryInterface(Ci.nsISSLStatusProvider).SSLStatus; + gSSService.processHeader(Ci.nsISiteSecurityService.HEADER_HSTS, uri, header, sslStatus, 0, Ci.nsISiteSecurityService.SOURCE_PRELOAD_LIST, {}, maxAge, includeSubdomains); + } catch (e) { + dump("ERROR: could not process header '" + header + "' from " + host.name + ": " + e + "\n"); + error = e; + } + } else if (status == 0) { + error = ERROR_CONNECTING_TO_HOST; + } else { + error = ERROR_NO_HSTS_HEADER; + } + + if (error == ERROR_NONE && maxAge.value < MINIMUM_REQUIRED_MAX_AGE) { + error = ERROR_MAX_AGE_TOO_LOW; + } + + return { + name: host.name, + maxAge: maxAge.value, + includeSubdomains: includeSubdomains.value, + error, + retries: host.retries - 1, + forceInclude: host.forceInclude + }; +} + +// RedirectAndAuthStopper prevents redirects and HTTP authentication +function RedirectAndAuthStopper() {} + +RedirectAndAuthStopper.prototype = { + // nsIChannelEventSink + asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { + throw new Error(Cr.NS_ERROR_ENTITY_CHANGED); + }, + + // nsIAuthPrompt2 + promptAuth(channel, level, authInfo) { + return false; + }, + + asyncPromptAuth(channel, callback, context, level, authInfo) { + throw new Error(Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + + getInterface(iid) { + return this.QueryInterface(iid); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink, Ci.nsIAuthPrompt2]) +}; + +function fetchstatus(host) { + let xhr = new XMLHttpRequest(); + let uri = "https://" + host.name + "/"; + + xhr.open("head", uri, true); + xhr.setRequestHeader("X-Automated-Tool", "https://hg.mozilla.org/mozilla-central/file/tip/security/manager/tools/getHSTSPreloadList.js"); + xhr.timeout = REQUEST_TIMEOUT; + + try { + xhr.channel.notificationCallbacks = new RedirectAndAuthStopper(); + xhr.send(); + } catch (e) { + dump("ERROR: exception making request to " + host.name + ": " + e + "\n"); + return processStsHeader(host, null, xhr.status, xhr.channel.securityInfo); + } + + let header = xhr.getResponseHeader("strict-transport-security"); + return processStsHeader(host, header, xhr.status, xhr.channel.securityInfo); +} + +async function getHSTSStatus(host) { + return new Promise((resolve, reject) => { + do { + host = fetchstatus(host); + } while (shouldRetry(host)); + resolve(host); + }); +} + +function compareHSTSStatus(a, b) { + if (a.name > b.name) { + return 1; + } + if (a.name < b.name) { + return -1; + } + return 0; +} + +function writeTo(string, fos) { + fos.write(string, string.length); +} + +// Determines and returns a string representing a declaration of when this +// preload list should no longer be used. +// This is the current time plus MINIMUM_REQUIRED_MAX_AGE. +function getExpirationTimeString() { + let now = new Date(); + let nowMillis = now.getTime(); + // MINIMUM_REQUIRED_MAX_AGE is in seconds, so convert to milliseconds + let expirationMillis = nowMillis + (MINIMUM_REQUIRED_MAX_AGE * 1000); + let expirationMicros = expirationMillis * 1000; + return "const PRTime gPreloadListExpirationTime = INT64_C(" + expirationMicros + ");\n"; +} + +function shouldRetry(response) { + return (response.error != ERROR_NO_HSTS_HEADER && response.error != ERROR_MAX_AGE_TOO_LOW && response.error != ERROR_NONE && response.retries > 0); +} + + +// Copied from browser/components/migration/MigrationUtils.jsm +function spinResolve(promise) { + if (!(promise instanceof Promise)) { + return promise; + } + let done = false; + let result = null; + let error = null; + promise.catch(e => { + error = e; + }).then(r => { + result = r; + done = true; + }); + + Services.tm.spinEventLoopUntil(() => done); + if (error) { + throw error; + } else { + return result; + } +} + +async function probeHSTSStatuses(inHosts) { + let promises = []; + + dump("Examining " + inHosts.length + " hosts.\n"); + + // Debug/testing on a small number of hosts + // while (inHosts.length > 40000) { + + while (inHosts.length > 0) { + let host = inHosts.shift(); + promises.push(getHSTSStatus(host)); + } + + dump("Waiting for " + promises.length + " responses.\n"); + + let result = await Promise.all(promises); + dump("HSTS Probe received " + result.length + " statuses.\n"); + return result; +} + +function readCurrentList(filename) { + var currentHosts = {}; + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(filename); + var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsILineInputStream); + fis.init(file, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF); + var line = {}; + + // While we generate entries matching the latest version format, + // we still need to be able to read entries in the previous version formats + // for bootstrapping a latest version preload list from a previous version + // preload list. Hence these regexes. + const entryRegexes = [ + /([^,]+), (0|1)/, // v3 + / {2}\/\* "([^"]*)", (true|false) \*\//, // v2 + / {2}{ "([^"]*)", (true|false) },/, // v1 + ]; + + while (fis.readLine(line)) { + let match; + entryRegexes.find((r) => { + match = r.exec(line.value); + return match; + }); + if (match) { + currentHosts[match[1]] = (match[2] == "1" || match[2] == "true"); + } + } + return currentHosts; +} + +function combineLists(newHosts, currentHosts) { + for (let currentHost in currentHosts) { + let found = false; + for (let newHost of newHosts) { + if (newHost.name == currentHost) { + found = true; + break; + } + } + if (!found) { + newHosts.push({name: currentHost, retries: MAX_RETRIES}); + } + } +} + +const TEST_ENTRIES = [ + { + name: "includesubdomains.preloaded.test", + includeSubdomains: true + }, { + name: "includesubdomains2.preloaded.test", + includeSubdomains: true + }, { + name: "noincludesubdomains.preloaded.test", + includeSubdomains: false + } +]; + +function deleteTestHosts(currentHosts) { + for (let testEntry of TEST_ENTRIES) { + delete currentHosts[testEntry.name]; + } +} + +function getTestHosts() { + let hosts = []; + for (let testEntry of TEST_ENTRIES) { + hosts.push({ + name: testEntry.name, maxAge: MINIMUM_REQUIRED_MAX_AGE, includeSubdomains: testEntry.includeSubdomains, error: ERROR_NONE, + // This deliberately doesn't have a value for `retries` (because we should + // never attempt to connect to this host). + forceInclude: true + }); + } + return hosts; +} + +async function insertHosts(inoutHostList, inAddedHosts) { + for (let host of inAddedHosts) { + inoutHostList.push(host); + } +} + +function filterForcedInclusions(inHosts, outNotForced, outForced) { + // Apply our filters (based on policy today) to determine which entries + // will be included without being checked (forced); the others will be + // checked using active probing. + for (let host of inHosts) { + if (host.policy == "google" || host.policy == "public-suffix" || host.policy == "public-suffix-requested") { + host.forceInclude = true; + host.error = ERROR_NONE; + outForced.push(host); + } else { + outNotForced.push(host); + } + } +} + +function output(statuses) { + dump("INFO: Writing output to " + OUTPUT + "\n"); + try { + ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); + + let file = FileUtils.getFile("CurWorkD", [OUTPUT]); + let fos = FileUtils.openSafeFileOutputStream(file); + writeTo(HEADER, fos); + writeTo(getExpirationTimeString(), fos); + + writeTo(GPERF_DELIM, fos); + + for (let status of statuses) { + let includeSubdomains = ( + status.includeSubdomains + ? 1 + : 0); + writeTo(status.name + ", " + includeSubdomains + "\n", fos); + } + + writeTo(GPERF_DELIM, fos); + FileUtils.closeSafeFileOutputStream(fos); + dump("finished writing output file\n"); + } catch (e) { + dump("ERROR: problem writing output to '" + OUTPUT + "': " + e + "\n"); + } +} + +function errorToString(status) { + return ( + status.error == ERROR_MAX_AGE_TOO_LOW + ? status.error + status.maxAge + : status.error); +} + +async function main(args) { + if (args.length != 1) { + throw new Error("Usage: getHSTSPreloadList.js "); + } + + // get the current preload list + let currentHosts = readCurrentList(args[0]); + // delete any hosts we use in tests so we don't actually connect to them + deleteTestHosts(currentHosts); + // disable the current preload list so it won't interfere with requests we make + Services.prefs.setBoolPref("network.stricttransportsecurity.preloadlist", false); + // download and parse the raw json file from the Chromium source + let rawdata = download(); + // get just the hosts with mode: "force-https" + let hosts = getHosts(rawdata); + // add hosts in the current list to the new list (avoiding duplicates) + combineLists(hosts, currentHosts); + + // Don't contact hosts that are forced to be included anyway + let hostsToContact = []; + let forcedHosts = []; + filterForcedInclusions(hosts, hostsToContact, forcedHosts); + + // Initialize the final status list + let hstsStatuses = []; + // Add the hosts we use in tests + dump("Adding test hosts\n"); + insertHosts(hstsStatuses, getTestHosts()); + // Add in the hosts that are forced + dump("Adding forced hosts\n"); + insertHosts(hstsStatuses, forcedHosts); + + let total = await probeHSTSStatuses(hostsToContact).then(function(probedStatuses) { + return hstsStatuses.concat(probedStatuses); + }).then(function(statuses) { + return statuses.sort(compareHSTSStatus); + }).then(function(statuses) { + for (let status of statuses) { + // If we've encountered an error for this entry (other than the site not + // sending an HSTS header), be safe and don't remove it from the list + // (given that it was already on the list). + if (!status.forceInclude && status.error != ERROR_NONE && status.error != ERROR_NO_HSTS_HEADER && status.error != ERROR_MAX_AGE_TOO_LOW && status.name in currentHosts) { + // dump("INFO: error connecting to or processing " + status.name + " - using previous status on list\n"); + status.maxAge = MINIMUM_REQUIRED_MAX_AGE; + status.includeSubdomains = currentHosts[status.name]; + } + } + return statuses; + }).then(function(statuses) { + // Filter out entries we aren't including. + var includedStatuses = statuses.filter(function(status) { + if (status.maxAge < MINIMUM_REQUIRED_MAX_AGE && !status.forceInclude) { + // dump("INFO: " + status.name + " NOT ON the preload list\n"); + return false; + } + + // dump("INFO: " + status.name + " ON the preload list (includeSubdomains: " + status.includeSubdomains + ")\n"); + if (status.forceInclude && status.error != ERROR_NONE) { + dump(status.name + ": " + errorToString(status) + " (error ignored - included regardless)\n"); + } + return true; + }); + return includedStatuses; + }); + + // Write the output file + output(total); + + dump("HSTS probing all done\n"); +} + +// arguments is a global within xpcshell +spinResolve(main(arguments)); diff --git a/taskcluster/docker/periodic_updates/scripts/periodic_file_updates.sh b/taskcluster/docker/periodic_updates/scripts/periodic_file_updates.sh new file mode 100755 index 000000000000..28940f56cb0e --- /dev/null +++ b/taskcluster/docker/periodic_updates/scripts/periodic_file_updates.sh @@ -0,0 +1,575 @@ +#!/bin/bash + +set -ex + +function usage { + cat <&2 + exit 21 + fi + rm -f ${VERSION_FILE} + echo "${PARSED_VERSION}" +} + +# Cleanup common artifacts. +function preflight_cleanup { + cd "${BASEDIR}" + rm -rf "${PRODUCT}" tests "${BROWSER_ARCHIVE}" "${TESTS_ARCHIVE}" +} + +function download_shared_artifacts_from_ftp { + cd "${BASEDIR}" + + # Download everything we need to run js with xpcshell + echo "INFO: Downloading all the necessary pieces from ${STAGEHOST}..." + ARTIFACT_DIR="nightly/latest-${REPODIR}" + if [ "${USE_MC}" == "true" ]; then + ARTIFACT_DIR="nightly/latest-mozilla-central" + fi + + BROWSER_ARCHIVE_URL="https://${STAGEHOST}/pub/mozilla.org/${PRODUCT}/${ARTIFACT_DIR}/${BROWSER_ARCHIVE}" + TESTS_ARCHIVE_URL="https://${STAGEHOST}/pub/mozilla.org/${PRODUCT}/${ARTIFACT_DIR}/${TESTS_ARCHIVE}" + + echo "INFO: ${WGET} ${BROWSER_ARCHIVE_URL}" + ${WGET} "${BROWSER_ARCHIVE_URL}" + echo "INFO: ${WGET} ${TESTS_ARCHIVE_URL}" + ${WGET} "${TESTS_ARCHIVE_URL}" +} + +function download_shared_artifacts_from_tc { + cd "${BASEDIR}" + TASKID_FILE="taskId.json" + + # Download everything we need to run js with xpcshell + echo "INFO: Downloading all the necessary pieces from the taskcluster index..." + TASKID_URL="https://index.taskcluster.net/v1/task/gecko.v2.${REPODIR}.latest.${PRODUCT}.linux64-opt" + if [ "${USE_MC}" == "true" ]; then + TASKID_URL="https://index.taskcluster.net/v1/task/gecko.v2.mozilla-central.latest.${PRODUCT}.linux64-opt" + fi + ${WGET} -O ${TASKID_FILE} ${TASKID_URL} + INDEX_TASK_ID="$($JQ -r '.taskId' ${TASKID_FILE})" + if [ -z "${INDEX_TASK_ID}" ]; then + echo "Failed to look up taskId at ${TASKID_URL}" + exit 22 + else + echo "INFO: Got taskId of $INDEX_TASK_ID" + fi + + TASKSTATUS_FILE="taskstatus.json" + STATUS_URL="https://queue.taskcluster.net/v1/task/${INDEX_TASK_ID}/status" + ${WGET} -O "${TASKSTATUS_FILE}" "${STATUS_URL}" + LAST_RUN_INDEX=$(($(jq '.status.runs | length' ${TASKSTATUS_FILE}) - 1)) + echo "INFO: Examining run number ${LAST_RUN_INDEX}" + + BROWSER_ARCHIVE_URL="https://queue.taskcluster.net/v1/task/${INDEX_TASK_ID}/runs/${LAST_RUN_INDEX}/artifacts/public/build/${BROWSER_ARCHIVE}" + echo "INFO: ${WGET} ${BROWSER_ARCHIVE_URL}" + ${WGET} "${BROWSER_ARCHIVE_URL}" + + TESTS_ARCHIVE_URL="https://queue.taskcluster.net/v1/task/${INDEX_TASK_ID}/runs/${LAST_RUN_INDEX}/artifacts/public/build/${TESTS_ARCHIVE}" + echo "INFO: ${WGET} ${TESTS_ARCHIVE_URL}" + ${WGET} "${TESTS_ARCHIVE_URL}" +} + +function unpack_artifacts { + cd "${BASEDIR}" + if [ ! -f "${BROWSER_ARCHIVE}" ]; then + echo "Downloaded file '${BROWSER_ARCHIVE}' not found in directory '$(pwd)'." >&2 + exit 31 + fi + if [ ! -f "${TESTS_ARCHIVE}" ]; then + echo "Downloaded file '${TESTS_ARCHIVE}' not found in directory '$(pwd)'." >&2 + exit 32 + fi + # Unpack the browser and move xpcshell in place for updating the preload list. + echo "INFO: Unpacking resources..." + ${UNPACK_CMD} "${BROWSER_ARCHIVE}" + mkdir -p tests + cd tests + ${UNZIP} "../${TESTS_ARCHIVE}" + cd "${BASEDIR}" + cp tests/bin/xpcshell "${PRODUCT}" +} + +# Downloads the current in-tree HSTS (HTTP Strict Transport Security) files. +# Runs a simple xpcshell script to generate up-to-date HSTS information. +# Compares the new HSTS output with the old to determine whether we need to update. +function compare_hsts_files { + cd "${BASEDIR}" + + HSTS_PRELOAD_INC_HG="${HGREPO}/raw-file/default/security/manager/ssl/$(basename "${HSTS_PRELOAD_INC}")" + + echo "INFO: Downloading existing include file..." + rm -rf "${HSTS_PRELOAD_ERRORS}" "${HSTS_PRELOAD_INC}" + echo "INFO: ${WGET} ${HSTS_PRELOAD_INC_HG}" + ${WGET} -O "${HSTS_PRELOAD_INC}" "${HSTS_PRELOAD_INC_HG}" + + if [ ! -f "${HSTS_PRELOAD_INC}" ]; then + echo "Downloaded file '${HSTS_PRELOAD_INC}' not found in directory '$(pwd)' - this should have been downloaded above from ${HSTS_PRELOAD_INC_HG}." >&2 + exit 41 + fi + + # Run the script to get an updated preload list. + echo "INFO: Generating new HSTS preload list..." + cd "${BASEDIR}/${PRODUCT}" + LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:. ./xpcshell "${HSTS_PRELOAD_SCRIPT}" "${HSTS_PRELOAD_INC}" + + # The created files should be non-empty. + echo "INFO: Checking whether new HSTS preload list is valid..." + if [ ! -s "${HSTS_PRELOAD_INC}" ]; then + echo "New HSTS preload list ${HSTS_PRELOAD_INC} is empty. That's less good." >&2 + exit 42 + fi + cd "${BASEDIR}" + + # Check for differences + echo "INFO: diffing old/new HSTS preload lists into ${HSTS_DIFF_ARTIFACT}" + ${DIFF} "${BASEDIR}/${PRODUCT}/$(basename "${HSTS_PRELOAD_INC}")" "${HSTS_PRELOAD_INC}" | tee "${HSTS_DIFF_ARTIFACT}" + if [ -s "${HSTS_DIFF_ARTIFACT}" ] + then + return 0 + fi + return 1 +} + +# Downloads the current in-tree HPKP (HTTP public key pinning) files. +# Runs a simple xpcshell script to generate up-to-date HPKP information. +# Compares the new HPKP output with the old to determine whether we need to update. +function compare_hpkp_files { + cd "${BASEDIR}" + HPKP_PRELOAD_JSON_HG="${HGREPO}/raw-file/default/security/manager/tools/$(basename "${HPKP_PRELOAD_JSON}")" + + HPKP_PRELOAD_OUTPUT_HG="${HGREPO}/raw-file/default/security/manager/ssl/${HPKP_PRELOAD_INC}" + + rm -f "${HPKP_PRELOAD_OUTPUT}" + ${WGET} -O "${HPKP_PRELOAD_INPUT}" "${HPKP_PRELOAD_OUTPUT_HG}" + ${WGET} -O "${HPKP_PRELOAD_JSON}" "${HPKP_PRELOAD_JSON_HG}" + + # Run the script to get an updated preload list. + echo "INFO: Generating new HPKP preload list..." + cd "${BASEDIR}/${PRODUCT}" + LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:. ./xpcshell "${HPKP_PRELOAD_SCRIPT}" "${HPKP_PRELOAD_JSON}" "${HPKP_PRELOAD_OUTPUT}" > "${HPKP_PRELOAD_ERRORS}" + + # The created files should be non-empty. + echo "INFO: Checking whether new HPKP preload list is valid..." + + if [ ! -s "${HPKP_PRELOAD_OUTPUT}" ]; then + echo "${HPKP_PRELOAD_OUTPUT} is empty. That's less good." >&2 + exit 52 + fi + cd "${BASEDIR}" + + echo "INFO: diffing old/new HPKP preload lists..." + ${DIFF} "${HPKP_PRELOAD_INPUT}" "${HPKP_PRELOAD_OUTPUT}" | tee "${HPKP_DIFF_ARTIFACT}" + if [ -s "${HPKP_DIFF_ARTIFACT}" ] + then + return 0 + fi + return 1 +} + +function is_valid_xml { + xmlfile=$1 + XMLLINT=$(which xmllint 2>/dev/null | head -n1) + + if [ ! -x "${XMLLINT}" ]; then + echo "ERROR: xmllint not found in PATH" + exit 60 + fi + ${XMLLINT} --nonet --noout "${xmlfile}" +} + +# Downloads the current in-tree blocklist file. +# Downloads the current blocklist file from AMO. +# Compares the AMO blocklist with the in-tree blocklist to determine whether we need to update. +function compare_blocklist_files { + BLOCKLIST_URL_AMO="https://blocklist.addons.mozilla.org/blocklist/3/${APP_ID}/${VERSION}/${APP_NAME}/20090105024647/blocklist-sync/en-US/nightly/blocklist-sync/default/default/" + BLOCKLIST_URL_HG="${HGREPO}/raw-file/default/${APP_DIR}/app/blocklist.xml" + + cd "${BASEDIR}" + rm -f ${BLOCKLIST_LOCAL_AMO} + echo "INFO: ${WGET} -O ${BLOCKLIST_LOCAL_AMO} ${BLOCKLIST_URL_AMO}" + ${WGET} -O "${BLOCKLIST_LOCAL_AMO}" "${BLOCKLIST_URL_AMO}" + + rm -f ${BLOCKLIST_LOCAL_HG} + echo "INFO: ${WGET} -O ${BLOCKLIST_LOCAL_HG} ${BLOCKLIST_URL_HG}" + ${WGET} -O "${BLOCKLIST_LOCAL_HG}" "${BLOCKLIST_URL_HG}" + + # The downloaded files should be non-empty and have a valid xml header + # if they were retrieved properly, and some random HTML garbage if not. + # set -x catches these + is_valid_xml ${BLOCKLIST_LOCAL_AMO} + is_valid_xml ${BLOCKLIST_LOCAL_HG} + + echo "INFO: diffing in-tree blocklist against the blocklist from AMO..." + ${DIFF} ${BLOCKLIST_LOCAL_HG} ${BLOCKLIST_LOCAL_AMO} | tee "${BLOCKLIST_DIFF_ARTIFACT}" + if [ -s "${BLOCKLIST_DIFF_ARTIFACT}" ] + then + return 0 + fi + return 1 +} + +function clone_build_tools { + rm -fr "${TOOLSDIR}" + CLONE_CMD="${HG} clone https://hg.mozilla.org/build/tools ${TOOLSDIR}" + ${CLONE_CMD} +} + +# Clones an hg repo, using hgtool preferentially. +function clone_repo { + cd "${BASEDIR}" + if [ ! -d "${REPODIR}" ]; then + CLONE_CMD="" + if [ -f "${HGTOOL}" ]; then + # Need to pass the default branch here to avoid pollution from buildprops.json + # when hgtool.py is run in production. + CLONE_CMD="${HGTOOL} --branch default" + else + echo "INFO: hgtool.py not found. Falling back to vanilla hg." + CLONE_CMD="${HG} clone" + fi + CLONE_CMD="${CLONE_CMD} ${HGREPO} ${REPODIR}" + ${CLONE_CMD} + fi + + ${HG} -R ${REPODIR} pull + ${HG} -R ${REPODIR} update -C default +} + +# Copies new HSTS files in place, and commits them. +function commit_hsts_files { + cd "${BASEDIR}" + + cp -f "${BASEDIR}/${PRODUCT}/$(basename "${HSTS_PRELOAD_INC}")" "${REPODIR}/security/manager/ssl/" + + COMMIT_MESSAGE="No bug, Automated HSTS preload list update" + if [ -n "${TASK_ID}" ]; then + COMMIT_MESSAGE="${COMMIT_MESSAGE} from task ${TASK_ID}" + fi + if [ ${DONTBUILD} == true ]; then + COMMIT_MESSAGE="${COMMIT_MESSAGE} - (DONTBUILD)" + fi + if [ ${CLOSED_TREE} == true ]; then + COMMIT_MESSAGE="${COMMIT_MESSAGE} - CLOSED TREE" + fi + if [ ${APPROVAL} == true ]; then + COMMIT_MESSAGE="${COMMIT_MESSAGE} - a=hsts-update" + fi + echo "INFO: committing HSTS changes" + ${HG} -R ${REPODIR} commit -u "${HG_SSH_USER}" -m "${COMMIT_MESSAGE}" +} + +# Copies new HPKP files in place, and commits them. +function commit_hpkp_files { + cd "${BASEDIR}" + + cp -f "${HPKP_PRELOAD_OUTPUT}" "${REPODIR}/security/manager/ssl/${HPKP_PRELOAD_INC}" + + COMMIT_MESSAGE="No bug, Automated HPKP preload list update" + if [ -n "${TASK_ID}" ]; then + COMMIT_MESSAGE="${COMMIT_MESSAGE} from task ${TASK_ID}" + fi + if [ ${DONTBUILD} == true ]; then + COMMIT_MESSAGE="${COMMIT_MESSAGE} - (DONTBUILD)" + fi + if [ ${CLOSED_TREE} == true ]; then + COMMIT_MESSAGE="${COMMIT_MESSAGE} - CLOSED TREE" + fi + if [ ${APPROVAL} == true ]; then + COMMIT_MESSAGE="${COMMIT_MESSAGE} - a=hpkp-update" + fi + echo "INFO: committing HPKP changes" + ${HG} -R ${REPODIR} commit -u "${HG_SSH_USER}" -m "${COMMIT_MESSAGE}" +} + +# Copies new blocklist file in place, and commits it. +function commit_blocklist_files { + cd "${BASEDIR}" + cp -f ${BLOCKLIST_LOCAL_AMO} ${REPODIR}/${APP_DIR}/app/blocklist.xml + COMMIT_MESSAGE="No bug, Automated blocklist update" + if [ -n "${TASK_ID}" ]; then + COMMIT_MESSAGE="${COMMIT_MESSAGE} from task ${TASK_ID}" + fi + if [ ${DONTBUILD} == true ]; then + COMMIT_MESSAGE="${COMMIT_MESSAGE} - (DONTBUILD)" + fi + if [ ${CLOSED_TREE} == true ]; then + COMMIT_MESSAGE="${COMMIT_MESSAGE} - CLOSED TREE" + fi + if [ ${APPROVAL} == true ]; then + COMMIT_MESSAGE="${COMMIT_MESSAGE} - a=blocklist-update" + fi + echo "INFO: committing blocklist changes" + ${HG} -R ${REPODIR} commit -u "${HG_SSH_USER}" -m "${COMMIT_MESSAGE}" +} + +# Push all pending commits to Phabricator +function push_repo { + cd "${REPODIR}" + if [ ! -r "${HOME}/.arcrc" ] + then + return 1 + fi + if ! ARC=$(which arc) + then + return 1 + fi + if [ -z "${REVIEWERS}" ] + then + return 1 + fi + $ARC diff --verbatim --reviewers "${REVIEWERS}" +} + + + +# Main + +# Parse our command-line options. +while [ $# -gt 0 ]; do + case "$1" in + -h) usage; exit 0 ;; + -p) PRODUCT="$2"; shift ;; + -b) BRANCH="$2"; shift ;; + -n) DRY_RUN=true ;; + -c) CLOSED_TREE=true ;; + -d) DONTBUILD=true ;; + -a) APPROVAL=true ;; + --pinset) DO_PRELOAD_PINSET=true ;; + --hsts) DO_HSTS=true ;; + --hpkp) DO_HPKP=true ;; + --blocklist) DO_BLOCKLIST=true ;; + -r) REPODIR="$2"; shift ;; + --use-mozilla-central) USE_MC=true ;; + --use-ftp-builds) USE_TC=false ;; + -*) usage + exit 11 ;; + *) break ;; # terminate while loop + esac + shift +done + +# Must supply a code branch to work with. +if [ "${BRANCH}" == "" ]; then + echo "Error: You must specify a branch with -b branchname." >&2 + usage + exit 12 +fi + +# Must choose at least one update action. +if [ "$DO_HSTS" == "false" ] && [ "$DO_HPKP" == "false" ] && [ "$DO_BLOCKLIST" == "false" ] +then + echo "Error: you must specify at least one action from: --hsts, --hpkp, --blocklist" >&2 + usage + exit 13 +fi + +# per-product constants +case "${PRODUCT}" in + thunderbird) + APP_DIR="mail" + APP_ID="%7B3550f703-e582-4d05-9a08-453d09bdfdc6%7D" + APP_NAME="Thunderbird" + ;; + firefox) + APP_DIR="browser" + APP_ID="%7Bec8030f7-c20a-464f-9b0e-13a3a9e97384%7D" + APP_NAME="Firefox" + ;; + *) + echo "Error: Invalid product specified" + usage + exit 14 + ;; +esac + +if [ "${REPODIR}" == "" ]; then + REPODIR="$(basename "${BRANCH}")" +fi + +HGREPO="https://${HGHOST}/${BRANCH}" +MCREPO="https://${HGHOST}/mozilla-central" + +# Remove once 52esr is off support +VERSION=$(get_version "${HGREPO}") +MAJOR_VERSION="${VERSION%.*}" +echo "INFO: parsed version is ${VERSION}" +if [ "${USE_MC}" == "true" ]; then + MCVERSION=$(get_version "${MCREPO}") + echo "INFO: parsed mozilla-central version is ${MCVERSION}" + MAJOR_VERSION="${MCVERSION%.*}" +fi + +BROWSER_ARCHIVE="${PRODUCT}-${VERSION}.en-US.${PLATFORM}.${PLATFORM_EXT}" +TESTS_ARCHIVE="${PRODUCT}-${VERSION}.en-US.${PLATFORM}.common.tests.zip" +if [ "${USE_MC}" == "true" ]; then + BROWSER_ARCHIVE="${PRODUCT}-${MCVERSION}.en-US.${PLATFORM}.${PLATFORM_EXT}" + TESTS_ARCHIVE="${PRODUCT}-${MCVERSION}.en-US.${PLATFORM}.common.tests.zip" +fi +# Simple name builds on >=53.0.0 +if [ "${MAJOR_VERSION}" -ge 53 ] ; then + BROWSER_ARCHIVE="target.${PLATFORM_EXT}" + TESTS_ARCHIVE="target.common.tests.zip" +fi +# End 'remove once 52esr is off support' + +preflight_cleanup +if [ "${DO_HSTS}" == "true" ] || [ "${DO_HPKP}" == "true" ] || [ "${DO_PRELOAD_PINSET}" == "true" ] +then + if [ "${USE_TC}" == "true" ]; then + download_shared_artifacts_from_tc + else + download_shared_artifacts_from_ftp + fi + unpack_artifacts +fi + +if [ "${DO_HSTS}" == "true" ]; then + if compare_hsts_files + then + HSTS_UPDATED=true + fi +fi +if [ "${DO_HPKP}" == "true" ]; then + if compare_hpkp_files + then + HPKP_UPDATED=true + fi +fi +if [ "${DO_BLOCKLIST}" == "true" ]; then + if compare_blocklist_files + then + BLOCKLIST_UPDATED=true + fi +fi + +if [ "${HSTS_UPDATED}" == "false" ] && [ "${HPKP_UPDATED}" == "false" ] && [ "${BLOCKLIST_UPDATED}" == "false" ]; then + echo "INFO: no updates required. Exiting." + exit 0 +else + if [ "${DRY_RUN}" == "true" ]; then + echo "INFO: Updates are available, not updating hg in dry-run mode." + exit 2 + fi +fi + +# Currently less reliable than regular 'hg' +# clone_build_tools + +clone_repo + +MUST_PUSH=false +if [ "${HSTS_UPDATED}" == "true" ] +then + commit_hsts_files + MUST_PUSH=true +fi + +if [ "${HPKP_UPDATED}" == "true" ] +then + commit_hpkp_files + MUST_PUSH=true +fi + +if [ "${BLOCKLIST_UPDATED}" == "true" ] +then + commit_blocklist_files + MUST_PUSH=true +fi + +if [ -n "${MUST_PUSH}" ] +then + push_repo +fi + +echo "All done" diff --git a/testing/mochitest/mach_commands.py b/testing/mochitest/mach_commands.py index 7d5dffbc6309..e0351bac7889 100644 --- a/testing/mochitest/mach_commands.py +++ b/testing/mochitest/mach_commands.py @@ -393,9 +393,13 @@ class MachCommands(MachCommandBase): if buildapp == 'android': from mozrunner.devices.android_device import grant_runtime_permissions from mozrunner.devices.android_device import verify_android_device + app = kwargs.get('app') + if not app: + app = self.substs["ANDROID_PACKAGE_NAME"] + # verify installation - verify_android_device(self, install=True, xre=False, app=kwargs['app']) - grant_runtime_permissions(self) + verify_android_device(self, install=True, xre=False, app=app) + grant_runtime_permissions(self, app) run_mochitest = mochitest.run_android_test else: run_mochitest = mochitest.run_desktop_test diff --git a/testing/mochitest/runtestsremote.py b/testing/mochitest/runtestsremote.py index c34e693f37ac..b21d9e06e5f7 100644 --- a/testing/mochitest/runtestsremote.py +++ b/testing/mochitest/runtestsremote.py @@ -314,7 +314,7 @@ def run_test_harness(parser, options): message_logger = MessageLogger(logger=None) counts = dict() process_args = {'messageLogger': message_logger, 'counts': counts} - auto = RemoteAutomation(None, "fennec", processArgs=process_args) + auto = RemoteAutomation(None, options.app, processArgs=process_args) if options is None: raise ValueError("Invalid options specified, use --help for a list of valid options") diff --git a/testing/mozbase/mozdevice/mozdevice/adb_android.py b/testing/mozbase/mozdevice/mozdevice/adb_android.py index a7ac482c082f..818fd563ae8b 100644 --- a/testing/mozbase/mozdevice/mozdevice/adb_android.py +++ b/testing/mozbase/mozdevice/mozdevice/adb_android.py @@ -400,7 +400,7 @@ class ADBAndroid(ADBDevice): if extra_args: extras['args'] = " ".join(extra_args) - self.launch_application(app_name, "org.mozilla.gecko.BrowserApp", + self.launch_application(app_name, ".App", intent, url=url, extras=extras, wait=wait, fail_if_running=fail_if_running, timeout=timeout) diff --git a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py index 852b6e3efbcc..b8515c5e9403 100644 --- a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py +++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py @@ -424,7 +424,7 @@ class DeviceManagerADB(DeviceManager): else: args = cmd[i:].strip() acmd.append("-n") - acmd.append(cmd[0:i] + "/org.mozilla.gecko.BrowserApp") + acmd.append(cmd[0:i] + "/.App") if args != "": acmd.append("--es") acmd.append("args") diff --git a/testing/mozbase/mozdevice/mozdevice/droid.py b/testing/mozbase/mozdevice/mozdevice/droid.py index f74ceafb215c..f75aeea65750 100644 --- a/testing/mozbase/mozdevice/mozdevice/droid.py +++ b/testing/mozbase/mozdevice/mozdevice/droid.py @@ -101,7 +101,7 @@ class DroidMixin(object): if extraArgs: extras['args'] = " ".join(extraArgs) - self.launchApplication(appName, "org.mozilla.gecko.BrowserApp", intent, url=url, + self.launchApplication(appName, ".App", intent, url=url, extras=extras, wait=wait, failIfRunning=failIfRunning) diff --git a/testing/mozbase/mozrunner/mozrunner/devices/android_device.py b/testing/mozbase/mozrunner/mozrunner/devices/android_device.py index f6e493ddd2e2..4cb9bd3b1039 100644 --- a/testing/mozbase/mozrunner/mozrunner/devices/android_device.py +++ b/testing/mozbase/mozrunner/mozrunner/devices/android_device.py @@ -366,12 +366,11 @@ def run_firefox_for_android(build_obj, params): return 0 -def grant_runtime_permissions(build_obj): +def grant_runtime_permissions(build_obj, app): """ Grant required runtime permissions to the specified app (typically org.mozilla.fennec_$USER). """ - app = build_obj.substs['ANDROID_PACKAGE_NAME'] adb_path = _find_sdk_exe(build_obj.substs, 'adb', False) if not adb_path: adb_path = 'adb' diff --git a/testing/mozharness/mozharness/mozilla/testing/talos.py b/testing/mozharness/mozharness/mozilla/testing/talos.py index e911c447354c..b60d10ccac4e 100755 --- a/testing/mozharness/mozharness/mozilla/testing/talos.py +++ b/testing/mozharness/mozharness/mozilla/testing/talos.py @@ -193,6 +193,7 @@ class Talos(TestingMixin, MercurialScript, BlobUploadMixin, TooltoolMixin, self.gecko_profile = self.config.get('gecko_profile') self.gecko_profile_interval = self.config.get('gecko_profile_interval') self.pagesets_name = None + self.benchmark_zip = None self.mitmproxy_rel_bin = None # some platforms download a mitmproxy release binary self.mitmproxy_recording_set = None # zip file found on tooltool that contains all of the mitmproxy recordings self.mitmproxy_recordings_file_list = self.config.get('mitmproxy', None) # files inside the recording set @@ -267,8 +268,20 @@ class Talos(TestingMixin, MercurialScript, BlobUploadMixin, TooltoolMixin, return self.pagesets_name if self.query_talos_json_config() and self.suite is not None: self.pagesets_name = self.talos_json_config['suites'][self.suite].get('pagesets_name') + self.pagesets_name_manifest = 'tp5n-pageset.manifest' return self.pagesets_name + def query_benchmark_zip(self): + """Certain suites require external benchmarks to be downloaded and + extracted. + """ + if self.benchmark_zip: + return self.benchmark_zip + if self.query_talos_json_config() and self.suite is not None: + self.benchmark_zip = self.talos_json_config['suites'][self.suite].get('benchmark_zip') + self.benchmark_zip_manifest = 'jetstream-benchmark.manifest' + return self.benchmark_zip + def query_mitmproxy_recordings_file_list(self): """ When using mitmproxy we also need the name of the playback files that are included inside the playback archive. @@ -389,28 +402,36 @@ class Talos(TestingMixin, MercurialScript, BlobUploadMixin, TooltoolMixin, # talos initiated in production via mozharness self.suite = self.config['suite'] - # now that have the suite name, check if pageset is required, if so download it - # the --no-download option will override this + + tooltool_artifacts = [] if self.query_pagesets_name(): + tooltool_artifacts.append({'name': self.pagesets_name, 'manifest': self.pagesets_name_manifest}) + + if self.query_benchmark_zip(): + tooltool_artifacts.append({'name': self.benchmark_zip, 'manifest': self.benchmark_zip_manifest}) + + # now that have the suite name, check if artifact is required, if so download it + # the --no-download option will override this + for artifact in tooltool_artifacts: if '--no-download' not in self.config.get('talos_extra_options', []): - self.info("Downloading pageset with tooltool...") + self.info("Downloading %s with tooltool..." % artifact) self.src_talos_webdir = os.path.join(self.talos_path, 'talos') src_talos_pageset = os.path.join(self.src_talos_webdir, 'tests') - if not os.path.exists(os.path.join(src_talos_pageset, self.pagesets_name)): - manifest_file = os.path.join(self.talos_path, 'tp5n-pageset.manifest') + if not os.path.exists(os.path.join(src_talos_pageset, artifact['name'])): + manifest_file = os.path.join(self.talos_path, artifact['manifest']) self.tooltool_fetch( manifest_file, output_dir=src_talos_pageset, cache=self.config.get('tooltool_cache') ) - archive = os.path.join(src_talos_pageset, self.pagesets_name) + archive = os.path.join(src_talos_pageset, artifact['name']) unzip = self.query_exe('unzip') unzip_cmd = [unzip, '-q', '-o', archive, '-d', src_talos_pageset] self.run_command(unzip_cmd, halt_on_failure=True) else: - self.info("pageset already available") + self.info("%s already available" % artifact) else: - self.info("Not downloading pageset because the no-download option was specified") + self.info("Not downloading %s because the no-download option was specified" % artifact) # if running webkit tests locally, need to copy webkit source into talos/tests if self.config.get('run_local') and ('speedometer' in self.suite or diff --git a/testing/talos/jetstream-benchmark.manifest b/testing/talos/jetstream-benchmark.manifest new file mode 100644 index 000000000000..b7ef28f32ebb --- /dev/null +++ b/testing/talos/jetstream-benchmark.manifest @@ -0,0 +1,9 @@ +[ + { + "size": 10025531, + "visibility": "public", + "digest": "4ff745d55505720b4b5929476527ac0f9fd75a98151030699d3dec84cb11f24d2ab18f24c98ed063912e709c5126424fa7921080da3daaee31cf50eae5c9591a", + "algorithm": "sha512", + "filename": "jetstream.zip" + } +] \ No newline at end of file diff --git a/testing/talos/talos.json b/testing/talos/talos.json index 3561feb469b6..5a8ec2395b31 100644 --- a/testing/talos/talos.json +++ b/testing/talos/talos.json @@ -66,7 +66,8 @@ "pagesets_name": "tp5n.zip" }, "motionmark-e10s": { - "tests": ["motionmark_animometer", "motionmark_htmlsuite", "ARES6"] + "tests": ["motionmark_animometer", "motionmark_htmlsuite", "JetStream", "ARES6"], + "benchmark_zip": "jetstream.zip" }, "svgr-e10s": { "tests": ["tsvgx", "tsvgr_opacity", "tart", "tscrollx", "tsvg_static"] diff --git a/testing/talos/talos/config.py b/testing/talos/talos/config.py index eb93f1c47359..d1e909205e27 100644 --- a/testing/talos/talos/config.py +++ b/testing/talos/talos/config.py @@ -313,6 +313,12 @@ def get_counters(config): def get_active_tests(config): activeTests = config.pop('activeTests').strip().split(':') + # on osx, ARES6 crashes about 50% of the time, bug 1437425 + if mozinfo.os not in ['linux', 'win'] and \ + 'ARES6' in activeTests and \ + not config['develop']: + activeTests.remove('ARES6') + # ensure tests are available availableTests = test.test_dict() if not set(activeTests).issubset(availableTests): diff --git a/testing/talos/talos/output.py b/testing/talos/talos/output.py index bf190a978717..0ecc81a09eda 100755 --- a/testing/talos/talos/output.py +++ b/testing/talos/talos/output.py @@ -256,9 +256,9 @@ class Output(object): return score @classmethod - def ares6_score(cls, val_list): + def benchmark_score(cls, val_list): """ - ares6_score: reported as 'geomean' + benchmark_score: ares6/jetstream self reported as 'geomean' """ results = [i for i, j in val_list if j == 'geomean'] return filter.mean(results) @@ -288,7 +288,9 @@ class Output(object): elif testname.startswith('kraken'): return self.JS_Metric(vals) elif testname.startswith('ares6'): - return self.ares6_score(vals) + return self.benchmark_score(vals) + elif testname.startswith('jetstream'): + return self.benchmark_score(vals) elif testname.startswith('speedometer'): return self.speedometer_score(vals) elif testname.startswith('stylebench'): diff --git a/testing/talos/talos/test.py b/testing/talos/talos/test.py index 90b9a5ce9358..3fdb03a5e743 100644 --- a/testing/talos/talos/test.py +++ b/testing/talos/talos/test.py @@ -828,6 +828,13 @@ class motionmark_htmlsuite(WebkitBenchmark): tpmanifest = '${talos}/tests/motionmark/htmlsuite.manifest' +@register_test() +class JetStream(WebkitBenchmark): + # JetStream benchmark used by many browser vendors (from webkit) + tpmanifest = '${talos}/tests/jetstream/jetstream.manifest' + tppagecycles = 1 + + @register_test() class perf_reftest(PageloaderTest): """ diff --git a/testing/talos/talos/tests/jetstream/jetstream.manifest b/testing/talos/talos/tests/jetstream/jetstream.manifest new file mode 100644 index 000000000000..3b6a706f1b3e --- /dev/null +++ b/testing/talos/talos/tests/jetstream/jetstream.manifest @@ -0,0 +1 @@ +% http://localhost/tests/JetStream/JetStream-1.1/index.html?gecko#long diff --git a/testing/web-platform/meta/credential-management/idl.https.html.ini b/testing/web-platform/meta/credential-management/idl.https.html.ini index 27e5b38bf349..43c79f74eee3 100644 --- a/testing/web-platform/meta/credential-management/idl.https.html.ini +++ b/testing/web-platform/meta/credential-management/idl.https.html.ini @@ -108,21 +108,12 @@ [Credential interface: new FederatedCredential({ id: "id", provider: "https://example.com", iconURL: "https://example.com/", name: "name" }) must inherit property "type" with the proper type (1)] expected: FAIL - [CredentialsContainer interface: operation preventSilentAccess()] - expected: FAIL - - [CredentialsContainer interface: navigator.credentials must inherit property "preventSilentAccess" with the proper type (3)] - expected: FAIL - [PasswordCredential interface: attribute password] expected: FAIL [PasswordCredential interface: new PasswordCredential({ id: "id", password: "pencil", iconURL: "https://example.com/", name: "name" }) must inherit property "password" with the proper type (0)] expected: FAIL - [CredentialsContainer interface: navigator.credentials must inherit property "preventSilentAccess()" with the proper type] - expected: FAIL - [PasswordCredential interface: new PasswordCredential({ id: "id", password: "pencil", iconURL: "https://example.com/", name: "name" }) must inherit property "password" with the proper type] expected: FAIL diff --git a/testing/web-platform/meta/webauthn/interfaces.https.html.ini b/testing/web-platform/meta/webauthn/interfaces.https.html.ini index f7aeef578062..62e41f620799 100644 --- a/testing/web-platform/meta/webauthn/interfaces.https.html.ini +++ b/testing/web-platform/meta/webauthn/interfaces.https.html.ini @@ -5,6 +5,3 @@ [PublicKeyCredential interface: existence and properties of interface prototype object] expected: FAIL - [PublicKeyCredential interface: operation getClientExtensionResults()] - expected: FAIL - diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 5a23e077ce42..ac0a91422d37 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -229,10 +229,11 @@ }, "BLOCKLIST_SYNC_FILE_LOAD": { "record_in_processes": ["main", "content"], - "alert_emails": ["rvitillo@mozilla.com"], - "expires_in_version": "35", + "bug_numbers": [1439405], + "alert_emails": ["gijs@mozilla.com"], + "expires_in_version": "64", "kind": "boolean", - "description": "blocklist.xml has been loaded synchronously *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" + "description": "Whether blocklist.xml was loaded synchronously. True means it was loaded synchronously, false means it was preloaded asynchronously." }, "CHECKERBOARD_DURATION": { "record_in_processes": ["main", "content", "gpu"], diff --git a/toolkit/components/telemetry/histogram-whitelists.json b/toolkit/components/telemetry/histogram-whitelists.json index 0cb4db1e31c9..b9ba5a66206f 100644 --- a/toolkit/components/telemetry/histogram-whitelists.json +++ b/toolkit/components/telemetry/histogram-whitelists.json @@ -572,7 +572,6 @@ "AUTO_REJECTED_TRANSLATION_OFFERS", "BACKGROUNDFILESAVER_THREAD_COUNT", "BAD_FALLBACK_FONT", - "BLOCKLIST_SYNC_FILE_LOAD", "BROWSERPROVIDER_XUL_IMPORT_BOOKMARKS", "BROWSER_IS_ASSIST_DEFAULT", "BROWSER_IS_USER_DEFAULT", diff --git a/toolkit/mozapps/extensions/nsBlocklistService.js b/toolkit/mozapps/extensions/nsBlocklistService.js index e0c7dae8e1b1..6f94ca9e27d8 100644 --- a/toolkit/mozapps/extensions/nsBlocklistService.js +++ b/toolkit/mozapps/extensions/nsBlocklistService.js @@ -69,14 +69,6 @@ var gLoggingEnabled = null; var gBlocklistEnabled = true; var gBlocklistLevel = DEFAULT_LEVEL; -XPCOMUtils.defineLazyServiceGetter(this, "gConsole", - "@mozilla.org/consoleservice;1", - "nsIConsoleService"); - -XPCOMUtils.defineLazyServiceGetter(this, "gVersionChecker", - "@mozilla.org/xpcom/version-comparator;1", - "nsIVersionComparator"); - // From appinfo in Services.jsm. It is not possible to use the one in // Services.jsm since it will not successfully QueryInterface nsIXULAppInfo in // xpcshell tests due to other code calling Services.appinfo before the @@ -150,7 +142,7 @@ XPCOMUtils.defineLazyGetter(this, "gCertUtils", function() { function LOG(string) { if (gLoggingEnabled) { dump("*** " + string + "\n"); - gConsole.logStringMessage(string); + Services.console.logStringMessage(string); } } @@ -613,15 +605,20 @@ Blocklist.prototype = { return; } - if (request.status == 304) { + let {status} = request; + if (status == 304) { LOG("Blocklist::onXMLLoad: up to date."); return; } - let responseXML = request.responseXML; - if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR || - (request.status != 200 && request.status != 0)) { - LOG("Blocklist::onXMLLoad: there was an error during load"); + if (status != 200 && status != 0) { + LOG("Blocklist::onXMLLoad: there was an error during load, got status: " + status); + return; + } + + let {responseXML} = request; + if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR) { + LOG("Blocklist::onXMLLoad: there was an error during load, we got invalid XML"); return; } @@ -635,7 +632,7 @@ Blocklist.prototype = { this._gfxEntries = []; this._pluginEntries = []; - this._loadBlocklistFromString(request.responseText); + this._loadBlocklistFromXML(responseXML); // We don't inform the users when the graphics blocklist changed at runtime. // However addons and plugins blocking status is refreshed. this._blocklistUpdated(oldAddonEntries, oldPluginEntries); @@ -677,17 +674,28 @@ Blocklist.prototype = { this._addonEntries = []; this._gfxEntries = []; this._pluginEntries = []; + + if (this._isBlocklistPreloaded()) { + Services.telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(false); + this._loadBlocklistFromString(this._preloadedBlocklistContent); + delete this._preloadedBlocklistContent; + return; + } + + Services.telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(true); + var profFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]); - if (profFile.exists()) { + try { this._loadBlocklistFromFile(profFile); - return; + } catch (ex) { + LOG("Blocklist::_loadBlocklist: couldn't load file from profile, trying app dir"); + try { + var appFile = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]); + this._loadBlocklistFromFile(appFile); + } catch (ex) { + LOG("Blocklist::_loadBlocklist: no XML File found"); + } } - var appFile = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]); - if (appFile.exists()) { - this._loadBlocklistFromFile(appFile); - return; - } - LOG("Blocklist::_loadBlocklist: no XML File found"); }, /** @@ -743,16 +751,9 @@ Blocklist.prototype = { # # # -# -# -# -# -# AkHVNA== -# -# -# -# -# +# +# +# # */ @@ -762,31 +763,15 @@ Blocklist.prototype = { return; } - let telemetry = Services.telemetry; - - if (this._isBlocklistPreloaded()) { - telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(false); - this._loadBlocklistFromString(this._preloadedBlocklistContent); - delete this._preloadedBlocklistContent; - return; - } - - if (!file.exists()) { - LOG("Blocklist::_loadBlocklistFromFile: XML File does not exist " + file.path); - return; - } - - telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(true); - let text = ""; let fstream = null; let cstream = null; try { - fstream = Components.classes["@mozilla.org/network/file-input-stream;1"] - .createInstance(Components.interfaces.nsIFileInputStream); - cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"] - .createInstance(Components.interfaces.nsIConverterInputStream); + fstream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + cstream = Cc["@mozilla.org/intl/converter-input-stream;1"] + .createInstance(Ci.nsIConverterInputStream); fstream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); cstream.init(fstream, "UTF-8", 0, 0); @@ -798,17 +783,23 @@ Blocklist.prototype = { read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value text += str.value; } while (read != 0); - } catch (e) { - LOG("Blocklist::_loadBlocklistFromFile: Failed to load XML file " + e); } finally { - if (cstream) - cstream.close(); - if (fstream) - fstream.close(); + // There's no catch block because the callers will catch exceptions, + // and may try to read another file if reading this file failed. + if (cstream) { + try { + cstream.close(); + } catch (ex) {} + } + if (fstream) { + try { + fstream.close(); + } catch (ex) {} + } } if (text) - this._loadBlocklistFromString(text); + this._loadBlocklistFromString(text); }, _isBlocklistLoaded() { @@ -872,12 +863,20 @@ Blocklist.prototype = { createInstance(Ci.nsIDOMParser); var doc = parser.parseFromString(text, "text/xml"); if (doc.documentElement.namespaceURI != XMLURI_BLOCKLIST) { - LOG("Blocklist::_loadBlocklistFromFile: aborting due to incorrect " + + LOG("Blocklist::_loadBlocklistFromString: aborting due to incorrect " + "XML Namespace.\r\nExpected: " + XMLURI_BLOCKLIST + "\r\n" + "Received: " + doc.documentElement.namespaceURI); return; } + } catch (e) { + LOG("Blocklist::_loadBlocklistFromString: Error constructing blocklist " + e); + return; + } + this._loadBlocklistFromXML(doc); + }, + _loadBlocklistFromXML(doc) { + try { var childNodes = doc.documentElement.childNodes; for (let element of childNodes) { if (!(element instanceof Ci.nsIDOMElement)) @@ -897,14 +896,14 @@ Blocklist.prototype = { this._handleGfxBlacklistNode); break; default: - LOG("Blocklist::_loadBlocklistFromString: ignored entries " + element.localName); + LOG("Blocklist::_loadBlocklistFromXML: ignored entries " + element.localName); } } if (this._gfxEntries.length > 0) { this._notifyObserversBlocklistGFX(); } } catch (e) { - LOG("Blocklist::_loadBlocklistFromFile: Error constructing blocklist " + e); + LOG("Blocklist::_loadBlocklistFromXML: Error constructing blocklist " + e); } }, @@ -1524,9 +1523,9 @@ BlocklistItemData.prototype = { * smaller. */ matchesRange(version, minVersion, maxVersion) { - if (minVersion && gVersionChecker.compare(version, minVersion) < 0) + if (minVersion && Services.vc.compare(version, minVersion) < 0) return false; - if (maxVersion && gVersionChecker.compare(version, maxVersion) > 0) + if (maxVersion && Services.vc.compare(version, maxVersion) > 0) return false; return true; }, diff --git a/widget/android/GeneratedJNIWrappers.h b/widget/android/GeneratedJNIWrappers.h index 8de944f44897..3eb178b0894e 100644 --- a/widget/android/GeneratedJNIWrappers.h +++ b/widget/android/GeneratedJNIWrappers.h @@ -7228,10 +7228,11 @@ public: mozilla::jni::Object::Param, mozilla::jni::String::Param, int32_t, - bool> Args; + bool, + mozilla::jni::String::Param> Args; static constexpr char name[] = "open"; static constexpr char signature[] = - "(Lorg/mozilla/geckoview/GeckoSession$Window;Lorg/mozilla/gecko/gfx/LayerSession$Compositor;Lorg/mozilla/gecko/EventDispatcher;Lorg/mozilla/gecko/util/GeckoBundle;Ljava/lang/String;IZ)V"; + "(Lorg/mozilla/geckoview/GeckoSession$Window;Lorg/mozilla/gecko/gfx/LayerSession$Compositor;Lorg/mozilla/gecko/EventDispatcher;Lorg/mozilla/gecko/util/GeckoBundle;Ljava/lang/String;IZLjava/lang/String;)V"; static const bool isStatic = true; static const mozilla::jni::ExceptionMode exceptionMode = mozilla::jni::ExceptionMode::ABORT; diff --git a/widget/android/nsWindow.cpp b/widget/android/nsWindow.cpp index b611c427f146..fa662dbd6b6a 100644 --- a/widget/android/nsWindow.cpp +++ b/widget/android/nsWindow.cpp @@ -282,7 +282,8 @@ public: jni::Object::Param aSettings, jni::String::Param aChromeURI, int32_t aScreenId, - bool aPrivateMode); + bool aPrivateMode, + jni::String::Param aId); // Close and destroy the nsWindow. void Close(); @@ -1248,7 +1249,8 @@ nsWindow::GeckoViewSupport::Open(const jni::Class::LocalRef& aCls, jni::Object::Param aSettings, jni::String::Param aChromeURI, int32_t aScreenId, - bool aPrivateMode) + bool aPrivateMode, + jni::String::Param aId) { MOZ_ASSERT(NS_IsMainThread()); @@ -1278,7 +1280,7 @@ nsWindow::GeckoViewSupport::Open(const jni::Class::LocalRef& aCls, chromeFlags += ",private"; } nsCOMPtr domWindow; - ww->OpenWindow(nullptr, url.get(), nullptr, chromeFlags.get(), + ww->OpenWindow(nullptr, url.get(), aId->ToCString().get(), chromeFlags.get(), androidView, getter_AddRefs(domWindow)); MOZ_RELEASE_ASSERT(domWindow); diff --git a/xpcom/tests/gtest/TestAllocReplacement.cpp b/xpcom/tests/gtest/TestAllocReplacement.cpp index 9f6e978c8a6d..a596f44823f4 100644 --- a/xpcom/tests/gtest/TestAllocReplacement.cpp +++ b/xpcom/tests/gtest/TestAllocReplacement.cpp @@ -106,21 +106,30 @@ ValidateHookedAllocation(void* (*aAllocator)(void), return before == after; } -TEST(AllocReplacement, malloc_check) +// We use the "*DeathTest" suffix for all tests in this file to ensure they +// run before other GTests. As noted at the top, this is important because +// other tests might spawn threads that interfere with heap memory +// measurements. +// +// See +// for more information about death tests in the GTest framework. +TEST(AllocReplacementDeathTest, malloc_check) { ASSERT_ALLOCATION_HAPPENED([] { return malloc(kAllocAmount); }); } -TEST(AllocReplacement, calloc_check) +// See above for an explanation of the "*DeathTest" suffix used here. +TEST(AllocReplacementDeathTest, calloc_check) { ASSERT_ALLOCATION_HAPPENED([] { return calloc(1, kAllocAmount); }); } -TEST(AllocReplacement, realloc_check) +// See above for an explanation of the "*DeathTest" suffix used here. +TEST(AllocReplacementDeathTest, realloc_check) { ASSERT_ALLOCATION_HAPPENED([] { return realloc(nullptr, kAllocAmount); @@ -128,7 +137,8 @@ TEST(AllocReplacement, realloc_check) } #if defined(HAVE_POSIX_MEMALIGN) -TEST(AllocReplacement, posix_memalign_check) +// See above for an explanation of the "*DeathTest" suffix used here. +TEST(AllocReplacementDeathTest, posix_memalign_check) { ASSERT_ALLOCATION_HAPPENED([] { void* p = nullptr; @@ -150,7 +160,8 @@ TEST(AllocReplacement, posix_memalign_check) HeapFree(GetProcessHeap(), 0, p); \ })); -TEST(AllocReplacement, HeapAlloc_check) +// See above for an explanation of the "*DeathTest" suffix used here. +TEST(AllocReplacementDeathTest, HeapAlloc_check) { ASSERT_ALLOCATION_HAPPENED([] { HANDLE h = GetProcessHeap(); @@ -158,7 +169,8 @@ TEST(AllocReplacement, HeapAlloc_check) }); } -TEST(AllocReplacement, HeapReAlloc_check) +// See above for an explanation of the "*DeathTest" suffix used here. +TEST(AllocReplacementDeathTest, HeapReAlloc_check) { ASSERT_ALLOCATION_HAPPENED([] { HANDLE h = GetProcessHeap();