1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  * vim: set ts=8 sts=2 et sw=2 tw=80:
3  * This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "vm/Watchtower.h"
8 
9 #include "js/CallAndConstruct.h"
10 #include "js/Id.h"
11 #include "vm/Compartment.h"
12 #include "vm/JSContext.h"
13 #include "vm/JSObject.h"
14 #include "vm/NativeObject.h"
15 #include "vm/Realm.h"
16 
17 #include "vm/Compartment-inl.h"
18 #include "vm/JSObject-inl.h"
19 
20 using namespace js;
21 
InvokeWatchtowerCallback(JSContext * cx,const char * kind,HandleObject obj,HandleValue extra)22 static bool InvokeWatchtowerCallback(JSContext* cx, const char* kind,
23                                      HandleObject obj, HandleValue extra) {
24   // Invoke the callback set by the setWatchtowerCallback testing function with
25   // arguments (kind, obj, extra).
26 
27   if (!cx->watchtowerTestingCallbackRef()) {
28     return true;
29   }
30 
31   RootedString kindString(cx, NewStringCopyZ<CanGC>(cx, kind));
32   if (!kindString) {
33     return false;
34   }
35 
36   constexpr size_t NumArgs = 3;
37   JS::RootedValueArray<NumArgs> argv(cx);
38   argv[0].setString(kindString);
39   argv[1].setObject(*obj);
40   argv[2].set(extra);
41 
42   RootedValue funVal(cx, ObjectValue(*cx->watchtowerTestingCallbackRef()));
43   AutoRealm ar(cx, &funVal.toObject());
44 
45   for (size_t i = 0; i < NumArgs; i++) {
46     if (!cx->compartment()->wrap(cx, argv[i])) {
47       return false;
48     }
49   }
50 
51   RootedValue rval(cx);
52   return JS_CallFunctionValue(cx, nullptr, funVal, HandleValueArray(argv),
53                               &rval);
54 }
55 
ReshapeForShadowedProp(JSContext * cx,HandleNativeObject obj,HandleId id)56 static bool ReshapeForShadowedProp(JSContext* cx, HandleNativeObject obj,
57                                    HandleId id) {
58   // |obj| has been used as the prototype of another object. Check if we're
59   // shadowing a property on its proto chain. In this case we need to reshape
60   // that object for shape teleporting to work correctly.
61   //
62   // See also the 'Shape Teleporting Optimization' comment in jit/CacheIR.cpp.
63 
64   MOZ_ASSERT(obj->isUsedAsPrototype());
65 
66   // Lookups on integer ids cannot be cached through prototypes.
67   if (id.isInt()) {
68     return true;
69   }
70 
71   RootedObject proto(cx, obj->staticPrototype());
72   while (proto) {
73     // Lookups will not be cached through non-native protos.
74     if (!proto->is<NativeObject>()) {
75       break;
76     }
77 
78     if (proto->as<NativeObject>().contains(cx, id)) {
79       return JSObject::setInvalidatedTeleporting(cx, proto);
80     }
81 
82     proto = proto->staticPrototype();
83   }
84 
85   return true;
86 }
87 
InvalidateMegamorphicCache(JSContext * cx,HandleNativeObject obj)88 static void InvalidateMegamorphicCache(JSContext* cx, HandleNativeObject obj) {
89   // The megamorphic cache only checks the receiver object's shape. We need to
90   // invalidate the cache when a prototype object changes its set of properties,
91   // to account for cached properties that are deleted, turned into an accessor
92   // property, or shadowed by another object on the proto chain.
93 
94   MOZ_ASSERT(obj->isUsedAsPrototype());
95 
96   cx->caches().megamorphicCache.bumpGeneration();
97 }
98 
99 // static
watchPropertyAddSlow(JSContext * cx,HandleNativeObject obj,HandleId id)100 bool Watchtower::watchPropertyAddSlow(JSContext* cx, HandleNativeObject obj,
101                                       HandleId id) {
102   MOZ_ASSERT(watchesPropertyAdd(obj));
103 
104   if (obj->isUsedAsPrototype()) {
105     if (!ReshapeForShadowedProp(cx, obj, id)) {
106       return false;
107     }
108     if (!id.isInt()) {
109       InvalidateMegamorphicCache(cx, obj);
110     }
111   }
112 
113   if (MOZ_UNLIKELY(obj->useWatchtowerTestingCallback())) {
114     RootedValue val(cx, IdToValue(id));
115     if (!InvokeWatchtowerCallback(cx, "add-prop", obj, val)) {
116       return false;
117     }
118   }
119 
120   return true;
121 }
122 
ReshapeForProtoMutation(JSContext * cx,HandleObject obj)123 static bool ReshapeForProtoMutation(JSContext* cx, HandleObject obj) {
124   // To avoid the JIT guarding on each prototype in the proto chain to detect
125   // prototype mutation, we can instead reshape the rest of the proto chain such
126   // that a guard on any of them is sufficient. To avoid excessive reshaping and
127   // invalidation, we apply heuristics to decide when to apply this and when
128   // to require a guard.
129   //
130   // There are two cases:
131   //
132   // (1) The object is not marked IsUsedAsPrototype. This is the common case.
133   //     Because shape implies proto, we rely on the caller changing the
134   //     object's shape. The JIT guards on this object's shape or prototype so
135   //     there's nothing we have to do here for objects on the proto chain.
136   //
137   // (2) The object is marked IsUsedAsPrototype. This implies the object may be
138   //     participating in shape teleporting. To invalidate JIT ICs depending on
139   //     the proto chain being unchanged, set the InvalidatedTeleporting shape
140   //     flag for this object and objects on its proto chain.
141   //
142   //     This flag disables future shape teleporting attempts, so next time this
143   //     happens the loop below will be a no-op.
144   //
145   // NOTE: We only handle NativeObjects and don't propagate reshapes through
146   //       any non-native objects on the chain.
147   //
148   // See Also:
149   //  - GeneratePrototypeGuards
150   //  - GeneratePrototypeHoleGuards
151 
152   MOZ_ASSERT(obj->isUsedAsPrototype());
153 
154   RootedObject pobj(cx, obj);
155 
156   while (pobj && pobj->is<NativeObject>()) {
157     if (!pobj->hasInvalidatedTeleporting()) {
158       if (!JSObject::setInvalidatedTeleporting(cx, pobj)) {
159         return false;
160       }
161     }
162     pobj = pobj->staticPrototype();
163   }
164 
165   return true;
166 }
167 
168 // static
watchProtoChangeSlow(JSContext * cx,HandleObject obj)169 bool Watchtower::watchProtoChangeSlow(JSContext* cx, HandleObject obj) {
170   MOZ_ASSERT(watchesProtoChange(obj));
171 
172   if (obj->isUsedAsPrototype()) {
173     if (!ReshapeForProtoMutation(cx, obj)) {
174       return false;
175     }
176     if (obj->is<NativeObject>()) {
177       InvalidateMegamorphicCache(cx, obj.as<NativeObject>());
178     }
179   }
180 
181   if (MOZ_UNLIKELY(obj->useWatchtowerTestingCallback())) {
182     if (!InvokeWatchtowerCallback(cx, "proto-change", obj,
183                                   JS::UndefinedHandleValue)) {
184       return false;
185     }
186   }
187 
188   return true;
189 }
190 
191 // static
watchPropertyRemoveSlow(JSContext * cx,HandleNativeObject obj,HandleId id)192 bool Watchtower::watchPropertyRemoveSlow(JSContext* cx, HandleNativeObject obj,
193                                          HandleId id) {
194   MOZ_ASSERT(watchesPropertyRemove(obj));
195 
196   if (obj->isUsedAsPrototype() && !id.isInt()) {
197     InvalidateMegamorphicCache(cx, obj);
198   }
199 
200   if (MOZ_UNLIKELY(obj->useWatchtowerTestingCallback())) {
201     RootedValue val(cx, IdToValue(id));
202     if (!InvokeWatchtowerCallback(cx, "remove-prop", obj, val)) {
203       return false;
204     }
205   }
206 
207   return true;
208 }
209 
210 // static
watchPropertyChangeSlow(JSContext * cx,HandleNativeObject obj,HandleId id)211 bool Watchtower::watchPropertyChangeSlow(JSContext* cx, HandleNativeObject obj,
212                                          HandleId id) {
213   MOZ_ASSERT(watchesPropertyChange(obj));
214 
215   if (obj->isUsedAsPrototype() && !id.isInt()) {
216     InvalidateMegamorphicCache(cx, obj);
217   }
218 
219   if (MOZ_UNLIKELY(obj->useWatchtowerTestingCallback())) {
220     RootedValue val(cx, IdToValue(id));
221     if (!InvokeWatchtowerCallback(cx, "change-prop", obj, val)) {
222       return false;
223     }
224   }
225 
226   return true;
227 }
228 
229 // static
watchFreezeOrSealSlow(JSContext * cx,HandleNativeObject obj)230 bool Watchtower::watchFreezeOrSealSlow(JSContext* cx, HandleNativeObject obj) {
231   MOZ_ASSERT(watchesFreezeOrSeal(obj));
232 
233   if (MOZ_UNLIKELY(obj->useWatchtowerTestingCallback())) {
234     if (!InvokeWatchtowerCallback(cx, "freeze-or-seal", obj,
235                                   JS::UndefinedHandleValue)) {
236       return false;
237     }
238   }
239 
240   return true;
241 }
242 
243 // static
watchObjectSwapSlow(JSContext * cx,HandleObject a,HandleObject b)244 bool Watchtower::watchObjectSwapSlow(JSContext* cx, HandleObject a,
245                                      HandleObject b) {
246   MOZ_ASSERT(watchesObjectSwap(a, b));
247 
248   if (a->isUsedAsPrototype() && a->is<NativeObject>()) {
249     InvalidateMegamorphicCache(cx, a.as<NativeObject>());
250   }
251   if (b->isUsedAsPrototype() && b->is<NativeObject>()) {
252     InvalidateMegamorphicCache(cx, b.as<NativeObject>());
253   }
254 
255   if (MOZ_UNLIKELY(a->useWatchtowerTestingCallback() ||
256                    b->useWatchtowerTestingCallback())) {
257     RootedValue extra(cx, ObjectValue(*b));
258     if (!InvokeWatchtowerCallback(cx, "object-swap", a, extra)) {
259       // The JSObject::swap caller unfortunately assumes failures are OOM and
260       // crashes. Ignore non-OOM exceptions for now.
261       if (cx->isThrowingOutOfMemory()) {
262         return false;
263       }
264       cx->clearPendingException();
265     }
266   }
267 
268   return true;
269 }
270