Problem

  1. When running a web test via the Simpletest UI (not run-tests.sh), then the session of the test runner leaks into the test.

Proposed solution

  1. Write and close the session in TestBase::prepareEnvironment().

  2. Back up the current session data in TestBase::prepareEnvironment().

  3. Restore the original session data in TestBase::restoreEnvironment().

  4. Stop disabling sessions for tests.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

sun’s picture

Status: Active » Needs review
FileSize
5.2 KB

Status: Needs review » Needs work

The last submitted patch, 1: test.session.1.patch, failed testing.

sun’s picture

sun’s picture

Status: Postponed » Needs review

1: test.session.1.patch queued for re-testing.

Status: Needs review » Needs work

The last submitted patch, 1: test.session.1.patch, failed testing.

znerol’s picture

Status: Needs work » Needs review
FileSize
6.95 KB

Let's try a different approach: Prevent that session data is leaked into the UI test runner by closing (saving) the session in TestBase::prepareEnvironment() and then setting the session-name (i.e. the name of the session cookie) to a random value. If a test starts a new session, then it will be associated with a different session-name. After the test-run (TestBase::restoreEnvironment() it can be safely destroyed.

Another benefit of not using SessionManager::disable() in the test-runner is that we can implement the temporary session write-protection in a better way. I would like to remove that responsibility from the session-manager and instead implement it directly in the session-handler (or even better, a session-handler proxy).

sun’s picture

+++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
@@ -1512,4 +1533,15 @@ public function copyConfig(StorageInterface $source_storage, StorageInterface $t
+  public function sessionName() {
+    return $this->originalSessionName;
+  }

Hm. This patch looked fine until I saw this ;-)

All of the original* properties are supposed to be private and not supposed to be available/reachable within tests; cf. #2177079: Test classes have access to TestBase::$original* properties

I first wanted to mention that the method is missing a 'get' prefix and should also contain "Original" in its name...

But when doing so, we'd only "officially accept/endorse" that the session does leak into tests... which doesn't really resolve the stated problem of this issue :-/

TBH, I don't know why the cURL code takes over the session name of the test runner by default. That clearly seems to be one part of the root cause, and shouldn't be required, since the CLI test runner has no session to begin with. (Although it has a session_name(), since that is set up in drupal_environment_initialize() already, but that should be irrelevant for tests)

znerol’s picture

But when doing so, we'd only "officially accept/endorse" that the session does leak into tests... which doesn't really resolve the stated problem of this issue :-/

All that the patch is supposed to do is to ensure that test code (DUTB as well as the parent site when running WTB) has no way to clobber a) in-memory session data ($_SESSION) and b) on-disk session data of the execution environment.

It is indeed unfortunate that the WTB child site also reuses the session-name of the execution environment, but that is a different problem. We can solve that by making the session-name configurable from within settings.php. Given that this will involve changing non-test code, I'd prefer to not extend the scope of this issue here.

All of the original* properties are supposed to be private and not supposed to be available/reachable within tests. [...] I don't know why the cURL code takes over the session name of the test runner by default.

Fixed. In order to keep track of changes to the session, the WTB parent site needs to know the session name of the child site.

Also I found that we need to make sure that a session cookie set during test-execution is deleted when the session is destroyed.

sun’s picture

Status: Needs review » Reviewed & tested by the community

OK, this is definitely an improvement over status quo, so I think it's fine to move forward here. (It also sounds as if this fix would help us to move forward on session system refactoring?)

Normally, this would need explicit test coverage in SimpleTestTest to prove that the regression is fixed (i.e., session data of the test runner does not leak into a test and session data of a test does not leak into the test runner). The existing test coverage only ensures that a logged-in user can execute the UI test runner, which doesn't cover the actual session data.

Regarding a test-specific session name, I assume that's probably as simple as checking drupal_valid_test_ua() to conditionally add a "TESTING" prefix to the default session name in drupal_environment_initialize().

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 8: 2239969-fix-session-leak-8.diff, failed testing.

znerol’s picture

8: 2239969-fix-session-leak-8.diff queued for re-testing.

