|
| 1 | +--TEST-- |
| 2 | +CGI/FastCGI: boundary rendezvous objects live in a per-uid private directory and fail closed when it is squatted |
| 3 | +--SKIPIF-- |
| 4 | +<?php |
| 5 | +if (PHP_OS_FAMILY === 'Windows') die('skip boundary shared memory is not supported on Windows'); |
| 6 | +if (!function_exists('proc_open')) die('skip proc_open() not available'); |
| 7 | +$root = dirname(__DIR__, 3); |
| 8 | +$candidates = [ |
| 9 | + getenv('TEST_PHP_CGI_EXECUTABLE') ?: null, |
| 10 | + $root . '/sapi/cgi/php-cgi', |
| 11 | +]; |
| 12 | +foreach ($candidates as $candidate) { |
| 13 | + if ($candidate !== null && is_file($candidate) && is_executable($candidate)) { |
| 14 | + return; |
| 15 | + } |
| 16 | +} |
| 17 | +die('skip CGI SAPI binary not available'); |
| 18 | +?> |
| 19 | +--FILE-- |
| 20 | +<?php |
| 21 | + |
| 22 | +function user_cache_cgi_binary(): string |
| 23 | +{ |
| 24 | + $root = dirname(__DIR__, 3); |
| 25 | + foreach ([getenv('TEST_PHP_CGI_EXECUTABLE') ?: null, $root . '/sapi/cgi/php-cgi'] as $candidate) { |
| 26 | + if ($candidate !== null && is_file($candidate) && is_executable($candidate)) { |
| 27 | + return $candidate; |
| 28 | + } |
| 29 | + } |
| 30 | + |
| 31 | + throw new RuntimeException('CGI SAPI binary not available'); |
| 32 | +} |
| 33 | + |
| 34 | +function user_cache_cgi_run(string $phpCgi, string $script, string $docRoot, string $lockfilePath, string $query): array |
| 35 | +{ |
| 36 | + $process = proc_open( |
| 37 | + [ |
| 38 | + $phpCgi, '-n', '-q', |
| 39 | + '-d', 'user_cache.enable=1', |
| 40 | + '-d', 'user_cache.shm_size=16M', |
| 41 | + '-d', 'user_cache.lockfile_path=' . $lockfilePath, |
| 42 | + '-d', 'display_errors=0', |
| 43 | + '-d', 'log_errors=0', |
| 44 | + ], |
| 45 | + [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], |
| 46 | + $pipes, |
| 47 | + $docRoot, |
| 48 | + [ |
| 49 | + 'REDIRECT_STATUS' => '1', |
| 50 | + 'REQUEST_METHOD' => 'GET', |
| 51 | + 'SCRIPT_FILENAME' => $script, |
| 52 | + 'DOCUMENT_ROOT' => $docRoot, |
| 53 | + 'SERVER_NAME' => 'private-dir.local', |
| 54 | + 'QUERY_STRING' => $query, |
| 55 | + ] |
| 56 | + ); |
| 57 | + if (!is_resource($process)) { |
| 58 | + throw new RuntimeException('Unable to start php-cgi'); |
| 59 | + } |
| 60 | + |
| 61 | + fclose($pipes[0]); |
| 62 | + $stdout = stream_get_contents($pipes[1]); |
| 63 | + $stderr = stream_get_contents($pipes[2]); |
| 64 | + fclose($pipes[1]); |
| 65 | + fclose($pipes[2]); |
| 66 | + proc_close($process); |
| 67 | + |
| 68 | + /* A warning raised during request startup flushes the CGI headers |
| 69 | + * before -q applies; the script body is the last line either way. */ |
| 70 | + $lines = preg_split('/\r?\n/', trim($stdout)); |
| 71 | + |
| 72 | + return [end($lines), trim($stderr)]; |
| 73 | +} |
| 74 | + |
| 75 | +function user_cache_cgi_rm_rf(string $path): void |
| 76 | +{ |
| 77 | + if (!file_exists($path) && !is_link($path)) { |
| 78 | + return; |
| 79 | + } |
| 80 | + |
| 81 | + if (!is_dir($path) || is_link($path)) { |
| 82 | + unlink($path); |
| 83 | + return; |
| 84 | + } |
| 85 | + |
| 86 | + foreach (scandir($path) as $entry) { |
| 87 | + if ($entry === '.' || $entry === '..') { |
| 88 | + continue; |
| 89 | + } |
| 90 | + user_cache_cgi_rm_rf($path . DIRECTORY_SEPARATOR . $entry); |
| 91 | + } |
| 92 | + |
| 93 | + rmdir($path); |
| 94 | +} |
| 95 | + |
| 96 | +function user_cache_private_dir_entries(string $dir): array |
| 97 | +{ |
| 98 | + $entries = scandir($dir); |
| 99 | + |
| 100 | + return $entries === false ? [] : array_values(array_diff($entries, ['.', '..'])); |
| 101 | +} |
| 102 | + |
| 103 | +$root = sys_get_temp_dir() . '/php-user-cache-cgi-private-dir-' . getmypid(); |
| 104 | +$docRoot = $root . '/doc'; |
| 105 | +$script = $docRoot . '/index.php'; |
| 106 | + |
| 107 | +user_cache_cgi_rm_rf($root); |
| 108 | +mkdir($docRoot, 0777, true); |
| 109 | + |
| 110 | +/* A directory created by this process is owned by the effective uid. */ |
| 111 | +$uid = fileowner($root); |
| 112 | +$privateName = '.ZendUserCacheBnd.' . $uid; |
| 113 | + |
| 114 | +file_put_contents($script, <<<'PHP' |
| 115 | +<?php |
| 116 | +$cache = UserCache\Cache::getPool('private-dir'); |
| 117 | +if (($_GET['action'] ?? 'fetch') === 'seed') { |
| 118 | + $cache->store('key', 'seeded'); |
| 119 | +} |
| 120 | +echo UserCache\Cache::getStatus()->getAvailability()->name, ':', $cache->fetch('key', 'MISS'), "\n"; |
| 121 | +PHP); |
| 122 | + |
| 123 | +try { |
| 124 | + $phpCgi = user_cache_cgi_binary(); |
| 125 | + |
| 126 | + /* Healthy startup: two independently started processes rendezvous |
| 127 | + * through the private directory. */ |
| 128 | + $lockfilePath = $root . '/lock'; |
| 129 | + mkdir($lockfilePath, 0777); |
| 130 | + |
| 131 | + [$stdout, $stderr] = user_cache_cgi_run($phpCgi, $script, $docRoot, $lockfilePath, 'action=seed'); |
| 132 | + echo "first: $stdout", $stderr !== '' ? " [stderr: $stderr]" : '', "\n"; |
| 133 | + [$stdout, $stderr] = user_cache_cgi_run($phpCgi, $script, $docRoot, $lockfilePath, 'action=fetch'); |
| 134 | + echo "second: $stdout", $stderr !== '' ? " [stderr: $stderr]" : '', "\n"; |
| 135 | + |
| 136 | + $privateDir = $lockfilePath . '/' . $privateName; |
| 137 | + var_dump(is_dir($privateDir) && !is_link($privateDir)); |
| 138 | + var_dump(fileowner($privateDir) === $uid); |
| 139 | + var_dump(sprintf('%04o', fileperms($privateDir) & 0777)); |
| 140 | + |
| 141 | + $entries = user_cache_private_dir_entries($privateDir); |
| 142 | + sort($entries); |
| 143 | + var_dump(count($entries)); |
| 144 | + var_dump(filesize($privateDir . '/salt')); |
| 145 | + var_dump(sprintf('%04o', fileperms($privateDir . '/salt') & 0777)); |
| 146 | + var_dump(preg_match('/^[0-9a-f]{24}\.lock$/', $entries[0]) === 1); |
| 147 | + var_dump(sprintf('%04o', fileperms($privateDir . '/' . $entries[0]) & 0777)); |
| 148 | + /* Nothing but the private directory lands in user_cache.lockfile_path. */ |
| 149 | + var_dump(user_cache_private_dir_entries($lockfilePath) === [$privateName]); |
| 150 | + |
| 151 | + /* Squatted directory (same uid, wrong mode): startup fails closed, |
| 152 | + * names the path, and creates nothing inside. */ |
| 153 | + $lockfilePath = $root . '/lock-mode'; |
| 154 | + mkdir($lockfilePath . '/' . $privateName, 0755, true); |
| 155 | + |
| 156 | + [$stdout, $stderr] = user_cache_cgi_run($phpCgi, $script, $docRoot, $lockfilePath, 'action=seed'); |
| 157 | + echo "wrong-mode: $stdout\n"; |
| 158 | + echo str_replace($root, '%ROOT%', $stderr), "\n"; |
| 159 | + var_dump(user_cache_private_dir_entries($lockfilePath . '/' . $privateName)); |
| 160 | + |
| 161 | + /* Symlink planted at the directory name: never followed. */ |
| 162 | + $lockfilePath = $root . '/lock-symlink'; |
| 163 | + mkdir($lockfilePath, 0777); |
| 164 | + mkdir($root . '/symlink-target', 0700); |
| 165 | + symlink($root . '/symlink-target', $lockfilePath . '/' . $privateName); |
| 166 | + |
| 167 | + [$stdout, $stderr] = user_cache_cgi_run($phpCgi, $script, $docRoot, $lockfilePath, 'action=seed'); |
| 168 | + echo "symlink: $stdout\n"; |
| 169 | + echo str_replace($root, '%ROOT%', $stderr), "\n"; |
| 170 | + var_dump(user_cache_private_dir_entries($root . '/symlink-target')); |
| 171 | + |
| 172 | + echo "Done\n"; |
| 173 | +} finally { |
| 174 | + user_cache_cgi_rm_rf($root); |
| 175 | +} |
| 176 | + |
| 177 | +?> |
| 178 | +--EXPECTF-- |
| 179 | +first: Available:seeded |
| 180 | +second: Available:seeded |
| 181 | +bool(true) |
| 182 | +bool(true) |
| 183 | +string(4) "0700" |
| 184 | +int(2) |
| 185 | +int(32) |
| 186 | +string(4) "0600" |
| 187 | +bool(true) |
| 188 | +string(4) "0600" |
| 189 | +bool(true) |
| 190 | +wrong-mode: UnavailableBySharedMemoryInitializationFailed:MISS |
| 191 | +UserCache boundary directory %ROOT%/lock-mode/.ZendUserCacheBnd.%d is unusable (not a private directory owned by this uid); it must be a directory owned by uid %d with mode 0700 (see user_cache.lockfile_path) |
| 192 | +UserCache partition startup failed; UserCache will be unavailable |
| 193 | +array(0) { |
| 194 | +} |
| 195 | +symlink: UnavailableBySharedMemoryInitializationFailed:MISS |
| 196 | +UserCache boundary directory %ROOT%/lock-symlink/.ZendUserCacheBnd.%d is unusable (%s); it must be a directory owned by uid %d with mode 0700 (see user_cache.lockfile_path) |
| 197 | +UserCache partition startup failed; UserCache will be unavailable |
| 198 | +array(0) { |
| 199 | +} |
| 200 | +Done |
0 commit comments