ID: 43182 User updated by: chris_se at gmx dot net Reported By: chris_se at gmx dot net Status: Wont fix Bug Type: Streams related Operating System: Any POSIX-compatible OS PHP Version: 5.2.4 New Comment:
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. Previous Comments: ------------------------------------------------------------------------ [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