Skip to content

Commit 2d0dec9

Browse files
committed
Fix #79019: Copied cURL handles upload empty file
To cater to `curl_copy_handle()` of cURL handles with attached `CURLFile`s, we must not attach the opened stream, because the stream may not be seekable, so that we could rewind, when the same stream is going to be uploaded multiple times. Instead, we're opening the stream lazily in the read callback. Since `curl_multi_perfom()` processes easy handles asynchronously, we have no control of the operation sequence. Since duplicated cURL handles may be used with multi handles, we cannot use a single arg structure, but actually have to rebuild the whole mime structure on handle duplication and attach this to the new handle. In order to better test this behavior, we extend the test responder to print the size of the upload, and patch the existing tests accordingly.
1 parent a0c93bf commit 2d0dec9

11 files changed

+222
-70
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ PHP NEWS
1616
- CURL:
1717
. Fixed bug #79078 (Hypothetical use-after-free in curl_multi_add_handle()).
1818
(cmb)
19+
. Fixed bug #79019 (Copied cURL handles upload empty file). (cmb)
1920

2021
- FFI:
2122
. Fixed bug #79096 (FFI Struct Segfault). (cmb)

ext/curl/interface.c

Lines changed: 105 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,11 +1795,20 @@ static void curl_free_post(void **post)
17951795
}
17961796
/* }}} */
17971797

1798-
/* {{{ curl_free_stream
1798+
struct mime_data_cb_arg {
1799+
zend_string *filename;
1800+
php_stream *stream;
1801+
};
1802+
1803+
/* {{{ curl_free_cb_arg
17991804
*/
1800-
static void curl_free_stream(void **post)
1805+
static void curl_free_cb_arg(void **cb_arg_p)
18011806
{
1802-
php_stream_close((php_stream *)*post);
1807+
struct mime_data_cb_arg *cb_arg = (struct mime_data_cb_arg *) *cb_arg_p;
1808+
1809+
ZEND_ASSERT(cb_arg->stream == NULL);
1810+
zend_string_release(cb_arg->filename);
1811+
efree(cb_arg);
18031812
}
18041813
/* }}} */
18051814

@@ -1900,11 +1909,13 @@ php_curl *alloc_curl_handle()
19001909

19011910
zend_llist_init(&ch->to_free->str, sizeof(char *), (llist_dtor_func_t)curl_free_string, 0);
19021911
zend_llist_init(&ch->to_free->post, sizeof(struct HttpPost *), (llist_dtor_func_t)curl_free_post, 0);
1903-
zend_llist_init(&ch->to_free->stream, sizeof(php_stream *), (llist_dtor_func_t)curl_free_stream, 0);
1912+
zend_llist_init(&ch->to_free->stream, sizeof(struct mime_data_cb_arg *), (llist_dtor_func_t)curl_free_cb_arg, 0);
19041913

19051914
ch->to_free->slist = emalloc(sizeof(HashTable));
19061915
zend_hash_init(ch->to_free->slist, 4, NULL, curl_free_slist, 0);
1907-
1916+
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
1917+
ZVAL_UNDEF(&ch->postfields);
1918+
#endif
19081919
return ch;
19091920
}
19101921
/* }}} */
@@ -2094,62 +2105,48 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source)
20942105
(*source->clone)++;
20952106
}
20962107

