mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-26 15:30:58 +01:00
Improve a race condition in session establishment code
Summary: If you try to establish several sessions quickly (e.g., by running several copies of "arc" at once, as in "arc x | arc y"), the current logic has a high chance of making them all pick the same conduit session to refresh (since it's the oldest one when each process selects the current sessions). This means they all issue updates against "conduit-3" (or whatever) and one ends up with a bogus session. Instead, do an update against the table with the session key we read, so only one process wins the race. If we don't win the race, try again until we do or have tried every session slot. Test Plan: - Wiped conduit sessions, ran arc commands to verify the fresh session case. - Ran a bunch of arc piped to itself, e.g. "arc list | arc list | arc list | ...". It succeeds up to the session limit, and above that gets failures as expected. - Manually checked the session table to make sure things seemed reasonable there. - Generally ran a bunch of arc commands. - Logged out and logged in on the web interface. Reviewers: btrahan, jungejason Reviewed By: btrahan CC: aran, btrahan Maniphest Tasks: T687 Differential Revision: https://secure.phabricator.com/D1329
This commit is contained in:
parent
1903bb80bb
commit
d16454d45d
2 changed files with 88 additions and 43 deletions
|
@ -309,7 +309,7 @@ return array(
|
|||
|
||||
// Maximum number of simultaneous Conduit sessions each user is permitted
|
||||
// to have.
|
||||
'auth.sessions.conduit' => 3,
|
||||
'auth.sessions.conduit' => 5,
|
||||
|
||||
// Set this true to enable the Settings -> SSH Public Keys panel, which will
|
||||
// allow users to associated SSH public keys with their accounts. This is only
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2011 Facebook, Inc.
|
||||
* Copyright 2012 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -240,54 +240,99 @@ class PhabricatorUser extends PhabricatorUserDAO {
|
|||
"Session limit for '{$session_type}' must be at least 1!");
|
||||
}
|
||||
|
||||
// Load all the currently active sessions.
|
||||
$sessions = queryfx_all(
|
||||
$conn_w,
|
||||
'SELECT type, sessionStart FROM %T WHERE userPHID = %s AND type LIKE %>',
|
||||
PhabricatorUser::SESSION_TABLE,
|
||||
$this->getPHID(),
|
||||
$session_type.'-');
|
||||
|
||||
// Choose which 'type' we'll actually establish, i.e. what number we're
|
||||
// going to append to the basic session type. To do this, just check all
|
||||
// the numbers sequentially until we find an available session.
|
||||
$establish_type = null;
|
||||
$sessions = ipull($sessions, null, 'type');
|
||||
for ($ii = 1; $ii <= $session_limit; $ii++) {
|
||||
if (empty($sessions[$session_type.'-'.$ii])) {
|
||||
$establish_type = $session_type.'-'.$ii;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find an available session, choose the oldest session and
|
||||
// overwrite it.
|
||||
if (!$establish_type) {
|
||||
$sessions = isort($sessions, 'sessionStart');
|
||||
$oldest = reset($sessions);
|
||||
$establish_type = $oldest['type'];
|
||||
}
|
||||
// NOTE: Session establishment is sensitive to race conditions, as when
|
||||
// piping `arc` to `arc`:
|
||||
//
|
||||
// arc export ... | arc paste ...
|
||||
//
|
||||
// To avoid this, we overwrite an old session only if it hasn't been
|
||||
// re-established since we read it.
|
||||
|
||||
// Consume entropy to generate a new session key, forestalling the eventual
|
||||
// heat death of the universe.
|
||||
$session_key = Filesystem::readRandomCharacters(40);
|
||||
|
||||
// UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet.
|
||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
|
||||
queryfx(
|
||||
// Load all the currently active sessions.
|
||||
$sessions = queryfx_all(
|
||||
$conn_w,
|
||||
'INSERT INTO %T '.
|
||||
'(userPHID, type, sessionKey, sessionStart)'.
|
||||
' VALUES '.
|
||||
'(%s, %s, %s, UNIX_TIMESTAMP()) '.
|
||||
'ON DUPLICATE KEY UPDATE '.
|
||||
'sessionKey = VALUES(sessionKey), '.
|
||||
'sessionStart = VALUES(sessionStart)',
|
||||
self::SESSION_TABLE,
|
||||
'SELECT type, sessionKey, sessionStart FROM %T
|
||||
WHERE userPHID = %s AND type LIKE %>',
|
||||
PhabricatorUser::SESSION_TABLE,
|
||||
$this->getPHID(),
|
||||
$establish_type,
|
||||
$session_key);
|
||||
$session_type.'-');
|
||||
$sessions = ipull($sessions, null, 'type');
|
||||
$sessions = isort($sessions, 'sessionStart');
|
||||
|
||||
$existing_sessions = array_keys($sessions);
|
||||
|
||||
$retries = 0;
|
||||
while (true) {
|
||||
|
||||
// Choose which 'type' we'll actually establish, i.e. what number we're
|
||||
// going to append to the basic session type. To do this, just check all
|
||||
// the numbers sequentially until we find an available session.
|
||||
$establish_type = null;
|
||||
for ($ii = 1; $ii <= $session_limit; $ii++) {
|
||||
$try_type = $session_type.'-'.$ii;
|
||||
if (!in_array($try_type, $existing_sessions)) {
|
||||
$establish_type = $try_type;
|
||||
$expect_key = $session_key;
|
||||
$existing_sessions[] = $try_type;
|
||||
|
||||
// Ensure the row exists so we can issue an update below. We don't
|
||||
// care if we race here or not.
|
||||
queryfx(
|
||||
$conn_w,
|
||||
'INSERT IGNORE INTO %T (userPHID, type, sessionKey, sessionStart)
|
||||
VALUES (%s, %s, %s, 0)',
|
||||
self::SESSION_TABLE,
|
||||
$this->getPHID(),
|
||||
$establish_type,
|
||||
$session_key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find an available session, choose the oldest session and
|
||||
// overwrite it.
|
||||
if (!$establish_type) {
|
||||
$oldest = reset($sessions);
|
||||
$establish_type = $oldest['type'];
|
||||
$expect_key = $oldest['sessionKey'];
|
||||
}
|
||||
|
||||
// UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet.
|
||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
|
||||
// This is so that we'll only overwrite the session if it hasn't been
|
||||
// refreshed since we read it. If it has, the session key will be
|
||||
// different and we know we're racing other processes. Whichever one
|
||||
// won gets the session, we go back and try again.
|
||||
|
||||
queryfx(
|
||||
$conn_w,
|
||||
'UPDATE %T SET sessionKey = %s, sessionStart = UNIX_TIMESTAMP()
|
||||
WHERE userPHID = %s AND type = %s AND sessionKey = %s',
|
||||
self::SESSION_TABLE,
|
||||
$session_key,
|
||||
$this->getPHID(),
|
||||
$establish_type,
|
||||
$expect_key);
|
||||
|
||||
unset($unguarded);
|
||||
|
||||
if ($conn_w->getAffectedRows()) {
|
||||
// The update worked, so the session is valid.
|
||||
break;
|
||||
} else {
|
||||
// We know this just got grabbed, so don't try it again.
|
||||
unset($sessions[$establish_type]);
|
||||
}
|
||||
|
||||
if (++$retries > $session_limit) {
|
||||
throw new Exception("Failed to establish a session!");
|
||||
}
|
||||
}
|
||||
|
||||
$log = PhabricatorUserLog::newLog(
|
||||
$this,
|
||||
|
|
Loading…
Reference in a new issue