Can rm be used to synchronize lock file checks?

Viewed 51

I've been trying to improve a piece of shell code implementing a broken a locking mechanism.

My idea was to let only one caller through the synchronisation by calling rm on a file.

PIDFILE=/tmp/test.pid

flag=$PIDFILE.flag
touch $flag

if [ -f $PIDFILE ]; then
  ps | grep -qE '^\s*'$(cat $PIDFILE) && exit
fi

echo $$ > $PIDFILE
# this should succeed only for one process
rm $flag || exit
echo $$ > $PIDFILE

I've done a few concurrent calls and thrown my brain against it and haven't run into a failure.

But it is actually safe?

2 Answers

It's not safe.

Assume three copies of your script (A, B and C) are started simultaneously and /tmp/test.pid does not exist initially.

Let A and B complete the initial statements of the script:

PIDFILE=/tmp/test.pid

flag=$PIDFILE.flag
touch $flag

if [ -f $PIDFILE ]; then
  ps | grep -qE '^\s*'$(cat $PIDFILE) && exit
fi

Switch to A and let it run two more statements:

echo $$ > $PIDFILE
rm $flag || exit

This succeeds; $PIDFILE now contains A's PID.

Switch over to B and let it run the same statements. rm fails and so B exits, but $PIDFILE now contains B's PID.

Switch over to C. C has just started running, so the first thing it does is to recreate $flag:

PIDFILE=/tmp/test.pid

flag=$PIDFILE.flag
touch $flag

Now comes the PID check:

if [ -f $PIDFILE ]; then
  ps | grep -qE '^\s*'$(cat $PIDFILE) && exit
fi

This passes because $PIDFILE contains B's PID, but B is no longer running.

Now we get to

echo $$ > $PIDFILE
rm $flag || exit

This also passes because C has just recreated the $flag file.

Now we have both A and C running, racing against each other to overwrite $PIDFILE again.


Apart from that there's also a "false positive" problem:

if [ -f $PIDFILE ]; then
  ps | grep -qE '^\s*'$(cat $PIDFILE) && exit
fi

You might have a stale $PIDFILE, but the PID it contains has been reused for another process. In that case you don't get a race (and too many instances of your script), but a denial of service (too few instances of your script: 0). Your script will see the running process that just happens to have the "wrong" PID and exit.

Your code is not safe, but maybe not for the reason you think.

Here's one possible sequence that may allow a process to pass the check while another one is still running:

  1. Two processes A and B start running the script, and end up at the point before the first echo $$ > $PIDFILE at the same time. For some reason, process B briefly stops here.
  2. Process A writes its PID to the pidfile, successfully unlinks the flag file, writes its PID to the pidfile again for good measure, and continues running.
  3. Now process B resumes running. It writes its PID to pidfile, overwriting it, then fails to unlink the flag file and exits. Process A is still running, but now the pidfile no longer contains its PID!
  4. Now process C comes along, recreates the flag file, notes that the pidfile exists but contains the PID of process B (which is no longer running), and thus proceeds happily past all the checks.

Other possible sequences of events leading to the same outcome exist as well.

Related