如何使用php:// fd / wrapper?

问题描述

我一直在尝试了解如何使用PHP://fd/<n>包装器。 documentation声明以下内容

PHP://fd允许直接访问给定的文件描述符。例如,PHP://fd/3引用文件描述符3。

在我看来,这意味着PHP://fd包装器提供了对底层文件描述符的访问,这在操作系统进程的上下文中可以理解。例如,我希望以下信件能够成立:

  • PHP中,fread(fopen('PHP://fd/3'),10)在C代码中映射到read(3,buf,10)

但是,在测试中,我实际上无法使该包装器正常工作。考虑以下测试PHP代码,该代码打开/etc/passwd文件并查找500个字节。它还具有一些实用程序代码,用于列出目录包括文件内容

<?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);
}
?>

由于上述代码/etc/passwd文件中的位置500处保留了打开的文件描述符,因此我希望可以使用PHP://fd包装器从该位置继续读取文件。但是,这似乎是不可能的。

在第一个示例中,它打印/proc/self/fd目录的内容(可以看到open fd为12),然后尝试使用PHP://fd/12语法打开此fd。请注意,目录中列出的字段为:nameuidgidmodesizemodify datereadlink

$ 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 />

如果使用file_get_contents代替fread(fopen(...,结果是相同的。

作为第二个示例,我想知道文档是否意味着PHP资源号而不是操作系统文件描述符,因此将其更改为使用数字2(如输出的第一行所示)而不是12。但是再次,结果是一样的:

$ 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 />

但是,由于/proc/self/fd内容是符号链接,因此以下内容是可能的,但我所希望理解的不是它,因为它打开了一个新的文件描述符而不重用现有的文件描述符:

$ 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...

我尝试在Internet上搜索有关打算使用此包装器的示例,但没有找到任何可行的示例。任何帮助将不胜感激。

此外,我并不是真正在尝试解决特定问题,而是在尝试理解其工作原理。

以上测试是在Apache 2.4.41PHP 7.4.3上进行的。

更新:答案

cli解释器中测试以下文件后,我注意到PHP://fd/包装程序的行为符合预期:

<?PHP
ini_set('display_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";

这将提供以下输出

$ PHP script.PHP
opened fd: 3
root:x:0:0
10
10

但是,从Apache模块运行时,它提供以下内容

$ 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 />

硬编码

比较cliApache模块间的配置后,我最终检查了源代码。看来此行为在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;
        }

此实现的落实时间可追溯到2012年:https://github.com/php/php-src/commit/df2a38e7f8603f51afa4c2257b3369067817d818

但是,我没有在文档中看到此限制,但是在更改日志中。但尽管如此,它还是个好消息。

解决方法

不是100%回答正在做的事情,但希望能提供一些见识。

玩了一会儿,最终打开了第二个文件,所以我可以使用ftell()来查看文件指针是什么。所以

$f = fopen("/etc/passwd","r");
fread($f,500);
echo "$f\n";
echo ftell($f).PHP_EOL;

给予

Resource id #86
500

和第二部分

} elseif (isset($_GET["fopen"])) {
    $f1 = fopen($_GET["fopen"],"r");
    echo ftell($f1).PHP_EOL;
    echo ">".fread($f1,10)."<".PHP_EOL;
}

在我的机器上给出...

3087
><

这使我相信系统实际上已经缓冲了读取,并且文件指针(在这种情况下)当前位于文件的末尾,因此没有内容。

所以接下来要做的是尝试一个大文件(5.1MB,不确定我从何处下载)...

$f = fopen("annual-enterprise-survey-2019-financial-year-provisional-csv.csv",500);
echo "$f\n";
echo ftell($f).PHP_EOL;

再次击掌...

Resource id #86
500

第二部分给出...

8192
>),H10,Indi<

因此它必须以8K块为单位缓冲。