|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2007-11-03 16:45 UTC] 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: ''
PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Sat Oct 25 14:00:01 2025 UTC |
Oh, I forgot: There's a solution for this problem that is quite a bit easier as the other one I suggested: ALWAYS open the file in append mode and truncate it AFTER acquiring the lock. Normally this has the drawback that the append mode does NOT have the same semantics as the write mode (i.e. using fseek to reposition the write pointer won't work as the contents will always be appended to the end of the file) but that doesn't matter here since a) with ftruncate() the end of the file will be at byte zero and b) file_put_contents only writes out the complete data and does not need to reposition the write pointer of the file. Well, at least with normal C and fopen(3) it's not that complicated, with PHP's streams API it gets complicated again because some stream wrappers don't support truncating of a file (e.g. the FTP wrapper). Those don't support locking either, so that wouldn't be a problem anyway except that it's as far as I could see not possible to determine whether truncating the file will be possible BEFORE opening it. Anyway, here's a patch against the PHP 5.2 branch that fixes this bug: =================================================================== RCS file: /repository/php-src/ext/standard/file.c,v retrieving revision 1.409.2.6.2.28 diff -u -r1.409.2.6.2.28 file.c --- ext/standard/file.c 4 Sep 2007 12:51:49 -0000 1.409.2.6.2.28 +++ ext/standard/file.c 6 Nov 2007 18:49:48 -0000 @@ -604,16 +604,44 @@ context = php_stream_context_from_zval(zcontext, flags & PHP_FILE_NO_DEFAULT_CONTEXT); - stream = php_stream_open_wrapper_ex(filename, (flags & PHP_FILE_APPEND) ? "ab" : "wb", + // open in append mode and truncate later with LOCK_EX + // note that this will cause the stream to be closed and opened again + // if truncating is not supported - but there is no way with the + // current streams API to check before actually opening the file + stream = php_stream_open_wrapper_ex(filename, (flags & PHP_FILE_APPEND || flags & LOCK_EX) ? "ab" : "wb", ((flags & PHP_FILE_USE_INCLUDE_PATH) ? USE_PATH : 0) | ENFORCE_SAFE_MODE | REPORT_ERRORS, NULL, context); if (stream == NULL) { RETURN_FALSE; } + // worst-case scenario: user wanted LOCK_EX but the stream can't be + // truncated. So, reopen in 'wb' mode and ignore locking + // (TODO: possibly generate a warning that LOCK_EX wasn't possible with + // this stream?) + if (!(flags & PHP_FILE_APPEND) && (flags & LOCK_EX) && !php_stream_truncate_supported(stream)) { + php_stream_close(stream); + stream = php_stream_open_wrapper_ex(filename, "wb", + ((flags & PHP_FILE_USE_INCLUDE_PATH) ? USE_PATH : 0) | ENFORCE_SAFE_MODE | REPORT_ERRORS, NULL, context); + // remove LOCK_EX flag - locking will be useless now anyway + // (we just truncated the file) + flags = flags & ~LOCK_EX; + } if (flags & LOCK_EX && (!php_stream_supports_lock(stream) || php_stream_lock(stream, LOCK_EX))) { php_stream_close(stream); RETURN_FALSE; } + + // if the file was to be locked and not to be opened in append mode, + // we now need to truncate it (we already checked for truncate support + // and reopened the stream and removed LOCK_EX if it wasn't available) + if (!(flags & PHP_FILE_APPEND) && (flags & LOCK_EX)) { + if (php_stream_truncate_set_size(stream, 0)) { + // we couldn't truncate the stream even though + // truncating was supported + php_stream_close(stream); + RETURN_FALSE; + } + } switch (Z_TYPE_P(data)) { case IS_RESOURCE: =================================================================== It is a bit ugly because of the problem described above: It can only be determined whether it's possible to truncate the file after it was opened, so if it's not, it has to be closed and opened again. The function remains functional in any case, just causes a little additional overhead if a stream wrapper without file truncation support (e.g. FTP) AND LOCK_EX are used together. So, you have four possibilities now: 1) Use the patch as-is. 2) Improve the patch by somehow detecting truncate support before opening the file (and therefore removing the need to reopen the file if such a wrapper is used in combination with LOCK_EX). 3) Add the already mentioned file mode that doesn't include O_TRUNC 4) Remove the LOCK_EX flag from file_put_contents alltogether.