2097-
/* {{{ proto resource curl_copy_handle(resource ch)
2098-
Copy a cURL handle along with all of it's preferences */
2099-
PHP_FUNCTION(curl_copy_handle)
2108+
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
2109+
static size_t read_cb(char *buffer, size_t size, size_t nitems, void *arg) /* {{{ */
21002110
{
2101-
CURL *cp;
2102-
zval *zid;
2103-
php_curl *ch, *dupch;
2104-
2105-
ZEND_PARSE_PARAMETERS_START(1,1)
2106-
Z_PARAM_RESOURCE(zid)
2107-
ZEND_PARSE_PARAMETERS_END();
2111+
struct mime_data_cb_arg *cb_arg = (struct mime_data_cb_arg *) arg;
2112+
ssize_t numread;
21082113

2109-
if ((ch = (php_curl*)zend_fetch_resource(Z_RES_P(zid), le_curl_name, le_curl)) == NULL) {
2110-
RETURN_FALSE;
2114+
if (cb_arg->stream == NULL) {
2115+
if (!(cb_arg->stream = php_stream_open_wrapper(ZSTR_VAL(cb_arg->filename), "rb", IGNORE_PATH, NULL))) {
2116+
return CURL_READFUNC_ABORT;
2117+
}
21112118
}
2112-
2113-
cp = curl_easy_duphandle(ch->cp);
2114-
if (!cp) {
2115-
php_error_docref(NULL, E_WARNING, "Cannot duplicate cURL handle");
2116-
RETURN_FALSE;
2119+
numread = php_stream_read(cb_arg->stream, buffer, nitems * size);
2120+
if (numread < 0) {
2121+
php_stream_close(cb_arg->stream);
2122+
cb_arg->stream = NULL;
2123+
return CURL_READFUNC_ABORT;
21172124
}
2118-
2119-
dupch = alloc_curl_handle();
2120-
dupch->cp = cp;
2121-
2122-
_php_setup_easy_copy_handlers(dupch, ch);
2123-
2124-
Z_ADDREF_P(zid);
2125-
2126-
ZVAL_RES(return_value, zend_register_resource(dupch, le_curl));
2127-
dupch->res = Z_RES_P(return_value);
2125+
return numread;
21282126
}
21292127
/* }}} */
21302128

2131-
#if LIBCURL_VERSION_NUM >= 0x073800
2132-
static size_t read_cb(char *buffer, size_t size, size_t nitems, void *arg) /* {{{ */
2129+
static int seek_cb(void *arg, curl_off_t offset, int origin) /* {{{ */
21332130
{
2134-
php_stream *stream = (php_stream *) arg;
2135-
ssize_t numread = php_stream_read(stream, buffer, nitems * size);
2131+
struct mime_data_cb_arg *cb_arg = (struct mime_data_cb_arg *) arg;
2132+
int res;
21362133

2137-
if (numread < 0) {
2138-
return CURL_READFUNC_ABORT;
2134+
if (cb_arg->stream == NULL) {
2135+
return CURL_SEEKFUNC_CANTSEEK;
21392136
}
2140-
return numread;
2137+
res = php_stream_seek(cb_arg->stream, offset, origin);
2138+
return res == SUCCESS ? CURL_SEEKFUNC_OK : CURL_SEEKFUNC_CANTSEEK;
21412139
}
21422140
/* }}} */
21432141

2144-
static int seek_cb(void *arg, curl_off_t offset, int origin) /* {{{ */
2142+
static void free_cb(void *arg) /* {{{ */
21452143
{
2146-
php_stream *stream = (php_stream *) arg;
2147-
int res = php_stream_seek(stream, offset, origin);
2144+
struct mime_data_cb_arg *cb_arg = (struct mime_data_cb_arg *) arg;
21482145

2149-
if (res) {
2150-
return CURL_SEEKFUNC_CANTSEEK;
2146+
if (cb_arg->stream != NULL) {
2147+
php_stream_close(cb_arg->stream);
2148+
cb_arg->stream = NULL;
21512149
}
2152-
return CURL_SEEKFUNC_OK;
21532150
}
21542151
/* }}} */
21552152
#endif
@@ -2160,7 +2157,7 @@ static inline int build_mime_structure_from_hash(php_curl *ch, zval *zpostfields
21602157
zval *current;
21612158
HashTable *postfields;
21622159
zend_string *string_key;
2163-
zend_ulong num_key;
2160+
zend_ulong num_key;
21642161
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
21652162
curl_mime *mime = NULL;
21662163
curl_mimepart *part;
@@ -2202,7 +2199,7 @@ static inline int build_mime_structure_from_hash(php_curl *ch, zval *zpostfields
22022199
zval *prop, rv;
22032200
char *type = NULL, *filename = NULL;
22042201
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
2205-
php_stream *stream;
2202+
struct mime_data_cb_arg *cb_arg;
22062203
#endif
22072204

22082205
prop = zend_read_property(curl_CURLFile_class, current, "name", sizeof("name")-1, 0, &rv);
@@ -2225,24 +2222,25 @@ static inline int build_mime_structure_from_hash(php_curl *ch, zval *zpostfields
22252222
}
22262223

22272224
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
2228-
if (!(stream = php_stream_open_wrapper(ZSTR_VAL(postval), "rb", IGNORE_PATH, NULL))) {
2229-
zend_string_release_ex(string_key, 0);
2230-
return FAILURE;
2231-
}
2225+
zval_ptr_dtor(&ch->postfields);
2226+
ZVAL_COPY(&ch->postfields, zpostfields);
2227+
2228+
cb_arg = emalloc(sizeof *cb_arg);
2229+
cb_arg->filename = zend_string_copy(postval);
2230+
cb_arg->stream = NULL;
2231+
22322232
part = curl_mime_addpart(mime);
22332233
if (part == NULL) {
2234-
php_stream_close(stream);
22352234
zend_string_release_ex(string_key, 0);
22362235
return FAILURE;
22372236
}
22382237
if ((form_error = curl_mime_name(part, ZSTR_VAL(string_key))) != CURLE_OK
2239-
|| (form_error = curl_mime_data_cb(part, -1, read_cb, seek_cb, NULL, stream)) != CURLE_OK
2238+
|| (form_error = curl_mime_data_cb(part, -1, read_cb, seek_cb, free_cb, cb_arg)) != CURLE_OK
22402239
|| (form_error = curl_mime_filename(part, filename ? filename : ZSTR_VAL(postval))) != CURLE_OK
22412240
|| (form_error = curl_mime_type(part, type ? type : "application/octet-stream")) != CURLE_OK) {
2242-
php_stream_close(stream);
22432241
error = form_error;
22442242
}
2245-
zend_llist_add_element(&ch->to_free->stream, &stream);
2243+
zend_llist_add_element(&ch->to_free->stream, &cb_arg);
22462244
#else
22472245
form_error = curl_formadd(&first, &last,
22482246
CURLFORM_COPYNAME, ZSTR_VAL(string_key),
@@ -2310,11 +2308,60 @@ static inline int build_mime_structure_from_hash(php_curl *ch, zval *zpostfields
23102308
zend_llist_add_element(&ch->to_free->post, &first);
23112309
error = curl_easy_setopt(ch->cp, CURLOPT_HTTPPOST, first);
23122310
#endif
2311+
23132312
SAVE_CURL_ERROR(ch, error);
23142313
return error == CURLE_OK ? SUCCESS : FAILURE;
23152314
}
23162315
/* }}} */
23172316

2317+
/* {{{ proto resource curl_copy_handle(resource ch)
2318+
Copy a cURL handle along with all of it's preferences */
2319+
PHP_FUNCTION(curl_copy_handle)
2320+
{
2321+
CURL *cp;
2322+
zval *zid;
2323+
php_curl *ch, *dupch;
2324+
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
2325+
zval *postfields;
2326+
#endif
2327+
2328+
ZEND_PARSE_PARAMETERS_START(1,1)
2329+
Z_PARAM_RESOURCE(zid)
2330+
ZEND_PARSE_PARAMETERS_END();
2331+
2332+
if ((ch = (php_curl*)zend_fetch_resource(Z_RES_P(zid), le_curl_name, le_curl)) == NULL) {
2333+
RETURN_FALSE;
2334+
}
2335+
2336+
cp = curl_easy_duphandle(ch->cp);
2337+
if (!cp) {
2338+
php_error_docref(NULL, E_WARNING, "Cannot duplicate cURL handle");
2339+
RETURN_FALSE;
2340+
}
2341+
2342+
dupch = alloc_curl_handle();
2343+
dupch->cp = cp;
2344+
2345+
_php_setup_easy_copy_handlers(dupch, ch);
2346+
2347+
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
2348+
postfields = &ch->postfields;
2349+
if (Z_TYPE_P(postfields) != IS_UNDEF) {
2350+
if (build_mime_structure_from_hash(dupch, postfields) != SUCCESS) {
2351+
_php_curl_close_ex(dupch);
2352+
php_error_docref(NULL, E_WARNING, "Cannot rebuild mime structure");
2353+
RETURN_FALSE;
2354+
}
2355+
}
2356+
#endif
2357+
2358+
Z_ADDREF_P(zid);
2359+
2360+
ZVAL_RES(return_value, zend_register_resource(dupch, le_curl));
2361+
dupch->res = Z_RES_P(return_value);
2362+
}
2363+
/* }}} */
2364+
23182365
static int _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue) /* {{{ */
23192366
{
23202367
CURLcode error = CURLE_OK;
@@ -3613,6 +3660,9 @@ static void _php_curl_close_ex(php_curl *ch)
36133660
#endif
36143661

36153662
efree(ch->handlers);
3663+
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
3664+
zval_ptr_dtor(&ch->postfields);
3665+
#endif
36163666
efree(ch);
36173667
}
36183668
/* }}} */

ext/curl/php_curl.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ typedef struct {
183183
struct _php_curl_error err;
184184
zend_bool in_callback;
185185
uint32_t* clone;
186+
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
187+
zval postfields;
188+
#endif
186189
} php_curl;
187190

188191
#define CURLOPT_SAFE_UPLOAD -1

ext/curl/tests/bug27023.phpt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ var_dump(curl_exec($ch));
3838
curl_close($ch);
3939
?>
4040
--EXPECTF--
41-
string(%d) "curl_testdata1.txt|application/octet-stream"
42-
string(%d) "curl_testdata1.txt|text/plain"
43-
string(%d) "foo.txt|application/octet-stream"
44-
string(%d) "foo.txt|text/plain"
41+
string(%d) "curl_testdata1.txt|application/octet-stream|6"
42+
string(%d) "curl_testdata1.txt|text/plain|6"
43+
string(%d) "foo.txt|application/octet-stream|6"
44+
string(%d) "foo.txt|text/plain|6"

ext/curl/tests/bug77711.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ curl_close($ch);
2424
===DONE===
2525
--EXPECTF--
2626
bool(true)
27-
string(%d) "АБВ.txt|application/octet-stream"
27+
string(%d) "АБВ.txt|application/octet-stream|5"
2828
===DONE===
2929
--CLEAN--
3030
<?php

ext/curl/tests/curl_copy_handle_variation3.phpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ curl_close($ch2);
2929
===DONE===
3030
--EXPECTF--
3131
bool(true)
32-
string(%d) "АБВ.txt|application/octet-stream"
33-
string(%d) "АБВ.txt|application/octet-stream"
32+
string(%d) "АБВ.txt|application/octet-stream|5"
33+
string(%d) "АБВ.txt|application/octet-stream|5"
3434
===DONE===
3535
--CLEAN--
3636
<?php
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
--TEST--
2+
curl_copy_handle() allows to post CURLFile multiple times with curl_multi_exec()
3+
--SKIPIF--
4+
<?php include 'skipif.inc'; ?>
5+
--FILE--
6+
<?php
7+
include 'server.inc';
8+
$host = curl_cli_server_start();
9+
10+
$ch1 = curl_init();
11+
curl_setopt($ch1, CURLOPT_SAFE_UPLOAD, 1);
12+
curl_setopt($ch1, CURLOPT_URL, "{$host}/get.php?test=file");
13+
// curl_setopt($ch1, CURLOPT_RETURNTRANSFER, 1);
14+
15+
$filename = __DIR__ . '/АБВ.txt';
16+
file_put_contents($filename, "Test.");
17+
$file = curl_file_create($filename);
18+
$params = array('file' => $file);
19+
var_dump(curl_setopt($ch1, CURLOPT_POSTFIELDS, $params));
20+
21+
$ch2 = curl_copy_handle($ch1);
22+
$ch3 = curl_copy_handle($ch1);
23+
24+
$mh = curl_multi_init();
25+
curl_multi_add_handle($mh, $ch1);
26+
curl_multi_add_handle($mh, $ch2);
27+
do {
28+
$status = curl_multi_exec($mh, $active);
29+
if ($active) {
30+
curl_multi_select($mh);
31+
}
32+
} while ($active && $status == CURLM_OK);
33+
34+
curl_multi_remove_handle($mh, $ch1);
35+
curl_multi_remove_handle($mh, $ch2);
36+
curl_multi_remove_handle($mh, $ch3);
37+
curl_multi_close($mh);
38+
?>
39+
===DONE===
40+
--EXPECTF--
41+
bool(true)
42+
АБВ.txt|application/octet-stream|5АБВ.txt|application/octet-stream|5===DONE===
43+
--CLEAN--
44+
<?php
45+
@unlink(__DIR__ . '/АБВ.txt');
46+
?>

0 commit comments

Comments
 (0)