Skip to content

Commit 726db27

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 or seek 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. This requires to store the zval that has been originally provided for `CURLOPT_POSTFIELDS`. Ideally, we would store it in an additional member of `php_curl`, but since that would constitute a BC break, we work around by actually storing a union in `to_free->stream`, and we retrieve the appropriate zval by sequentially searching the list. 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 a6d385a commit 726db27

9 files changed

+244
-65
lines changed

ext/curl/interface.c

Lines changed: 131 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1791,11 +1791,30 @@ static void curl_free_post(void **post)
17911791
}
17921792
/* }}} */
17931793

1794-
/* {{{ curl_free_stream
1794+
struct mime_data_cb_arg {
1795+
php_curl *ch;
1796+
union {
1797+
zval postfields;
1798+
struct {
1799+
zend_string *filename;
1800+
php_stream *stream;
1801+
};
1802+
};
1803+
};
1804+
1805+
/* {{{ curl_free_cb_arg
17951806
*/
1796-
static void curl_free_stream(void **post)
1807+
static void curl_free_cb_arg(void **cb_arg_p)
17971808
{
1798-
php_stream_close((php_stream *)*post);
1809+
struct mime_data_cb_arg *cb_arg = (struct mime_data_cb_arg *) *cb_arg_p;
1810+
1811+
if (cb_arg->ch) {
1812+
Z_DELREF(cb_arg->postfields);
1813+
} else {
1814+
ZEND_ASSERT(cb_arg->stream == NULL);
1815+
zend_string_release(cb_arg->filename);
1816+
}
1817+
efree(cb_arg);
17991818
}
18001819
/* }}} */
18011820

@@ -1896,7 +1915,7 @@ php_curl *alloc_curl_handle()
18961915

18971916
zend_llist_init(&ch->to_free->str, sizeof(char *), (llist_dtor_func_t)curl_free_string, 0);
18981917
zend_llist_init(&ch->to_free->post, sizeof(struct HttpPost *), (llist_dtor_func_t)curl_free_post, 0);
1899-
zend_llist_init(&ch->to_free->stream, sizeof(php_stream *), (llist_dtor_func_t)curl_free_stream, 0);
1918+
zend_llist_init(&ch->to_free->stream, sizeof(struct mime_data_cb_arg *), (llist_dtor_func_t)curl_free_cb_arg, 0);
19001919

19011920
ch->to_free->slist = emalloc(sizeof(HashTable));
19021921
zend_hash_init(ch->to_free->slist, 4, NULL, curl_free_slist, 0);
@@ -2090,62 +2109,71 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source)
20902109
(*source->clone)++;
20912110
}
20922111

