Bug 1809518 - Use stencil parsing to do the Javascript check for ORB in Utility Process r=farre,smaug,tcampbell

This patch allows JS Validator to parse the incoming data into
stencil to verify if its a Javascript file.

Differential Revision: https://phabricator.services.mozilla.com/D166484
This commit is contained in:
Sean Feng 2023-02-28 19:46:09 +00:00
parent 4d890942b8
commit 81c8e77c0d
12 changed files with 152 additions and 58 deletions

View File

@ -15,6 +15,28 @@ using namespace mozilla::dom;
static mozilla::StaticRefPtr<JSOracleChild> sOracleSingletonChild; static mozilla::StaticRefPtr<JSOracleChild> sOracleSingletonChild;
static mozilla::StaticAutoPtr<JSContextHolder> sJSContextHolder;
/* static */
void JSContextHolder::MaybeInit() {
if (!sJSContextHolder) {
sJSContextHolder = new JSContextHolder();
ClearOnShutdown(&sJSContextHolder);
}
}
/* static */
JSContext* JSOracleChild::JSContext() {
MOZ_ASSERT(sJSContextHolder);
return sJSContextHolder->mCx;
}
/* static */
JSObject* JSOracleChild::JSObject() {
MOZ_ASSERT(sJSContextHolder);
return sJSContextHolder->mGlobal;
}
JSOracleChild* JSOracleChild::GetSingleton() { JSOracleChild* JSOracleChild::GetSingleton() {
MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NS_IsMainThread());
if (!sOracleSingletonChild) { if (!sOracleSingletonChild) {
@ -30,5 +52,6 @@ already_AddRefed<PJSValidatorChild> JSOracleChild::AllocPJSValidatorChild() {
void JSOracleChild::Start(Endpoint<PJSOracleChild>&& aEndpoint) { void JSOracleChild::Start(Endpoint<PJSOracleChild>&& aEndpoint) {
DebugOnly<bool> ok = std::move(aEndpoint).Bind(this); DebugOnly<bool> ok = std::move(aEndpoint).Bind(this);
JSContextHolder::MaybeInit();
MOZ_ASSERT(ok); MOZ_ASSERT(ok);
} }

View File

@ -9,10 +9,60 @@
#include "mozilla/dom/PJSOracleChild.h" #include "mozilla/dom/PJSOracleChild.h"
#include "js/CharacterEncoding.h"
#include "js/HeapAPI.h"
#include "js/Initialization.h"
#include "jsapi.h"
#include "js/CompilationAndEvaluation.h"
#include "js/Context.h"
namespace mozilla::ipc { namespace mozilla::ipc {
class UtilityProcessParent; class UtilityProcessParent;
} }
namespace mozilla::dom { namespace mozilla::dom {
struct JSContextHolder {
JSContextHolder() {
MOZ_RELEASE_ASSERT(JS_IsInitialized(),
"UtilityProcessChild::Init should have JS initialized");
mCx = JS_NewContext(JS::DefaultHeapMaxBytes);
if (!mCx) {
MOZ_CRASH("Failed to create JS Context");
return;
}
if (!JS::InitSelfHostedCode(mCx)) {
MOZ_CRASH("Failed to initialize the runtime's self-hosted code");
return;
}
static JSClass jsValidatorGlobalClass = {
"JSValidatorGlobal", JSCLASS_GLOBAL_FLAGS, &JS::DefaultGlobalClassOps};
JS::Rooted<JSObject*> global(
mCx, JS_NewGlobalObject(mCx, &jsValidatorGlobalClass, nullptr,
JS::FireOnNewGlobalHook, JS::RealmOptions()));
if (!global) {
MOZ_CRASH("Failed to create the global");
return;
}
mGlobal.init(mCx, global);
}
~JSContextHolder() {
if (mCx) {
JS_DestroyContext(mCx);
}
}
static void MaybeInit();
JSContext* mCx;
JS::PersistentRooted<JSObject*> mGlobal;
};
class PJSValidatorChild; class PJSValidatorChild;
@ -24,6 +74,9 @@ class JSOracleChild final : public PJSOracleChild {
void Start(Endpoint<PJSOracleChild>&& aEndpoint); void Start(Endpoint<PJSOracleChild>&& aEndpoint);
static struct JSContext* JSContext();
static class JSObject* JSObject();
private: private:
~JSOracleChild() = default; ~JSOracleChild() = default;

View File

@ -5,8 +5,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/JSValidatorChild.h" #include "mozilla/dom/JSValidatorChild.h"
#include "js/JSON.h"
#include "mozilla/dom/JSOracleChild.h"
#include "mozilla/ipc/Endpoint.h" #include "mozilla/ipc/Endpoint.h"
#include "js/experimental/JSStencil.h"
#include "js/SourceText.h"
#include "js/Exception.h"
#include "js/GlobalObject.h"
#include "js/CompileOptions.h"
#include "js/RealmOptions.h"
using namespace mozilla::dom; using namespace mozilla::dom;
mozilla::ipc::IPCResult JSValidatorChild::RecvIsOpaqueResponseAllowed( mozilla::ipc::IPCResult JSValidatorChild::RecvIsOpaqueResponseAllowed(
@ -72,16 +82,42 @@ void JSValidatorChild::Resolve(ValidatorResult aResult) {
} }
JSValidatorChild::ValidatorResult JSValidatorChild::ShouldAllowJS() const { JSValidatorChild::ValidatorResult JSValidatorChild::ShouldAllowJS() const {
// mSourceBytes could be empty when
// 1. No OnDataAvailable calls
// 2. Failed to allocate shmem
// The empty document parses as JavaScript, so for clarity we have a condition // The empty document parses as JavaScript, so for clarity we have a condition
// separately for that. // separately for that.
if (mSourceBytes.IsEmpty()) { if (mSourceBytes.IsEmpty()) {
return ValidatorResult::JavaScript; return ValidatorResult::JavaScript;
} }
JSContext* cx = JSOracleChild::JSContext();
if (!cx) {
return ValidatorResult::Failure;
}
JS::Rooted<JSObject*> global(cx, JSOracleChild::JSObject());
if (!global) {
return ValidatorResult::Failure;
}
JS::SourceText<Utf8Unit> srcBuf;
if (!srcBuf.init(cx, mSourceBytes.BeginReading(), mSourceBytes.Length(),
JS::SourceOwnership::Borrowed)) {
JS_ClearPendingException(cx);
return ValidatorResult::Failure;
}
JSAutoRealm ar(cx, global);
// Parse to JavaScript
RefPtr<JS::Stencil> stencil =
CompileGlobalScriptToStencil(cx, JS::CompileOptions(cx), srcBuf);
if (!stencil) {
JS_ClearPendingException(cx);
return ValidatorResult::Other;
}
// The stencil parsing lacks the JSON support, so we have to do the
// JSON parsing separately.
if (StringBeginsWith(NS_ConvertUTF8toUTF16(mSourceBytes), u"{"_ns)) { if (StringBeginsWith(NS_ConvertUTF8toUTF16(mSourceBytes), u"{"_ns)) {
return ValidatorResult::JSON; return ValidatorResult::JSON;
} }

View File

@ -39,6 +39,9 @@ void JSValidatorParent::IsOpaqueResponseAllowed(
Tie(data, result) = aResult.ResolveValue(); Tie(data, result) = aResult.ResolveValue();
aCallback(std::move(data), result); aCallback(std::move(data), result);
} else { } else {
// For cases like the Utility Process crashes, the promise will be
// rejected due to sending failures, and we'll block the request
// since we can't validate it.
aCallback(Nothing(), ValidatorResult::Failure); aCallback(Nothing(), ValidatorResult::Failure);
} }
}); });

View File

@ -110,6 +110,15 @@ bool UtilityProcessChild::Init(mozilla::ipc::UntypedEndpoint&& aEndpoint,
mSandbox = (SandboxingKind)aSandboxingKind; mSandbox = (SandboxingKind)aSandboxingKind;
// At the moment, only ORB uses JSContext in the
// Utility Process and ORB uses GENERIC_UTILITY
if (mSandbox == SandboxingKind::GENERIC_UTILITY) {
JS::DisableJitBackend();
if (!JS_Init()) {
return false;
}
}
profiler_set_process_name(nsCString("Utility Process")); profiler_set_process_name(nsCString("Utility Process"));
// Notify the parent process that we have finished our init and that it can // Notify the parent process that we have finished our init and that it can
@ -117,9 +126,12 @@ bool UtilityProcessChild::Init(mozilla::ipc::UntypedEndpoint&& aEndpoint,
SendInitCompleted(); SendInitCompleted();
RunOnShutdown( RunOnShutdown(
[] { [sandboxKind = mSandbox] {
StaticMutexAutoLock lock(sUtilityProcessChildMutex); StaticMutexAutoLock lock(sUtilityProcessChildMutex);
sUtilityProcessChild = nullptr; sUtilityProcessChild = nullptr;
if (sandboxKind == SandboxingKind::GENERIC_UTILITY) {
JS_ShutDown();
}
}, },
ShutdownPhase::XPCOMShutdownFinal); ShutdownPhase::XPCOMShutdownFinal);

View File

@ -11,6 +11,7 @@
#include "mozilla/RandomNum.h" #include "mozilla/RandomNum.h"
#include "mozilla/TaggedAnonymousMemory.h" #include "mozilla/TaggedAnonymousMemory.h"
#include "jit/JitOptions.h"
#include "js/HeapAPI.h" #include "js/HeapAPI.h"
#include "js/Utility.h" #include "js/Utility.h"
#include "util/Memory.h" #include "util/Memory.h"
@ -402,10 +403,12 @@ void InitMemorySubsystem() {
numAddressBits = 32; numAddressBits = 32;
#endif #endif
#ifdef RLIMIT_AS #ifdef RLIMIT_AS
rlimit as_limit; if (jit::HasJitBackend()) {
if (getrlimit(RLIMIT_AS, &as_limit) == 0 && rlimit as_limit;
as_limit.rlim_max != RLIM_INFINITY) { if (getrlimit(RLIMIT_AS, &as_limit) == 0 &&
virtualMemoryLimit = as_limit.rlim_max; as_limit.rlim_max != RLIM_INFINITY) {
virtualMemoryLimit = as_limit.rlim_max;
}
} }
#endif #endif
} }

View File

@ -1,5 +0,0 @@
[script-html-correctly-labeled.tentative.sub.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[CORB-blocked script has no syntax errors]
expected: FAIL

View File

@ -2,21 +2,9 @@
prefs: [browser.opaqueResponseBlocking:true, browser.opaqueResponseBlocking.javascriptValidator:true] prefs: [browser.opaqueResponseBlocking:true, browser.opaqueResponseBlocking.javascriptValidator:true]
expected: expected:
if (os == "android") and fission: [OK, TIMEOUT] if (os == "android") and fission: [OK, TIMEOUT]
[CORB-blocks 'text/html' that starts with the following JSON parser breaker: )\]}']
expected: FAIL
[CORB-blocks 'application/javascript' that starts with the following JSON parser breaker: )\]}'] [CORB-blocks 'application/javascript' that starts with the following JSON parser breaker: )\]}']
expected: FAIL expected: FAIL
[CORB-blocks 'text/xml' that starts with the following JSON parser breaker: )\]}']
expected: FAIL
[CORB-blocks 'text/plain' that starts with the following JSON parser breaker: )\]}']
expected: FAIL
[CORB-blocks 'text/json' that starts with the following JSON parser breaker: )\]}']
expected: FAIL
[CORB-blocks 'application/javascript' that starts with the following JSON parser breaker: {}&&] [CORB-blocks 'application/javascript' that starts with the following JSON parser breaker: {}&&]
expected: FAIL expected: FAIL

