【ZeroRange WebRTC】KVS WebRTC C SDK 崩溃分析报告
KVS WebRTC C SDK 崩溃分析报告
项目:Amazon Kinesis Video Streams WebRTC C SDK(Master 示例,使用系统 OpenSSL 与系统 libwebsockets)
项目地址:https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-c
摘要
- 现象:运行 kvsWebrtcClientMaster 示例时出现 “stack smashing detected: terminated”,在开启 AddressSanitizer(ASan)后报告为栈缓冲区溢出。
- 根因:
IceAgent.c的定时器回调在一次执行中复制了超过固定上限数量的本地 ICE 候选到栈数组,导致越界写入。 - 修复:
- 为本地候选复制逻辑增加严格上限检查,确保不会写出数组边界。
- 在信令路径中补充字符串终止与空列表处理,避免边界条件触发栈破坏。
- 为缺失的 libwebsockets 函数提供本地替代实现,解决链接期符号不可用问题。
- 结论:崩溃源于栈数组越界,已通过边界保护修复;建议继续在 ASan 下验证,并对 JSON 拼接做进一步长度检查以提高健壮性。
环境与构建
- 操作系统:Linux
- 构建选项(使用系统依赖):
cmake .. -DBUILD_DEPENDENCIES=OFF -DUSE_OPENSSL=ON
- 诊断构建(建议用于验证与定位):
cmake .. -DCMAKE_BUILD_TYPE=Debug -DADDRESS_SANITIZER=ON -DCOMPILER_WARNINGS=ONmake -j$(nproc)
现象与日志
- 非 ASan 情况:
*** stack smashing detected ***: terminated - ASan(关键摘录):
- 报错位置:
src/source/Ice/IceAgent.c:1636,函数iceAgentGatherCandidateTimerCallback - 越界对象:栈数组
newLocalCandidates[...]被写出边界 - 写入大小与结构体
IceCandidate大小一致
- 报错位置:
分析过程
- 初次构建依赖拉取失败(网络超时),切换为系统依赖构建(OpenSSL、libwebsockets)。
- 链接期出现
lws_http_date_parse_unix未定义;系统 libwebsockets 不提供该符号:- 在
LwsApiCalls.c添加本地parse_http_date_unix(解析 RFC 7231 IMF‑fixdate),替换原调用点。
- 在
- 运行示例进入 ICE 交换阶段时触发 “stack smashing detected”。
- 启用 ASan 后精确定位为
IceAgent.c:1636的栈数组越界。 - 分析代码逻辑并确认:一次定时器回调中可能有超过固定上限的 “VALID 且未报告” 的候选被复制至栈数组,导致越界。
- 实施修复并验证。
根因与原理
问题代码(摘录)
文件:src/source/Ice/IceAgent.c
函数:iceAgentGatherCandidateTimerCallback
- 关键栈数组:
IceCandidate newLocalCandidates[KVS_ICE_MAX_NEW_LOCAL_CANDIDATES_TO_REPORT_AT_ONCE];(默认上限 10)
- 原始复制逻辑:
- 无上限判断地执行:
newLocalCandidates[newLocalCandidateCount++] = *pIceCandidate;pIceCandidate->reported = TRUE;
- 若当前回调中有超过 10 个候选满足条件(
VALID && !reported),则发生越界写。
- 无上限判断地执行:
为什么看起来在 “Client is writable” 附近崩溃
- 栈保护(Stack Protector)的 “金丝雀” 校验通常在函数返回或随后调用过程中触发。实际越界写发生在更早的定时器回调;当执行到可写回调或其他路径时,才检测到栈被破坏并抛错。
栈保护与 ASan 简述
- 栈保护器在局部栈帧中放置“金丝雀”值以检测越界写;返回时校验金丝雀是否被破坏。
- ASan利用影子内存监控访问合法性,对栈/堆红区的越界写会立即报错,并提供精确的源码行与调用栈。
ASan 工作原理详解与使用建议
原理概述
- AddressSanitizer(ASan)在编译期对内存访问插桩,并在运行时维护一段“影子内存(shadow memory)”。程序内存区域周边会被“红区”保护(例如栈红区、堆红区),任何对红区的读/写都被视为越界并立即上报。
- ASan 报告中的影子字节图示(例如
f3 f3 f3 ...)代表不同类型的“红区/毒化”状态:f1/f2/f3等常见值表示栈的左/中/右红区;对这些区域的访问就是典型栈越界。fa表示堆左红区;fb/fd等用于区分不同堆状态(释放后区域、内部使用等)。
- 报告会包含:违规访问类型(READ/WRITE)、大小、精确源文件/行号、调用栈,以及“该栈帧的对象布局”,帮助定位具体变量越界。
示例 ASan 报告(节选)与字段解读
WRITE of size 96 at 0x... thread T6#0 iceAgentGatherCandidateTimerCallback .../Ice/IceAgent.c:1636#1 timerQueueExecutor .../kvspic-src/src/utils/src/TimerQueue.c:587#2 start_thread nptl/pthread_create.c:442#3 __libc ...Address ... is located in stack of thread T6 at offset 1120 in frame#0 iceAgentGatherCandidateTimerCallback .../Ice/IceAgent.c:1595This frame has 4 object(s):[160, 1120) 'newLocalCandidates' <== Memory access at offset 1120 overflows this variableShadow bytes around the buggy address:... f3 f3 f3 f3 f3 f3 f3 f3 ...
Shadow byte legend:Stack right redzone: f3Stack left redzone: f1Heap left redzone: faFreed heap region: fd
- 关键字段说明:
WRITE of size 96:写入了 96 字节(与IceCandidate结构体大小一致)。IceAgent.c:1636:精确定位到源码行,便于在 IDE 中快速跳转修复。This frame has N object(s):罗列当前栈帧内的局部变量布局,标注越界变量(本案为newLocalCandidates)。Shadow bytes ... f3:f3代表“栈右红区”,即典型栈越界写;结合legend可快速判断越界类型。
如何阅读 ASan 报告(结合本案)
WRITE of size 96 ... in IceAgent.c:1636:表示写入 96 字节(等于IceCandidate结构体大小),定位到源代码第 1636 行。- “This frame has N object(s)” 与后续
<== Memory access at offset ... overflows this variable指明哪个局部变量越界(本案为newLocalCandidates)。 - “Shadow bytes around the buggy address” 用于确认内存区域性质(
f3为栈右红区,典型的栈越界写)。
启用与配置建议
- 构建:
cmake .. -DCMAKE_BUILD_TYPE=Debug -DADDRESS_SANITIZER=ON -DCOMPILER_WARNINGS=ONmake -j$(nproc)
- 运行时推荐环境:
ASAN_OPTIONS="detect_stack_use_after_return=1:strict_string_checks=1:abort_on_error=1"- 如需更详细符号化:确保安装符号文件并在
CMAKE_BUILD_TYPE=Debug下编译。
- 与 GDB 联用(必要时):可用
gdb --args ./samples/kvsWebrtcClientMaster <ChannelName>运行,在崩溃点查看线程与栈帧现场。
常见问题与排查策略
- 报告定位在某函数,但越界写可能更早发生:这通常由于栈红区在函数返回或后续栈访问时才触发检查,因此需结合调用栈前后函数来回溯真实写入点。
- 字符串操作高危:
strcat/strcpy/sprintf等非安全函数容易踩红区。建议统一使用有界版本(strn*、snprintf)并进行长度预检与手动终止符处理。
栈保护(Stack Protector)与“金丝雀”详解
机制简介
- 编译器在函数栈帧中插入一个随机值(“金丝雀”),位于局部缓冲区与返回地址之间。
- 函数返回前对金丝雀做校验:若被改写(例如栈缓冲越界覆盖),则判定为“栈破坏”,立即报错并终止进程(常见信息为
*** stack smashing detected ***: terminated)。
触发时机与表现
- 越界写可能发生在 A 函数,但错误信息常在 A 返回或后续 B 函数执行时出现;这是因为金丝雀在返回点或下一次栈解构时才进行校验。
- 与 ASan 不同:ASan 倾向于在越界写当下就报告(依赖于插桩覆盖),栈保护更像“退出时校验”的最后防线;两者结合使用可最大化发现问题。
与本案关系
- 本案中,多条日志显示在 “Client is writable” 附近崩溃;实际越界写发生在更早的
iceAgentGatherCandidateTimerCallback中,栈保护在后续路径才检测到金丝雀被改写,从而统一报错并终止。
项目集成:ASan 与栈保护如何启用、两者关系
ASan 集成(CMake)
- 顶层
CMakeLists.txt提供多个 Sanitizer 开关:
option(ADDRESS_SANITIZER "Build with AddressSanitizer." OFF)
option(MEMORY_SANITIZER "Build with MemorySanitizer." OFF)
option(THREAD_SANITIZER "Build with ThreadSanitizer." OFF)
option(UNDEFINED_BEHAVIOR_SANITIZER "Build with UndefinedBehaviorSanitizer." OFF)if("${CMAKE_C_COMPILER_ID}" MATCHES "GNU|Clang")if(ADDRESS_SANITIZER)enableSanitizer("address")endif()...
endif()
CMake/Utilities.cmake中的enableSanitizer会追加编译与链接标志:
function(enableSanitizer SANITIZER)set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g -fsanitize=${SANITIZER} -fno-omit-frame-pointer" PARENT_SCOPE)set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g -fsanitize=${SANITIZER} -fno-omit-frame-pointer -fno-optimize-sibling-calls" PARENT_SCOPE)set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=${SANITIZER}" PARENT_SCOPE)
endfunction()
- 使用方式:配置时开启相应选项,例如:
cmake .. -DCMAKE_BUILD_TYPE=Debug -DADDRESS_SANITIZER=ON -DCOMPILER_WARNINGS=ON
make -j$(nproc)
ASAN_OPTIONS="detect_stack_use_after_return=1:strict_string_checks=1:abort_on_error=1" ./samples/kvsWebrtcClientMaster <ChannelName>
栈保护(Stack Protector)集成
- 本工程未在 CMake 中显式设置
-fstack-protector*,栈保护通常由系统编译器默认启用(例如许多 Linux 发行版默认启用-fstack-protector-strong)。 - 如果需要在工程层面强制启用或增强栈保护,可在配置时追加编译旗标:
cmake .. -DCMAKE_BUILD_TYPE=Debug -DADDRESS_SANITIZER=ON \-DCMAKE_C_FLAGS="${CMAKE_C_FLAGS} -fstack-protector-strong" \-DCOMPILER_WARNINGS=ON
两者关系与建议
- ASan 与栈保护互为补充:
- ASan:在越界读/写发生时即刻通过影子内存检测,提供更丰富的上下文与调用栈;适用于开发/调试阶段。
- 栈保护:在函数返回或后续栈操作时校验“金丝雀”,作为运行时最后防线;一般在生产构建也可保留。
- 性能与内存开销:
- ASan 会显著增加内存占用与运行时开销(常见 1.5×~2×),建议在 Debug 或专门的检测构建中启用;
- 栈保护开销较小,建议长期启用
-fstack-protector-strong。
详细分析过程(分步)
- 初始构建阶段:
- 外部依赖(OpenSSL/boringssl 子模块)拉取失败 → 采用系统依赖(
BUILD_DEPENDENCIES=OFF、USE_OPENSSL=ON),成功生成工程。
- 外部依赖(OpenSSL/boringssl 子模块)拉取失败 → 采用系统依赖(
- 链接期修复:
- 系统 libwebsockets 不提供
lws_http_date_parse_unix→ 在LwsApiCalls.c添加parse_http_date_unix(RFC 7231 IMF‑fixdate 解析),并替换原调用,解决链接未定义符号。
- 系统 libwebsockets 不提供
- 运行与初步怀疑:
- 在发送
ICE_CANDIDATE后出现 “stack smashing detected”。 - 初步检查信令路径的 JSON 构造、
payload终止符与 URIs 尾逗号处理,修复潜在边界问题(空列表与手动\0终止)。
- 在发送
- 启用 ASan 精确定位:
- 开启 ASan 并重现问题,报告显示
IceAgent.c:1636的WRITE of size 96,越界对象为栈数组newLocalCandidates。 - 结合常量
KVS_ICE_MAX_NEW_LOCAL_CANDIDATES_TO_REPORT_AT_ONCE=10,判断在一次回调中复制超过 10 个“VALID 且未报告”的候选导致溢出。
- 开启 ASan 并重现问题,报告显示
- 根因确认与修复:
- 增加上限保护:仅在
newLocalCandidateCount < 上限时复制并标记reported=TRUE;超过上限的候选留待下一轮定时器回调。
- 增加上限保护:仅在
- 回归与验证:
- 在 ASan 构建下运行,确认不再出现
stack-buffer-overflow。 - 观察候选报告是否分批进行,符合原注释“每次最多报告 N 个”的设计意图。
- 在 ASan 构建下运行,确认不再出现
- 加固与建议:
- 在信令 JSON 拼接处加入长度预检(
encodedUris、encodedIceConfig),保持所有有界拷贝后显式\0终止;将 ASan 与编译器警告纳入常态化验证。
- 在信令 JSON 拼接处加入长度预检(
修复方案(已应用)
1)为栈数组复制增加上限保护
只在未达到上限时复制并标记为已报告;超出上限的候选保留为未报告,交由下次回调继续处理(符合原注释“每次最多报告 N 个”的设计意图)。
示例修复代码:
else if (pIceCandidate->state == ICE_CANDIDATE_STATE_VALID && !pIceCandidate->reported) {if (newLocalCandidateCount < KVS_ICE_MAX_NEW_LOCAL_CANDIDATES_TO_REPORT_AT_ONCE) {newLocalCandidates[newLocalCandidateCount++] = *pIceCandidate;pIceCandidate->reported = TRUE;}if (pIceCandidate->iceCandidateType == ICE_CANDIDATE_TYPE_SERVER_REFLEXIVE) {CHK_STATUS(createIceCandidatePairs(pIceAgent, pIceCandidate, FALSE));}
}
2)信令路径的安全性增强
LwsApiCalls.c:- 构造 ICE URIs 时删除末尾逗号前先判断长度(空列表不做递减),避免下标越界:
- 若
urisLen > 0,再执行encodedUris[--urisLen] = '\0';
- 若
- 构造 ICE URIs 时删除末尾逗号前先判断长度(空列表不做递减),避免下标越界:
samples/Common.c:- 将
candidateJson拷贝到message.payload后显式添加字符串结束符,避免后续日志或编码读取越界:message.payload[message.payloadLen] = '\0';
- 将
3)libwebsockets 缺失符号的兼容处理
系统 libwebsockets 不提供 lws_http_date_parse_unix;在 LwsApiCalls.c 中添加本地 parse_http_date_unix(const char* buf, size_t len, time_t* out) 并替换调用,以兼容系统库。
验证步骤
- 建议在开启 ASan 的构建下验证:
cmake .. -DCMAKE_BUILD_TYPE=Debug -DADDRESS_SANITIZER=ON -DCOMPILER_WARNINGS=ONmake -j$(nproc)- 运行示例:
./samples/kvsWebrtcClientMaster <ChannelName>
- 观察是否仍有
stack-buffer-overflow报错;同时确认 ICE 候选报告行为符合“分批上报”的预期。
进一步加固建议
为了提升字符串处理的健壮性,建议增加长度预检:
encodedUris拼接前检查:- 每次追加前判断:
STRLEN(encodedUris) + 3 + STRLEN(uri) <= MAX_ICE_SERVER_URI_STR_LEN(3 为引号和逗号额外字符) - 超出则停止追加或截断
- 每次追加前判断:
encodedIceConfig累计长度检查:- 判断:
iceConfigLen + MAX_ICE_SERVER_INFO_STR_LEN <= MAX_ENCODED_ICE_SERVER_INFOS_STR_LEN - 超出则不打包
IceServerList(Offer 不带该字段仍可工作)
- 判断:
- 保持在有界拷贝后显式添加
'\0'(已在样例中应用)
经验总结
- 固定大小的栈数组必须在运行时对聚合数量做上限保护,尤其是当数据可能在某个时刻集中变为有效状态时(如 ICE 候选)。
- 代码注释的设计约束(“每次最多报告 N 个”)必须由实际代码逻辑保证,而非仅停留在说明。
- 使用系统库时可能出现符号差异(libwebsockets 缺函数),需添加兼容实现以保证可构建与可运行。
- ASan 是定位内存越界的高效工具,建议作为本地调试与 CI 的常设选项之一。
关键常量与尺寸(摘录)
KVS_ICE_MAX_NEW_LOCAL_CANDIDATES_TO_REPORT_AT_ONCE = 10(每次回调最多报告的本地候选数量)MAX_SDP_ATTRIBUTE_VALUE_LENGTH = 512(候选 SDP 字符串长度上限)MAX_ICE_CONFIG_URI_LEN = 127,MAX_ICE_CONFIG_URI_COUNT = 4MAX_SIGNALING_MESSAGE_LEN = 18750LWS_MESSAGE_BUFFER_SIZE = SIZEOF(CHAR) * (MAX_SIGNALING_MESSAGE_LEN + LWS_PRE)
构建与运行示例
- 使用系统依赖构建:
rm -rf build && mkdir build && cd buildcmake .. -DBUILD_DEPENDENCIES=OFF -DUSE_OPENSSL=ONmake -j$(nproc)
- 启用 ASan 验证:
cmake .. -DCMAKE_BUILD_TYPE=Debug -DADDRESS_SANITIZER=ON -DCOMPILER_WARNINGS=ONmake -j$(nproc)./samples/kvsWebrtcClientMaster <ChannelName>
变更摘要
IceAgent.c:为newLocalCandidates复制加上限保护,避免栈数组越界。LwsApiCalls.c:修正空 URIs 列表尾逗号处理;添加本地parse_http_date_unix替换缺失符号。samples/Common.c:拷贝候选 JSON 后显式'\0'终止,避免读取越界。
