Bug 1782495 - Replace watchtower testing callback with a log-based mechanism. r=iain

The ability to run arbitrary JS can cause various problems. This replaces the callback
with a different mechanism to avoid this.

Differential Revision: https://phabricator.services.mozilla.com/D159513
This commit is contained in:
Jan de Mooij 2022-10-18 08:17:43 +00:00
Родитель 10083d82bf
Коммит 253c86b5e0
10 изменённых файлов: 116 добавлений и 99 удалений

Просмотреть файл

@ -3012,18 +3012,31 @@ static bool CheckObjectWithManyReservedSlots(JSContext* cx, unsigned argc,
return true;
}
static bool SetWatchtowerCallback(JSContext* cx, unsigned argc, Value* vp) {
static bool GetWatchtowerLog(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
JSFunction* fun = nullptr;
if (args.length() != 1 || !IsFunctionObject(args[0], &fun)) {
JS_ReportErrorASCII(cx, "Expected a single function argument.");
Rooted<GCVector<Value>> values(cx, GCVector<Value>(cx));
if (auto* log = cx->runtime()->watchtowerTestingLog.ref().get()) {
Rooted<JSObject*> elem(cx);
for (PlainObject* obj : *log) {
elem = obj;
if (!cx->compartment()->wrap(cx, &elem)) {
return false;
}
if (!values.append(ObjectValue(*elem))) {
return false;
}
}
log->clearAndFree();
}
ArrayObject* arr = NewDenseCopiedArray(cx, values.length(), values.begin());
if (!arr) {
return false;
}
cx->watchtowerTestingCallbackRef() = fun;
args.rval().setUndefined();
args.rval().setObject(*arr);
return true;
}
@ -3035,8 +3048,16 @@ static bool AddWatchtowerTarget(JSContext* cx, unsigned argc, Value* vp) {
return false;
}
if (!cx->runtime()->watchtowerTestingLog.ref()) {
auto vec = cx->make_unique<JSRuntime::RootedPlainObjVec>(cx);
if (!vec) {
return false;
}
cx->runtime()->watchtowerTestingLog = std::move(vec);
}
RootedObject obj(cx, &args[0].toObject());
if (!JSObject::setUseWatchtowerTestingCallback(cx, obj)) {
if (!JSObject::setUseWatchtowerTestingLog(cx, obj)) {
return false;
}
@ -8176,10 +8197,11 @@ static const JSFunctionSpecWithHelp TestingFunctions[] = {
" Checks the reserved slots set by newObjectWithManyReservedSlots still hold the expected\n"
" values."),
JS_FN_HELP("setWatchtowerCallback", SetWatchtowerCallback, 1, 0,
"setWatchtowerCallback(function)",
" Use the given function as callback for objects added to Watchtower by\n"
" addWatchtowerTarget. The callback is called with the following arguments:\n"
JS_FN_HELP("getWatchtowerLog", GetWatchtowerLog, 0, 0,
"getWatchtowerLog()",
" Returns the Watchtower log recording object changes for objects for which\n"
" addWatchtowerTarget was called. The internal log is cleared. The return\n"
" value is an array of plain objects with the following properties:\n"
" - kind: a string describing the kind of mutation, for example \"add-prop\"\n"
" - object: the object being mutated\n"
" - extra: an extra value, for example the name of the property being added"),

Просмотреть файл

@ -1,17 +1,16 @@
setWatchtowerCallback(function(kind, object, extra) {
assertEq(object, o);
if (typeof extra === "symbol") {
extra = "<symbol>";
}
log.push(kind + (extra ? ": " + extra : ""));
});
let log;
let o;
function getLogString(obj) {
let log = getWatchtowerLog();
return log.map(item => {
assertEq(item.object, obj);
if (typeof item.extra === "symbol") {
item.extra = "<symbol>";
}
return item.kind + (item.extra ? ": " + item.extra : "");
}).join("\n");
}
function testBasic() {
log = [];
o = {};
let o = {};
addWatchtowerTarget(o);
o.x = 1;
@ -28,7 +27,8 @@ function testBasic() {
Object.seal(o);
Object.freeze(o);
assertEq(log.join("\n"),
let log = getLogString(o);
assertEq(log,
`add-prop: x
add-prop: y
add-prop: <symbol>
@ -49,38 +49,38 @@ for (var i = 0; i < 20; i++) {
// Object.assign edge case.
function testAssign() {
o = {};
let o = {};
o.x = 1;
delete o.x;
let from = {x: 1, y: 2, z: 3, a: 4};
log = [];
addWatchtowerTarget(o);
addWatchtowerTarget(from);
Object.assign(o, from);
assertEq(log.join("\n"), "add-prop: x\nadd-prop: y\nadd-prop: z\nadd-prop: a");
let log = getLogString(o);
assertEq(log, "add-prop: x\nadd-prop: y\nadd-prop: z\nadd-prop: a");
}
testAssign();
function testJit() {
for (var i = 0; i < 20; i++) {
o = {};
let o = {};
addWatchtowerTarget(o);
log = [];
o.x = 1;
o.y = 2;
assertEq(log.join("\n"), "add-prop: x\nadd-prop: y");
let log = getLogString(o);
assertEq(log, "add-prop: x\nadd-prop: y");
}
}
testJit();
// array.length is a custom data property.
function testCustomDataProp() {
o = [];
log = [];
let o = [];
addWatchtowerTarget(o);
Object.defineProperty(o, "length", {writable: false});
assertEq(log.join("\n"), "change-prop: length");
let log = getLogString(o);
assertEq(log, "change-prop: length");
}
for (var i = 0; i < 20; i++) {
testCustomDataProp();

Просмотреть файл

@ -1027,7 +1027,6 @@ JSContext::JSContext(JSRuntime* runtime, const JS::ContextOptions& options)
#endif
generatingError(this, false),
cycleDetectorVector_(this, this),
watchtowerTestingCallback_(this),
data(nullptr),
asyncStackForNewActivations_(this),
asyncCauseForNewActivations(this, nullptr),

Просмотреть файл

@ -684,17 +684,6 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext,
return cycleDetectorVector_.ref();
}
private:
js::ContextData<JS::PersistentRooted<JSFunction*>> watchtowerTestingCallback_;
public:
JSFunction*& watchtowerTestingCallbackRef() {
if (!watchtowerTestingCallback_.ref().initialized()) {
watchtowerTestingCallback_.ref().init(this);
}
return watchtowerTestingCallback_.ref().get();
}
/* Client opaque pointer. */
js::UnprotectedData<void*> data;

Просмотреть файл

@ -186,12 +186,11 @@ class JSObject
return setFlag(cx, obj, js::ObjectFlag::IsUsedAsPrototype);
}
bool useWatchtowerTestingCallback() const {
return hasFlag(js::ObjectFlag::UseWatchtowerTestingCallback);
bool useWatchtowerTestingLog() const {
return hasFlag(js::ObjectFlag::UseWatchtowerTestingLog);
}
static bool setUseWatchtowerTestingCallback(JSContext* cx,
JS::HandleObject obj) {
return setFlag(cx, obj, js::ObjectFlag::UseWatchtowerTestingCallback);
static bool setUseWatchtowerTestingLog(JSContext* cx, JS::HandleObject obj) {
return setFlag(cx, obj, js::ObjectFlag::UseWatchtowerTestingLog);
}
// A "qualified" varobj is the object on which "qualified" variable

Просмотреть файл

@ -61,8 +61,8 @@ enum class ObjectFlag : uint16_t {
// objects. See also the SMDOC comment in vm/GetterSetter.h.
HadGetterSetterChange = 1 << 10,
// If set, invoke the watchtower testing callback for changes to this object.
UseWatchtowerTestingCallback = 1 << 11,
// If set, use the watchtower testing mechanism to log changes to this object.
UseWatchtowerTestingLog = 1 << 11,
};
using ObjectFlags = EnumFlags<ObjectFlag>;

Просмотреть файл

@ -121,6 +121,7 @@ JSRuntime::JSRuntime(JSRuntime* parentRuntime)
defaultLocale(nullptr),
profilingScripts(false),
scriptAndCountsVector(nullptr),
watchtowerTestingLog(nullptr),
lcovOutput_(),
jitRuntime_(nullptr),
gc(thisFromCtor()),
@ -220,6 +221,8 @@ void JSRuntime::destroyRuntime() {
sharedIntlData.ref().destroyInstance();
#endif
watchtowerTestingLog.ref().reset();
// Caches might hold on ScriptData which are saved in the ScriptDataTable.
// Clear all stencils from caches to remove ScriptDataTable entries.
caches().purgeStencils();

Просмотреть файл

@ -678,6 +678,10 @@ struct JSRuntime {
js::MainThreadData<JS::PersistentRooted<js::ScriptAndCountsVector>*>
scriptAndCountsVector;
using RootedPlainObjVec = JS::PersistentRooted<
JS::GCVector<js::PlainObject*, 0, js::SystemAllocPolicy>>;
js::MainThreadData<js::UniquePtr<RootedPlainObjVec>> watchtowerTestingLog;
private:
/* Code coverage output. */
js::UnprotectedData<js::coverage::LCovRuntime> lcovOutput_;

Просмотреть файл

@ -11,6 +11,7 @@
#include "vm/JSContext.h"
#include "vm/JSObject.h"
#include "vm/NativeObject.h"
#include "vm/PlainObject.h"
#include "vm/Realm.h"
#include "vm/Compartment-inl.h"
@ -19,38 +20,38 @@
using namespace js;
static bool InvokeWatchtowerCallback(JSContext* cx, const char* kind,
HandleObject obj, HandleValue extra) {
// Invoke the callback set by the setWatchtowerCallback testing function with
// arguments (kind, obj, extra).
static bool AddToWatchtowerLog(JSContext* cx, const char* kind,
HandleObject obj, HandleValue extra) {
// Add an object storing {kind, object, extra} to the log for testing
// purposes.
if (!cx->watchtowerTestingCallbackRef()) {
return true;
}
MOZ_ASSERT(obj->useWatchtowerTestingLog());
RootedString kindString(cx, NewStringCopyZ<CanGC>(cx, kind));
if (!kindString) {
return false;
}
constexpr size_t NumArgs = 3;
JS::RootedValueArray<NumArgs> argv(cx);
argv[0].setString(kindString);
argv[1].setObject(*obj);
argv[2].set(extra);
RootedValue funVal(cx, ObjectValue(*cx->watchtowerTestingCallbackRef()));
AutoRealm ar(cx, &funVal.toObject());
for (size_t i = 0; i < NumArgs; i++) {
if (!cx->compartment()->wrap(cx, argv[i])) {
return false;
}
Rooted<PlainObject*> logObj(cx, NewPlainObjectWithProto(cx, nullptr));
if (!logObj) {
return false;
}
if (!JS_DefineProperty(cx, logObj, "kind", kindString, JSPROP_ENUMERATE)) {
return false;
}
if (!JS_DefineProperty(cx, logObj, "object", obj, JSPROP_ENUMERATE)) {
return false;
}
if (!JS_DefineProperty(cx, logObj, "extra", extra, JSPROP_ENUMERATE)) {
return false;
}
RootedValue rval(cx);
return JS_CallFunctionValue(cx, nullptr, funVal, HandleValueArray(argv),
&rval);
if (!cx->runtime()->watchtowerTestingLog->append(logObj)) {
ReportOutOfMemory(cx);
return false;
}
return true;
}
static bool ReshapeForShadowedProp(JSContext* cx, Handle<NativeObject*> obj,
@ -111,9 +112,9 @@ bool Watchtower::watchPropertyAddSlow(JSContext* cx, Handle<NativeObject*> obj,
}
}
if (MOZ_UNLIKELY(obj->useWatchtowerTestingCallback())) {
if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
RootedValue val(cx, IdToValue(id));
if (!InvokeWatchtowerCallback(cx, "add-prop", obj, val)) {
if (!AddToWatchtowerLog(cx, "add-prop", obj, val)) {
return false;
}
}
@ -179,9 +180,9 @@ bool Watchtower::watchProtoChangeSlow(JSContext* cx, HandleObject obj) {
}
}
if (MOZ_UNLIKELY(obj->useWatchtowerTestingCallback())) {
if (!InvokeWatchtowerCallback(cx, "proto-change", obj,
JS::UndefinedHandleValue)) {
if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
if (!AddToWatchtowerLog(cx, "proto-change", obj,
JS::UndefinedHandleValue)) {
return false;
}
}
@ -199,9 +200,9 @@ bool Watchtower::watchPropertyRemoveSlow(JSContext* cx,
InvalidateMegamorphicCache(cx, obj);
}
if (MOZ_UNLIKELY(obj->useWatchtowerTestingCallback())) {
if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
RootedValue val(cx, IdToValue(id));
if (!InvokeWatchtowerCallback(cx, "remove-prop", obj, val)) {
if (!AddToWatchtowerLog(cx, "remove-prop", obj, val)) {
return false;
}
}
@ -219,9 +220,9 @@ bool Watchtower::watchPropertyChangeSlow(JSContext* cx,
InvalidateMegamorphicCache(cx, obj);
}
if (MOZ_UNLIKELY(obj->useWatchtowerTestingCallback())) {
if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
RootedValue val(cx, IdToValue(id));
if (!InvokeWatchtowerCallback(cx, "change-prop", obj, val)) {
if (!AddToWatchtowerLog(cx, "change-prop", obj, val)) {
return false;
}
}
@ -234,9 +235,9 @@ bool Watchtower::watchFreezeOrSealSlow(JSContext* cx,
Handle<NativeObject*> obj) {
MOZ_ASSERT(watchesFreezeOrSeal(obj));
if (MOZ_UNLIKELY(obj->useWatchtowerTestingCallback())) {
if (!InvokeWatchtowerCallback(cx, "freeze-or-seal", obj,
JS::UndefinedHandleValue)) {
if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
if (!AddToWatchtowerLog(cx, "freeze-or-seal", obj,
JS::UndefinedHandleValue)) {
return false;
}
}

Просмотреть файл

@ -46,28 +46,28 @@ class Watchtower {
public:
static bool watchesPropertyAdd(NativeObject* obj) {
return obj->hasAnyFlag({ObjectFlag::IsUsedAsPrototype,
ObjectFlag::UseWatchtowerTestingCallback});
return obj->hasAnyFlag(
{ObjectFlag::IsUsedAsPrototype, ObjectFlag::UseWatchtowerTestingLog});
}
static bool watchesPropertyRemove(NativeObject* obj) {
return obj->hasAnyFlag({ObjectFlag::IsUsedAsPrototype,
ObjectFlag::UseWatchtowerTestingCallback});
return obj->hasAnyFlag(
{ObjectFlag::IsUsedAsPrototype, ObjectFlag::UseWatchtowerTestingLog});
}
static bool watchesPropertyChange(NativeObject* obj) {
return obj->hasAnyFlag({ObjectFlag::IsUsedAsPrototype,
ObjectFlag::UseWatchtowerTestingCallback});
return obj->hasAnyFlag(
{ObjectFlag::IsUsedAsPrototype, ObjectFlag::UseWatchtowerTestingLog});
}
static bool watchesFreezeOrSeal(NativeObject* obj) {
return obj->hasAnyFlag({ObjectFlag::UseWatchtowerTestingCallback});
return obj->hasAnyFlag({ObjectFlag::UseWatchtowerTestingLog});
}
static bool watchesProtoChange(JSObject* obj) {
return obj->hasAnyFlag({ObjectFlag::IsUsedAsPrototype,
ObjectFlag::UseWatchtowerTestingCallback});
return obj->hasAnyFlag(
{ObjectFlag::IsUsedAsPrototype, ObjectFlag::UseWatchtowerTestingLog});
}
static bool watchesObjectSwap(JSObject* a, JSObject* b) {
auto watches = [](JSObject* obj) {
return obj->hasAnyFlag({ObjectFlag::IsUsedAsPrototype,
ObjectFlag::UseWatchtowerTestingCallback});
return obj->hasAnyFlag(
{ObjectFlag::IsUsedAsPrototype, ObjectFlag::UseWatchtowerTestingLog});
};
return watches(a) || watches(b);
}