Coverage Report

Created: 2026-09-14 06:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/php-src/ext/standard/php_fopen_wrapper.c
Line
Count
Source
1
/*
2
   +----------------------------------------------------------------------+
3
   | Copyright © The PHP Group and Contributors.                          |
4
   +----------------------------------------------------------------------+
5
   | This source file is subject to the Modified BSD License that is      |
6
   | bundled with this package in the file LICENSE, and is available      |
7
   | through the World Wide Web at <https://www.php.net/license/>.        |
8
   |                                                                      |
9
   | SPDX-License-Identifier: BSD-3-Clause                                |
10
   +----------------------------------------------------------------------+
11
   | Authors: Rasmus Lerdorf <rasmus@php.net>                             |
12
   |          Jim Winstead <jimw@php.net>                                 |
13
   |          Hartmut Holzgraefe <hholzgra@php.net>                       |
14
   +----------------------------------------------------------------------+
15
 */
16
17
#include <stdio.h>
18
#include <stdlib.h>
19
#ifdef HAVE_UNISTD_H
20
# include <unistd.h>
21
#endif
22
23
#include "php.h"
24
#include "php_globals.h"
25
#include "php_standard.h"
26
#include "php_memory_streams.h"
27
#include "php_fopen_wrappers.h"
28
#include "SAPI.h"
29
30
static ssize_t php_stream_output_write(php_stream *stream, const char *buf, size_t count) /* {{{ */
31
0
{
32
0
  PHPWRITE(buf, count);
33
0
  return count;
34
0
}
35
/* }}} */
36
37
static ssize_t php_stream_output_read(php_stream *stream, char *buf, size_t count) /* {{{ */
38
0
{
39
0
  stream->eof = 1;
40
0
  return -1;
41
0
}
42
/* }}} */
43
44
static int php_stream_output_close(php_stream *stream, int close_handle) /* {{{ */
45
0
{
46
0
  return 0;
47
0
}
48
/* }}} */
49
50
static const php_stream_ops php_stream_output_ops = {
51
  php_stream_output_write,
52
  php_stream_output_read,
53
  php_stream_output_close,
54
  NULL, /* flush */
55
  "Output",
56
  NULL, /* seek */
57
  NULL, /* cast */
58
  NULL, /* stat */
59
  NULL  /* set_option */
60
};
61
62
typedef struct php_stream_input { /* {{{ */
63
  php_stream *body;
64
  zend_off_t position;
65
} php_stream_input_t;
66
/* }}} */
67
68
static ssize_t php_stream_input_write(php_stream *stream, const char *buf, size_t count) /* {{{ */
69
0
{
70
0
  return -1;
71
0
}
72
/* }}} */
73
74
static ssize_t php_stream_input_read(php_stream *stream, char *buf, size_t count) /* {{{ */
75
0
{
76
0
  php_stream_input_t *input = stream->abstract;
77
0
  ssize_t read;
78
79
0
  if (!SG(post_read) && SG(read_post_bytes) < (int64_t)(input->position + count)) {
80
    /* read requested data from SAPI */
81
0
    size_t read_bytes = sapi_read_post_block(buf, count);
82
83
0
    if (read_bytes > 0) {
84
0
      php_stream_seek(input->body, 0, SEEK_END);
85
0
      php_stream_write(input->body, buf, read_bytes);
86
0
    }
87
0
  }
88
89
0
  if (!input->body->readfilters.head) {
90
    /* If the input stream contains filters, it's not really seekable. The
91
      input->position is likely to be wrong for unfiltered data. */
92
0
    php_stream_seek(input->body, input->position, SEEK_SET);
93
0
  }
94
0
  read = php_stream_read(input->body, buf, count);
95
96
0
  if (!read || read == (size_t) -1) {
97
0
    stream->eof = 1;
98
0
  } else {
99
0
    input->position += read;
100
0
  }
101
102
0
  return read;
103
0
}
104
/* }}} */
105
106
static int php_stream_input_close(php_stream *stream, int close_handle) /* {{{ */
107
0
{
108
0
  efree(stream->abstract);
109
0
  stream->abstract = NULL;
110
111
0
  return 0;
112
0
}
113
/* }}} */
114
115
static int php_stream_input_flush(php_stream *stream) /* {{{ */
116
0
{
117
0
  return -1;
118
0
}
119
/* }}} */
120
121
static int php_stream_input_seek(php_stream *stream, zend_off_t offset, int whence, zend_off_t *newoffset) /* {{{ */
122
0
{
123
0
  php_stream_input_t *input = stream->abstract;
124
125
0
  if (input->body) {
126
0
    int sought = php_stream_seek(input->body, offset, whence);
127
0
    *newoffset = input->position = (input->body)->position;
128
0
    return sought;
129
0
  }
130
131
0
  return -1;
132
0
}
133
/* }}} */
134
135
static const php_stream_ops php_stream_input_ops = {
136
  php_stream_input_write,
137
  php_stream_input_read,
138
  php_stream_input_close,
139
  php_stream_input_flush,
140
  "Input",
141
  php_stream_input_seek,
142
  NULL, /* cast */
143
  NULL, /* stat */
144
  NULL  /* set_option */
145
};
146
147
static const zend_long max_filter_count_default = 16;
148
149
static zend_result php_stream_apply_filter_list(php_stream *stream, char *filterlist, int read_chain, int write_chain, php_stream_context *context) /* {{{ */
150
171
{
151
171
  char *p, *token = NULL;
152
171
  php_stream_filter *temp_filter;
153
154
171
  zend_long max_filter_count = max_filter_count_default;
155
171
  bool max_filter_count_configured = false;
156
171
  if (context != NULL) {
157
0
    zval *option_val = php_stream_context_get_option(context, "filter", "max_filter_count");
158
0
    if (option_val) {
159
0
      max_filter_count = zval_get_long(option_val);
160
0
      max_filter_count_configured = true;
161
0
    }
162
0
  }
163
164
171
  p = php_strtok_r(filterlist, "|", &token);
165
351
  while (p) {
166
180
    zend_long read_count = read_chain ? stream->readfilters.num_filters : 0;
167
180
    zend_long write_count = write_chain ? stream->writefilters.num_filters : 0;
168
169
180
    if (read_count == max_filter_count || write_count == max_filter_count) {
170
0
      if (max_filter_count_configured) {
171
0
        return FAILURE;
172
0
      } else {
173
        // No max_filter_count configured; raise deprecation error if over default
174
0
        zend_error(E_DEPRECATED, "Using more than " ZEND_LONG_FMT " filters in a php://filter URL is deprecated, "
175
0
          "set this limit using the stream context option max_filter_count, or use stream_filter_append", max_filter_count_default);
176
0
      }
177
0
    }
178
179
180
    php_url_decode(p, strlen(p));
180
180
    if (read_chain) {
181
180
      if ((temp_filter = php_stream_filter_create(p, NULL, php_stream_is_persistent(stream)))) {
182
68
        php_stream_filter_append(&stream->readfilters, temp_filter);
183
112
      } else {
184
112
        php_stream_wrapper_warn_nt(NULL, PHP_STREAM_CONTEXT(stream), REPORT_ERRORS,
185
112
          CreateFailed,
186
112
          "Unable to create filter (%s)", p);
187
112
      }
188
180
    }
189
180
    if (write_chain) {
190
0
      if ((temp_filter = php_stream_filter_create(p, NULL, php_stream_is_persistent(stream)))) {
191
0
        php_stream_filter_append(&stream->writefilters, temp_filter);
192
0
      } else {
193
0
        php_stream_wrapper_warn_nt(NULL, PHP_STREAM_CONTEXT(stream), REPORT_ERRORS,
194
0
          CreateFailed,
195
0
          "Unable to create filter (%s)", p);
196
0
      }
197
0
    }
198
180
    p = php_strtok_r(NULL, "|", &token);
199
180
  }
200
171
  return SUCCESS;
201
171
}
202
/* }}} */
203
204
static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *path, const char *mode, int options,
205
                   zend_string **opened_path, php_stream_context *context STREAMS_DC) /* {{{ */
