25

I need to use mutexes or semaphores in PHP, and it scares me. To clarify, I'm not scared of writing deadlock-free code that synchronizes properly or afraid of the perils of concurrent programming, but of how well PHP handles fringe cases.

Quick background: writing a credit card handler interface that sits between the users and the 3rd party credit card gateway. Need to prevent duplicate requests, and already have a system in place that works, but if the user hits submit (w/out JS enabled so I can't disable the button for them) milliseconds apart, a race condition ensues where my PHP script does not realize that a duplicate request has been made. Need a semaphore/mutex so I can ensure only one successful request goes through for each unique transaction.

I'm running PHP behind nginx via PHP-FPM with multiple processes on a multi-core Linux machine. I want to be sure that

  1. semaphores are shared between all php-fpm processes and across all cores (i686 kernel).
  2. php-fpm handles a PHP process crash while holding a mutex/semaphore and releases it accordingly.
  3. php-fpm handles a session abort while holding a mutex/semaphore and releases it accordingly.

Yes, I know. Very basic questions, and it would be foolish to think that a proper solution doesn't exist for any other piece of software. But this is PHP, and it was most certainly not built with concurrency in mind, it crashes often (depending on which extensions you have loaded), and is in a volatile environment (PHP-FPM and on the web).

With regards to (1), I'm assuming if PHP is using the POSIX functions that both these conditions hold true on a SMP i686 machine. As for (2), I see from briefly skimming the docs that there is a parameter that decides this behavior (though why would one ever want PHP to NOT release a mutex is the session is killed I don't understand). But (3) is my main concern and I don't know if it's safe to assume that php-fpm properly handles all fringe cases for me. I (obviously) don't ever want a deadlock, but I'm not sure I can trust PHP to never leave my code in a state where it cannot obtain a mutex because the session that grabbed it was either gracefully or ungracefully terminated.

I have considered using a MySQL LOCK TABLES approach, but there's even more doubt there because while I trust the MySQL lock more than the PHP lock, I fear if PHP aborts a request (with*out* crashing) while holding the MySQL session lock, MySQL might keep the table locked (esp. because I can easily envision the code that would cause this to take place).

Honestly, I'd be most comfortable with a very basic C extension where I can see exactly what POSIX calls are being made and with what params to ensure the exact behavior I want.. but I don't look forward to writing that code.

Anyone have any concurrency-related best practices regarding PHP they'd like to share?

Mahmoud Al-Qudsi
  • 28,357
  • 12
  • 85
  • 125
  • 1
    I'm unfamiliar with your set up, but when I see that you need to stop duplicate requests from being processed, I'm wondering why you can't check a [form key](http://net.tutsplus.com/tutorials/php/secure-your-forms-with-form-keys/) that is stored in the user's session. – nickb Feb 15 '12 at 07:14
  • That's basically what my existing duplicate request prevention is, and it seems there's a certain point in time where, for certain setups with multiple PHP processes, there exists a race condition with both PHP handling backends see the token/nonce/whatever as having been not yet processed. Nonces are generally used to prevent replay attacks (or general XSS), but in my case that's already protected against - except for the case where it's replayed milliseconds later as a race condition. – Mahmoud Al-Qudsi Feb 15 '12 at 07:28
  • maybe this (slightly outdated) paper on PHP and session locking / race-conditions is interesting for you. However, it seems at least PHP 5.3.2 is already using consistent `flock()` for sessions: http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/ – Kaii Feb 15 '12 at 08:45
  • For what it's worth, PHPs posix extension gives you exact control over semaphores and system calls. But you don't need it to solve your problem. https://www.php.net/sem – Kaii Nov 03 '21 at 13:36

5 Answers5

12

In fact, i think there is no need for a complex mutex / semaphore whatever solution.

Form keys stored in a PHP $_SESSION are all you need. As a nice side effect, this method also protects your form against CSRF attacks.

In PHP, sessions are locked by aquiring a POSIX flock() and PHP's session_start() waits until the user session is released. You just have to unset() the form key on the first valid request. The second request has to wait until the first one releases the session.

However, when running in a (not session or source ip based) load balancing scenario involving multiple hosts things are getting more complicated. For such a scenario, i'm sure you will find a valuable solution in this great paper: http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/

I reproduced your use case with the following demonstration. just throw this file onto your webserver and test it:

