PAYATHON 2026

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 得到的只是指针大小。

在三个位置比较数据

按顺序检查以下位置,可以缩小数据首次发生变化的范围:

  1. writeCb 完成追加后,打印 mem->memory 和 mem->size。
  2. curl_easy_perform 返回后、复制前,打印 chunk.memory 和 chunk.size。
  3. 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 的长度限制,也不能断言存在特定的分块复制缺陷。

备注:内容仅供参考。