Skip to content

Commit

Permalink
Bug 1782495 - Replace watchtower testing callback with a log-based me…
Browse files Browse the repository at this point in the history
…chanism. 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
  • Loading branch information
jandem committed Oct 18, 2022
1 parent 10083d8 commit 253c86b
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 98 deletions.
46 changes: 34 additions & 12 deletions js/src/builtin/TestingFunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
return false;
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();
}

cx->watchtowerTestingCallbackRef() = fun;
ArrayObject* arr = NewDenseCopiedArray(cx, values.length(), values.begin());
if (!arr) {
return false;
}

args.rval().setUndefined();
args.rval().setObject(*arr);
return true;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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"),
Expand Down
44 changes: 22 additions & 22 deletions js/src/jit-test/tests/watchtower/basic.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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>
Expand All @@ -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();
Expand Down
1 change: 0 additions & 1 deletion js/src/vm/JSContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
11 changes: 0 additions & 11 deletions js/src/vm/JSContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
9 changes: 4 additions & 5 deletions js/src/vm/JSObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions js/src/vm/ObjectFlags.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
3 changes: 3 additions & 0 deletions js/src/vm/Runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ JSRuntime::JSRuntime(JSRuntime* parentRuntime)
defaultLocale(nullptr),
profilingScripts(false),
scriptAndCountsVector(nullptr),
watchtowerTestingLog(nullptr),
lcovOutput_(),
jitRuntime_(nullptr),
gc(thisFromCtor()),
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions js/src/vm/Runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
Expand Down
69 changes: 35 additions & 34 deletions js/src/vm/Watchtower.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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());
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;
}

for (size_t i = 0; i < NumArgs; i++) {
if (!cx->compartment()->wrap(cx, argv[i])) {
return false;
}
if (!cx->runtime()->watchtowerTestingLog->append(logObj)) {
ReportOutOfMemory(cx);
return false;
}

RootedValue rval(cx);
return JS_CallFunctionValue(cx, nullptr, funVal, HandleValueArray(argv),
&rval);
return true;
}

static bool ReshapeForShadowedProp(JSContext* cx, Handle<NativeObject*> obj,
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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;
}
}
Expand All @@ -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;
}
}
Expand All @@ -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;
}
}
Expand Down
Loading

0 comments on commit 253c86b

Please sign in to comment.