<?php
session_start();
if (isset($_REQUEST['do_stuff'])) {
  // do stuff
  if ($_REQUEST['uniquehash'] == $_SESSION['uniquehash']) {
    echo "valid, doing stuff now ... "; flush();
    // delete formkey from session
    unset($_SESSION['uniquehash']);
    // release session early - after committing the session data is read-only
    session_write_close();
    sleep(20);  
    echo "stuff done!";
  }
  else {
    echo "nope, {$_REQUEST['uniquehash']} is invalid.";
  }     
}
else {
  // show form with formkey
  $_SESSION['uniquehash'] = md5("foo".microtime().rand(1,999999));
?>
<html>
<head><title>session race condition example</title></head>
<body>
  <form method="POST">
    <input type="hidden" name="PHPSESSID" value="<?=session_id()?>">
    <input type="text" name="uniquehash" 
      value="<?= $_SESSION['uniquehash'] ?>">
    <input type="submit" name="do_stuff" value="Do stuff!">
  </form>
</body>
</html>
<?php } ?>
Kaii
  • 20,122
  • 3
  • 38
  • 60
  • Thanks for that link, it gave me the info I needed. But PHP really needs to step their game up - defaulting to `flock` under the hood for session protection? Really? I was hoping to use semaphores/mutexes/mysql/etc to avoid doing that myself for performance reasons! – Mahmoud Al-Qudsi Feb 15 '12 at 15:21
  • The latter half of the document gives me all the info I need on releasing MySQL locks (esp. MySQL will release locks if the session dies, not necessarily if it aborts assuming persistent connections) that I need to roll my own session handler. Thanks! btw your code sample above is still vulnerable to a race condition if both requests reach the comparison on line 5 before either request reaches the unset. – Mahmoud Al-Qudsi Feb 15 '12 at 15:32
  • 3
    Watch out, sessions are locked *only* if you're using the default implementation on the filesystem. If you're using a custom session handler, it'll be it's responsibility to implement locking. And honestly, there's *nothing* wrong whatsoever with `flock`. – Charles Feb 15 '12 at 15:34
  • 1
    @MahmoudAl-Qudsi "still vulnerable to a race condition" - how so? session_start does locking on your behalf. – zaf Feb 15 '12 at 19:33
  • 1
    Turns out this isn't good enough. Users without cookies aren't protected by this. *sigh* PHP. – Mahmoud Al-Qudsi Feb 18 '12 at 04:26
  • 1
    @MahmoudAl-Qudsi you can submit the session id with your form without the need of cookes! `` [see docs](http://php.net/manual/en/session.idpassing.php) ... i included it in my updated answer.. hope this helps – Kaii Feb 18 '12 at 13:27
3

An interesting question you have but you don't have any data or code to show.

For 80% of cases the chances of anything nasty happening because of PHP itself are virtually zero if you follow the standard procedures and practices regarding stopping users from submitting forms multiple times, which applies to nearly every other setup, not just PHP.

If you're the 20% and your environment demands it, then one option is using message queues which I'm sure you are familiar with. Again, this idea is language agnostic. Nothing to do with languages. Its all about how data moves around.

zaf
  • 22,776
  • 12
  • 65
  • 95
1

you can store a random hash in an array within your session data as well as print that hash as a hidden form input value. when a request comes in, if the hidden hash value exists in your session array, you can delete the hash from the session and process the form, otherwise don't.

this should prevent duplicate form submits as well as help prevent csrf attacks.

dqhendricks
  • 19,030
  • 11
  • 50
  • 83
  • Please see my comment on the question itself. I already have nonces to protect against replay and CSRF/XSS attacks. They do not prevent against race conditions as the session or mysql access is not atomic. – Mahmoud Al-Qudsi Feb 15 '12 at 07:31
  • session writes *are* atomic through `flock()` – Kaii Feb 15 '12 at 15:27
0

What I do in order to prevent session race condition in the code is after the last operation that stores data in session I use PHP function session_write_close() notice that if you are using PHP 7 you need to disable default output buffering in php.ini. If you have time consuming operations it'd be better to execute them after session_write_close() is invoked.

I hope it'll help someone, for me it saved my life :)

Robert
  • 19,800
  • 5
  • 55
  • 85
0

If the problem only arises when hitting a button milliseconds apart, wouldn't a software debouncer work? Like saving the time of a button press in a session variable and not allowing any more for, say, a second? Just a before-my-morning-coffee idea. Cheers.

tommyo
  • 531
  • 4
  • 10
  • That's basically what my existing duplicate request prevention is, and it seems there's a certain point in time where, for certain setups with multiple PHP processes, there exists a race condition with both PHP handling backends see the token/nonce/whatever as having been not yet processed. – Mahmoud Al-Qudsi Feb 15 '12 at 07:29