ID:               43182
 User updated by:  chris_se at gmx dot net
 Reported By:      chris_se at gmx dot net
-Status:           Wont fix
+Status:           Closed
 Bug Type:         Streams related
 Operating System: Any POSIX-compatible OS
 PHP Version:      5.2.4
 New Comment:

Closed to reopen the bug.


Previous Comments:
------------------------------------------------------------------------

[2007-11-04 16:12:02] chris_se at gmx dot net

Excuse me, I don't intend to sound rude - but did you even read my
report? I never even mentioned O_EXCL and it has NOTHING to do with the
problem I reported.

To summarize the problem again (and perhaps make myself clearer):

file_put_contents has - according to the documentation - a third
parameter called $flags. In the documentation, it is stated, that the
LOCK_EX constant may be passed as a flag. The documentation for
file_put_contents states:

> LOCK_EX: Acquire an exclusive lock on the file while proceeding to
the writing.

So, one may assume that the file will be locked exclusively BEFORE
writing to it.

Now, the problem is, that this is not the case! Have a look at
ext/standard/file.c:
http://cvs.php.net/viewvc.cgi/php-src/ext/standard/file.c?revision=1.409.2.6.2.27&view=markup

There you see this line of code:

        stream = php_stream_open_wrapper_ex(filename, (flags &
PHP_FILE_APPEND) ? "ab" : "wb", 
                        ((flags & PHP_FILE_USE_INCLUDE_PATH) ? USE_PATH : 0) |
ENFORCE_SAFE_MODE | REPORT_ERRORS, NULL, context);

Followed by that line:

        if (flags & LOCK_EX && (!php_stream_supports_lock(stream) ||
php_stream_lock(stream, LOCK_EX))) {

So, what does this code do? (if FILE_APPEND was not specified)

1) It openes the file in 'wb' mode for writing.
2) It locks the file exclusively
3) THEN it actually starts writing the file contents

So, the problem actually is the following:

'wb' as a fopen(3) mode translates to O_WRONLY | O_CREAT | O_TRUNC as
an open(2) mode. What does that do? It truncates the file UPON OPENING
IT! And AFTER THAT it tries to acquire the lock. Since locks on
POSIX-compatible systems are advisory (flock(2) anyway), any possible
other lock on the file will NOT be honoured by file_put_contents and the
$flag == LOCK_EX parameter is completely ineffective.

Please, have a look at my test case - it's really simple. One PHP
script opens the file for reading, acquires a shared lock and goes to
sleep. If the other PHP script is executed in the mean time, the file is
opened and then the other script tries to acquire an exclusive lock on
the same file - and has to wait until the first script releases it.
That's fine. But what's not fine is that prior to acquiring the
exclusive lock it has ALREADY modified the file! So after the first
script returns from it's sleeping phase, it will see an empty file
because it was truncated by the other script upon opening.

What the CURRENT PHP code translates to is basically:

int fd = open ('file.txt', O_WRONLY | O_CREAT | O_TRUNC, 0666);
flock (fd, LOCK_EX);

Which causes exactly the described problems.

And there IS a solution for this. The following C code CORRECTLY
acquires an exclusive lock for writing to a file WITHOUT truncating it
before it is safe to do so:

int fd = open ('file.txt', O_WRONLY | O_CREAT, 0666);
flock (fd, LOCK_EX);
ftruncate (fd, 0);
// now write something to fd

This is absolutely correct in POSIX and completely portable (assuming
flock(2) is provided by the OS, but if fcntl(2) is used as a
replacement, it does not change anything).

So now the ONLY question remains: How is it possible to integrate that
fix with PHP code?

And that is a bit more tricky than the mere C code becaues PHP uses
fopen(3)-style file modes for the stream wrapper API instead of numeric
file modes as provided by open(2). And there is currently no
fopen(3)-style file mode translating to O_WRONLY | O_CREAT.

So, I see two possible solutions:

 * Add another fopen(3) style file mode that does exactly that.
 * Remove the LOCK_EX flag from file_put_contents completely.

It DOES NOT make sense to keep the flag but leave this bug unfixed,
because

 a) It is utterly and completely useless with advisory locking.
    Keeping it will only cause people who read the documentation
    to assume it's safe to use the flag - which is plainly WRONG.

 b) If mandatory locking is used (i.e. when using Windows), the
    OTHER LOCK that is already in place on the file will take care
    for the data consistency, i.e. a lock created by another
    program will delay the truncating of the file until AFTER the
    lock is released. The exclusive lock created on the file itself
    will have NO interaction with any other lock in place prior to
    the file_put_contents_call.

------------------------------------------------------------------------

[2007-11-04 15:30:54] [EMAIL PROTECTED]

On *nix systems O_CREAT and O_EXCL are mutually exclusive and will 
prevent creation of a file if one already exists. Therefore lock needs

to be created separately or you need to create another file on the same

disk and then use atomic rename.

------------------------------------------------------------------------

[2007-11-03 16:45:01] chris_se at gmx dot net

Description:
------------
The LOCK_EX flag of file_put_contents suggests that the function will
use an advisory lock to ensure transaction safety. This is NOT the case
(except when combined with FILE_APPEND). It actually DOES request an
exclusive lock on the file but only does so AFTER opening it in the 'wb'
mode which will truncate the file on opening BEFORE the actual lock can
be acquired.

The correct behaviour would be to open the file for writing without
truncating it, in C for example using

int fileno = open (file, O_WRONLY | O_CREAT, 0666);

(WITHOUT adding O_TRUNC!), THEN acquiring the lock using flock() and
THEN truncating the file to 0 bytes length.

I don't know if there's a simple possibility to integrate it with the
current streams API (since there's no fopen mode that will map to either
O_WRONLY | O_CREAT or O_RWDR | O_CREAT) but if it's not possible to fix
it, you should at least remove the option, since it suggests something
it can't provide with advisory locking.

This is not a problem on Windows since Windows locks are always
mandatory.

Reproduce code:
---------------
First script (start in in a first window using any P):

<?php

file_put_contents ('file.txt', 'Hello World!');

$f = fopen ('file.txt', 'r') or die ("Could not open file!\n");
flock ($f, LOCK_SH) or die ("Could not acaquire lock!\n");
echo "Sleeping for 20 seconds (please use file_put_contents script in
the mean time!)\n";
sleep (20);
$x .= fread ($f, 1024);
fclose ($f);

echo "Contents was: '" . $x . "'\n";

?>

Second script (start it in a second window in the same directory while
the first one is sleeping):

<?php
file_put_contents ('file.txt', 'ByeBye Joe!', LOCK_EX);
?>

Expected result:
----------------
The first script should output:

Sleeping for 20 seconds (please use file_put_contents script in the
mean time!)
Contents was: 'Hello World!'


Actual result:
--------------
The first script outputs:

Sleeping for 20 seconds (please use file_put_contents script in the
mean time!)
Contents was: ''



------------------------------------------------------------------------


-- 
Edit this bug report at http://bugs.php.net/?id=43182&edit=1

Reply via email to