libcurl 在 Raspberry Pi 上执行 HTTP POST 后响应数据异常,如何排查?
结论
现有日志无法确定唯一根因。应先检查 response 的实际容量、调用方使用的长度,以及复制前后的内存内容。代码中有一处可以确定的长度处理问题:复制可能截断数据,函数却仍返回完整的接收长度。
如果这次回调确实只执行了一次,并收到 787 bytes,就没有证据支持“先收到 632 bytes,再收到 155 bytes”的解释。输出开头出现尾部数据,只能说明内容有重复,无法据此还原网络分块过程。
当前回调打印的是 contents,尚未验证 realloc 后的 chunk.memory 是否保存了相同的数据。可以从这里继续排查。
先核对容量和返回长度
原代码的关键部分是:
responseLength = chunk.size;
memcpy(response, chunk.memory,
chunk.size < maxResponseLength ? chunk.size : maxResponseLength);
这里有两种问题:
- 如果
maxResponseLength大于response的实际分配容量,memcpy仍可能越界写入。长度参数无法验证指针背后的内存大小。 - 如果
maxResponseLength小于chunk.size,复制会截断,但函数仍返回chunk.size。调用方若按返回值打印或解析,就会读取未复制的数据,甚至越界读取。
例如,收到 787 bytes、目标容量为 512 bytes 时,函数只复制 512 bytes,却返回 787。这个缺陷可以直接从代码确定,但它是否导致了当前异常,还需要查看调用方代码。
还需核对 response 的定义和分配方式、传入的 maxResponseLength,以及函数返回后的打印循环。如果目标是数组,可以在数组仍可见的位置使用 sizeof response;如果目标是动态分配的指针,应传入分配时记录的容量,因为此时 sizeof response 得到的只是指针大小。
在三个位置比较数据
按顺序检查以下位置,可以缩小数据首次发生变化的范围:
writeCb完成追加后,打印mem->memory和mem->size。curl_easy_perform返回后、复制前,打印chunk.memory和chunk.size。memcpy完成后、释放chunk.memory前,比较源缓冲区与目标缓冲区。
第三步可以加入以下诊断代码:
size_t copied = chunk.size < maxResponseLength
? chunk.size
: maxResponseLength;
fprintf(stderr, "received=%zu capacity=%zu copied=%zu\n",
chunk.size, (size_t)maxResponseLength, copied);
/* 前提:response 实际至少有 copied bytes 可写空间。 */
if (copied > 0) {
memcpy(response, chunk.memory, copied);
fprintf(stderr, "equal immediately after copy: %s\n",
memcmp(response, chunk.memory, copied) == 0 ? "yes" : "no");
}
如果复制后两者相同,返回调用方后数据却不同,应继续检查后续写操作、缓冲区生命周期、其他线程和打印代码本身。如果复制前 chunk.memory 就已异常,则重点检查回调写入和其他可能破坏内存的代码。
这段诊断代码也要求容量真实有效,不能替代容量检查。
明确选择截断还是报错
对于需要完整解析的二进制响应,容量不足时直接报错通常更容易处理。可以把复制逻辑封装为:
#include <stddef.h>
#include <string.h>
static int copy_response(const struct MemoryStruct *chunk,
unsigned char *response,
size_t capacity,
size_t *written)
{
if (written == NULL) {
return -1;
}
*written = 0;
if (chunk == NULL || chunk->size > capacity) {
return -1;
}
if (chunk->size > 0) {
if (response == NULL || chunk->memory == NULL) {
return -1;
}
memcpy(response, chunk->memory, chunk->size);
}
*written = chunk->size;
return 0;
}
在 curl_easy_perform 成功后调用它,并让复制失败走统一的资源清理路径。这里的 capacity 必须来自真实分配容量。
外层接口也建议把状态和长度分开,例如:
int httpPostRequest(char *endpoint,
char *additionalFormData,
uint8_t *response,
size_t capacity,
size_t *responseLength);
约定成功返回 0,失败返回非零值。*responseLength 在开始时置零,成功时只表示交给调用方的数据长度;所有失败路径都应设置明确的错误状态。
原来的 int16_t 无法表示超过 32767 的正长度,uint16_t 的容量也有范围限制。它们不能解释这次 787 bytes 的异常,但不适合作为通用响应长度接口。原函数在 curl_global_init 失败时返回错误码,成功时返回长度;两者都是正数,调用方容易误判。
修正日志与其他内存风险
贴出的代码有两个地方需要先核对:
printf("Curl callback:\r\n")
printf("%02X", contents[i]);
第一行缺少分号。第二行直接对 void * 使用下标,这不是标准 C/C++ 中有效的字节访问方式。实际运行的代码应当与贴出的片段存在差异。字节日志可以这样写:
const unsigned char *bytes = (const unsigned char *)contents;
printf("Curl callback, chunksize = %zu\n", realsize);
for (size_t i = 0; i < realsize; ++i) {
printf("%02X", (unsigned int)bytes[i]);
}
printf("\n");
还应检查以下位置:
- 两处
sprintf都没有容量限制。改用snprintf,并检查返回值是否为负数或大于等于目标数组容量;发生截断时应停止请求。 - 回调中的
size * nmemb和mem->size + realsize + 1应检查整数溢出。当前 787 bytes 本身不会触及正常的size_t上限。 url和request是static数组。如果函数可能并发执行,它们会被不同调用共享,需要消除并发写入。- 响应包含
00,应始终按字节长度处理。不能用strlen或%s判断这类二进制响应是否完整。
Ubuntu 上运行正常也不能排除越界或其他 undefined behavior。平台、编译选项和内存布局的变化都可能改变错误的表现。
用检测工具寻找首次非法访问
在工具链和运行环境支持的情况下,可以给应用启用编译警告和 AddressSanitizer,例如:
cc -g -O1 -Wall -Wextra -Wpedantic \
-fsanitize=address,undefined -fno-omit-frame-pointer \
app.c -lcurl -o app
将 app.c 替换为实际源文件,并保留项目需要的其他编译和链接参数。如果 Raspberry Pi 环境无法使用这些检测器,也应保留前面的分阶段日志,先确定数据第一次变化的位置。
目前还需要调用方的缓冲区分配和打印代码。在这些信息明确之前,不能把 485 bytes 当作 libcurl 的长度限制,也不能断言存在特定的分块复制缺陷。
备注:内容仅供参考。
