diff options
author | Jason Carey <jcarey@argv.me> | 2015-09-16 18:18:46 -0400 |
---|---|---|
committer | Jason Carey <jcarey@argv.me> | 2015-09-21 18:25:39 -0400 |
commit | ee4f910322988cb9ba4784472a38a16ce2c0cdc9 (patch) | |
tree | d06b26e9ef598b71937f1c6aab5f10488663556d /src/mongo/scripting/mozjs | |
parent | a74ecb2a746e4d8a8ab78610c07d509788c4d8ad (diff) | |
download | mongo-ee4f910322988cb9ba4784472a38a16ce2c0cdc9.tar.gz |
SERVER-19607 no recursion in JS -> BSON conversion
Replace functional recursion in javascript object to bson conversion
with an explicit stack to minimize the memory cost of processing very
deep / cyclical objects. This prevents stack overflows on debug and
non-optimized builds on some platforms.
Diffstat (limited to 'src/mongo/scripting/mozjs')
-rw-r--r-- | src/mongo/scripting/mozjs/implscope.cpp | 10 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/jsthread.cpp | 7 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/lifetimestack.h | 134 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/nativefunction.cpp | 6 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/objectwrapper.cpp | 131 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/objectwrapper.h | 71 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/proxyscope.cpp | 49 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/proxyscope.h | 6 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/valuereader.cpp | 10 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/valuereader.h | 3 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/valuewriter.cpp | 204 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/valuewriter.h | 19 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/wraptype.h | 4 |
13 files changed, 488 insertions, 166 deletions
diff --git a/src/mongo/scripting/mozjs/implscope.cpp b/src/mongo/scripting/mozjs/implscope.cpp index 2f6cf57e2bd..384dd2ce83e 100644 --- a/src/mongo/scripting/mozjs/implscope.cpp +++ b/src/mongo/scripting/mozjs/implscope.cpp @@ -278,6 +278,9 @@ MozJSImplScope::MozJSImplScope(MozJSScriptEngine* engine) _checkErrorState(JS_InitStandardClasses(_context, _global)); installBSONTypes(); + + JS_FireOnNewGlobalObject(_context, _global); + execSetup(JSFiles::assert); execSetup(JSFiles::types); @@ -425,10 +428,11 @@ BSONObj MozJSImplScope::callThreadArgs(const BSONObj& args) { _checkErrorState(JS::Call(_context, thisv, function, argv, &out), false, true); - BSONObjBuilder b; - ValueWriter(_context, out).writeThis(&b, "ret"); + JS::RootedObject rout(_context, JS_NewPlainObject(_context)); + ObjectWrapper wout(_context, rout); + wout.setValue("ret", out); - return b.obj(); + return wout.toBSON(); } bool hasFunctionIdentifier(StringData code) { diff --git a/src/mongo/scripting/mozjs/jsthread.cpp b/src/mongo/scripting/mozjs/jsthread.cpp index 3b457851efd..5c8eef51b65 100644 --- a/src/mongo/scripting/mozjs/jsthread.cpp +++ b/src/mongo/scripting/mozjs/jsthread.cpp @@ -87,16 +87,17 @@ public: "first argument must be a function", args.get(0).isObject() && JS_ObjectIsFunction(cx, args.get(0).toObjectOrNull())); - BSONObjBuilder b; + JS::RootedObject robj(cx, JS_NewPlainObject(cx)); + ObjectWrapper wobj(cx, robj); for (unsigned i = 0; i < args.length(); ++i) { // 10 decimal digits for a 32 bit unsigned, then 1 for the null char buf[11]; std::sprintf(buf, "%i", i); - ValueWriter(cx, args.get(i)).writeThis(&b, buf); + wobj.setValue(buf, args.get(i)); } - _sharedData->_args = b.obj(); + _sharedData->_args = wobj.toBSON(); _sharedData->_stack = currentJSStackToString(cx); diff --git a/src/mongo/scripting/mozjs/lifetimestack.h b/src/mongo/scripting/mozjs/lifetimestack.h new file mode 100644 index 00000000000..0954a525ccb --- /dev/null +++ b/src/mongo/scripting/mozjs/lifetimestack.h @@ -0,0 +1,134 @@ +/* Copyright 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#pragma once + +#include <cstddef> +#include <type_traits> +#include <utility> + +#include "mongo/util/assert_util.h" + +namespace mongo { +namespace mozjs { + +/** + * Implements a stack with: + * o specific LIFO lifetime guarantees + * o builtin storage (based on template parameter) + * + * This is useful for manipulating stacks of types which are non-movable and + * non-copyable (and thus cannot be put into standard containers). The lack of + * an allocator additionally supports types that must live in particular + * region of memory (like the stack vs. the heap). + * + * We need this to store GC Rooting types from spidermonkey safely. + */ +template <typename T, std::size_t N> +class LifetimeStack { +public: + // Boiler plate typedefs + using value_type = T; + using size_type = std::size_t; + using reference = value_type&; + using const_reference = const value_type&; + using pointer = T*; + using const_pointer = const T*; + + LifetimeStack() = default; + + ~LifetimeStack() { + while (size()) { + pop(); + } + } + + /** + * Stacks are non-copyable and non-movable + */ + LifetimeStack(const LifetimeStack&) = delete; + LifetimeStack& operator=(const LifetimeStack&) = delete; + + LifetimeStack(LifetimeStack&& other) = delete; + LifetimeStack& operator=(LifetimeStack&& other) = delete; + + template <typename... Args> + void emplace(Args&&... args) { + invariant(_size <= N); + + new (_data() + _size) T(std::forward<Args>(args)...); + + _size++; + } + + void pop() { + invariant(_size > 0); + + (&top())->~T(); + + _size--; + } + + const_reference top() const { + invariant(_size > 0); + return _data()[_size - 1]; + } + + reference top() { + invariant(_size > 0); + return _data()[_size - 1]; + } + + size_type size() const { + return _size; + } + + bool empty() const { + return _size == 0; + } + + size_type capacity() const { + return N; + } + +private: + pointer _data() { + return reinterpret_cast<pointer>(&_storage); + } + + const_pointer _data() const { + return reinterpret_cast<const_pointer>(&_storage); + } + +private: + typename std::aligned_storage<sizeof(T) * N, std::alignment_of<T>::value>::type _storage; + + size_type _size = 0; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/nativefunction.cpp b/src/mongo/scripting/mozjs/nativefunction.cpp index ef5423f0f21..4d019a5b347 100644 --- a/src/mongo/scripting/mozjs/nativefunction.cpp +++ b/src/mongo/scripting/mozjs/nativefunction.cpp @@ -78,6 +78,8 @@ void NativeFunctionInfo::call(JSContext* cx, JS::CallArgs args) { } BSONObjBuilder bob; + JS::RootedObject robj(cx, JS_NewPlainObject(cx)); + ObjectWrapper wobj(cx, robj); for (unsigned i = 0; i < args.length(); i++) { // 11 is enough here. unsigned's are only 32 bits, and 1 << 32 is only @@ -85,10 +87,10 @@ void NativeFunctionInfo::call(JSContext* cx, JS::CallArgs args) { char buf[11]; std::sprintf(buf, "%i", i); - ValueWriter(cx, args.get(i)).writeThis(&bob, buf); + wobj.setValue(buf, args.get(i)); } - BSONObj out = holder->_func(bob.obj(), holder->_ctx); + BSONObj out = holder->_func(wobj.toBSON(), holder->_ctx); ValueReader(cx, args.rval()).fromBSONElement(out.firstElement(), false); } diff --git a/src/mongo/scripting/mozjs/objectwrapper.cpp b/src/mongo/scripting/mozjs/objectwrapper.cpp index 2dc17726b37..2213ec2a029 100644 --- a/src/mongo/scripting/mozjs/objectwrapper.cpp +++ b/src/mongo/scripting/mozjs/objectwrapper.cpp @@ -41,6 +41,10 @@ namespace mongo { namespace mozjs { +#ifndef _MSC_EXTENSIONS +const int ObjectWrapper::kMaxWriteFieldDepth; +#endif // _MSC_EXTENSIONS + void ObjectWrapper::Key::get(JSContext* cx, JS::HandleObject o, JS::MutableHandleValue value) { switch (_type) { case Type::Field: @@ -173,11 +177,11 @@ std::string ObjectWrapper::Key::toString(JSContext* cx) { cx, ErrorCodes::InternalError, "Failed to toString a ObjectWrapper::Key"); } -ObjectWrapper::ObjectWrapper(JSContext* cx, JS::HandleObject obj, int depth) - : _context(cx), _object(cx, obj), _depth(depth) {} +ObjectWrapper::ObjectWrapper(JSContext* cx, JS::HandleObject obj) + : _context(cx), _object(cx, obj) {} -ObjectWrapper::ObjectWrapper(JSContext* cx, JS::HandleValue value, int depth) - : _context(cx), _object(cx, value.toObjectOrNull()), _depth(depth) {} +ObjectWrapper::ObjectWrapper(JSContext* cx, JS::HandleValue value) + : _context(cx), _object(cx, value.toObjectOrNull()) {} double ObjectWrapper::getNumber(Key key) { JS::RootedValue x(_context); @@ -225,7 +229,7 @@ BSONObj ObjectWrapper::getObject(Key key) { JS::RootedValue x(_context); getValue(key, &x); - return ValueWriter(_context, x, _depth).toBSON(); + return ValueWriter(_context, x).toBSON(); } void ObjectWrapper::getValue(Key key, JS::MutableHandleValue value) { @@ -255,14 +259,14 @@ void ObjectWrapper::setBoolean(Key key, bool val) { void ObjectWrapper::setBSONElement(Key key, const BSONElement& elem, bool readOnly) { JS::RootedValue value(_context); - ValueReader(_context, &value, _depth).fromBSONElement(elem, readOnly); + ValueReader(_context, &value).fromBSONElement(elem, readOnly); setValue(key, value); } void ObjectWrapper::setBSON(Key key, const BSONObj& obj, bool readOnly) { JS::RootedValue value(_context); - ValueReader(_context, &value, _depth).fromBSON(obj, readOnly); + ValueReader(_context, &value).fromBSON(obj, readOnly); setValue(key, value); } @@ -339,54 +343,121 @@ void ObjectWrapper::callMethod(JS::HandleValue fun, JS::MutableHandleValue out) callMethod(fun, args, out); } -void ObjectWrapper::writeThis(BSONObjBuilder* b) { - auto scope = getScope(_context); - - BSONObj* originalBSON = nullptr; - if (scope->getProto<BSONInfo>().instanceOf(_object)) { +BSONObj ObjectWrapper::toBSON() { + if (getScope(_context)->getProto<BSONInfo>().instanceOf(_object)) { + BSONObj* originalBSON = nullptr; bool altered; std::tie(originalBSON, altered) = BSONInfo::originalBSON(_context, _object); - if (originalBSON && !altered) { - b->appendElements(*originalBSON); - return; - } + if (originalBSON && !altered) + return *originalBSON; } + JS::RootedId id(_context); + + // INCREDIBLY SUBTLE BEHAVIOR: + // + // (jcarey): Be very careful about how the Rooting API is used in + // relationship to WriteFieldRecursionFrames. Mozilla'a API more or less + // demands that the rooting types are on the stack and only manipulated as + // regular objects, which we aren't doing here. The reason they do this is + // because the rooting types must be global created and destroyed in an + // entirely linear order. This is impossible to screw up in regular use, + // but our unwinding of the recursion frames makes it easy to do here. + // + // The roots above need to be before the first frame is emplaced (so + // they'll be destroyed after it) and none of the roots in the below code + // (or in ValueWriter::writeThis) can live longer than until the call to + // emplace() inside ValueWriter. The runtime asserts enabled by MozJS's + // debug mode will catch runtime errors, but be aware of how difficult this + // is to get right and what to look for if one of them bites you. + WriteFieldRecursionFrames frames; + frames.emplace(_context, _object, nullptr, StringData{}); + + BSONObjBuilder b; + // We special case the _id field in top-level objects and move it to the front. // This matches other drivers behavior and makes finding the _id field quicker in BSON. - if (_depth == 0 && hasField("_id")) { - _writeField(b, "_id", originalBSON); + if (hasField("_id")) { + _writeField(&b, "_id", &frames, frames.top().originalBSON); } - enumerate([&](JS::HandleId id) { - JS::RootedValue x(_context); + while (frames.size()) { + auto& frame = frames.top(); + + // If the index is the same as length, we've seen all the keys at this + // level and should go up a level + if (frame.idx == frame.ids.length()) { + frames.pop(); + continue; + } - IdWrapper idw(_context, id); + if (frame.idx == 0 && frame.originalBSON && !frame.altered) { + // If this is our first look at the object and it has an unaltered + // bson behind it, move idx to the end so we'll roll up on the next + // pass through the loop. + frame.subbob_or(&b)->appendElements(*frame.originalBSON); + frame.idx = frame.ids.length(); + continue; + } - if (_depth == 0 && idw.isString() && idw.equals("_id")) - return; + id.set(frame.ids[frame.idx++]); - _writeField(b, id, originalBSON); - }); + if (frames.size() == 1) { + IdWrapper idw(_context, id); - const int sizeWithEOO = b->len() + 1 /*EOO*/ - 4 /*BSONObj::Holder ref count*/; + if (idw.isString() && idw.equals("_id")) { + continue; + } + } + + // writeField invokes ValueWriter with the frame stack, which will push + // onto frames for subobjects, which will effectively recurse the loop. + _writeField(frame.subbob_or(&b), JS::HandleId(id), &frames, frame.originalBSON); + } + + const int sizeWithEOO = b.len() + 1 /*EOO*/ - 4 /*BSONObj::Holder ref count*/; uassert(17260, str::stream() << "Converting from JavaScript to BSON failed: " << "Object size " << sizeWithEOO << " exceeds limit of " << BSONObjMaxInternalSize << " bytes.", sizeWithEOO <= BSONObjMaxInternalSize); + + return b.obj(); +} + +ObjectWrapper::WriteFieldRecursionFrame::WriteFieldRecursionFrame(JSContext* cx, + JSObject* obj, + BSONObjBuilder* parent, + StringData sd) + : thisv(cx, obj), ids(cx, JS_Enumerate(cx, thisv)) { + if (parent) { + subbob.emplace(JS_IsArrayObject(cx, thisv) ? parent->subarrayStart(sd) + : parent->subobjStart(sd)); + } + + if (!ids) { + throwCurrentJSException( + cx, ErrorCodes::JSInterpreterFailure, "Failure to enumerate object"); + } + + if (getScope(cx)->getProto<BSONInfo>().instanceOf(thisv)) { + std::tie(originalBSON, altered) = BSONInfo::originalBSON(cx, thisv); + } } -void ObjectWrapper::_writeField(BSONObjBuilder* b, Key key, BSONObj* originalParent) { +void ObjectWrapper::_writeField(BSONObjBuilder* b, + Key key, + WriteFieldRecursionFrames* frames, + BSONObj* originalParent) { JS::RootedValue value(_context); - key.get(_context, _object, &value); + key.get(_context, frames->top().thisv, &value); - ValueWriter x(_context, value, _depth); + ValueWriter x(_context, value); x.setOriginalBSON(originalParent); - x.writeThis(b, key.toString(_context)); + x.writeThis(b, key.toString(_context), frames); } std::string ObjectWrapper::getClassName() { diff --git a/src/mongo/scripting/mozjs/objectwrapper.h b/src/mongo/scripting/mozjs/objectwrapper.h index 57bdc728494..3134083f243 100644 --- a/src/mongo/scripting/mozjs/objectwrapper.h +++ b/src/mongo/scripting/mozjs/objectwrapper.h @@ -31,8 +31,10 @@ #include <jsapi.h> #include <string> +#include "mongo/bson/bsonobjbuilder.h" #include "mongo/platform/decimal128.h" #include "mongo/scripting/mozjs/exception.h" +#include "mongo/scripting/mozjs/lifetimestack.h" namespace mongo { @@ -43,6 +45,7 @@ class BSONElement; namespace mozjs { class MozJSImplScope; +class ValueWriter; /** * Wraps JSObject's with helpers for accessing their properties @@ -51,6 +54,8 @@ class MozJSImplScope; * not movable or copyable */ class ObjectWrapper { + friend class ValueWriter; + public: /** * Helper subclass that provides some easy boilerplate for accessing @@ -86,12 +91,8 @@ public: Type _type; }; - /** - * The depth parameter here allows us to detect overly nested or circular - * objects and bail without blowing the stack. - */ - ObjectWrapper(JSContext* cx, JS::HandleObject obj, int depth = 0); - ObjectWrapper(JSContext* cx, JS::HandleValue value, int depth = 0); + ObjectWrapper(JSContext* cx, JS::HandleObject obj); + ObjectWrapper(JSContext* cx, JS::HandleValue value); double getNumber(Key key); int getNumberInt(Key key); @@ -152,9 +153,9 @@ public: } /** - * concatenates all of the fields in the object into the associated builder + * Writes a bson object reflecting the contents of the object */ - void writeThis(BSONObjBuilder* b); + BSONObj toBSON(); JS::HandleObject thisv() { return _object; @@ -164,21 +165,61 @@ public: private: /** + * The maximum depth of recursion for writeField + */ + static const int kMaxWriteFieldDepth = 150; + + /** + * The state needed to write a single level of a nested javascript object as a + * bson object. + * + * We use this between ObjectWrapper and ValueWriter to avoid recursion in + * translating js to bson. + */ + struct WriteFieldRecursionFrame { + WriteFieldRecursionFrame(JSContext* cx, + JSObject* obj, + BSONObjBuilder* parent, + StringData sd); + + BSONObjBuilder* subbob_or(BSONObjBuilder* option) { + return subbob ? &subbob.get() : option; + } + + JS::RootedObject thisv; + + // ids for the keys of thisv + JS::AutoIdArray ids; + + // Current index of the current key we're working on + std::size_t idx = 0; + + boost::optional<BSONObjBuilder> subbob; + BSONObj* originalBSON = nullptr; + bool altered; + }; + + /** + * Synthetic stack of variables for writeThis + * + * We use a LifetimeStack here because we have SpiderMonkey Rooting types which + * are non-copyable and non-movable and have to be on the stack. + */ + using WriteFieldRecursionFrames = LifetimeStack<WriteFieldRecursionFrame, kMaxWriteFieldDepth>; + + /** * writes the field "key" into the associated builder * * optional originalBSON is used to track updates to types (NumberInt * overwritten by a float, but coercible to the original type, etc.) */ - void _writeField(BSONObjBuilder* b, Key key, BSONObj* originalBSON); + void _writeField(BSONObjBuilder* b, + Key key, + WriteFieldRecursionFrames* frames, + BSONObj* originalBSON); JSContext* _context; JS::RootedObject _object; - - /** - * The depth of an object wrapper has to do with how many parents it has. - * Used to avoid circular object graphs and associate stack smashing. - */ - int _depth; }; } // namespace mozjs diff --git a/src/mongo/scripting/mozjs/proxyscope.cpp b/src/mongo/scripting/mozjs/proxyscope.cpp index 833e083001a..ee1614d8509 100644 --- a/src/mongo/scripting/mozjs/proxyscope.cpp +++ b/src/mongo/scripting/mozjs/proxyscope.cpp @@ -47,7 +47,17 @@ MozJSProxyScope::MozJSProxyScope(MozJSScriptEngine* engine) _state(State::Idle), _status(Status::OK()), _condvar(), - _thread(&MozJSProxyScope::implThread, this) { + // Despite calling PR_CreateThread, we're actually using our own + // implementation of PosixNSPR.cpp in this directory. So these threads + // are actually hosted on top of stdx::threads and most of the flags + // don't matter. + _thread(PR_CreateThread(PR_USER_THREAD, + implThread, + this, + PR_PRIORITY_NORMAL, + PR_LOCAL_THREAD, + PR_JOINABLE_THREAD, + 0)) { // Test the child on startup to make sure it's awake and that the // implementation scope sucessfully constructed. try { @@ -249,7 +259,7 @@ void MozJSProxyScope::runOnImplThread(std::function<void()> f) { // methods on it from there. If we're on the same thread, it's safe to // simply call back in, so let's do that. - if (_thread.get_id() == std::this_thread::get_id()) { + if (_thread == PR_GetCurrentThread()) { return f(); } @@ -281,7 +291,7 @@ void MozJSProxyScope::shutdownThread() { _condvar.notify_one(); - _thread.join(); + PR_JoinThread(_thread); } /** @@ -296,7 +306,9 @@ void MozJSProxyScope::shutdownThread() { * Shutdown: Shutdown -> _ * break out of the loop and return. */ -void MozJSProxyScope::implThread() { +void MozJSProxyScope::implThread(void* arg) { + auto proxy = static_cast<MozJSProxyScope*>(arg); + if (hasGlobalServiceContext()) Client::initThread("js"); @@ -305,35 +317,38 @@ void MozJSProxyScope::implThread() { // This will leave _status set for the first noop runOnImplThread(), which // captures the startup exception that way try { - scope.reset(new MozJSImplScope(_engine)); - _implScope = scope.get(); + scope.reset(new MozJSImplScope(proxy->_engine)); + proxy->_implScope = scope.get(); } catch (...) { - _status = exceptionToStatus(); + proxy->_status = exceptionToStatus(); } while (true) { - stdx::unique_lock<stdx::mutex> lk(_mutex); - _condvar.wait( - lk, [this] { return _state == State::ProxyRequest || _state == State::Shutdown; }); - - if (_state == State::Shutdown) + stdx::unique_lock<stdx::mutex> lk(proxy->_mutex); + proxy->_condvar.wait(lk, + [proxy] { + return proxy->_state == State::ProxyRequest || + proxy->_state == State::Shutdown; + }); + + if (proxy->_state == State::Shutdown) break; try { - _function(); + proxy->_function(); } catch (...) { - _status = exceptionToStatus(); + proxy->_status = exceptionToStatus(); } int exitCode; - if (_implScope && _implScope->getQuickExit(&exitCode)) { + if (proxy->_implScope && proxy->_implScope->getQuickExit(&exitCode)) { scope.reset(); quickExit(exitCode); } - _state = State::ImplResponse; + proxy->_state = State::ImplResponse; - _condvar.notify_one(); + proxy->_condvar.notify_one(); } } diff --git a/src/mongo/scripting/mozjs/proxyscope.h b/src/mongo/scripting/mozjs/proxyscope.h index 3d472a5a05e..de932fe35a8 100644 --- a/src/mongo/scripting/mozjs/proxyscope.h +++ b/src/mongo/scripting/mozjs/proxyscope.h @@ -28,6 +28,8 @@ #pragma once +#include "vm/PosixNSPR.h" + #include "mongo/client/dbclientcursor.h" #include "mongo/scripting/mozjs/engine.h" #include "mongo/stdx/condition_variable.h" @@ -174,7 +176,7 @@ public: private: void runOnImplThread(std::function<void()> f); void shutdownThread(); - void implThread(); + static void implThread(void* proxy); MozJSScriptEngine* const _engine; MozJSImplScope* _implScope; @@ -189,7 +191,7 @@ private: Status _status; stdx::condition_variable _condvar; - stdx::thread _thread; + PRThread* _thread; }; } // namespace mozjs diff --git a/src/mongo/scripting/mozjs/valuereader.cpp b/src/mongo/scripting/mozjs/valuereader.cpp index 89289c2f976..6a0b9dc6fe1 100644 --- a/src/mongo/scripting/mozjs/valuereader.cpp +++ b/src/mongo/scripting/mozjs/valuereader.cpp @@ -45,8 +45,8 @@ namespace mongo { namespace mozjs { -ValueReader::ValueReader(JSContext* cx, JS::MutableHandleValue value, int depth) - : _context(cx), _value(value), _depth(depth) {} +ValueReader::ValueReader(JSContext* cx, JS::MutableHandleValue value) + : _context(cx), _value(value) {} void ValueReader::fromBSONElement(const BSONElement& elem, bool readOnly) { auto scope = getScope(_context); @@ -92,8 +92,8 @@ void ValueReader::fromBSONElement(const BSONElement& elem, bool readOnly) { sprintf(str, "%i", i++); JS::RootedValue member(_context); - ValueReader(_context, &member, _depth + 1).fromBSONElement(subElem, readOnly); - ObjectWrapper(_context, array, _depth + 1).setValue(str, member); + ValueReader(_context, &member).fromBSONElement(subElem, readOnly); + ObjectWrapper(_context, array).setValue(str, member); } _value.setObjectOrNull(array); return; @@ -225,7 +225,7 @@ void ValueReader::fromBSON(const BSONObj& obj, bool readOnly) { ValueReader(_context, args[0]).fromBSONElement(ref, readOnly); // id can be a subobject - ValueReader(_context, args[1], _depth + 1).fromBSONElement(id, readOnly); + ValueReader(_context, args[1]).fromBSONElement(id, readOnly); JS::RootedObject obj(_context); diff --git a/src/mongo/scripting/mozjs/valuereader.h b/src/mongo/scripting/mozjs/valuereader.h index e96ede814f1..223600aa5e4 100644 --- a/src/mongo/scripting/mozjs/valuereader.h +++ b/src/mongo/scripting/mozjs/valuereader.h @@ -45,7 +45,7 @@ public: * Depth is used when readers are invoked from ObjectWrappers to avoid * reading out overly nested objects */ - ValueReader(JSContext* cx, JS::MutableHandleValue value, int depth = 0); + ValueReader(JSContext* cx, JS::MutableHandleValue value); void fromBSONElement(const BSONElement& elem, bool readOnly); void fromBSON(const BSONObj& obj, bool readOnly); @@ -55,7 +55,6 @@ public: private: JSContext* _context; JS::MutableHandleValue _value; - int _depth; }; } // namespace mozjs diff --git a/src/mongo/scripting/mozjs/valuewriter.cpp b/src/mongo/scripting/mozjs/valuewriter.cpp index 7fdb1596fa3..6f57d48c460 100644 --- a/src/mongo/scripting/mozjs/valuewriter.cpp +++ b/src/mongo/scripting/mozjs/valuewriter.cpp @@ -43,8 +43,8 @@ namespace mongo { namespace mozjs { -ValueWriter::ValueWriter(JSContext* cx, JS::HandleValue value, int depth) - : _context(cx), _value(value), _depth(depth), _originalParent(nullptr) {} +ValueWriter::ValueWriter(JSContext* cx, JS::HandleValue value) + : _context(cx), _value(value), _originalParent(nullptr) {} void ValueWriter::setOriginalBSON(BSONObj* obj) { _originalParent = obj; @@ -117,20 +117,7 @@ BSONObj ValueWriter::toBSON() { JS::RootedObject obj(_context, _value.toObjectOrNull()); - if (getScope(_context)->getProto<BSONInfo>().instanceOf(obj)) { - BSONObj* originalBSON; - bool altered; - - std::tie(originalBSON, altered) = BSONInfo::originalBSON(_context, obj); - - if (originalBSON && !altered) - return *originalBSON; - } - - BSONObjBuilder bob; - ObjectWrapper(_context, obj, _depth).writeThis(&bob); - - return bob.obj(); + return ObjectWrapper(_context, obj).toBSON(); } std::string ValueWriter::toString() { @@ -189,11 +176,13 @@ Decimal128 ValueWriter::toDecimal128() { uasserted(ErrorCodes::BadValue, str::stream() << "Unable to write Decimal128 value."); } -void ValueWriter::writeThis(BSONObjBuilder* b, StringData sd) { +void ValueWriter::writeThis(BSONObjBuilder* b, + StringData sd, + ObjectWrapper::WriteFieldRecursionFrames* frames) { uassert(17279, - str::stream() << "Exceeded depth limit of " << 150 + str::stream() << "Exceeded depth limit of " << ObjectWrapper::kMaxWriteFieldDepth << " when converting js object to BSON. Do you have a cycle?", - _depth < 149); + frames->size() < ObjectWrapper::kMaxWriteFieldDepth); // Null char should be at the end, not in the string uassert(16985, @@ -221,8 +210,7 @@ void ValueWriter::writeThis(BSONObjBuilder* b, StringData sd) { b->append(sd, val); } else if (_value.isObject()) { - JS::RootedObject childObj(_context, _value.toObjectOrNull()); - _writeObject(b, sd, childObj); + _writeObject(b, sd, frames); } else if (_value.isBoolean()) { b->appendBool(sd, _value.toBoolean()); } else if (_value.isUndefined()) { @@ -235,72 +223,126 @@ void ValueWriter::writeThis(BSONObjBuilder* b, StringData sd) { } } -void ValueWriter::_writeObject(BSONObjBuilder* b, StringData sd, JS::HandleObject obj) { +void ValueWriter::_writeObject(BSONObjBuilder* b, + StringData sd, + ObjectWrapper::WriteFieldRecursionFrames* frames) { auto scope = getScope(_context); - ObjectWrapper o(_context, obj, _depth); - - if (JS_ObjectIsFunction(_context, _value.toObjectOrNull())) { - uassert(16716, - "cannot convert native function to BSON", - !scope->getProto<NativeFunctionInfo>().instanceOf(obj)); - b->appendCode(sd, ValueWriter(_context, _value).toString()); - } else if (JS_ObjectIsRegExp(_context, obj)) { - JS::RootedValue v(_context); - v.setObjectOrNull(obj); - - std::string regex = ValueWriter(_context, v).toString(); - regex = regex.substr(1); - std::string r = regex.substr(0, regex.rfind('/')); - std::string o = regex.substr(regex.rfind('/') + 1); - - b->appendRegex(sd, r, o); - } else if (JS_ObjectIsDate(_context, obj)) { - JS::RootedValue dateval(_context); - o.callMethod("getTime", &dateval); - - auto d = Date_t::fromMillisSinceEpoch(ValueWriter(_context, dateval).toNumber()); - b->appendDate(sd, d); - } else if (scope->getProto<OIDInfo>().instanceOf(obj)) { - b->append(sd, OID(o.getString("str"))); - } else if (scope->getProto<NumberLongInfo>().instanceOf(obj)) { - long long out = NumberLongInfo::ToNumberLong(_context, obj); - b->append(sd, out); - } else if (scope->getProto<NumberIntInfo>().instanceOf(obj)) { - b->append(sd, NumberIntInfo::ToNumberInt(_context, obj)); - } else if (scope->getProto<NumberDecimalInfo>().instanceOf(obj)) { - b->append(sd, NumberDecimalInfo::ToNumberDecimal(_context, obj)); - } else if (scope->getProto<DBPointerInfo>().instanceOf(obj)) { - JS::RootedValue id(_context); - o.getValue("id", &id); - - b->appendDBRef(sd, o.getString("ns"), OID(ObjectWrapper(_context, id).getString("str"))); - } else if (scope->getProto<BinDataInfo>().instanceOf(obj)) { - auto str = static_cast<std::string*>(JS_GetPrivate(obj)); - - auto binData = base64::decode(*str); - - b->appendBinData(sd, - binData.size(), - static_cast<mongo::BinDataType>(static_cast<int>(o.getNumber("type"))), - binData.c_str()); - } else if (scope->getProto<TimestampInfo>().instanceOf(obj)) { - Timestamp ot(o.getNumber("t"), o.getNumber("i")); - b->append(sd, ot); - } else if (scope->getProto<MinKeyInfo>().instanceOf(obj)) { - b->appendMinKey(sd); - } else if (scope->getProto<MaxKeyInfo>().instanceOf(obj)) { - b->appendMaxKey(sd); - } else { - // nested object or array + // We open a block here because it's important that the two rooting types + // we need (obj and o) go out of scope before we actually open a + // new WriteFieldFrame (in the emplace at the bottom of the function). If + // we don't do this, we'll destroy the local roots in this function body + // before the frame we added, which will break the gc rooting list. + { + JS::RootedObject obj(_context, _value.toObjectOrNull()); + ObjectWrapper o(_context, obj); + + if (JS_ObjectIsFunction(_context, _value.toObjectOrNull())) { + uassert(16716, + "cannot convert native function to BSON", + !scope->getProto<NativeFunctionInfo>().instanceOf(obj)); + b->appendCode(sd, ValueWriter(_context, _value).toString()); + return; + } + + if (JS_ObjectIsRegExp(_context, obj)) { + JS::RootedValue v(_context); + v.setObjectOrNull(obj); + + std::string regex = ValueWriter(_context, v).toString(); + regex = regex.substr(1); + std::string r = regex.substr(0, regex.rfind('/')); + std::string o = regex.substr(regex.rfind('/') + 1); + + b->appendRegex(sd, r, o); + + return; + } + + if (JS_ObjectIsDate(_context, obj)) { + JS::RootedValue dateval(_context); + o.callMethod("getTime", &dateval); + + auto d = Date_t::fromMillisSinceEpoch(ValueWriter(_context, dateval).toNumber()); + b->appendDate(sd, d); + + return; + } + + if (scope->getProto<OIDInfo>().instanceOf(obj)) { + b->append(sd, OID(o.getString("str"))); + + return; + } + + if (scope->getProto<NumberLongInfo>().instanceOf(obj)) { + long long out = NumberLongInfo::ToNumberLong(_context, obj); + b->append(sd, out); + + return; + } - BSONObjBuilder subbob(JS_IsArrayObject(_context, obj) ? b->subarrayStart(sd) - : b->subobjStart(sd)); + if (scope->getProto<NumberIntInfo>().instanceOf(obj)) { + b->append(sd, NumberIntInfo::ToNumberInt(_context, obj)); - ObjectWrapper child(_context, obj, _depth + 1); + return; + } + + if (scope->getProto<NumberDecimalInfo>().instanceOf(obj)) { + b->append(sd, NumberDecimalInfo::ToNumberDecimal(_context, obj)); + + return; + } + + if (scope->getProto<DBPointerInfo>().instanceOf(obj)) { + JS::RootedValue id(_context); + o.getValue("id", &id); + + b->appendDBRef( + sd, o.getString("ns"), OID(ObjectWrapper(_context, id).getString("str"))); - child.writeThis(b); + return; + } + + if (scope->getProto<BinDataInfo>().instanceOf(obj)) { + auto str = static_cast<std::string*>(JS_GetPrivate(obj)); + + auto binData = base64::decode(*str); + + b->appendBinData(sd, + binData.size(), + static_cast<mongo::BinDataType>(static_cast<int>(o.getNumber("type"))), + binData.c_str()); + + return; + } + + if (scope->getProto<TimestampInfo>().instanceOf(obj)) { + Timestamp ot(o.getNumber("t"), o.getNumber("i")); + b->append(sd, ot); + + return; + } + + if (scope->getProto<MinKeyInfo>().instanceOf(obj)) { + b->appendMinKey(sd); + + return; + } + + if (scope->getProto<MaxKeyInfo>().instanceOf(obj)) { + b->appendMaxKey(sd); + + return; + } } + + // nested object or array + + // This emplace is effectively a recursive function call, as this code path + // unwinds back to ObjectWrapper::toBSON. In that function we'll actually + // write the child we've just pushed onto the frames stack. + frames->emplace(_context, _value.toObjectOrNull(), b, sd); } } // namespace mozjs diff --git a/src/mongo/scripting/mozjs/valuewriter.h b/src/mongo/scripting/mozjs/valuewriter.h index 1945303867b..65f6f418d09 100644 --- a/src/mongo/scripting/mozjs/valuewriter.h +++ b/src/mongo/scripting/mozjs/valuewriter.h @@ -32,6 +32,7 @@ #include <string> #include "mongo/bson/bsonobj.h" +#include "mongo/scripting/mozjs/objectwrapper.h" namespace mongo { namespace mozjs { @@ -39,14 +40,12 @@ namespace mozjs { /** * Writes C++ values out of JS Values * - * depth is used to trap circular objects in js and prevent stack smashing - * * originalBSON is a hack to keep integer types in their original type when * they're read out, manipulated in js and saved back. */ class ValueWriter { public: - ValueWriter(JSContext* cx, JS::HandleValue value, int depth = 0); + ValueWriter(JSContext* cx, JS::HandleValue value); BSONObj toBSON(); @@ -69,8 +68,15 @@ public: /** * Writes the value into a bsonobjbuilder under the name in sd. + * + * We take WriteFieldRecursionFrames so we can push a new object on if the underlying + * value is an object. This allows us to recurse without C++ stack frames. + * + * Look in toBSON on ObjectWrapper for the top of that loop. */ - void writeThis(BSONObjBuilder* b, StringData sd); + void writeThis(BSONObjBuilder* b, + StringData sd, + ObjectWrapper::WriteFieldRecursionFrames* frames); void setOriginalBSON(BSONObj* obj); @@ -78,11 +84,12 @@ private: /** * Writes the object into a bsonobjbuilder under the name in sd. */ - void _writeObject(BSONObjBuilder* b, StringData sd, JS::HandleObject obj); + void _writeObject(BSONObjBuilder* b, + StringData sd, + ObjectWrapper::WriteFieldRecursionFrames* frames); JSContext* _context; JS::HandleValue _value; - int _depth; BSONObj* _originalParent; }; diff --git a/src/mongo/scripting/mozjs/wraptype.h b/src/mongo/scripting/mozjs/wraptype.h index d569bcb9d86..3729e964c0e 100644 --- a/src/mongo/scripting/mozjs/wraptype.h +++ b/src/mongo/scripting/mozjs/wraptype.h @@ -240,8 +240,12 @@ public: // before the other types are installed. Might as well just do it // in the constructor. if (T::classFlags & JSCLASS_GLOBAL_FLAGS) { + _jsclass.trace = JS_GlobalObjectTraceHook; + JS::RootedObject proto(_context); + JSAutoRequest ar(_context); + _proto.init(_context, _assertPtr(JS_NewGlobalObject( _context, &_jsclass, nullptr, JS::DontFireOnNewGlobalHook))); |