I have been trying to understand how to use the php://fd/<n> wrapper. The documentation states the following:
php://fdallows direct access to the given file descriptor. For example,php://fd/3refers to file descriptor 3.
In my head, this means that the php://fd wrapper provides access to the underlying file descriptors as understood within the context of the process to the operating system. E.g., I would expect the following correspondence to hold:
fread(fopen('php://fd/3'), 10) maps to read(3, buf, 10) in C code.However, in tests I am unable to actually get this wrapper to work. Consider the following test php code which opens the /etc/passwd file and seeks 500 bytes. It also has some utility code that is used to list a directory and include the contents of a file.
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$f = fopen("/etc/passwd", "r");
fread($f, 500);
echo "$f\n";
function listdir($d) {
foreach (scandir($d) as $f) {
if (file_exists("$d/$f")) {
$s=lstat("$d/$f");
$l=($s[2] & 0120000) == 0120000 ? readlink("$d/$f") : '';
printf("%-25s %6d %6d %6o %8d %s %s\n", $f, $s[4], $s[5], $s[2], $s[7], strftime('%F %T', $s[9]), $l);
}
}
}
if (isset($_GET["dir"])) {
listdir($_GET["dir"]);
}
if (isset($_GET["file"])) {
include_once($_GET["file"]);
} elseif (isset($_GET["content"])) {
echo file_get_contents($_GET["content"]);
} elseif (isset($_GET["fopen"])) {
echo fread(fopen($_GET["fopen"], "r"), 1024);
}
?>
As the above code leaves an open file descriptor at position 500 in the /etc/passwd file, I would have expected that its possible to use the php://fd wrapper to continue reading the file from that position. However, this does not seem possible.
In the first example, it prints the contents of the /proc/self/fd directory (where it can be seen that the open fd is 12) and then tries to open this fd using the php://fd/12 syntax. Note the fields listed of the directory are: name, uid, gid, mode, size, modify date and readlink.
$ curl '192.168.56.47?dir=/proc/self/fd&fopen=php://fd/12'
Resource id #2
. 33 33 40500 0 2020-09-24 09:57:46
.. 33 33 40555 0 2020-09-24 09:53:22
0 33 33 120500 64 2020-09-24 10:09:08 /dev/null
1 33 33 120300 64 2020-09-24 10:09:08 /dev/null
10 33 33 120700 64 2020-09-24 10:09:08 anon_inode:[eventpoll]
11 33 33 120700 64 2020-09-24 10:27:43 socket:[50335]
12 33 33 120500 64 2020-09-24 14:16:50 /etc/passwd
2 33 33 120300 64 2020-09-24 10:09:08 /var/log/apache2/error.log
3 33 33 120700 64 2020-09-24 10:09:08 socket:[46732]
4 33 33 120700 64 2020-09-24 10:09:08 socket:[46733]
5 33 33 120500 64 2020-09-24 10:09:08 pipe:[47544]
6 33 33 120300 64 2020-09-24 10:09:08 pipe:[47544]
7 33 33 120300 64 2020-09-24 10:09:08 /var/log/apache2/other_vhosts_access.log
8 33 33 120300 64 2020-09-24 10:09:08 /var/log/apache2/access.log
9 33 33 120700 64 2020-09-24 10:09:08 /tmp/.ZendSem.C011w0 (deleted)
<br />
<b>Warning</b>: fopen(php://fd/12): failed to open stream: operation failed in <b>/var/www/html/index.php</b> on line <b>25</b><br />
<br />
<b>Warning</b>: fread() expects parameter 1 to be resource, bool given in <b>/var/www/html/index.php</b> on line <b>25</b><br />
The result is the same if using file_get_contents in place of the fread(fopen(....
As a second example, I wondered if the documentation meant a php resource number instead of an operating system file descriptor, so changed it to use number 2 (as printed out on the first line of output) instead of 12. But again, the result is the same:
$ curl '192.168.56.47?dir=/proc/self/fd&fopen=php://fd/2'
Resource id #2
. 33 33 40500 0 2020-09-24 09:54:03
.. 33 33 40555 0 2020-09-24 09:53:22
0 33 33 120500 64 2020-09-24 10:09:08 /dev/null
1 33 33 120300 64 2020-09-24 10:09:08 /dev/null
10 33 33 120700 64 2020-09-24 10:09:08 anon_inode:[eventpoll]
11 33 33 120700 64 2020-09-24 10:27:58 socket:[50337]
12 33 33 120500 64 2020-09-24 14:13:27 /etc/passwd
2 33 33 120300 64 2020-09-24 10:09:08 /var/log/apache2/error.log
3 33 33 120700 64 2020-09-24 10:09:08 socket:[46732]
4 33 33 120700 64 2020-09-24 10:09:08 socket:[46733]
5 33 33 120500 64 2020-09-24 10:09:08 pipe:[47544]
6 33 33 120300 64 2020-09-24 10:09:08 pipe:[47544]
7 33 33 120300 64 2020-09-24 10:09:08 /var/log/apache2/other_vhosts_access.log
8 33 33 120300 64 2020-09-24 10:09:08 /var/log/apache2/access.log
9 33 33 120700 64 2020-09-24 10:09:08 /tmp/.ZendSem.C011w0 (deleted)
<br />
<b>Warning</b>: fopen(php://fd/2): failed to open stream: operation failed in <b>/var/www/html/index.php</b> on line <b>25</b><br />
<br />
<b>Warning</b>: fread() expects parameter 1 to be resource, bool given in <b>/var/www/html/index.php</b> on line <b>25</b><br />
However, as the contents of /proc/self/fd are symlinks, the following is possible but not what I am looking to understand as it opens a new file descriptor and not reuse an existing one:
$ curl '192.168.56.47?fopen=/proc/self/fd/12'
Resource id #2
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
...snip...
I have tried searching on the internet for examples of how this wrapper is intended to be used but did not find any working examples. Any help would be appreciated.
Also, I am not really trying to solve a specific problem but more trying to understand how it works.
The above tests were conducted on Apache 2.4.41 with php 7.4.3.
After testing with the following file in the cli interpreter I noticed the php://fd/ wrapper was behaving as expected:
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// below taken from: https://stackoverflow.com/a/7033247/5660642
function fd($realpath) {
$dir = '/proc/self/fd/';
if ($dh = opendir($dir)) {
while (($file = readdir($dh)) !== false) {
$filename = $dir . $file;
if (filetype($filename) == 'link' && realpath($filename) == $realpath) {
closedir($dh);
return $file;
}
}
closedir($dh);
}
return FALSE;
}
$f = fopen('/etc/passwd', 'r');
$fd = fd('/etc/passwd');
echo "opened fd: $fd\n";
stream_set_read_buffer($f, 0); // disable buffering before reading
echo fread($f, 10)."\n";
$g = fopen("php://fd/$fd", 'r');
echo ftell($f)."\n";
echo ftell($g)."\n";
This gives the following output:
$ php script.php
opened fd: 3
root:x:0:0
10
10
However, when run from the Apache module it gives the following:
$ curl '192.168.56.47/script.php'
opened fd: 12
root:x:0:0
<br />
<b>Warning</b>: fopen(php://fd/12): failed to open stream: operation failed in <b>/var/www/html/script.php</b> on line <b>25</b><br />
10
<br />
<b>Warning</b>: ftell() expects parameter 1 to be resource, bool given in <b>/var/www/html/script.php</b> on line <b>27</b><br />
After comparing configuration between the cli and Apache module I ended up checking the source code. It appears this behaviour is hard coded in the php source code:
} else if (!strncasecmp(path, "fd/", 3)) {
const char *start;
char *end;
zend_long fildes_ori;
int dtablesize;
if (strcmp(sapi_module.name, "cli")) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP");
}
return NULL;
}
The commit this was implemented in was back in 2012: https://github.com/php/php-src/commit/df2a38e7f8603f51afa4c2257b3369067817d818
However, I dont see this restriction mentioned in the documentation, but it is in the changelog. But nonetheless, its good to know.
Not a 100% answer to what it's doing, but hopefully some insight.
Had a bit of a play, and ended up opening the second file separately so I could use ftell() to see what the file pointer was. So
$f = fopen("/etc/passwd", "r");
fread($f, 500);
echo "$f\n";
echo ftell($f).PHP_EOL;
gives
Resource id #86
500
and the second part
} elseif (isset($_GET["fopen"])) {
$f1 = fopen($_GET["fopen"], "r");
echo ftell($f1).PHP_EOL;
echo ">".fread($f1, 10)."<".PHP_EOL;
}
on my machine gives...
3087
><
Which leads me to believe that the system has in fact buffered the reading and the file pointer (in this instance) is currently at the end of the file - hence no content.
So next thing is to try a large file (5.1MB, not sure where I downloaded it from)...
$f = fopen("annual-enterprise-survey-2019-financial-year-provisional-csv.csv", "r");
fread($f, 500);
echo "$f\n";
echo ftell($f).PHP_EOL;
again fives...
Resource id #86
500
and the second part gives...
8192
>),H10,Indi<
So it must be buffering in 8K chunks.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With