The last submitted patch, 8: 2239969-fix-session-leak-8.diff, failed testing.

Xano’s picture

What are the symptoms of this bug? I have a feeling this may be related to session issues I have been having since #1289536: Switch Watchdog to a PSR-3 logging framework was committed. That one causes SQL errors in the installer and maybe LogicException: Cannot change the ID of an active session in Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy->setId() as well.

znerol’s picture

@@ -1107,7 +1123,6 @@ protected function rebuildContainer($environment = 'prod') {
     else {
       $this->container->get('request_stack')->push($request);
     }
-    $this->container->get('current_user')->setAccount(\Drupal::currentUser());
 
     // The request context is normally set by the router_listener from within
     // its KernelEvents::REQUEST listener. In the simpletest parent site this

This fixes the LogicException: Cannot change the ID of an active session in most if not all cases. Setting the account on the current_user service is in fact a noop here (because $this->container is set to Drupal::getContainer() a couple of lines before). However since #2238087: Rebase SessionManager onto Symfony NativeSessionStorage it may cause side-effects.

Other than that, this patch only affects testing through the UI (command line testing is not affected).

znerol’s picture

8: 2239969-fix-session-leak-8.diff queued for re-testing.

Xano’s picture

I haven't dug into this a lot, but removing that line from WebTestBase does not prevent the errors in my particular case.

znerol’s picture

Status: Needs work » Reviewed & tested by the community

Back to RTBC, test failures from #10 and #12 were bogus.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Nice - this fixes a bug in KernelTestBase tests (eg TextPlainUnitTest) that could not be run through the UI.

Committed 5a3ef30 and pushed to 8.x. Thanks!

  • Commit 5a3ef30 on 8.x by alexpott:
    Issue #2239969 by znerol, sun: Session of (UI) test runner leaks into...
xjm’s picture

Title: Session of (UI) test runner leaks into web tests » [HEAD BROKEN] Session of (UI) test runner leaks into web tests
Priority: Normal » Critical
Status: Fixed » Reviewed & tested by the community

This causes:

Drupal core - 8.x fail: https://qa.drupal.org/8.x-status

Overall Summary
==============================================

FAILED: [[SimpleTest]]: [PHP 5.4 MySQL] 71,771 pass(es), 1 fail(s), and 0 exception(s).

Individual Environment Summaries
==============================================

-- [[SimpleTest]]: [PHP 5.4 MySQL] --

* Drupal\simpletest\Tests\InstallationProfileModuleTestsTest (27 pass(es), 1 fail(s), and 0 exception(s))
   - [fail] [Other] ""SystemListingCompatibleTest test executed." found" in InstallationProfileModuleTestsTest.php on line 63 of Drupal\simpletest\Tests\InstallationProfileModuleTestsTest->testInstallationProfileTests().

Reverting it resolved the failure for me locally.

  • Commit 6eec619 on 8.x by webchick:
    Revert "Issue #2239969 by znerol, sun: Session of (UI) test runner leaks...
webchick’s picture

Priority: Critical » Normal
Status: Reviewed & tested by the community » Needs work

Reverted 5a3ef30 for now.

webchick’s picture

Title: [HEAD BROKEN] Session of (UI) test runner leaks into web tests » Session of (UI) test runner leaks into web tests
xjm’s picture

8: 2239969-fix-session-leak-8.diff queued for re-testing.

alexpott’s picture

And actually whilst it fixes KernelTestBase tests being run from the UI, in fact all WebTestBase tests appear to by broken with this patch applied.

The last submitted patch, 8: 2239969-fix-session-leak-8.diff, failed testing.

znerol’s picture

Oh, fun. Seems like this is in fact incompatible with #1289536: Switch Watchdog to a PSR-3 logging framework. #8 only green turned green again because the watchdog patch was reverted intermittently.

Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy->setId('oDa6BpniF7lKzhBL2R58P22TaIf_bt8sTy8enMZhXBk')
Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->setId('oDa6BpniF7lKzhBL2R58P22TaIf_bt8sTy8enMZhXBk')
Drupal\Core\Session\SessionManager->initialize()
Drupal\Core\Authentication\Provider\Cookie->authenticate(Object)
Drupal\Core\Authentication\AuthenticationManager->authenticate(Object)
Drupal\Core\Session\AccountProxy->getAccount()
Drupal\Core\Session\AccountProxy->id()
Drupal\Core\Logger\LoggerChannel->log(6, '%module module installed.', Array)
watchdog('system', '%module module installed.', Array, 6)
Drupal\Core\Extension\ModuleHandler->install(Array, 1)
Drupal\simpletest\WebTestBase->setUp()
Drupal\simpletest\Tests\InstallationProfileModuleTestsTest->setUp()
Drupal\simpletest\TestBase->run()
_simpletest_batch_operation(Array, '3', Array)
call_user_func_array('_simpletest_batch_operation', Array)
_batch_process()
_batch_do()
_batch_page(Object)
Drupal\system\Controller\BatchController->batchPage(Object)
call_user_func_array(Array, Array)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1)
Drupal\Core\HttpKernel->handle(Object, 1, 1)
Drupal\Core\DrupalKernel->handle(Object)
drupal_handle_request()
znerol’s picture