206
80
{
207
80
  int fd = -1;
208
80
  int mode_rw = 0;
209
80
  php_stream * stream = NULL;
210
80
  char *p, *token = NULL, *pathdup;
211
80
  zend_long max_memory;
212
80
  FILE *file = NULL;
213
#ifdef PHP_WIN32
214
  int pipe_requested = 0;
215
#endif
216
217
80
  if (!strncasecmp(path, "php://", 6)) {
218
80
    path += 6;
219
80
  }
220
221
80
  if (!strncasecmp(path, "temp", 4)) {
222
0
    path += 4;
223
0
    max_memory = PHP_STREAM_MAX_MEM;
224
0
    if (!strncasecmp(path, "/maxmemory:", 11)) {
225
0
      path += 11;
226
0
      max_memory = ZEND_STRTOL(path, NULL, 10);
227
0
      if (max_memory < 0) {
228
0
        zend_argument_value_error(2, "must be greater than or equal to 0");
229
0
        return NULL;
230
0
      }
231
0
    }
232
0
    mode_rw = php_stream_mode_from_str(mode);
233
0
    return php_stream_temp_create(mode_rw, max_memory);
234
0
  }
235
236
80
  if (!strcasecmp(path, "memory")) {
237
0
    mode_rw = php_stream_mode_from_str(mode);
238
0
    return php_stream_memory_create(mode_rw);
239
0
  }
240
241
80
  if (!strcasecmp(path, "output")) {
242
0
    return php_stream_alloc(&php_stream_output_ops, NULL, 0, "wb");
243
0
  }
244
245
80
  if (!strcasecmp(path, "input")) {
246
0
    php_stream_input_t *input;
247
248
0
    if ((options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include) ) {
249
0
      php_stream_wrapper_warn(wrapper, context, options,
250
0
        Disabled,
251
0
        "URL file-access is disabled in the server configuration");
252
0
      return NULL;
253
0
    }
254
255
0
    input = ecalloc(1, sizeof(*input));
256
0
    if ((input->body = SG(request_info).request_body)) {
257
0
      php_stream_rewind(input->body);
258
0
    } else {
259
0
      input->body = php_stream_temp_create_ex(TEMP_STREAM_DEFAULT, SAPI_POST_BLOCK_SIZE, PG(upload_tmp_dir));
260
0
      SG(request_info).request_body = input->body;
261
0
    }
262
263
0
    return php_stream_alloc(&php_stream_input_ops, input, 0, "rb");
264
0
  }
265
266
80
  if (!strcasecmp(path, "stdin")) {
267
0
    if ((options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include) ) {
268
0
      php_stream_wrapper_warn(wrapper, context, options,
269
0
        Disabled,
270
0
        "URL file-access is disabled in the server configuration");
271
0
      return NULL;
272
0
    }
273
0
    if (!strcmp(sapi_module.name, "cli")) {
274
0
      static int cli_in = 0;
275
0
      fd = STDIN_FILENO;
276
0
      if (cli_in) {
277
0
        fd = dup(fd);
278
0
      } else {
279
0
        cli_in = 1;
280
0
        file = stdin;
281
0
      }
282
0
    } else {
283
0
      fd = dup(STDIN_FILENO);
284
0
    }
285
#ifdef PHP_WIN32
286
    pipe_requested = 1;
287
#endif
288
80
  } else if (!strcasecmp(path, "stdout")) {
289
0
    if (!strcmp(sapi_module.name, "cli")) {
290
0
      static int cli_out = 0;
291
0
      fd = STDOUT_FILENO;
292
0
      if (cli_out++) {
293
0
        fd = dup(fd);
294
0
      } else {
295
0
        cli_out = 1;
296
0
        file = stdout;
297
0
      }
298
0
    } else {
299
0
      fd = dup(STDOUT_FILENO);
300
0
    }
301
#ifdef PHP_WIN32
302
    pipe_requested = 1;
303
#endif
304
80
  } else if (!strcasecmp(path, "stderr")) {
305
0
    if (!strcmp(sapi_module.name, "cli")) {
306
0
      static int cli_err = 0;
307
0
      fd = STDERR_FILENO;
308
0
      if (cli_err++) {
309
0
        fd = dup(fd);
310
0
      } else {
311
0
        cli_err = 1;
312
0
        file = stderr;
313
0
      }
314
0
    } else {
315
0
      fd = dup(STDERR_FILENO);
316
0
    }
317
#ifdef PHP_WIN32
318
    pipe_requested = 1;
319
#endif
320
80
  } else if (!strncasecmp(path, "fd/", 3)) {
321
0
    const char *start;
322
0
    char       *end;
323
0
    zend_long  fildes_ori;
324
0
    int      dtablesize;
325
326
0
    if (strcmp(sapi_module.name, "cli")) {
327
0
      php_stream_wrapper_warn(wrapper, context, options,
328
0
        Disabled,
329
0
        "Direct access to file descriptors is only available from command-line PHP");
330
0
      return NULL;
331
0
    }
332
333
0
    if ((options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include) ) {
334
0
      php_stream_wrapper_warn(wrapper, context, options,
335
0
        Disabled,
336
0
        "URL file-access is disabled in the server configuration");
337
0
      return NULL;
338
0
    }
339
340
0
    start = &path[3];
341
0
    fildes_ori = ZEND_STRTOL(start, &end, 10);
342
0
    if (end == start || *end != '\0') {
343
0
      php_stream_wrapper_log_warn(wrapper, context, options,
344
0
        InvalidUrl,
345
0
        "php://fd/ stream must be specified in the form php://fd/<orig fd>");
346
0
      return NULL;
347
0
    }
348
349
0
#ifdef HAVE_UNISTD_H
350
0
    dtablesize = getdtablesize();
351
#else
352
    dtablesize = INT_MAX;
353
#endif
354
355
0
    if (fildes_ori < 0 || fildes_ori >= dtablesize) {
356
0
      php_stream_wrapper_log_warn(wrapper, context, options,
357
0
        InvalidParam,
358
0
        "The file descriptors must be non-negative numbers smaller than %d", dtablesize);
359
0
      return NULL;
360
0
    }
361
362
0
    fd = dup((int)fildes_ori);
363
0
    if (fd == -1) {
364
0
      php_stream_wrapper_log_warn(wrapper, context, options,
365
0
        DupFailed,
366
0
        "Error duping file descriptor " ZEND_LONG_FMT "; possibly it doesn't exist: "
367
0
        "[%d]: %s", fildes_ori, errno, strerror(errno));
368
0
      return NULL;
369
0
    }
370
80
  } else if (!strncasecmp(path, "filter/", 7)) {
371
    /* Save time/memory when chain isn't specified */
372
71
    if (strchr(mode, 'r') || strchr(mode, '+')) {
373
71
      mode_rw |= PHP_STREAM_FILTER_READ;
374
71
    }
375
71
    if (strchr(mode, 'w') || strchr(mode, '+') || strchr(mode, 'a')) {
376
0
      mode_rw |= PHP_STREAM_FILTER_WRITE;
377
0
    }
378
71
    pathdup = estrndup(path + 6, strlen(path + 6));
379
71
    p = strstr(pathdup, "/resource=");
380
71
    if (!p) {
381
4
      zend_throw_error(NULL, "No URL resource specified");
382
4
      efree(pathdup);
383
4
      return NULL;
384
4
    }
385
386
67
    if (!(stream = php_stream_open_wrapper_ex(p + 10, mode, options, opened_path, context))) {
387
7
      efree(pathdup);
388
7
      return NULL;
389
7
    }
390
391
60
    zend_result safl_result = SUCCESS;
392
60
    *p = '\0';
393
394
60
    p = php_strtok_r(pathdup + 1, "/", &token);
395
231
    while (p) {
396
171
      if (!strncasecmp(p, "read=", 5)) {
397
82
        safl_result = php_stream_apply_filter_list(stream, p + 5, 1, 0, context);
398
89
      } else if (!strncasecmp(p, "write=", 6)) {
399
0
        safl_result = php_stream_apply_filter_list(stream, p + 6, 0, 1, context);
400
89
      } else {
401
89
        safl_result = php_stream_apply_filter_list(stream, p, mode_rw & PHP_STREAM_FILTER_READ, mode_rw & PHP_STREAM_FILTER_WRITE, context);
402
89
      }
403
404
171
      if (safl_result == FAILURE) {
405
0
        break;
406
0
      }
407
408
171
      p = php_strtok_r(NULL, "/", &token);
409
171
    }
410
60
    efree(pathdup);
411
412
60
    if (EG(exception)) {
413
0
      php_stream_close(stream);
414
0
      return NULL;
415
0
    }
416
417
60
    if (safl_result == FAILURE) {
418
0
      php_stream_wrapper_log_warn(wrapper, context, options,
419
0
        PathTooLong, "too many filters");
420
0
      php_stream_close(stream);
421
0
      return NULL;
422
0
    }
423
424
60
    return stream;
425
60
  } else {
426
    /* invalid php://thingy */
427
9
    php_stream_wrapper_warn(wrapper, context, options,
428
9
        InvalidUrl, "Invalid php:// URL specified");
429
9
    return NULL;
430
9
  }
431
432
  /* must be stdin, stderr or stdout */
433
0
  if (fd == -1) {
434
    /* failed to dup */
435
0
    return NULL;
436
0
  }
437
438
0
#if defined(S_IFSOCK) && !defined(PHP_WIN32)
439
0
  do {
440
0
    zend_stat_t st = {0};
441
0
    memset(&st, 0, sizeof(st));
442
0
    if (zend_fstat(fd, &st) == 0 && (st.st_mode & S_IFMT) == S_IFSOCK) {
443
0
      stream = php_stream_sock_open_from_socket(fd, NULL);
444
0
      if (stream) {
445
0
        stream->ops = &php_stream_socket_ops;
446
0
        return stream;
447
0
      }
448
0
    }
449
0
  } while (0);
450
0
#endif
451
452
0
  if (file) {
453
0
    stream = php_stream_fopen_from_file(file, mode);
454
0
  } else {
455
0
    stream = php_stream_fopen_from_fd(fd, mode, NULL);
456
0
    if (stream == NULL) {
457
0
      close(fd);
458
0
    }
459
0
  }
460
461
#ifdef PHP_WIN32
462
  if (pipe_requested && stream && context) {
463
    zval *blocking_pipes = php_stream_context_get_option(context, "pipe", "blocking");
464
    if (blocking_pipes) {
465
      php_stream_set_option(stream, PHP_STREAM_OPTION_PIPE_BLOCKING, zval_get_long(blocking_pipes), NULL);
466
    }
467
  }
468
#endif
469
0
  return stream;
470
0
}
471
/* }}} */
472
473
static const php_stream_wrapper_ops php_stdio_wops = {
474
  php_stream_url_wrap_php,
475
  NULL, /* close */
476
  NULL, /* fstat */
477
  NULL, /* stat */
478
  NULL, /* opendir */
479
  "PHP",
480
  NULL, /* unlink */
481
  NULL, /* rename */
482
  NULL, /* mkdir */
483
  NULL, /* rmdir */
484
  NULL
485
};
486
487
PHPAPI const php_stream_wrapper php_stream_php_wrapper =  {
488
  &php_stdio_wops,
489
  NULL,
490
  0, /* is_url */
491
};