View File

@ -1 +1 @@
prefs: [browser.opaqueResponseBlocking:true] prefs: [browser.opaqueResponseBlocking:true, browser.opaqueResponseBlocking.javascriptValidator:true]

View File

@ -1,30 +0,0 @@
[known-mime-type.sub.any.worker.html]
prefs: [browser.opaqueResponseBlocking.javascriptValidator:true]
[ORB should block opaque font/ttf]
expected: FAIL
[ORB should block opaque text/plain]
expected: FAIL
[ORB should block opaque application/json]
expected:
if not debug and (os == "mac"): [PASS, FAIL]
if not debug and (os == "linux"): [PASS, FAIL]
if not debug and (os == "android"): [PASS, FAIL]
[known-mime-type.sub.any.html]
prefs: [browser.opaqueResponseBlocking.javascriptValidator:true]
expected:
if (os == "android") and fission: TIMEOUT
[ORB should block opaque font/ttf]
expected: FAIL
[ORB should block opaque text/plain]
expected: FAIL
[ORB should block opaque application/json]
expected:
if not debug and (os == "linux"): [PASS, FAIL]
if not debug and (os == "android"): [PASS, FAIL]
if not debug and (os == "mac"): [PASS, FAIL]

View File

@ -0,0 +1 @@
{}

View File

@ -29,7 +29,17 @@ promise_test(
TypeError, TypeError,
fetchORB(`${path}/data.json`, null, contentType("application/json")) fetchORB(`${path}/data.json`, null, contentType("application/json"))
), ),
"ORB should block opaque application/json" "ORB should block opaque application/json (non-empty)"
);
promise_test(
t =>
promise_rejects_js(
t,
TypeError,
fetchORB(`${path}/empty.json`, null, contentType("application/json"))
),
"ORB should block opaque application/json (empty)"
); );
promise_test(async () => { promise_test(async () => {