{"record":{"id":"d58328d3d6a0603f","repo":"yiisoft/yii2","slug":"failed-to-change-permissions-for-directory-path","errorCode":null,"errorMessage":"Failed to change permissions for directory \"{$path}\": {message}","messagePattern":"Failed to change permissions for directory \"(.+?)\": (.+?)","errorType":"exception","errorClass":"yii\\base\\Exception","httpStatus":null,"severity":"error","filePath":"framework/helpers/BaseFileHelper.php","lineNumber":726,"sourceCode":"        }\n        $parentDir = dirname($path);\n        // recurse if parent dir does not exist and we are not at the root of the file system.\n        if ($recursive && !is_dir($parentDir) && $parentDir !== $path) {\n            static::createDirectory($parentDir, $mode, true);\n        }\n        try {\n            if (!mkdir($path, $mode)) {\n                return false;\n            }\n        } catch (\\Exception $e) {\n            if (!is_dir($path)) {// https://github.com/yiisoft/yii2/issues/9288\n                throw new \\yii\\base\\Exception(\"Failed to create directory \\\"$path\\\": \" . $e->getMessage(), $e->getCode(), $e);\n            }\n        }\n        try {\n            return chmod($path, $mode);\n        } catch (\\Exception $e) {\n            throw new \\yii\\base\\Exception(\"Failed to change permissions for directory \\\"$path\\\": \" . $e->getMessage(), $e->getCode(), $e);\n        }\n    }\n\n    /**\n     * Performs a simple comparison of file or directory names.\n     *\n     * Based on match_basename() from dir.c of git 1.8.5.3 sources.\n     *\n     * @param string $baseName file or directory name to compare with the pattern\n     * @param string $pattern the pattern that $baseName will be compared against\n     * @param int|bool $firstWildcard location of first wildcard character in the $pattern\n     * @param int $flags pattern flags\n     * @return bool whether the name matches against pattern\n     */\n    private static function matchBasename($baseName, $pattern, $firstWildcard, $flags)\n    {\n        if ($firstWildcard === false) {\n            if ($pattern === $baseName) {","sourceCodeStart":708,"sourceCodeEnd":744,"githubUrl":"https://github.com/yiisoft/yii2/blob/66f00d18a29b520f85e8e8f1e32d1e7e7b556cac/framework/helpers/BaseFileHelper.php#L708-L744","documentation":"Thrown by yii\\helpers\\BaseFileHelper::createDirectory() when the directory was created (or already existed) but the subsequent native chmod($path, $mode) call raised an exception. Yii re-throws it as yii\\base\\Exception with chmod's message. Native chmod() normally only warns and returns false, so the throw is reached when an error handler converts the E_WARNING to an ErrorException (common under Yii's ErrorHandler or strict custom handlers) or when PHP 8 raises a ValueError/TypeError for an invalid $mode argument.","triggerScenarios":"Calling FileHelper::createDirectory($path, $mode) when: the directory exists but is owned by a different user than the PHP process (chmod only allowed for the owner); the filesystem is NFS/root-squash, a read-only bind mount, or has restrictive ACLs/immutable attributes; $mode is passed as a non-octal string like '0777' or an invalid int on PHP 8 (ValueError); or a set_error_handler turns chmod()'s warning into ErrorException.","commonSituations":"A deploy pipeline or cron job pre-creates runtime/cache dirs as root, then php-fpm as www-data cannot chmod them; NFS shares with root_squash mapping the web user to nobody; directories with the immutable flag (chattr +i) from hardening; developers passing the mode as a string '0775' instead of the octal literal 0775; upgrades to PHP 8 surfacing argument type errors that PHP 7 ignored.","solutions":["Fix ownership so the PHP process owns the directory it must chmod: sudo chown -R www-data:www-data <dir> (only the owner may chmod), then retry createDirectory.","Pass the mode as an octal integer literal (0775), never a string ('0775') — on PHP 8 the wrong type raises ValueError/TypeError before chmod runs.","If the directory already exists with correct permissions, skip the mode-enforcing call: guard with if (!is_dir($path)) { FileHelper::createDirectory($path, $mode); } since the chmod branch only runs after a fresh mkdir.","On NFS/ACL/immutable filesystems, adjust the export options (no_root_squash for service accounts), clear chattr +i, or pre-create the directory with the desired mode from the owning side.","If a global set_error_handler converts every E_WARNING to exceptions, exempt filesystem functions or wrap createDirectory in a handler that tolerates chmod warnings when is_dir($path) already holds."],"exampleFix":"// before\nFileHelper::createDirectory($cachePath, '0777'); // string mode -> PHP 8 ValueError caught and re-thrown here\n\n// after\n$mode = 0775; // octal int\nif (!is_dir($cachePath)) {\n    FileHelper::createDirectory($cachePath, $mode);\n}","handlingStrategy":"validation","validationCode":"$mode = 0775; // octal int, never a string\nif (!is_int($mode) || $mode < 0 || $mode > 07777) {\n    throw new InvalidArgumentException('mode must be an octal int like 0775');\n}\nif (is_dir($path)) {\n    $owner = fileowner($path);\n    $current = posix_geteuid();\n    if ($owner !== false && $current !== false && $owner !== $current && !function_exists('posix_geteuid') === false && $owner !== $current) {\n        // chmod will fail: only the owner may change mode\n        clearstatcache(true, $path);\n        if (fileowner($path) !== $current) {\n            Yii::warning(\"Cannot chmod $path: owned by uid \" . fileowner($path) . \", running as $current\");\n        }\n    }\n} else {\n    $parent = dirname($path);\n    while (!is_dir($parent)) { $parent = dirname($parent); }\n    if (!is_writable($parent)) {\n        throw new RuntimeException(\"Parent $parent not writable; mkdir would fail first\");\n    }\n}","typeGuard":"function modeIsSafeForChmod(int|string $mode): bool\n{\n    return is_int($mode) && $mode >= 0 && $mode <= 07777;\n}\n\nfunction directoryOwnedByCurrentProcess(string $path): bool\n{\n    clearstatcache(true, $path);\n    return is_dir($path) && fileowner($path) === posix_geteuid(); // false => chmod() cannot succeed\n}","tryCatchPattern":"try {\n    FileHelper::createDirectory($path, 0775, true);\n} catch (\\yii\\base\\Exception $e) {\n    if (strpos($e->getMessage(), 'Failed to change permissions') === 0 && is_dir($path)) {\n        // Directory exists; only the mode could not be enforced (owner mismatch / ACL).\n        Yii::warning(\"{$path} created but mode not applied: {$e->getMessage()}\", __METHOD__);\n    } else {\n        throw $e; // creation itself failed — do not swallow\n    }\n}","preventionTips":["Create runtime/cache directories during provisioning with the exact final ownership and mode, so the chmod in createDirectory is a no-op on an existing dir.","Never pass $mode as a string ('0775'); always the octal literal 0775 — PHP 8 raises ValueError for bad types.","Ensure the PHP-FPM/CLI user owns every directory createDirectory will chmod; only the owner may chmod, regardless of write bits.","On NFS or hardened hosts, verify with `lsattr` that the target is not immutable and that ACLs (getfacl) grant the web user control.","Log fileowner($path) vs posix_geteuid() at startup for each writable path; a mismatch predicts this exact exception."],"tags":["filesystem","permissions","chmod","php","yii2","ownership"],"backgroundTag":"chmod-permission-denied","analyzedSha":"66f00d18a29b520f85e8e8f1e32d1e7e7b556cac","analyzedAt":"2026-08-17T05:17:23.470Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}