I see two options on how this could be fixed.

  1. Easy: Fix SessionManager::initialize() such that it can be called multiple times, i.e. when initializing a lazy session for anonymous users, do not attempt to set the session-id if it already exists.
  2. Align the session with the current user service, i.e. do not persist the session_manager service. This means we'd need to properly close/write the session before constructing a new container and restart it afterwards (if appropriate), similar to how the request is handled in DrupalKernel::initializeContainer()

Option 1 would be easy to implement, but I fear we'd just hide the problem. Option 2 could be tricky but it would also help with #2256257: Move token seed in SessionManager in isSessionObsolete() and regenerate() to the MetadataBag I think.

xjm’s picture

And actually whilst it fixes KernelTestBase tests being run from the UI, in fact all WebTestBase tests appear to by broken with this patch applied.

Uh. How on earth do we not have test coverage for that?

alexpott’s picture

So this is yet another example of why persisted services are a bad idea. +1 for option 2

I've opened #2272879: Can not run a WebTestBase or KernelTest base tests from inside a simpletest to test running tests through the UI

alexpott’s picture

I think we should postpone this on #2272879: Can not run a WebTestBase or KernelTest base tests from inside a simpletest as that contains a commented out test for KernelTestBase and an actual test and fix for WebTestBase - so we can commit this patch knowing we have not broken anything and proving that we're fixing stuff :)

sun’s picture

By now, we're trying to cope with unmanageable effects and symptoms. The root cause seems pretty clear, and even though I hate to say it, we most likely have to revert #1289536: Switch Watchdog to a PSR-3 logging framework a second time.

znerol’s picture

alexpott’s picture

re #32 I'm not sure about that since the errors are caused by not having a clear separation of sessions between the test runner and the test.

znerol’s picture

Just a quick note: with #2272987-9: Do not persist session manager and #19 reapplied, both of the following tests pass on the command line as well as in the UI test-runner:

  • Formatter\TextPlainUnitTest
  • InstallationProfileModuleTestsTest
alexpott’s picture

znerol’s picture

Status: Needs work » Needs review
FileSize
7.55 KB

Simple reroll. Should fail the same way as #8.

Status: Needs review » Needs work

The last submitted patch, 37: 2239969-fix-session-leak-37.diff, failed testing.

neclimdul’s picture

The failing test creates a site with the testing profile. It then logs in and submits the web test runner with a test provide by a module in that profile. The error I got was in the test results of the test:

The test did not complete due to a fatal error.

