SavedStacks.h (15192B)
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 #ifndef vm_SavedStacks_h 8 #define vm_SavedStacks_h 9 10 #include "mozilla/Attributes.h" 11 #include "mozilla/FastBernoulliTrial.h" 12 #include "mozilla/Maybe.h" 13 14 #include "js/ColumnNumber.h" // JS::TaggedColumnNumberOneOrigin 15 #include "js/HashTable.h" 16 #include "js/Stack.h" 17 #include "vm/SavedFrame.h" 18 19 namespace JS { 20 enum class SavedFrameSelfHosted; 21 } 22 23 namespace js { 24 25 class FrameIter; 26 27 // # Saved Stacks 28 // 29 // The `SavedStacks` class provides a compact way to capture and save JS stacks 30 // as `SavedFrame` `JSObject` subclasses. A single `SavedFrame` object 31 // represents one frame that was on the stack, and has a strong reference to its 32 // parent `SavedFrame` (the next youngest frame). This reference is null when 33 // the `SavedFrame` object is the oldest frame that was on the stack. 34 // 35 // This comment documents implementation. For usage documentation, see the 36 // `js/src/doc/SavedFrame/SavedFrame.md` file and relevant `SavedFrame` 37 // functions in `js/src/jsapi.h`. 38 // 39 // ## Compact 40 // 41 // Older saved stack frame tails are shared via hash consing, to deduplicate 42 // structurally identical data. `SavedStacks` contains a hash table of weakly 43 // held `SavedFrame` objects, and when the owning compartment is swept, it 44 // removes entries from this table that aren't held alive in any other way. When 45 // saving new stacks, we use this table to find pre-existing `SavedFrame` 46 // objects. If such an object is already extant, it is reused; otherwise a new 47 // `SavedFrame` is allocated and inserted into the table. 48 // 49 // Naive | Hash Consing 50 // --------------+------------------ 51 // c -> b -> a | c -> b -> a 52 // | ^ 53 // d -> b -> a | d ---| 54 // | | 55 // e -> b -> a | e ---' 56 // 57 // This technique is effective because of the nature of the events that trigger 58 // capturing the stack. Currently, these events consist primarily of `JSObject` 59 // allocation (when an observing `Debugger` has such tracking), `Promise` 60 // settlement, and `Error` object creation. While these events may occur many 61 // times, they tend to occur only at a few locations in the JS source. For 62 // example, if we enable Object allocation tracking and run the esprima 63 // JavaScript parser on its own JavaScript source, there are approximately 54700 64 // total `Object` allocations, but just ~1400 unique JS stacks at allocation 65 // time. There's only ~200 allocation sites if we capture only the youngest 66 // stack frame. 67 // 68 // ## Security and Wrappers 69 // 70 // We save every frame on the stack, regardless of whether the `SavedStack`'s 71 // compartment's principals subsume the frame's compartment's principals or 72 // not. This gives us maximum flexibility down the line when accessing and 73 // presenting captured stacks, but at the price of some complication involved in 74 // preventing the leakage of privileged stack frames to unprivileged callers. 75 // 76 // When a `SavedFrame` method or accessor is called, we compare the caller's 77 // compartment's principals to each `SavedFrame`'s captured principals. We avoid 78 // using the usual `CallNonGenericMethod` and `nativeCall` machinery which 79 // enters the `SavedFrame` object's compartment before we can check these 80 // principals, because we need access to the original caller's compartment's 81 // principals (unlike other `CallNonGenericMethod` users) to determine what view 82 // of the stack to present. Instead, we take a similar approach to that used by 83 // DOM methods, and manually unwrap wrappers until we get the underlying 84 // `SavedFrame` object, find the first `SavedFrame` in its stack whose captured 85 // principals are subsumed by the caller's principals, access the reserved slots 86 // we care about, and then rewrap return values as necessary. 87 // 88 // Consider the following diagram: 89 // 90 // Content Compartment 91 // +---------------------------------------+ 92 // | | 93 // | +------------------------+ | 94 // Chrome Compartment | | | | 95 // +--------------------+ | | SavedFrame C (content) | | 96 // | | | | | | 97 // | +--------------+ +------------------------+ | 98 // | | | ^ | 99 // | var x -----> | Xray Wrapper |-----. | | 100 // | | | | | | 101 // | +--------------+ | +------------------------+ | 102 // | | | | | | | 103 // | +--------------+ | | SavedFrame B (content) | | 104 // | | | | | | | 105 // | var y -----> | CCW (waived) |--. | +------------------------+ | 106 // | | | | | ^ | 107 // | +--------------+ | | | | 108 // | | | | | | | 109 // +--------------------+ | | | +------------------------+ | 110 // | | '-> | | | 111 // | | | SavedFrame A (chrome) | | 112 // | '----> | | | 113 // | +------------------------+ | 114 // | ^ | 115 // | | | 116 // | var z -----' | 117 // | | 118 // +---------------------------------------+ 119 // 120 // CCW is a plain cross-compartment wrapper, yielded by waiving Xray vision. A 121 // is the youngest `SavedFrame` and represents a frame that was from the chrome 122 // compartment, while B and C are from frames from the content compartment. C is 123 // the oldest frame. 124 // 125 // Note that it is always safe to waive an Xray around a SavedFrame object, 126 // because SavedFrame objects and the SavedFrame prototype are always frozen you 127 // will never run untrusted code. 128 // 129 // Depending on who the caller is, the view of the stack will be different, and 130 // is summarized in the table below. 131 // 132 // Var | View 133 // -----+------------ 134 // x | A -> B -> C 135 // y, z | B -> C 136 // 137 // In the case of x, the `SavedFrame` accessors are called with an Xray wrapper 138 // around the `SavedFrame` object as the `this` value, and the chrome 139 // compartment as the cx's current principals. Because the chrome compartment's 140 // principals subsume both itself and the content compartment's principals, x 141 // has the complete view of the stack. 142 // 143 // In the case of y, the cross-compartment machinery automatically enters the 144 // content compartment, and calls the `SavedFrame` accessors with the wrapped 145 // `SavedFrame` object as the `this` value. Because the cx's current compartment 146 // is the content compartment, and the content compartment's principals do not 147 // subsume the chrome compartment's principals, it can only see the B and C 148 // frames. 149 // 150 // In the case of z, the `SavedFrame` accessors are called with the `SavedFrame` 151 // object in the `this` value, and the content compartment as the cx's current 152 // compartment. Similar to the case of y, only the B and C frames are exposed 153 // because the cx's current compartment's principals do not subsume A's captured 154 // principals. 155 156 class SavedStacks { 157 friend class SavedFrame; 158 friend bool JS::ubi::ConstructSavedFrameStackSlow( 159 JSContext* cx, JS::ubi::StackFrame& ubiFrame, 160 MutableHandleObject outSavedFrameStack); 161 162 public: 163 SavedStacks() 164 : bernoulliSeeded(false), 165 bernoulli(1.0, 0x59fdad7f6b4cc573, 0x91adf38db96a9354), 166 creatingSavedFrame(false) {} 167 168 [[nodiscard]] bool saveCurrentStack( 169 JSContext* cx, MutableHandle<SavedFrame*> frame, 170 JS::StackCapture&& capture = JS::StackCapture(JS::AllFrames()), 171 HandleObject startAt = nullptr); 172 [[nodiscard]] bool copyAsyncStack( 173 JSContext* cx, HandleObject asyncStack, HandleString asyncCause, 174 MutableHandle<SavedFrame*> adoptedStack, 175 const mozilla::Maybe<size_t>& maxFrameCount); 176 void traceWeak(JSTracer* trc); 177 void trace(JSTracer* trc); 178 uint32_t count(); 179 void clear(); 180 void chooseSamplingProbability(JS::Realm* realm); 181 182 // Set the sampling random number generator's state to |state0| and 183 // |state1|. One or the other must be non-zero. See the comments for 184 // mozilla::non_crypto::XorShift128PlusRNG::setState for details. 185 void setRNGState(uint64_t state0, uint64_t state1) { 186 bernoulli.setRandomState(state0, state1); 187 } 188 189 size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf); 190 191 // An alloction metadata builder that marks cells with the JavaScript stack 192 // at which they were allocated. 193 struct MetadataBuilder : public AllocationMetadataBuilder { 194 MetadataBuilder() = default; 195 virtual JSObject* build(JSContext* cx, HandleObject obj, 196 AutoEnterOOMUnsafeRegion& oomUnsafe) const override; 197 }; 198 199 static const MetadataBuilder metadataBuilder; 200 201 private: 202 SavedFrame::Set frames; 203 bool bernoulliSeeded; 204 mozilla::FastBernoulliTrial bernoulli; 205 bool creatingSavedFrame; 206 207 // Similar to mozilla::ReentrancyGuard, but instead of asserting against 208 // reentrancy, just change the behavior of SavedStacks::saveCurrentStack to 209 // return a nullptr SavedFrame. 210 struct MOZ_RAII AutoReentrancyGuard { 211 SavedStacks& stacks; 212 213 explicit AutoReentrancyGuard(SavedStacks& stacks) : stacks(stacks) { 214 stacks.creatingSavedFrame = true; 215 } 216 217 ~AutoReentrancyGuard() { stacks.creatingSavedFrame = false; } 218 }; 219 220 [[nodiscard]] bool insertFrames(JSContext* cx, 221 MutableHandle<SavedFrame*> frame, 222 JS::StackCapture&& capture, 223 HandleObject startAt); 224 [[nodiscard]] bool adoptAsyncStack( 225 JSContext* cx, MutableHandle<SavedFrame*> asyncStack, 226 Handle<JSAtom*> asyncCause, const mozilla::Maybe<size_t>& maxFrameCount); 227 [[nodiscard]] bool checkForEvalInFramePrev( 228 JSContext* cx, MutableHandle<SavedFrame::Lookup> lookup); 229 SavedFrame* getOrCreateSavedFrame(JSContext* cx, 230 Handle<SavedFrame::Lookup> lookup); 231 SavedFrame* createFrameFromLookup(JSContext* cx, 232 Handle<SavedFrame::Lookup> lookup); 233 void setSamplingProbability(double probability); 234 235 // Cache for memoizing PCToLineNumber lookups. 236 237 struct PCKey { 238 PCKey(JSScript* script, jsbytecode* pc) : script(script), pc(pc) {} 239 240 WeakHeapPtr<JSScript*> script; 241 jsbytecode* pc; 242 243 void trace(JSTracer* trc) { /* PCKey is weak. */ } 244 bool traceWeak(JSTracer* trc) { 245 return TraceWeakEdge(trc, &script, "traceWeak"); 246 } 247 }; 248 249 public: 250 struct LocationValue { 251 LocationValue() : source(nullptr), sourceId(0), line(0) {} 252 LocationValue(JSAtom* source, uint32_t sourceId, size_t line, 253 JS::TaggedColumnNumberOneOrigin column) 254 : source(source), sourceId(sourceId), line(line), column(column) {} 255 256 void trace(JSTracer* trc) { 257 TraceNullableEdge(trc, &source, "SavedStacks::LocationValue::source"); 258 } 259 260 bool traceWeak(JSTracer* trc) { 261 MOZ_ASSERT(source); 262 // TODO: Bug 1501334: IsAboutToBeFinalized doesn't work for atoms. 263 // Otherwise we should assert TraceWeakEdge always returns true; 264 return TraceWeakEdge(trc, &source, "traceWeak"); 265 } 266 267 WeakHeapPtr<JSAtom*> source; 268 uint32_t sourceId; 269 270 // Line number (1-origin). 271 size_t line; 272 273 // Column number in UTF-16 code units. 274 JS::TaggedColumnNumberOneOrigin column; 275 }; 276 277 private: 278 struct PCLocationHasher : public DefaultHasher<PCKey> { 279 using ScriptPtrHasher = DefaultHasher<JSScript*>; 280 using BytecodePtrHasher = DefaultHasher<jsbytecode*>; 281 282 static HashNumber hash(const PCKey& key) { 283 return mozilla::AddToHash(ScriptPtrHasher::hash(key.script), 284 BytecodePtrHasher::hash(key.pc)); 285 } 286 287 static bool match(const PCKey& l, const PCKey& k) { 288 return ScriptPtrHasher::match(l.script, k.script) && 289 BytecodePtrHasher::match(l.pc, k.pc); 290 } 291 }; 292 293 // We eagerly Atomize the script source stored in LocationValue because 294 // wasm does not always have a JSScript and the source might not be 295 // available when we need it later. However, since the JSScript does not 296 // actually hold this atom, we have to trace it strongly to keep it alive. 297 // Thus, it takes two GC passes to fully clean up this table: the first GC 298 // removes the dead script; the second will clear out the source atom since 299 // it is no longer held by the table. 300 using PCLocationMap = 301 GCHashMap<PCKey, LocationValue, PCLocationHasher, SystemAllocPolicy>; 302 PCLocationMap pcLocationMap; 303 304 [[nodiscard]] bool getLocation(JSContext* cx, const FrameIter& iter, 305 MutableHandle<LocationValue> locationp); 306 }; 307 308 template <typename Wrapper> 309 struct WrappedPtrOperations<SavedStacks::LocationValue, Wrapper> { 310 JSAtom* source() const { return loc().source; } 311 uint32_t sourceId() const { return loc().sourceId; } 312 size_t line() const { return loc().line; } 313 JS::TaggedColumnNumberOneOrigin column() const { return loc().column; } 314 315 private: 316 const SavedStacks::LocationValue& loc() const { 317 return static_cast<const Wrapper*>(this)->get(); 318 } 319 }; 320 321 template <typename Wrapper> 322 struct MutableWrappedPtrOperations<SavedStacks::LocationValue, Wrapper> 323 : public WrappedPtrOperations<SavedStacks::LocationValue, Wrapper> { 324 void setSource(JSAtom* v) { loc().source = v; } 325 void setSourceId(uint32_t v) { loc().sourceId = v; } 326 void setLine(size_t v) { loc().line = v; } 327 void setColumn(JS::TaggedColumnNumberOneOrigin v) { loc().column = v; } 328 329 private: 330 SavedStacks::LocationValue& loc() { 331 return static_cast<Wrapper*>(this)->get(); 332 } 333 }; 334 335 JS::UniqueChars BuildUTF8StackString(JSContext* cx, JSPrincipals* principals, 336 HandleObject stack); 337 338 js::SavedFrame* UnwrapSavedFrame(JSContext* cx, JSPrincipals* principals, 339 HandleObject obj, 340 JS::SavedFrameSelfHosted selfHosted, 341 bool& skippedAsync); 342 343 } /* namespace js */ 344 345 #endif /* vm_SavedStacks_h */