Coverage Report

Created: 2026-10-28 06:20

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
1
{
151
1
  char *p, *token = NULL;
152
1
  php_stream_filter *temp_filter;
153
154
1
  zend_long max_filter_count = max_filter_count_default;
155
1
  if (context != NULL) {
156
0
    zval *option_val = php_stream_context_get_option(context, "filter", "max_filter_count");
157
0
    if (option_val) {
158
0
      max_filter_count = zval_get_long(option_val);
159
0
    }
160
0
  }
161
162
1
  p = php_strtok_r(filterlist, "|", &token);
163
2
  while (p) {
164
1
    zend_long read_count = read_chain ? stream->readfilters.num_filters : 0;
165
1
    zend_long write_count = write_chain ? stream->writefilters.num_filters : 0;
166
167
1
    if (read_count == max_filter_count || write_count == max_filter_count) {
168
0
      return FAILURE;
169
0
    }
170
171
1
    php_url_decode(p, strlen(p));
172
1
    if (read_chain) {
173
1
      if ((temp_filter = php_stream_filter_create(p, NULL, php_stream_is_persistent(stream)))) {
174
1
        php_stream_filter_append(&stream->readfilters, temp_filter);
175
1
      } else {
176
0
        php_stream_wrapper_warn_nt(NULL, PHP_STREAM_CONTEXT(stream), REPORT_ERRORS,
177
0
          CreateFailed,
178
0
          "Unable to create filter (%s)", p);
179
0
      }
180
1
    }
181
1
    if (write_chain) {
182
0
      if ((temp_filter = php_stream_filter_create(p, NULL, php_stream_is_persistent(stream)))) {
183
0
        php_stream_filter_append(&stream->writefilters, temp_filter);
184
0
      } else {
185
0
        php_stream_wrapper_warn_nt(NULL, PHP_STREAM_CONTEXT(stream), REPORT_ERRORS,
186
0
          CreateFailed,
187
0
          "Unable to create filter (%s)", p);
188
0
      }
189
0
    }
190
1
    p = php_strtok_r(NULL, "|", &token);
191
1
  }
192
1
  return SUCCESS;
193
1
}
194
/* }}} */
195
196
static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *path, const char *mode, int options,
197
                   zend_string **opened_path, php_stream_context *context STREAMS_DC) /* {{{ */