2093-
/* {{{ proto resource curl_copy_handle(resource ch)
2094-
Copy a cURL handle along with all of it's preferences */
2095-
PHP_FUNCTION(curl_copy_handle)
2112+
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
2113+
static size_t read_cb(char *buffer, size_t size, size_t nitems, void *arg) /* {{{ */
20962114
{
2097-
CURL *cp;
2098-
zval *zid;
2099-
php_curl *ch, *dupch;
2115+
struct mime_data_cb_arg *cb_arg = (struct mime_data_cb_arg *) arg;
2116+
ssize_t numread;
21002117

2101-
ZEND_PARSE_PARAMETERS_START(1,1)
2102-
Z_PARAM_RESOURCE(zid)
2103-
ZEND_PARSE_PARAMETERS_END();
2118+
ZEND_ASSERT(!cb_arg->ch);
21042119

2105-
if ((ch = (php_curl*)zend_fetch_resource(Z_RES_P(zid), le_curl_name, le_curl)) == NULL) {
2106-
RETURN_FALSE;
2120+
if (cb_arg->stream == NULL) {
2121+
if (!(cb_arg->stream = php_stream_open_wrapper(ZSTR_VAL(cb_arg->filename), "rb", IGNORE_PATH, NULL))) {
2122+
return CURL_READFUNC_ABORT;
2123+
}
21072124
}
2108-
2109-
cp = curl_easy_duphandle(ch->cp);
2110-
if (!cp) {
2111-
php_error_docref(NULL, E_WARNING, "Cannot duplicate cURL handle");
2112-
RETURN_FALSE;
2125+
numread = php_stream_read(cb_arg->stream, buffer, nitems * size);
2126+
if (numread < 0) {
2127+
php_stream_close(cb_arg->stream);
2128+
cb_arg->stream = NULL;
21132129
}
2130+
return (numread >= 0) ? numread : CURL_READFUNC_ABORT;
2131+
}
2132+
/* }}} */
21142133

2115-
dupch = alloc_curl_handle();
2116-
dupch->cp = cp;
2117-
2118-
_php_setup_easy_copy_handlers(dupch, ch);
2134+
static int seek_cb(void *arg, curl_off_t offset, int origin) /* {{{ */
2135+
{
2136+
struct mime_data_cb_arg *cb_arg = (struct mime_data_cb_arg *) arg;
2137+
int res;
21192138

2120-
Z_ADDREF_P(zid);
2139+
ZEND_ASSERT(!cb_arg->ch);
21212140

2122-
ZVAL_RES(return_value, zend_register_resource(dupch, le_curl));
2123-
dupch->res = Z_RES_P(return_value);
2141+
if (cb_arg->stream == NULL) {
2142+
if (!(cb_arg->stream = php_stream_open_wrapper(ZSTR_VAL(cb_arg->filename), "rb", IGNORE_PATH, NULL))) {
2143+
return CURL_SEEKFUNC_CANTSEEK;
2144+
}
2145+
}
2146+
res = php_stream_seek(cb_arg->stream, offset, origin);
2147+
return res == SUCCESS ? CURL_SEEKFUNC_OK : CURL_SEEKFUNC_CANTSEEK;
21242148
}
21252149
/* }}} */
21262150

2127-
#if LIBCURL_VERSION_NUM >= 0x073800
2128-
static size_t read_cb(char *buffer, size_t size, size_t nitems, void *arg) /* {{{ */
2151+
static void free_cb(void *arg) /* {{{ */
21292152
{
2130-
php_stream *stream = (php_stream *) arg;
2131-
ssize_t numread = php_stream_read(stream, buffer, nitems * size);
2153+
struct mime_data_cb_arg *cb_arg = (struct mime_data_cb_arg *) arg;
21322154

2133-
if (numread < 0) {
2134-
return CURL_READFUNC_ABORT;
2155+
ZEND_ASSERT(!cb_arg->ch);
2156+
2157+
if (cb_arg->stream != NULL) {
2158+
php_stream_close(cb_arg->stream);
2159+
cb_arg->stream = NULL;
21352160
}
2136-
return numread;
21372161
}
21382162
/* }}} */
21392163

2140-
static int seek_cb(void *arg, curl_off_t offset, int origin) /* {{{ */
2164+
static zval *get_postfields_of_handle(php_curl *ch) /* {{{ */
21412165
{
2142-
php_stream *stream = (php_stream *) arg;
2143-
int res = php_stream_seek(stream, offset, origin);
2166+
struct mime_data_cb_arg *cb_arg, **cb_arg_p;
21442167

2145-
if (res) {
2146-
return CURL_SEEKFUNC_CANTSEEK;
2168+
cb_arg_p = zend_llist_get_last(&ch->to_free->stream);
2169+
while (cb_arg_p) {
2170+
cb_arg = *cb_arg_p;
2171+
if (cb_arg->ch == ch) {
2172+
return &cb_arg->postfields;
2173+
}
2174+
cb_arg_p = zend_llist_get_prev(&ch->to_free->stream);
21472175
}
2148-
return CURL_SEEKFUNC_OK;
2176+
return NULL;
21492177
}
21502178
/* }}} */
21512179
#endif
@@ -2156,7 +2184,7 @@ static inline int build_mime_structure_from_hash(php_curl *ch, zval *zpostfields
21562184
zval *current;
21572185
HashTable *postfields;
21582186
zend_string *string_key;
2159-
zend_ulong num_key;
2187+
zend_ulong num_key;
21602188
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
21612189
curl_mime *mime = NULL;
21622190
curl_mimepart *part;
@@ -2198,7 +2226,7 @@ static inline int build_mime_structure_from_hash(php_curl *ch, zval *zpostfields
21982226
zval *prop, rv;
21992227
char *type = NULL, *filename = NULL;
22002228
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
2201-
php_stream *stream;
2229+
struct mime_data_cb_arg *cb_arg;
22022230
#endif
22032231

22042232
prop = zend_read_property(curl_CURLFile_class, current, "name", sizeof("name")-1, 0, &rv);
@@ -2221,24 +2249,28 @@ static inline int build_mime_structure_from_hash(php_curl *ch, zval *zpostfields
22212249
}
22222250

22232251
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
2224-
if (!(stream = php_stream_open_wrapper(ZSTR_VAL(postval), "rb", IGNORE_PATH, NULL))) {
2225-
zend_string_release_ex(string_key, 0);
2226-
return FAILURE;
2227-
}
2252+
cb_arg = emalloc(sizeof *cb_arg);
2253+
cb_arg->ch = ch;
2254+
ZVAL_COPY(&cb_arg->postfields, zpostfields);
2255+
zend_llist_add_element(&ch->to_free->stream, &cb_arg);
2256+
2257+
cb_arg = emalloc(sizeof *cb_arg);
2258+
cb_arg->ch = NULL;
2259+
cb_arg->filename = zend_string_copy(postval);
2260+
cb_arg->stream = NULL;
2261+
22282262
part = curl_mime_addpart(mime);
22292263
if (part == NULL) {
2230-
php_stream_close(stream);
22312264
zend_string_release_ex(string_key, 0);
22322265
return FAILURE;
22332266
}
22342267
if ((form_error = curl_mime_name(part, ZSTR_VAL(string_key))) != CURLE_OK
2235-
|| (form_error = curl_mime_data_cb(part, -1, read_cb, seek_cb, NULL, stream)) != CURLE_OK
2268+
|| (form_error = curl_mime_data_cb(part, -1, read_cb, seek_cb, free_cb, cb_arg)) != CURLE_OK
22362269
|| (form_error = curl_mime_filename(part, filename ? filename : ZSTR_VAL(postval))) != CURLE_OK
22372270
|| (form_error = curl_mime_type(part, type ? type : "application/octet-stream")) != CURLE_OK) {
2238-
php_stream_close(stream);
22392271
error = form_error;
22402272
}
2241-
zend_llist_add_element(&ch->to_free->stream, &stream);
2273+
zend_llist_add_element(&ch->to_free->stream, &cb_arg);
22422274
#else
22432275
form_error = curl_formadd(&first, &last,
22442276
CURLFORM_COPYNAME, ZSTR_VAL(string_key),
@@ -2306,11 +2338,60 @@ static inline int build_mime_structure_from_hash(php_curl *ch, zval *zpostfields
23062338
zend_llist_add_element(&ch->to_free->post, &first);
23072339
error = curl_easy_setopt(ch->cp, CURLOPT_HTTPPOST, first);
23082340
#endif
2341+
23092342
SAVE_CURL_ERROR(ch, error);
23102343
return error == CURLE_OK ? SUCCESS : FAILURE;
23112344
}
23122345
/* }}} */
23132346

2347+
/* {{{ proto resource curl_copy_handle(resource ch)
2348+
Copy a cURL handle along with all of it's preferences */
2349+
PHP_FUNCTION(curl_copy_handle)
2350+
{
2351+
CURL *cp;
2352+
zval *zid;
2353+
php_curl *ch, *dupch;
2354+
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
2355+
zval *postfields;
2356+
#endif
2357+
2358+
ZEND_PARSE_PARAMETERS_START(1,1)
2359+
Z_PARAM_RESOURCE(zid)
2360+
ZEND_PARSE_PARAMETERS_END();
2361+
2362+
if ((ch = (php_curl*)zend_fetch_resource(Z_RES_P(zid), le_curl_name, le_curl)) == NULL) {
2363+
RETURN_FALSE;
2364+
}
2365+
2366+
cp = curl_easy_duphandle(ch->cp);
2367+
if (!cp) {
2368+
php_error_docref(NULL, E_WARNING, "Cannot duplicate cURL handle");
2369+
RETURN_FALSE;
2370+
}
2371+
2372+
dupch = alloc_curl_handle();
2373+
dupch->cp = cp;
2374+
2375+
_php_setup_easy_copy_handlers(dupch, ch);
2376+
2377+
#if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */
2378+
postfields = get_postfields_of_handle(ch);
2379+
if (postfields) {
2380+
if (build_mime_structure_from_hash(dupch, postfields) != SUCCESS) {
2381+
_php_curl_close_ex(dupch);
2382+
php_error_docref(NULL, E_WARNING, "Cannot rebuild mime structure");
2383+
RETURN_FALSE;
2384+
}
2385+
}
2386+
#endif
2387+
2388+
Z_ADDREF_P(zid);
2389+
2390+
ZVAL_RES(return_value, zend_register_resource(dupch, le_curl));
2391+
dupch->res = Z_RES_P(return_value);
2392+
}
2393+
/* }}} */
2394+
23142395
static int _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue) /* {{{ */
23152396
{
23162397
CURLcode error = CURLE_OK;

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+
?>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
--TEST--
2+
curl_copy_handle() allows to post CURLFile multiple times if postfields change
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__ . '/abc.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+
23+
$filename = __DIR__ . '/def.txt';
24+
file_put_contents($filename, "Other test.");
25+
$file = curl_file_create($filename);
26+
$params = array('file' => $file);
27+
var_dump(curl_setopt($ch2, CURLOPT_POSTFIELDS, $params));
28+
29+
$ch3 = curl_copy_handle($ch2);
30+
31+
var_dump(curl_exec($ch1));
32+
curl_close($ch1);
33+
34+
var_dump(curl_exec($ch2));
35+
curl_close($ch2);
36+
37+
var_dump(curl_exec($ch3));
38+
curl_close($ch3);
39+
?>
40+
===DONE===
41+
--EXPECTF--
42+
bool(true)
43+
bool(true)
44+
string(%d) "abc.txt|application/octet-stream|5"
45+
string(%d) "def.txt|application/octet-stream|11"
46+
string(%d) "def.txt|application/octet-stream|11"
47+
===DONE===
48+
--CLEAN--
49+
<?php
50+
@unlink(__DIR__ . '/abc.txt');
51+
@unlink(__DIR__ . '/def.txt');
52+
?>

0 commit comments

Comments
 (0)