Subject: Introduce a time-of-check-time-of-use (TOCTOU) save file
  handler, this makes sure that from the time we check it is available
  until we get the file handler that no one attempted to put in a
  soft / hard link instead of what we checked for.
Origin: http://svn.php.net/viewvc?view=revision&revision=309042

CVE-2011-1144

Index: PEAR/REST.php
===================================================================
--- PEAR/REST.php	(revision 309041)
+++ PEAR/REST.php	(revision 309042)
@@ -228,59 +228,75 @@
         $cacheidfile = $d . 'rest.cacheid';
         $cachefile   = $d . 'rest.cachefile';
 
+        if (!is_dir($cache_dir)) {
+            if (System::mkdir(array('-p', $cache_dir) === false)) {
+              return PEAR::raiseError("The value of config option cache_dir ($cache_dir) is not a directory and attempts to create the directory failed.");
+            }
+        }
+
         if ($cacheid === null && $nochange) {
             $cacheid = unserialize(implode('', file($cacheidfile)));
         }
 
-        if (is_link($cacheidfile)) {
-            return PEAR::raiseError('SECURITY ERROR: Will not write to ' . $cacheidfile . ' as it is symlinked to ' . readlink($cacheidfile) . ' - Possible symlink attack');
-        }
+        $idData = serialize(array(
+            'age'        => time(),
+            'lastChange' => ($nochange ? $cacheid['lastChange'] : $lastmodified),
+        ));
 
-        if (is_link($cachefile)) {
-            return PEAR::raiseError('SECURITY ERROR: Will not write to ' . $cacheidfile . ' as it is symlinked to ' . readlink($cacheidfile) . ' - Possible symlink attack');
+        $result = $this->saveCacheFile($cacheidfile, $idData);
+        if (PEAR::isError($result)) {
+            return $result;
+        } elseif ($nochange) {
+            return true;
         }
 
-        $cacheidfile_fp = @fopen($cacheidfile, 'wb');
-        if (!$cacheidfile_fp) {
-            if (is_dir($cache_dir)) {
-                return PEAR::raiseError("The value of config option cache_dir ($cache_dir) is not a directory. ");
+        $result = $this->saveCacheFile($cachefile, serialize($contents));
+        if (PEAR::isError($result)) {
+            if (file_exists($cacheidfile)) {
+              @unlink($cacheidfile);
             }
 
-            System::mkdir(array('-p', $cache_dir));
-            $cacheidfile_fp = @fopen($cacheidfile, 'wb');
-            if (!$cacheidfile_fp) {
-                return PEAR::raiseError("Could not open $cacheidfile for writing.");
-            }
+            return $result;
         }
 
-        if ($nochange) {
-            fwrite($cacheidfile_fp, serialize(array(
-                'age'        => time(),
-                'lastChange' => $cacheid['lastChange'],
-                ))
-            );
+        return true;
+    }
 
-            fclose($cacheidfile_fp);
-            return true;
-        }
+    function saveCacheFile($file, $contents)
+    {
+        $len = strlen($contents);
 
-        fwrite($cacheidfile_fp, serialize(array(
-            'age'        => time(),
-            'lastChange' => $lastmodified,
-            ))
-        );
-        fclose($cacheidfile_fp);
+        $cachefile_fp = @fopen($file, 'xb'); // x is the O_CREAT|O_EXCL mode
+        if ($cachefile_fp !== false) { // create file
+            if (fwrite($cachefile_fp, $contents, $len) < $len) {
+                fclose($cachefile_fp);
+                return PEAR::raiseError("Could not write $file.");
+            }
+        } else { // update file
+            $cachefile_lstat = lstat($file);
+            $cachefile_fp = @fopen($file, 'wb');
+            if (!$cachefile_fp) {
+                return PEAR::raiseError("Could not open $file for writing.");
+            }
 
-        $cachefile_fp = @fopen($cachefile, 'wb');
-        if (!$cachefile_fp) {
-            if (file_exists($cacheidfile)) {
-                @unlink($cacheidfile);
+            $cachefile_fstat = fstat($cachefile_fp);
+            if (
+              $cachefile_lstat['mode'] == $cachefile_fstat['mode'] &&
+              $cachefile_lstat['ino']  == $cachefile_fstat['ino'] &&
+              $cachefile_lstat['dev']  == $cachefile_fstat['dev'] &&
+              $cachefile_fstat['nlink'] === 1
+            ) {
+                if (fwrite($cachefile_fp, $contents, $len) < $len) {
+                    fclose($cachefile_fp);
+                    return PEAR::raiseError("Could not write $file.");
+                }
+            } else {
+                fclose($cachefile_fp);
+                $link = function_exists('readlink') ? readlink($file) : $file;
+                return PEAR::raiseError('SECURITY ERROR: Will not write to ' . $file . ' as it is symlinked to ' . $link . ' - Possible symlink attack');
             }
-
-            return PEAR::raiseError("Could not open $cacheidfile for writing.");
         }
 
-        fwrite($cachefile_fp, serialize($contents));
         fclose($cachefile_fp);
         return true;
     }
@@ -464,4 +480,4 @@
 
         return $data;
     }
-}
\ No newline at end of file
+}
