diff options
author | Mathias Stearn <mathias@10gen.com> | 2013-08-22 15:11:53 -0400 |
---|---|---|
committer | Mathias Stearn <redbeard0531@gmail.com> | 2013-08-27 12:01:45 -0400 |
commit | 5f724c01611a8c12da6b803b2edff97788a756c1 (patch) | |
tree | 4b06e9a1fb0185a33e32f456e398719c7a16d2f3 | |
parent | 26f638873eadfc3dd8e17bb37eb68a1c613146c3 (diff) | |
download | mongo-5f724c01611a8c12da6b803b2edff97788a756c1.tar.gz |
SERVER-10596 Globalize formerly per-thread Pool of JS Scopes
This ensures that the limit of 10 pooled scopes is actually enforced.
With a per-thread Pool, long-lived connections could cause very high
memory usage (both real and virtual) even if they haven't used JS in a
long time.
Manually fixed backport of commit 73841f7a1ec1322d96179eb2712ab438f56add00
Conflicts:
jstests/auth/js_scope_leak.js
src/mongo/db/auth/auth_external_state.h
src/mongo/db/auth/auth_external_state_d.cpp
src/mongo/db/auth/auth_external_state_d.h
src/mongo/db/auth/auth_external_state_s.cpp
src/mongo/db/auth/auth_external_state_s.h
src/mongo/db/auth/authorization_session.cpp
src/mongo/db/auth/authorization_session.h
src/mongo/db/commands/group.cpp
src/mongo/db/matcher/expression_where.cpp
src/mongo/scripting/engine.cpp
-rw-r--r-- | jstests/auth/js_scope_leak.js | 139 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_manager.cpp | 12 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_manager.h | 4 | ||||
-rw-r--r-- | src/mongo/db/commands/group.cpp | 6 | ||||
-rw-r--r-- | src/mongo/db/commands/mr.cpp | 6 | ||||
-rw-r--r-- | src/mongo/db/db.cpp | 1 | ||||
-rw-r--r-- | src/mongo/db/dbeval.cpp | 4 | ||||
-rw-r--r-- | src/mongo/db/matcher.cpp | 5 | ||||
-rw-r--r-- | src/mongo/scripting/engine.cpp | 135 | ||||
-rw-r--r-- | src/mongo/scripting/engine.h | 17 |
10 files changed, 238 insertions, 91 deletions
diff --git a/jstests/auth/js_scope_leak.js b/jstests/auth/js_scope_leak.js new file mode 100644 index 00000000000..c8c19b61aef --- /dev/null +++ b/jstests/auth/js_scope_leak.js @@ -0,0 +1,139 @@ +// Test for SERVER-9129 +// Verify global scope data does not persist past logout or auth. +// NOTE: Each test case covers 3 state transitions: +// no auth -> auth user 'a' +// auth user 'a' -> auth user 'b' +// auth user 'b' -> logout +// +// These transitions are tested for dbEval, $where, MapReduce and $group + +var conn = MongoRunner.runMongod({ auth: "", smallfiles: ""}); +var test = conn.getDB("test"); + +// insert a single document and add two test users +test.foo.insert({a:1}); +test.getLastError(); +assert.eq(1, test.foo.findOne().a); +test.addUser('a', 'a'); +test.addUser('b', 'b'); + +function missingOrEquals(string) { + return 'function() { ' + + 'var global = function(){return this;}.call();' + // Uncomment the next line when debugging. + // + 'print(global.hasOwnProperty("someGlobal") ? someGlobal : "MISSING" );' + + 'return !global.hasOwnProperty("someGlobal")' + + ' || someGlobal == unescape("' + escape(string) + '");' + +'}()' +} + +function testDbEval() { + // set the global variable 'someGlobal' before authenticating + test.eval('someGlobal = "noUsers";'); + + // test new user auth causes scope to be cleared + test.auth('a', 'a'); + assert(test.eval('return ' + missingOrEquals('a')), "dbEval: Auth user 'a'"); + + // test auth as another user causes scope to be cleared + test.eval('someGlobal = "a";'); + test.auth('b', 'b'); + assert(test.eval('return ' + missingOrEquals('a&b')), "dbEval: Auth user 'b'"); + + // test user logout causes scope to be cleared + test.eval('someGlobal = "a&b";'); + test.logout(); + assert(test.eval('return ' + missingOrEquals('noUsers')), "dbEval: log out"); +} +testDbEval(); +testDbEval(); + +// test $where +function testWhere() { + // set the global variable 'someGlobal' before authenticating + test.foo.findOne({$where:'someGlobal = "noUsers";'}); + + // test new user auth causes scope to be cleared + test.auth('a', 'a'); + assert.eq(1, + test.foo.count({$where: 'return ' + missingOrEquals('a')}), + "$where: Auth user 'a"); + + // test auth as another user causes scope to be cleared + test.foo.findOne({$where:'someGlobal = "a";'}); + test.auth('b', 'b'); + assert(test.foo.count({$where: 'return ' + missingOrEquals('a&b')}), "$where: Auth user 'b'"); + // test user logout causes scope to be cleared + test.foo.findOne({$where:'someGlobal = "a&b";'}); + test.logout(); + assert(test.foo.count({$where: 'return ' + missingOrEquals('noUsers')}), "$where: log out"); +} +testWhere(); +testWhere(); + +function testMapReduce() { + var mapSet = function(string) { return Function('someGlobal = "' + string + '"'); } + var mapGet = function(string) { return Function('assert(' + missingOrEquals(string) +')'); } + var reduce = function(k, v) { } + var setGlobalInMap = function(string) { + test.foo.mapReduce(mapSet(string), reduce, {out:{inline:1}}); + } + var getGlobalFromMap = function(string) { + test.foo.mapReduce(mapGet(string), reduce, {out:{inline:1}}); + } + + // set the global variable 'someGlobal' before authenticating + setGlobalInMap('noUsers'); + + // test new user auth causes scope to be cleared + test.auth('a', 'a'); + getGlobalFromMap('a'); // throws on fail + + // test auth as another user causes scope to be cleared + setGlobalInMap('a'); + test.auth('b', 'b'); + getGlobalFromMap('a&b'); // throws on fail + + // test user logout causes scope to be cleared + setGlobalInMap('a&b'); + test.logout(); + getGlobalFromMap('noUsers'); // throws on fail +} +testMapReduce(); +testMapReduce(); + +function testGroup() { + var setGlobalInGroup = function(string) { + return test.foo.group({key: 'a', + reduce: Function('doc1', 'agg', + 'someGlobal = "' + string + '"'), + initial:{}}); + } + var getGlobalFromGroup = function(string) { + return test.foo.group({key: 'a', + reduce: Function('doc1', 'agg', + 'assert(' + missingOrEquals(string) +')'), + initial:{}}); + } + + // set the global variable 'someGlobal' before authenticating + setGlobalInGroup('noUsers'); + + // test new user auth causes scope to be cleared + test.auth('a', 'a'); + getGlobalFromGroup('a'); // throws on fail + + // test auth as another user causes scope to be cleared + setGlobalInGroup('a'); + test.auth('b', 'b'); + getGlobalFromGroup('a&b'); // throws on fail + + // test user logout causes scope to be cleared + setGlobalInGroup('a&b'); + test.logout(); + getGlobalFromGroup('noUsers'); // throws on fail +} +testGroup(); +testGroup(); + + diff --git a/src/mongo/db/auth/authorization_manager.cpp b/src/mongo/db/auth/authorization_manager.cpp index 1cf8efede39..f30443eda1b 100644 --- a/src/mongo/db/auth/authorization_manager.cpp +++ b/src/mongo/db/auth/authorization_manager.cpp @@ -443,6 +443,18 @@ namespace { return _authenticatedPrincipals.getNames(); } + std::string AuthorizationManager::getAuthenticatedPrincipalNamesToken() { + std::string ret; + for (PrincipalSet::NameIterator nameIter = getAuthenticatedPrincipalNames(); + nameIter.more(); + nameIter.next()) { + ret += '\0'; // Using a NUL byte which isn't valid in usernames to separate them. + ret += nameIter->getFullName(); + } + + return ret; + } + Status AuthorizationManager::acquirePrivilege(const Privilege& privilege, const PrincipalName& authorizingPrincipal) { if (!_authenticatedPrincipals.lookup(authorizingPrincipal)) { diff --git a/src/mongo/db/auth/authorization_manager.h b/src/mongo/db/auth/authorization_manager.h index a32710557dd..7131d7624a6 100644 --- a/src/mongo/db/auth/authorization_manager.h +++ b/src/mongo/db/auth/authorization_manager.h @@ -91,6 +91,10 @@ namespace mongo { // Gets an iterator over the names of all authenticated principals stored in this manager. PrincipalSet::NameIterator getAuthenticatedPrincipalNames(); + // Returns a string representing all logged-in principals on the current session. + // WARNING: this string will contain NUL bytes so don't call c_str()! + std::string getAuthenticatedPrincipalNamesToken(); + // Removes any authenticated principals whose authorization credentials came from the given // database, and revokes any privileges that were granted via that principal. void logoutDatabase(const std::string& dbname); diff --git a/src/mongo/db/commands/group.cpp b/src/mongo/db/commands/group.cpp index 441a1192905..20481521e91 100644 --- a/src/mongo/db/commands/group.cpp +++ b/src/mongo/db/commands/group.cpp @@ -20,9 +20,11 @@ #include <vector> +#include "mongo/db/auth/authorization_manager.h" #include "mongo/db/auth/action_set.h" #include "mongo/db/auth/action_type.h" #include "mongo/db/auth/privilege.h" +#include "mongo/db/client_basic.h" #include "mongo/db/commands.h" #include "mongo/db/instance.h" #include "mongo/scripting/engine.h" @@ -72,7 +74,9 @@ namespace mongo { string& errmsg, BSONObjBuilder& result ) { - auto_ptr<Scope> s = globalScriptEngine->getPooledScope( realdbname, "group"); + const string userToken = ClientBasic::getCurrent()->getAuthorizationManager() + ->getAuthenticatedPrincipalNamesToken(); + auto_ptr<Scope> s = globalScriptEngine->getPooledScope(realdbname, "group" + userToken); if ( reduceScope ) s->init( reduceScope ); diff --git a/src/mongo/db/commands/mr.cpp b/src/mongo/db/commands/mr.cpp index 9528e495ded..858ffcee4f4 100644 --- a/src/mongo/db/commands/mr.cpp +++ b/src/mongo/db/commands/mr.cpp @@ -22,6 +22,7 @@ #include "mongo/client/connpool.h" #include "mongo/client/parallel.h" +#include "mongo/db/auth/authorization_manager.h" #include "mongo/db/clientcursor.h" #include "mongo/db/commands.h" #include "mongo/db/db.h" @@ -622,7 +623,10 @@ namespace mongo { */ void State::init() { // setup js - _scope.reset(globalScriptEngine->getPooledScope( _config.dbname, "mapreduce" ).release() ); + const string userToken = ClientBasic::getCurrent()->getAuthorizationManager() + ->getAuthenticatedPrincipalNamesToken(); + _scope.reset(globalScriptEngine->getPooledScope( + _config.dbname, "mapreduce" + userToken).release()); if ( ! _config.scopeSetup.isEmpty() ) _scope->init( &_config.scopeSetup ); diff --git a/src/mongo/db/db.cpp b/src/mongo/db/db.cpp index 2361ec2a945..ec337c89885 100644 --- a/src/mongo/db/db.cpp +++ b/src/mongo/db/db.cpp @@ -232,7 +232,6 @@ namespace mongo { virtual void disconnected( AbstractMessagingPort* p ) { Client * c = currentClient.get(); if( c ) c->shutdown(); - globalScriptEngine->threadDone(); } }; diff --git a/src/mongo/db/dbeval.cpp b/src/mongo/db/dbeval.cpp index 5a6cc464c34..9e6a3360a49 100644 --- a/src/mongo/db/dbeval.cpp +++ b/src/mongo/db/dbeval.cpp @@ -57,7 +57,9 @@ namespace mongo { return false; } - auto_ptr<Scope> s = globalScriptEngine->getPooledScope( dbName, "dbeval" ); + const string userToken = ClientBasic::getCurrent()->getAuthorizationManager() + ->getAuthenticatedPrincipalNamesToken(); + auto_ptr<Scope> s = globalScriptEngine->getPooledScope( dbName, "dbeval" + userToken ); ScriptingFunction f = s->createFunction(code); if ( f == 0 ) { errmsg = (string)"compile failed: " + s->getError(); diff --git a/src/mongo/db/matcher.cpp b/src/mongo/db/matcher.cpp index 2e679591f2c..386415a81c6 100644 --- a/src/mongo/db/matcher.cpp +++ b/src/mongo/db/matcher.cpp @@ -27,6 +27,7 @@ #include "db.h" #include "queryutil.h" #include "client.h" +#include "mongo/db/auth/authorization_manager.h" #include "pdfile.h" @@ -74,8 +75,10 @@ namespace mongo { return; _initCalled = true; + const string userToken = ClientBasic::getCurrent()->getAuthorizationManager() + ->getAuthenticatedPrincipalNamesToken(); NamespaceString ns( _ns ); - _scope = globalScriptEngine->getPooledScope( ns.db.c_str(), "where" ); + _scope = globalScriptEngine->getPooledScope( ns.db.c_str(), "where" + userToken ); massert( 10341 , "code has to be set first!" , ! _jsCode.empty() ); diff --git a/src/mongo/scripting/engine.cpp b/src/mongo/scripting/engine.cpp index fc4d042c2af..e6bb8be819e 100644 --- a/src/mongo/scripting/engine.cpp +++ b/src/mongo/scripting/engine.cpp @@ -39,7 +39,7 @@ namespace mongo { Scope::Scope() : _localDBName(""), _loadedVersion(0), - _numTimeUsed(0), + _numTimesUsed(0), _lastRetIsNativeCode(false) { } @@ -259,90 +259,80 @@ namespace mongo { injectNative("benchFinish", BenchRunner::benchFinish); } - typedef map<string, list<Scope*> > PoolToScopes; - +namespace { class ScopeCache { public: - ScopeCache() : _mutex("ScopeCache") { - } + ScopeCache() : _mutex("ScopeCache") {} - ~ScopeCache() { - if (inShutdown()) - return; - clear(); - } - - void done(const string& pool, Scope* s) { + void release(const string& poolName, const boost::shared_ptr<Scope>& scope) { scoped_lock lk(_mutex); - list<Scope*>& l = _pools[pool]; - bool oom = s->hasOutOfMemoryException(); - // do not keep too many contexts, or use them for too long - if (l.size() > 10 || s->getTimeUsed() > 10 || oom || !s->getError().empty()) { - delete s; - } - else { - l.push_back(s); - s->reset(); + if (scope->hasOutOfMemoryException()) { + // make some room + log() << "Clearing all idle JS contexts due to out of memory" << endl; + _pools.clear(); + return; } - if (oom) { - // out of mem, make some room - log() << "Clearing all idle JS contexts due to out of memory" << endl; - clear(); + if (scope->getTimesUsed() > kMaxScopeReuse) + return; // used too many times to save + + if (!scope->getError().empty()) + return; // not saving errored scopes + + if (_pools.size() >= kMaxPoolSize) { + // prefer to keep recently-used scopes + _pools.pop_back(); } + + ScopeAndPool toStore = {scope, poolName}; + _pools.push_front(toStore); } - Scope* get(const string& pool) { + boost::shared_ptr<Scope> tryAcquire(const string& poolName) { scoped_lock lk(_mutex); - list<Scope*>& l = _pools[pool]; - if (l.size() == 0) - return 0; - - Scope* s = l.back(); - l.pop_back(); - s->reset(); - s->incTimeUsed(); - return s; - } - void clear() { - set<Scope*> seen; - for (PoolToScopes::iterator i = _pools.begin(); i != _pools.end(); ++i) { - for (list<Scope*>::iterator j = i->second.begin(); j != i->second.end(); ++j) { - Scope* s = *j; - fassert(16652, seen.insert(s).second); - delete s; + for (Pools::iterator it = _pools.begin(); it != _pools.end(); ++it) { + if (it->poolName == poolName) { + boost::shared_ptr<Scope> scope = it->scope; + _pools.erase(it); + scope->incTimesUsed(); + scope->reset(); + return scope; } } - _pools.clear(); + + return boost::shared_ptr<Scope>(); } private: - PoolToScopes _pools; + struct ScopeAndPool { + boost::shared_ptr<Scope> scope; + string poolName; + }; + + // Note: if these numbers change, reconsider choice of datastructure for _pools + static const unsigned kMaxPoolSize = 10; + static const int kMaxScopeReuse = 10; + + typedef deque<ScopeAndPool> Pools; // More-recently used Scopes are kept at the front. + Pools _pools; // protected by _mutex mongo::mutex _mutex; }; - thread_specific_ptr<ScopeCache> scopeCache; + ScopeCache scopeCache; +} // anonymous namespace class PooledScope : public Scope { public: - PooledScope(const std::string& pool, Scope* real) : _pool(pool), _real(real) { + PooledScope(const std::string& pool, const boost::shared_ptr<Scope>& real) + : _pool(pool) + , _real(real) { _real->loadStored(true); - }; + } + virtual ~PooledScope() { - ScopeCache* sc = scopeCache.get(); - if (sc) { - sc->done(_pool, _real); - _real = NULL; - } - else { - // this means that the Scope was killed from a different thread - // for example a cursor got timed out that has a $where clause - LOG(3) << "warning: scopeCache is empty!" << endl; - delete _real; - _real = 0; - } + scopeCache.release(_pool, _real); } // wrappers for the derived (_real) scope @@ -404,31 +394,24 @@ namespace mongo { private: string _pool; - Scope* _real; + boost::shared_ptr<Scope> _real; }; /** Get a scope from the pool of scopes matching the supplied pool name */ - auto_ptr<Scope> ScriptEngine::getPooledScope(const string& pool, const string& scopeType) { - if (!scopeCache.get()) - scopeCache.reset(new ScopeCache()); - - Scope* s = scopeCache->get(pool + scopeType); - if (!s) - s = newScope(); + auto_ptr<Scope> ScriptEngine::getPooledScope(const string& db, const string& scopeType) { + const string fullPoolName = db + scopeType; + boost::shared_ptr<Scope> s = scopeCache.tryAcquire(fullPoolName); + if (!s) { + s.reset(newScope()); + } auto_ptr<Scope> p; - p.reset(new PooledScope(pool + scopeType, s)); - p->setLocalDB(pool); + p.reset(new PooledScope(fullPoolName, s)); + p->setLocalDB(db); p->loadStored(true); return p; } - void ScriptEngine::threadDone() { - ScopeCache* sc = scopeCache.get(); - if (sc) - sc->clear(); - } - void (*ScriptEngine::_connectCallback)(DBClientWithCommands&) = 0; const char* (*ScriptEngine::_checkInterruptCallback)() = 0; unsigned (*ScriptEngine::_getCurrentOpIdCallback)() = 0; diff --git a/src/mongo/scripting/engine.h b/src/mongo/scripting/engine.h index 8233128e736..e0a3486a6ae 100644 --- a/src/mongo/scripting/engine.h +++ b/src/mongo/scripting/engine.h @@ -135,10 +135,10 @@ namespace mongo { static void validateObjectIdString(const string& str); /** increments the number of times a scope was used */ - void incTimeUsed() { ++_numTimeUsed; } + void incTimesUsed() { ++_numTimesUsed; } /** gets the number of times a scope was used */ - int getTimeUsed() { return _numTimeUsed; } + int getTimesUsed() { return _numTimesUsed; } /** return true if last invoke() return'd native code */ virtual bool isLastRetNativeCode() { return _lastRetIsNativeCode; } @@ -168,7 +168,7 @@ namespace mongo { set<string> _storedNames; static long long _lastVersion; FunctionCacheMap _cachedFunctions; - int _numTimeUsed; + int _numTimesUsed; bool _lastRetIsNativeCode; // v8 only: set to true if eval'd script returns a native func }; @@ -188,15 +188,12 @@ namespace mongo { static void setup(); /** gets a scope from the pool or a new one if pool is empty - * @param pool An identifier for the pool, usually the db name + * @param db The db name + * @param scopeType A unique id to limit scope sharing. + * This must include authenticated users. * @return the scope */ - auto_ptr<Scope> getPooledScope(const string& pool, const string& scopeType); - - /** - * call this method to release some JS resources when a thread is done - */ - void threadDone(); + auto_ptr<Scope> getPooledScope(const string& db, const string& scopeType); void setScopeInitCallback(void (*func)(Scope&)) { _scopeInitCallback = func; } static void setConnectCallback(void (*func)(DBClientWithCommands&)) { |