LogicException: Cannot change the ID of an active session in Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy->setId() (line 123 of /.../d8/core/vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/Session/Storage/Proxy/AbstractProxy.php).
Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy->setId('iYbw8hs_k-nhcQSTpoVOrjRDUJzBtw43RAXO6In8rU8')
Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->setId('iYbw8hs_k-nhcQSTpoVOrjRDUJzBtw43RAXO6In8rU8')
Drupal\Core\Session\SessionManager->initialize()
Drupal\Core\Authentication\Provider\Cookie->authenticate(Object)
Drupal\Core\Authentication\AuthenticationManager->authenticate(Object)
Drupal\Core\Session\AccountProxy->getAccount()
Drupal\Core\Session\AccountProxy->id()
Drupal\Core\Logger\LoggerChannel->log(6, '%module module installed.', Array)
watchdog('system', '%module module installed.', Array, 6)
Drupal\Core\Extension\ModuleHandler->install(Array, 1)
Drupal\simpletest\WebTestBase->setUp()
Drupal\simpletest\TestBase->run()
_simpletest_batch_operation(Array, '1', Array)
call_user_func_array('_simpletest_batch_operation', Array)
_batch_process(Array)
_batch_progress_page()
_batch_page(Object)
Drupal\system\Controller\BatchController->batchPage(Object)
call_user_func_array(Array, Array)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1)
Drupal\Core\HttpKernel->handle(Object, 1, 1)
Drupal\Core\DrupalKernel->handle(Object)

The executed test doesn't do anything strange. just install a minimal profile and a single test module and then execute:

$this->pass(__CLASS__ . ' test executed.');

Its all a pretty strange thing to be the only failure.

neclimdul’s picture

FileSize
7.86 KB
557 bytes

Wow, stepped through this and it is nasty. Sort of in the scope of what we're trying to fix though.

So the big problem is how we run through the installer which is working hard on the session and expecting multiple request from inside the setUp method, all in one request.

First, our session initialize() method gets called repeatedly through watchdog calls in the installer. This turns out is fine because the session is shut down.

Then we hit this code in the instaler:

  $account = user_load(1);
  user_login_finalize($account);

This logs the admin user in, or at least it should. It also calls SessionStorageInterface::regenerate() which actually triggers the session to be started. This is also where the first actual weird thing happens because the actual session handler registered with php has a reference to the parent site request stack which doesn't see simpletest session name cookie so when we hit SessionHandler::read() actually logs out the user back out.

Now, we hit initialize() from another watchdog request. This time from webtest installing extra modules (not that it really matters where) and we have a logged out user so we skip to the lazy anon block where we indescriminately call setId() and boom, and exception is thrown because the session is already started.

So the easy fix is to close the session after the installer runs. It seems like we should probably push a request on to the stack and maybe log uid 1 out.

neclimdul’s picture

Status: Needs work » Needs review

Take a look test bot. see how we're doing.

znerol’s picture

because the actual session handler registered with php has a reference to the parent site request stack

This is exactly the problem with persist-services. #2272987: Do not persist session manager will fix that problem, see #35.

neclimdul’s picture

I guess it isn't clear but I'm actually okay rtbc'ing this.

znerol’s picture

Reuploading #37 after #2272987: Do not persist session manager went in.

sun’s picture

