summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMathias Stearn <mathias@10gen.com>2013-08-22 15:11:53 -0400
committerMathias Stearn <redbeard0531@gmail.com>2013-08-27 12:01:45 -0400
commit5f724c01611a8c12da6b803b2edff97788a756c1 (patch)
tree4b06e9a1fb0185a33e32f456e398719c7a16d2f3
parent26f638873eadfc3dd8e17bb37eb68a1c613146c3 (diff)
downloadmongo-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.js139
-rw-r--r--src/mongo/db/auth/authorization_manager.cpp12
-rw-r--r--src/mongo/db/auth/authorization_manager.h4
-rw-r--r--src/mongo/db/commands/group.cpp6
-rw-r--r--src/mongo/db/commands/mr.cpp6
-rw-r--r--src/mongo/db/db.cpp1
-rw-r--r--src/mongo/db/dbeval.cpp4
-rw-r--r--src/mongo/db/matcher.cpp5
-rw-r--r--src/mongo/scripting/engine.cpp135
-rw-r--r--src/mongo/scripting/engine.h17
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&)) {