198
4
{
199
4
  int fd = -1;
200
4
  int mode_rw = 0;
201
4
  php_stream * stream = NULL;
202
4
  char *p, *token = NULL, *pathdup;
203
4
  zend_long max_memory;
204
4
  FILE *file = NULL;
205
#ifdef PHP_WIN32
206
  int pipe_requested = 0;
207
#endif
208
209
4
  if (!strncasecmp(path, "php://", 6)) {
210
4
    path += 6;
211
4
  }
212
213
4
  if (!strncasecmp(path, "temp", 4)) {
214
0
    path += 4;
215
0
    max_memory = PHP_STREAM_MAX_MEM;
216
0
    if (!strncasecmp(path, "/maxmemory:", 11)) {
217
0
      path += 11;
218
0
      max_memory = ZEND_STRTOL(path, NULL, 10);
219
0
      if (max_memory < 0) {
220
0
        zend_argument_value_error(2, "must be greater than or equal to 0");
221
0
        return NULL;
222
0
      }
223
0
    }
224
0
    mode_rw = php_stream_mode_from_str(mode);
225
0
    return php_stream_temp_create(mode_rw, max_memory);
226
0
  }
227
228
4
  if (!strcasecmp(path, "memory")) {
229
0
    mode_rw = php_stream_mode_from_str(mode);
230
0
    return php_stream_memory_create(mode_rw);
231
0
  }
232
233
4
  if (!strcasecmp(path, "output")) {
234
0
    return php_stream_alloc(&php_stream_output_ops, NULL, 0, "wb");
235
0
  }
236
237
4
  if (!strcasecmp(path, "input")) {
238
0
    php_stream_input_t *input;
239
240
0
    if ((options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include) ) {
241
0
      php_stream_wrapper_warn(wrapper, context, options,
242
0
        Disabled,
243
0
        "URL file-access is disabled in the server configuration");
244
0
      return NULL;
245
0
    }
246
247
0
    input = ecalloc(1, sizeof(*input));
248
0
    if ((input->body = SG(request_info).request_body)) {
249
0
      php_stream_rewind(input->body);
250
0
    } else {
251
0
      input->body = php_stream_temp_create_ex(TEMP_STREAM_DEFAULT, SAPI_POST_BLOCK_SIZE, PG(upload_tmp_dir));
252
0
      SG(request_info).request_body = input->body;
253
0
    }
254
255
0
    return php_stream_alloc(&php_stream_input_ops, input, 0, "rb");
256
0
  }
257
258
4
  if (!strcasecmp(path, "stdin")) {
259
0
    if ((options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include) ) {
260
0
      php_stream_wrapper_warn(wrapper, context, options,
261
0
        Disabled,
262
0
        "URL file-access is disabled in the server configuration");
263
0
      return NULL;
264
0
    }
265
0
    if (!strcmp(sapi_module.name, "cli")) {
266
0
      static int cli_in = 0;
267
0
      fd = STDIN_FILENO;
268
0
      if (cli_in) {
269
0
        fd = dup(fd);
270
0
      } else {
271
0
        cli_in = 1;
272
0
        file = stdin;
273
0
      }
274
0
    } else {
275
0
      fd = dup(STDIN_FILENO);
276
0
    }
277
#ifdef PHP_WIN32
278
    pipe_requested = 1;
279
#endif
280
4
  } else if (!strcasecmp(path, "stdout")) {
281
0
    if (!strcmp(sapi_module.name, "cli")) {
282
0
      static int cli_out = 0;
283
0
      fd = STDOUT_FILENO;
284
0
      if (cli_out++) {
285
0
        fd = dup(fd);
286
0
      } else {
287
0
        cli_out = 1;
288
0
        file = stdout;
289
0
      }
290
0
    } else {
291
0
      fd = dup(STDOUT_FILENO);
292
0
    }
293
#ifdef PHP_WIN32
294
    pipe_requested = 1;
295
#endif
296
4
  } else if (!strcasecmp(path, "stderr")) {
297
0
    if (!strcmp(sapi_module.name, "cli")) {
298
0
      static int cli_err = 0;
299
0
      fd = STDERR_FILENO;
300
0
      if (cli_err++) {
301
0
        fd = dup(fd);
302
0
      } else {
303
0
        cli_err = 1;
304
0
        file = stderr;
305
0
      }
306
0
    } else {
307
0
      fd = dup(STDERR_FILENO);
308
0
    }
309
#ifdef PHP_WIN32
310
    pipe_requested = 1;
311
#endif
312
4
  } else if (!strncasecmp(path, "fd/", 3)) {
313
0
    const char *start;
314
0
    char       *end;
315
0
    zend_long  fildes_ori;
316
0
    int      dtablesize;
317
318
0
    if (strcmp(sapi_module.name, "cli")) {
319
0
      php_stream_wrapper_warn(wrapper, context, options,
320
0
        Disabled,
321
0
        "Direct access to file descriptors is only available from command-line PHP");
322
0
      return NULL;
323
0
    }
324
325
0
    if ((options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include) ) {
326
0
      php_stream_wrapper_warn(wrapper, context, options,
327
0
        Disabled,
328
0
        "URL file-access is disabled in the server configuration");
329
0
      return NULL;
330
0
    }
331
332
0
    start = &path[3];
333
0
    fildes_ori = ZEND_STRTOL(start, &end, 10);
334
0
    if (end == start || *end != '\0') {
335
0
      php_stream_wrapper_log_warn(wrapper, context, options,
336
0
        InvalidUrl,
337
0
        "php://fd/ stream must be specified in the form php://fd/<orig fd>");
338
0
      return NULL;
339
0
    }
340
341
0
#ifdef HAVE_UNISTD_H
342
0
    dtablesize = getdtablesize();
343
#else
344
    dtablesize = INT_MAX;
345
#endif
346
347
0
    if (fildes_ori < 0 || fildes_ori >= dtablesize) {
348
0
      php_stream_wrapper_log_warn(wrapper, context, options,
349
0
        InvalidParam,
350
0
        "The file descriptors must be non-negative numbers smaller than %d", dtablesize);
351
0
      return NULL;
352
0
    }
353
354
0
    fd = dup((int)fildes_ori);
355
0
    if (fd == -1) {
356
0
      php_stream_wrapper_log_warn(wrapper, context, options,
357
0
        DupFailed,
358
0
        "Error duping file descriptor " ZEND_LONG_FMT "; possibly it doesn't exist: "
359
0
        "[%d]: %s", fildes_ori, errno, strerror(errno));
360
0
      return NULL;
361
0
    }
362
4
  } else if (!strncasecmp(path, "filter/", 7)) {
363
    /* Save time/memory when chain isn't specified */
364
2
    if (strchr(mode, 'r') || strchr(mode, '+')) {
365
2
      mode_rw |= PHP_STREAM_FILTER_READ;
366
2
    }
367
2
    if (strchr(mode, 'w') || strchr(mode, '+') || strchr(mode, 'a')) {
368
0
      mode_rw |= PHP_STREAM_FILTER_WRITE;
369
0
    }
370
2
    pathdup = estrndup(path + 6, strlen(path + 6));
371
2
    p = strstr(pathdup, "/resource=");
372
2
    if (!p) {
373
1
      zend_throw_error(NULL, "No URL resource specified");
374
1
      efree(pathdup);
375
1
      return NULL;
376
1
    }
377
378
1
    if (!(stream = php_stream_open_wrapper_ex(p + 10, mode, options, opened_path, context))) {
379
0
      efree(pathdup);
380
0
      return NULL;
381
0
    }
382
383
1
    zend_result safl_result = SUCCESS;
384
1
    *p = '\0';
385
386
1
    p = php_strtok_r(pathdup + 1, "/", &token);
387
2
    while (p) {
388
1
      if (!strncasecmp(p, "read=", 5)) {
389
1
        safl_result = php_stream_apply_filter_list(stream, p + 5, 1, 0, context);
390
1
      } else if (!strncasecmp(p, "write=", 6)) {
391
0
        safl_result = php_stream_apply_filter_list(stream, p + 6, 0, 1, context);
392
0
      } else {
393
0
        safl_result = php_stream_apply_filter_list(stream, p, mode_rw & PHP_STREAM_FILTER_READ, mode_rw & PHP_STREAM_FILTER_WRITE, context);
394
0
      }
395
396
1
      if (safl_result == FAILURE) {
397
0
        break;
398
0
      }
399
400
1
      p = php_strtok_r(NULL, "/", &token);
401
1
    }
402
1
    efree(pathdup);
403
404
1
    if (EG(exception)) {
405
0
      php_stream_close(stream);
406
0
      return NULL;
407
0
    }
408
409
1
    if (safl_result == FAILURE) {
410
0
      php_stream_wrapper_log_warn(wrapper, context, options,
411
0
        PathTooLong, "too many filters");
412
0
      php_stream_close(stream);
413
0
      return NULL;
414
0
    }
415
416
1
    return stream;
417
2
  } else {
418
    /* invalid php://thingy */
419
2
    php_stream_wrapper_warn(wrapper, context, options,
420
2
        InvalidUrl, "Invalid php:// URL specified");
421
2
    return NULL;
422
2
  }
423
424
  /* must be stdin, stderr or stdout */
425
0
  if (fd == -1) {
426
    /* failed to dup */
427
0
    return NULL;
428
0
  }
429
430
0
#if defined(S_IFSOCK) && !defined(PHP_WIN32)
431
0
  do {
432
0
    zend_stat_t st = {0};
433
0
    memset(&st, 0, sizeof(st));
434
0
    if (zend_fstat(fd, &st) == 0 && (st.st_mode & S_IFMT) == S_IFSOCK) {
435
0
      stream = php_stream_sock_open_from_socket(fd, NULL);
436
0
      if (stream) {
437
0
        stream->ops = &php_stream_socket_ops;
438
0
        return stream;
439
0
      }
440
0
    }
441
0
  } while (0);
442
0
#endif
443
444
0
  if (file) {
445
0
    stream = php_stream_fopen_from_file(file, mode);
446
0
  } else {
447
0
    stream = php_stream_fopen_from_fd(fd, mode, NULL);
448
0
    if (stream == NULL) {
449
0
      close(fd);
450
0
    }
451
0
  }
452
453
#ifdef PHP_WIN32
454
  if (pipe_requested && stream && context) {
455
    zval *blocking_pipes = php_stream_context_get_option(context, "pipe", "blocking");
456
    if (blocking_pipes) {
457
      php_stream_set_option(stream, PHP_STREAM_OPTION_PIPE_BLOCKING, zval_get_long(blocking_pipes), NULL);
458
    }
459
  }
460
#endif
461
0
  return stream;
462
0
}
463
/* }}} */
464
465
static const php_stream_wrapper_ops php_stdio_wops = {
466
  php_stream_url_wrap_php,
467
  NULL, /* close */
468
  NULL, /* fstat */
469
  NULL, /* stat */
470
  NULL, /* opendir */
471
  "PHP",
472
  NULL, /* unlink */
473
  NULL, /* rename */
474
  NULL, /* mkdir */
475
  NULL, /* rmdir */
476
  NULL
477
};
478
479
PHPAPI const php_stream_wrapper php_stream_php_wrapper =  {
480
  &php_stdio_wops,
481
  NULL,
482
  0, /* is_url */
483
};