skip to Main Content

I am trying to figure out why my php script is not able to find files outside of the web-root. It is part of a larger application but this seems to be the minimum for reproducing my issue.

I’m running php version 8.1.27 and nginx version 1.24.0.

This is the script in /var/www/testing/index.php:

# /var/www/testing/index.php
<?php
        $devPath = '/dev/dri/renderD128';
        if (!file_exists($devPath)) {
            echo "File does not exist!";
        } elseif (!is_readable($devPath)) {
            echo "File is not readable!";
        } else {
            echo "All good!";
        }
        clearstatcache();
?>

will display File does not exist!

But if I create:

# touch /var/www/testing/testing
# chown http:http /var/www/testing/testing

and change devPath to /var/www/testing/testing, the script displays All good!.

I was thinking that maybe the /dev/ files are special in this context for some reason so I created:

# touch /tmp/testing
# chown http:http /tmp/testing

and changed devPath to /tmp/testing. But I still get File does not exist!.

my php-fpm.conf looks like this:

[global]
error_log = log/php-fpm.error.log

[testing]

user = http

pm = dynamic
pm.max_children = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 2

listen = /run/php-fpm-legacy/testing.sock
listen.owner = http
listen.group = http
listen.mode = 0660

access.log = /var/lib/$pool/php-fpm-access.log

access.format = "%{%Y-%m-%dT%H:%M:%S%z}t %R: "%m %r%Q%q" %s %f %{milli}d %{kilo}M %C%%"

slowlog = /var/lib/$pool/php-fpm-slow.log

env[HOSTNAME] = $HOSTNAME
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp

php_admin_value[error_log] = /var/lib/$pool/php-fpm-error.log

php_admin_flag[log_errors] = on

and my php-fpm.ini is empty.

I serve the file using nginx, I suspect that config is ok as I can navigate to the server and see the results, but in either case, the config looks like this:

user http;
worker_processes auto;

error_log   /var/log/nginx/error.log;

events {
    worker_connections  1024;
}

http {
    root /var/www;
    types_hash_max_size 2048;
    types_hash_bucket_size 128;

    server {
         listen       80;
         server_name  123.123.0.123;
         root         /var/www/testing;

         access_log /var/log/nginx/testing.access.log;
         error_log  /var/log/nginx/testing.error.log error;
         index index.html index.htm index.php;

         location / {
            try_files $uri $uri/ /index.php$is_args$args;
         }

         location ~ .php$ {
            fastcgi_split_path_info ^(.+.php)(/.+)$;
            fastcgi_pass unix:/run/php-fpm-legacy/testing.sock;
            fastcgi_index index.php;
            include fastcgi.conf;
        }
    }

    server {
        listen 80 default_server;
        server_name _;
        return 444;
    }
}

To the best of my abilities I have verified that:

  • the file exists:
  • the http user is in the render group
  • it is the http user running the php process
  • the correct php-fpm.conf is used
# ls -lah /dev/dri/renderD128
crw-rw-rw- 1 root render 226, 128 Feb  2 22:49 /dev/dri/renderD128
# groups http
render redis http
# ps aux | grep php | grep testing
http      116949  0.0  0.0  79256 10036 ?        S    18:24   0:00 php-fpm: pool testing
# ps aux | grep php | grep master
root      116935  0.0  0.0  79404 19676 ?        Ss   18:24   0:00 php-fpm: master process (/etc/php-legacy/php-fpm.conf)

I’ve also tried to add an open_basedir directive to see if I can get a hint or if maybe it was set somewhere else, but it results in the following error:

Warning: file_exists(): open_basedir restriction in effect. File(/dev/dri/renderD128) is not within the allowed path(s): (/var/www/testing:/dev/dri/renderD128) in /var/www/testing/index.php on line 3

Somewhat expected, but it seems weird that it complains when the path is among the allowed ones.

I’ve also tried listing the files in the directory by changing the index.php script to:

<?php
        $devPath = '/dev/dri/';
        print_r(scandir($devPath));
?>

which gives me the following output:

Warning: scandir(/dev/dri/): Failed to open directory: No such file or directory in /var/www/testing/index.php on line 3

Warning: scandir(): (errno 13): Permission denied in /var/www/testing/index.php on line 3

However listing at a lower level, so /dev/ gives me some output:

Array ( [0] => . [1] => .. [2] => char [3] => core [4] => fd [5] => full [6] => hugepages [7] => log [8] => mqueue [9] => null [10] => ptmx [11] => pts [12] => random [13] => shm [14] => stderr [15] => stdin [16] => stdout [17] => tty [18] => urandom [19] => zero ) 

But the /dev/dri directory is nowhere to be seen in that output.

I’m all out of ideas on where to look next. What could be the cause of php not being able to find the file? Is there some default (or non default) setting that would prevent this?

EDIT: I’ve tried to make sure that the files should be readable and listable by the http user. The /dev and /dev/dri directories have 755 permissions; the /dev/dri/renderD128 file has 666 permissions and is owned by a group that the http user belongs to; the /tmp directory has 777 permissions; and the /tmp/testing file has 644 permissions and is owned by the http user.

2

Answers


  1. Chosen as BEST ANSWER

    Turns out that the php installation, on my system, defaults to setting the PrivateDevices=yes setting in the systemd service definition of php-fpm.

    The systemd docs says that the setting:

    If true, sets up a new /dev/ mount for the executed processes and only adds API pseudo devices such as /dev/null, /dev/zero or /dev/random (as well as the pseudo TTY subsystem) to it, but no physical devices such as /dev/sda, system memory /dev/mem, system ports /dev/port and others. This is useful to turn off physical device access by the executed process. Defaults to false.

    This basically mean that the process will not see any of the global /dev devices, this is reasonable and a safety feature. But it also means that /dev/dri/renderD128 will be hidden from the process, thus the reason for my error.

    Additionally the systemd service definition sets PrivateTmp=yes, which does the same thing but for /tmp so me testing the process's access by creating something in /tmp put me on the wrong track. I tried to create the external file in /home/http instead and the script could access it fine.

    We don't want to remove these settings as they are a security and safety feature, preventing the process from accessing other processes' data and system files/devices it shouldn't. However, we can create exceptions by adding:

    BindPaths=/dev/dri/renderD128
    DeviceAllow=/dev/dri/renderD128 rw
    

    to the [Service] section of php-fpm's systemd service file. This will allow the process to access that specific file even if devices in general are blocked.

    I could also add the open_basedir directive back without issues.


  2. Remember that PHP must have "r" access rights on the file renderD128 and "x" access rights on all its parent folders, e.g. /dev and /dev/dri.

    The ‘x’ (‘execute’ if on files but ‘list contents’ if on directories) access right is needed for the file_exists() operation because it allows PHP to traverse and access directories in the given path. Without execute permission on a directory in the path, PHP won’t be able to reach and check the existence of the specified file or directory.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search