C++ std::map 崩溃排查:operator< 违反严格弱序导致红黑树损坏
C++ std::map 崩溃排查:operator< 违反严格弱序导致红黑树损坏
问题背景
在生产环境遇到一个偶发崩溃,崩溃日志如下:
Jul 3 11:44:20.416814 cips_app@npt1250[1418:3649]: trace::Manager E Segmentation fault in cips_app: code=0x1, addr=0x4, pc=0xf657ab0
完整的调用栈信息:
Thread stack trace:
#F 0x0f657ab0 <private>
#H unknown
#P magnum/unknown
#0 0x14da3228 process::Signals::MakeBacktrace()+84
#1 0x14da1e30 CoreSigHandler+716
#2 0x001003a0 __kernel_sigtramp_rt32+0
#3 0x0e8f8a60 _ZNSt16allocator_traitsISaISt13_Rb_tree_nodeISt4pairIK20dpc_lif_arpeep_key_tjEEEE12_S_constructIS5_IRKSt21piecewise_construct_tSt5tupleIIRS3_EESC_IIEEEEENSt9enable_ifIXsrNS7_18__construct_helperIT_IDpT0_EEE5valueEvE4typeERS6_PSI_DpOSJ_+156
#4 0x0e8f7cfc std::_Rb_tree<dpc_lif_arpeep_key_t, std::pair<dpc_lif_arpeep_key_t const, unsigned int>, std::_Select1st<std::pair<dpc_lif_arpeep_key_t const, unsigned int> >, std::less<dpc_lif_arpeep_key_t>, std::allocator<std::pair<dpc_lif_arpeep_key_t const, unsigned int> > >::_M_get_insert_unique_pos(dpc_lif_arpeep_key_t const&)+456
#5 0x0e8f6404 std::_Rb_tree<dpc_lif_arpeep_key_t, std::pair<dpc_lif_arpeep_key_t const, unsigned int>, std::_Select1st<std::pair<dpc_lif_arpeep_key_t const, unsigned int> >, std::less<dpc_lif_arpeep_key_t>, std::allocator<std::pair<dpc_lif_arpeep_key_t const, unsigned int> > >::_M_get_insert_hint_unique_pos(std::_Rb_tree_const_iterator<std::pair<dpc_lif_arpeep_key_t const, unsigned int> >, dpc_lif_arpeep_key_t const&)+368
#6 0x0e8f5424 std::_Rb_tree_iterator<std::pair<dpc_lif_arpeep_key_t const, unsigned int> > std::_Rb_tree<dpc_lif_arpeep_key_t, std::pair<dpc_lif_arpeep_key_t const, unsigned int>, std::_Select1st<std::pair<dpc_lif_arpeep_key_t const, unsigned int> >, std::less<dpc_lif_arpeep_key_t>, std::allocator<std::pair<dpc_lif_arpeep_key_t const, unsigned int> > >::_M_emplace_hint_unique<std::piecewise_construct_t const&, std::tuple<dpc_lif_arpeep_key_t const&>, std::tuple<> >(std::_Rb_tree_const_iterator<std::pair<dpc_lif_arpeep_key_t const, unsigned int> >, std::piecewise_construct_t const&, std::tuple<dpc_lif_arpeep_key_t const&>&&, std::tuple<>&&)+240
#7 0x0e8f4ab4 std::map<dpc_lif_arpeep_key_t, unsigned int, std::less<dpc_lif_arpeep_key_t>, std::allocator<std::pair<dpc_lif_arpeep_key_t const, unsigned int> > >::operator[](dpc_lif_arpeep_key_t const&)+388
#8 0x0e8ece30 dpc_lif_arpeep_follow_lup_patch_(dpc_lif_arpeep_t*)+6272
#9 0x12139268 _dpc_lif_arpeep_ctor(void*, void*, void*, void*)+1276
#10 0x1485aef8 __osmCreateObject+620
#11 0x1485b230 osmCreateObject3+52
#12 0x144f90ec odata_create_obj3+88
#13 0x121363dc dpc_lif_arpeep_create(dpc_lif_arpeep_key_t*, dpc_lif_arpeep_attr_t*, unsigned int*, unsigned int*, unsigned int)+1420
#14 0x11df7740 _dpc_lif_arp_add_dps(COsmArpObject*)+1428
#15 0x11df8348 _dpc_lif_arp_add(dpc_lif_arp_key_t*, dpc_lif_arp_attr_t*)+956
#16 0x11dfac70 dpc_lif_add_arp+3608
#17 0x0e8f3a54 dpc_lif_arp_udpate_out_info_lup_patch_+6668
#18 0x1305b20c dpif_intf_config_lifarp_entry_outinfo_edit_impl+376
#19 0x14c3b668 <private>
#20 0x11a2aa04 <private>
#21 0x11a2b7e4 <private>
#22 0x11a2cbf8 _xmsg_ss_disp_acts_commit+1644
#23 0x119e8c68 <private>
#24 0x119ea078 _xmsg_ss_candi_commit+944
#25 0x119f54b0 _xmsg_ss_stub_invoke0_insafe+5904
#26 0x11a2aad0 <private>
#27 0x11a2b7e4 <private>
#28 0x11a2cbf8 _xmsg_ss_disp_acts_commit+1644
#29 0x119e8c68 <private>
#30 0x119ea36c _xmsg_ss_candi_sync_commit+152
#31 0x1199dac4 __xmsg_dpkg_smf_thread+2744
#32 0x11aaf410 <private>
#33 0x0fdd7c3c <private>
崩溃地址 addr=0x4 表示访问了空指针 + 偏移 4,这是典型的红黑树节点损坏导致的空指针解引用。
崩溃代码定位
崩溃发生在以下代码:
int dpc_lif_arpeep_follow_lup_patch_(dpc_lif_arpeep_t *eep_entry)
{
// ...
if (CIPS_SPEC_IS_DPP())
{
struct dpcc_lif_cfg lif_cfg = {0};
dpc_call_gt(dpcc_lif_cfg_get(eep_entry->attr.out_oidx, &lif_cfg));
dpc_call_gt(dpcc_intf_operstatus_follow(lif_cfg.intf_oidx,
(dpcc_intf_operstatus_follow_cb)dpc_lif_arp_dpcc_intf_operstatus_cb,
&eep_entry->key, sizeof(dpc_lif_arpeep_key_t)));
g_map_arpeep_key_to_intf_oidx[eep_entry->key] = lif_cfg.intf_oidx; // ← 崩溃行
}
// ...
}
根因分析
自定义比较函数
dpc_lif_arpeep_key_t 作为 std::map 的 key,需要自定义比较函数:
bool operator<(const dpc_lif_arpeep_key_t& k1, const dpc_lif_arpeep_key_t& k2)
{
if (k1.type < k2.type)
{
return true;
}
else
{
if (k1.arp_key.lif_oidx < k2.arp_key.lif_oidx)
{
return true;
}
else
{
if (memcmp(&k1.arp_key.ip, &k2.arp_key.ip, sizeof(dp_ip_key_t)) < 0)
{
return true;
}
else
{
return false;
}
}
}
}
逻辑缺陷
这个比较函数存在严重的逻辑错误:
当 k1.type > k2.type 时,没有返回 false,而是继续比较 lif_oidx。同理,当 k1.arp_key.lif_oidx > k2.arp_key.lif_oidx 时,也没有返回 false,而是继续比较 ip。
违反严格弱序
构造反例验证:
设 k1 = {type=2, lif_oidx=1}, k2 = {type=1, lif_oidx=2}:
k1 < k2: type(2) < type(1)? No → lif_oidx(1) < lif_oidx(2)? Yes → return true
k2 < k1: type(1) < type(2)? Yes → return true
结果:k1 < k2 和 k2 < k1 同时为 true,违反了反对称性(antisymmetry)。
为什么错误的比较函数会破坏红黑树结构
红黑树的插入过程依赖于比较函数来确定新节点的插入位置和执行平衡操作。当比较函数违反严格弱序时,会导致以下问题:
1. 比较函数在红黑树中的作用
红黑树在插入新节点时执行以下步骤:
步骤1:从根节点开始,用比较函数决定向左还是向右遍历
- 如果 key < node->key,向左子树遍历
- 如果 key >= node->key,向右子树遍历
步骤2:找到插入位置后,插入新节点
步骤3:执行旋转和重新着色,维护红黑树不变量:
- 节点只有红/黑两色
- 根是黑色
- 红色节点的子节点必须是黑色
- 从任一节点到其叶子的路径包含相同数量的黑色节点
2. 错误比较函数导致的问题
当比较函数违反反对称性时(即 a < b 和 b < a 同时为 true),会产生逻辑矛盾:
设 k1 = {type=2, lif_oidx=1}, k2 = {type=1, lif_oidx=2}
使用错误的比较函数:
k1 < k2: type(2) < type(1)? No → lif_oidx(1) < lif_oidx(2)? Yes → return true
k2 < k1: type(1) < type(2)? Yes → return true
结果:k1 < k2 和 k2 < k1 同时为 true
这导致:
- 插入路径不一致:根据插入时的比较方向,同一对 key 可能被判定为不同的相对位置
- 树结构错乱:节点的左右子树关系与实际比较结果不匹配
3. 树结构损坏的具体过程
假设按以下顺序插入 key:
步骤1:插入 k1 = {type=1, lif_oidx=1}
- 树为空,k1 成为根节点
步骤2:插入 k2 = {type=2, lif_oidx=2}
- 比较 k2 < k1:type(2) < type(1)? No → lif_oidx(2) < lif_oidx(1)? No → false
- 比较 k1 < k2:type(1) < type(2)? Yes → true
- 结论:k1 < k2,k2 插入到 k1 的右侧
步骤3:插入 k3 = {type=1, lif_oidx=2}
- 比较 k3 < k1:type(1) < type(1)? No → lif_oidx(2) < lif_oidx(1)? No → false
- 比较 k1 < k3:type(1) < type(1)? No → lif_oidx(2) < lif_oidx(1)? No → false
- 结论:k1 和 k3 不可比,继续遍历到 k2
- 比较 k3 < k2:type(1) < type(2)? Yes → true
- 结论:k3 < k2,插入到 k2 的左侧
此时树结构:
k1
\
k2
/
k3
步骤4:插入 k4 = {type=2, lif_oidx=1}
- 比较 k4 < k1:type(2) < type(1)? No → lif_oidx(1) < lif_oidx(1)? No → false
- 比较 k1 < k4:type(1) < type(2)? Yes → true
- 结论:k1 < k4,向右遍历到 k2
- 比较 k4 < k2:type(2) < type(2)? No → lif_oidx(1) < lif_oidx(2)? Yes → true
- 结论:k4 < k2,插入到 k2 的左侧
但此时 k2 的左侧已经有 k3,需要比较 k4 < k3:
- 比较 k4 < k3:type(2) < type(1)? No → lif_oidx(1) < lif_oidx(2)? Yes → true
- 结论:k4 < k3,k4 应该是 k3 的左子节点
但反向比较 k3 < k4:
- 比较 k3 < k4:type(1) < type(2)? Yes → true
- 结论:k3 < k4 也为 true
此时出现了矛盾:k4 < k3 和 k3 < k4 同时为 true,违反反对称性。
这种矛盾导致:
- 树的左右关系与比较结果不一致
- 某些节点可能被"遗漏"(无法通过比较找到)
- 某些节点可能被"重复"(多条路径可达)
4. 结构损坏导致崩溃
当树结构损坏后,节点的指针可能指向无效内存。后续操作如查找、插入、删除时:
- 访问
node->left或node->right指针 - 如果这些指针指向
0x4(空指针 + 偏移),解引用时触发 segfault - 崩溃栈显示:
_M_get_insert_unique_pos→ 访问节点指针字段(偏移 +4)
红黑树损坏过程
std::map 底层使用红黑树,其正确性依赖于比较函数满足严格弱序(Strict Weak Ordering)的三个性质:
| 性质 | 定义 | 违反后果 |
|---|---|---|
| 反对称性 | 若 a < b 为真,则 b < a 必须为假 |
树中可能出现循环依赖 |
| 传递性 | 若 a < b 且 b < c,则 a < c 必须为真 |
查找路径不一致 |
| 不可比性传递 | 若 a 与 b 不可比,b 与 c 不可比,则 a 与 c 不可比 |
等价类划分错误 |
当比较函数违反这些性质时:
- 红黑树在插入节点时,基于错误的比较结果决定插入位置
- 多次插入后,树结构逐渐损坏(节点指针指向无效内存)
- 遍历损坏的树时,访问空节点的指针字段(偏移 +4),触发 segfault
修复方案
正确的字典序比较
修复后的比较函数:
/**
* Strict weak ordering comparison operator for dpc_lif_arpeep_key_t.
*
* Structure definitions:
* typedef struct {
* euint16 iptype;
* euint16 mask;
* euint8 ip[DP_IP_MAX_LEN]; // DP_IP_MAX_LEN = 16
* } dp_ip_key_t;
*
* typedef struct {
* dpc_arp_type_e type;
* union {
* struct dpc_lif_arpeep_key_type_host _host;
* struct dpc_lif_arpeep_key_type_nh _nh;
* // ... other union members
* } u0;
* dpc_lif_arp_key_t arp_key; // contains lif_oidx and ip (dp_ip_key_t)
* } dpc_lif_arpeep_key_t;
*
* This implementation satisfies the strict weak ordering requirements:
* 1. Irreflexivity: !(a < a)
* 2. Asymmetry: if a < b, then !(b < a)
* 3. Transitivity: if a < b and b < c, then a < c
* 4. Transitivity of equivalence: if a is equivalent to b and b is equivalent to c, then a is equivalent to c
*
* The comparison uses lexicographic ordering on the key fields:
* - First compares 'type' field
* - Then compares 'arp_key.lif_oidx' field
* - Finally compares 'arp_key.ip' field using memcmp
*
* Note: The 'u0' union field is intentionally excluded from comparison as per service logic requirements.
* Currently, the union field 'u0' does not need to participate in the comparison based on current service logic.
* The dp_ip_key_t structure (2 bytes iptype + 2 bytes mask + 16 bytes ip = 20 bytes) has no padding,
* so memcmp comparison is safe and consistent.
*/
bool operator<(const dpc_lif_arpeep_key_t& k1, const dpc_lif_arpeep_key_t& k2)
{
if (k1.type != k2.type)
return k1.type < k2.type;
if (k1.arp_key.lif_oidx != k2.arp_key.lif_oidx)
return k1.arp_key.lif_oidx < k2.arp_key.lif_oidx;
return memcmp(&k1.arp_key.ip, &k2.arp_key.ip, sizeof(dp_ip_key_t)) < 0;
}
关键改进:
- 高优先级字段不等时立即返回,不再穿透到低优先级字段
- 实现标准的字典序(lexicographical)比较
- 满足严格弱序的所有性质
为什么不能用 memcmp 比较整个结构体
dpc_lif_arpeep_key_t 包含 union 和可能的填充字节:
typedef struct {
dpc_arp_type_e type;
union {
struct dpc_lif_arpeep_key_type_host _host;
struct dpc_lif_arpeep_key_type_nh _nh;
// ...
} u0;
dpc_lif_arp_key_t arp_key;
} dpc_lif_arpeep_key_t;
填充字节(Padding)问题
编译器为了内存对齐,会在结构体成员之间插入未使用的字节。例如:
struct Example {
char a; // 1 字节,偏移 0
int b; // 4 字节,需要 4 字节对齐
char c; // 1 字节
};
内存布局(假设 4 字节对齐):
偏移: 0 1 2 3 4 5 6 7 8 9 10 11
[a][X][X][X][b.......b][c][X][X][X]
↑ ↑ ↑
偏移0 偏移4 偏移8
a占 1 字节(偏移 0)- 填充 3 字节(偏移 1-3),使
b对齐到偏移 4 b占 4 字节(偏移 4-7)c占 1 字节(偏移 8)- 填充 3 字节(偏移 9-11),使整个结构体大小为 12 字节(4 的倍数)
为什么填充字节不能用 memcmp 比较
struct Example x = {1, 100, 2};
struct Example y = {1, 100, 2};
// 填充字节的值是未定义的
// x 的填充字节可能是 0x00, y 的填充字节可能是 0xCC
memcmp(&x, &y, sizeof(Example)); // 可能返回非零!
填充字节包含随机垃圾值,导致:
- 逻辑相等的对象比较结果不一致
- 同一对象与自身比较可能返回非零
- 违反比较函数的自反性要求:
!(a < a)必须为真
dpc_lif_arpeep_key_t 的具体情况
typedef struct {
dpc_arp_type_e type; // 4 字节(枚举)
union { ... } u0; // 大小取决于最大成员
dpc_lif_arp_key_t arp_key; // 结构体
} dpc_lif_arpeep_key_t;
潜在问题:
- union 填充:union 和
arp_key之间可能有填充字节 - union 内部填充:union 内部结构体也可能有填充
- 未使用的 union 字段:union 中未激活的成员包含未初始化的垃圾数据
- arp_key 填充:
dpc_lif_arp_key_t内部也可能有填充
比较顺序问题
即使没有填充,memcmp 按内存布局顺序比较:
内存布局顺序:[type][u0 填充][u0 数据][arp_key 填充][arp_key.lif_oidx][arp_key.ip]
业务期望顺序:type → lif_oidx → ip (忽略 u0)
如果内存布局顺序与业务期望不一致,会导致排序错误。
正确做法
字段逐个比较,明确比较逻辑,不受内存布局影响:
bool operator<(const dpc_lif_arpeep_key_t& k1, const dpc_lif_arpeep_key_t& k2)
{
if (k1.type != k2.type)
return k1.type < k2.type;
if (k1.arp_key.lif_oidx != k2.arp_key.lif_oidx)
return k1.arp_key.lif_oidx < k2.arp_key.lif_oidx;
return memcmp(&k1.arp_key.ip, &k2.arp_key.ip, sizeof(dp_ip_key_t)) < 0;
}
注意:这里只比较 arp_key.ip 部分,不比较整个 dpc_lif_arpeep_key_t。dp_ip_key_t 定义如下,没有填充:
typedef struct {
euint16 iptype; // 2 字节
euint16 mask; // 2 字节
euint8 ip[DP_IP_MAX_LEN]; // 16 字节
} dp_ip_key_t;
总大小:2 + 2 + 16 = 20 字节,自然对齐,无填充,memcmp 是安全的。
如果需要比较 union 字段
如果业务逻辑要求比较 u0 的内容,需要根据 type 来判断使用哪个 union 成员,因为 union 中未激活的成员包含垃圾数据,不能直接 memcmp 整个 union。
// union 成员定义
struct dpc_lif_arpeep_key_type_host { euint32 rev; };
struct dpc_lif_arpeep_key_type_nh { euint32 rev; };
struct dpc_lif_arpeep_key_type_autobw { euint32 rsvpte_oidx; };
struct dpc_lif_arpeep_key_type_dynamic_autobw { euint32 rsvpte_oidx; };
// ... 其他类型
修改后的比较函数:
bool operator<(const dpc_lif_arpeep_key_t& k1, const dpc_lif_arpeep_key_t& k2)
{
// 1. 先比较 type
if (k1.type != k2.type)
return k1.type < k2.type;
// 2. 根据 type 比较 u0 中对应的字段
switch (k1.type) {
case DPC_ARP_TYPE_HOST:
case DPC_ARP_TYPE_NH:
case DPC_ARP_TYPE_DYNAMIC_LSP:
case DPC_ARP_TYPE_STATIC:
case DPC_ARP_TYPE_MPLSTP:
case DPC_ARP_TYPE_PRIVATE:
// 这些类型使用 rev 字段
if (k1.u0._host.rev != k2.u0._host.rev)
return k1.u0._host.rev < k2.u0._host.rev;
break;
case DPC_ARP_TYPE_AUTOBW:
case DPC_ARP_TYPE_DYNAMIC_AUTOBW:
// 这些类型使用 rsvpte_oidx 字段
if (k1.u0._autobw.rsvpte_oidx != k2.u0._autobw.rsvpte_oidx)
return k1.u0._autobw.rsvpte_oidx < k2.u0._autobw.rsvpte_oidx;
break;
default:
// 其他类型不比较 u0
break;
}
// 3. 比较 arp_key.lif_oidx
if (k1.arp_key.lif_oidx != k2.arp_key.lif_oidx)
return k1.arp_key.lif_oidx < k2.arp_key.lif_oidx;
// 4. 比较 arp_key.ip
return memcmp(&k1.arp_key.ip, &k2.arp_key.ip, sizeof(dp_ip_key_t)) < 0;
}
关键点:
- 不能 memcmp 整个 union:union 中未激活的成员包含垃圾数据
- 根据 type 判断字段:不同 type 激活不同的 union 成员
- 字段可能不同:
revvsrsvpte_oidx,需要分别处理 - 保持字典序:type → u0 字段 → lif_oidx → ip
难以复现的原因
需要特定操作序列
红黑树损坏是累积性的,需要插入交叉模式的 key 才会触发:
正常模式(不会触发):
- type=1, lif_oidx=1
- type=1, lif_oidx=2
- type=1, lif_oidx=3
交叉模式(会触发):
- type=1, lif_oidx=1
- type=2, lif_oidx=2
- type=1, lif_oidx=2 ← 交叉:type 小但 lif_oidx 大
- type=2, lif_oidx=1 ← 交叉:type 大但 lif_oidx 小
未定义行为
违反严格弱序是未定义行为,可能表现为:
- 偶发崩溃
- 数据丢失
- 查找失败
- 完全正常(但内部已损坏)
经验教训
- 自定义比较函数必须满足严格弱序:这是 C++ 标准的硬性要求,违反会导致未定义行为
- 字典序比较的正确写法:高优先级字段不等时立即返回,使用
!=判断而非< - 避免 memcmp 比较复杂结构体:特别是包含 union、填充字节的结构体
- 偶发崩溃更危险:难以复现不代表不存在问题,必须修复逻辑错误
- 压力测试的重要性:需要构造交叉模式的测试用例,而非单调递增序列
总结
这个 crash 的根本原因是 operator< 比较函数违反了 C++ 标准要求的严格弱序,导致 std::map 内部的红黑树结构损坏。修复方法是实现正确的字典序比较,高优先级字段不等时立即返回。即使测试环境难以复现,也应该修复这个逻辑错误,因为生产环境的复杂场景可能触发崩溃。
参考:
- C++ Standard: Strict Weak Ordering
- Effective C++: Item 46 - Define non-member functions when type conversions are desired
- cppreference: std::map::operator[]

浙公网安备 33010602011771号