Assigned: sun » Unassigned
  1. +++ b/core/modules/simpletest/src/TestBase.php
    @@ -1035,8 +1041,17 @@ private function prepareEnvironment() {
    +    if (PHP_SAPI != 'cli' && session_status() == PHP_SESSION_ACTIVE) {
    
    @@ -1151,6 +1166,15 @@ protected function tearDown() {
    +    if (PHP_SAPI != 'cli' && session_status() == PHP_SESSION_ACTIVE) {
    

    Can we use strict operators for both comparisons?

  2. +++ b/core/modules/simpletest/src/TestBase.php
    @@ -1035,8 +1041,17 @@ private function prepareEnvironment() {
    +    $this->originalSessionName = session_name();
    +    session_name('SIMPLETEST' . Crypt::randomBytesBase64());
    

    Can be shortened to just one line, since session_name() returns the current/previous name.

  3. +++ b/core/modules/simpletest/src/WebTestBase.php
    +   * Return the session name in use on the child site.
    

    *Returns

  4. +++ b/core/modules/simpletest/src/WebTestBase.php
    @@ -816,6 +826,12 @@ protected function setUp() {
    +    // The simpletest child site currently uses the same session name as the
    +    // execution environment.
    

    "The test child site uses the same session name as the test runner."

  5. +++ b/core/modules/simpletest/src/WebTestBase.php
    @@ -816,6 +826,12 @@ protected function setUp() {
    +    $this->session_name = $this->originalSessionName;
    

    The only non-minor issue in this patch:

    This reassignment is confusing, because it does not change the actual session_name() within the test runner.

    I.e.: $this->session_name != session_name()

    Given that TestBase already closes/destroys the session as necessary, do we actually need the additional back and forth with the session name…?

Status: Needs review » Needs work

The last submitted patch, 44: 2239969-fix-session-leak-44.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
FileSize
1.55 KB
8.87 KB

Turns out that I forgot one hunk when rerolling #2272987: Do not persist session manager after the bootstrap-patch landed. Let's see what happens when it is restored, will #45 afterwards.

One note though:

@@ -907,11 +907,33 @@ protected function setUp() {
...
     $this->kernel = DrupalKernel::createFromRequest($request, drupal_classloader(), 'prod', TRUE);
+    $this->kernel->prepareLegacyRequest($request);
     // Force the container to be built from scratch instead of loaded from the
     // disk. This forces us to not accidently load the parent site.
     $container = $this->kernel->rebuildContainer();
-    $this->kernel->prepareLegacyRequest($request);
...

I guess the intention of rebuilding the kernel before preparing it was to avoid building the container repeatedly. However, the result was that the container still was compiled two times (once by prepareLegacyRequest() -> boot() -> initializeContainer() and once by rebuildContainer()). Additionally that sequence of calls somehow did lead to the session-manager having injected an empty request-stack service. Swapping the lines results in a) the container being compiled only once and b) the proper request-stack service injected into the session-manager.

neclimdul’s picture

I think it forced the rebuild first to ensure we didn't load the parent site container at all. We may have fixed that since then through other changes.

znerol’s picture

Addresses #45. Did not include 2 because that would stretch the line over 80 chars, also I like the explicitness of recording the old value vs. assigning the new one. Regarding 4..5, reworded the comment a little bit.

This patch essentially introduces throw-away sessions during execution of test-methods. That makes it impossible to access the proper session name (which is required for SessionTest and SessionHttpsTest) via session_name() from within a test-method.

alexpott’s picture

FileSize
1.44 KB
9.11 KB

I think we can simplify the code in WebTestBase to just close the session rather than restarting it.

alexpott’s picture

FileSize
1.69 KB
9.82 KB

Maybe we can simplify even more - the only reason we have a session to close is the user login in install_finished() which only really makes sense for interactive installs.

sun’s picture

Works for me. The non-interactive installer should not start a session or perform any other UI related operations.

I can't think of a non-installer & non-tests scenario in which we'd encounter this problem space, so this "API usage workaround" in the installer code looks sufficient to me.

+++ b/core/modules/simpletest/src/TestBase.php
@@ -1035,8 +1041,17 @@ private function prepareEnvironment() {
+    session_name('SIMPLETEST' . Crypt::randomBytesBase64());

Either here or in a separate issue:

Let's consider to change DrupalKernel::initializeCookieGlobals() to conditionally use the simpletest UA prefix as session_name() when being booted in a test child site.

That way, we could unconditionally set a new test-site-specific session name once in prepareEnvironment() and be done with it.

neclimdul’s picture

I started to comment on this yesterday but it seems to have gotten lost.

The only thing it _might_ affect is drush but clearly not significantly because Testbot ran fine.

Lets move the UA stuff into another issue. I have strong feelings about adding testing code to the kernel and I don't want to block this.

+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -816,6 +826,14 @@ protected function setUp() {
+    // @todo: Introduce a setting such that the session name can be customized
+    // for the child site.

Is this really needed? Otherwise looks rtbc.

alexpott’s picture

FileSize
9.71 KB
654 bytes

Ok let's remove the @todo - it't not properly formatted and really should have an existing issue.

neclimdul’s picture

Status: Needs review » Reviewed & tested by the community

Skimmed again and still looks good.

Dries’s picture

Status: Reviewed & tested by the community » Fixed

Looks good to me. Committed to 8.x.

  • Dries committed e45c1ea on 8.x
    Issue #2239969 by znerol, alexpott, neclimdul, sun: Fixed session of (UI...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.