Theme Preview

编译 OpenVPN 邂逅 ARM GCC bug

由 李晓岚 在 2017年05月07日发表

这几天折腾在 HG8120C 光猫(ONT)上运行 OpenVPN,历经千辛万苦编译成功了,结果运行中 Assert 失败,一路排查下来,最后发现居然是工具链 GCC 的错。这已经不是第一次遇到工具链的 bug了,第一次是 ARM ADS 乱序优化的 bug,第二次是 自己写的链接器,如果这也算的话。

发现

实现了 HG8120C 的持久 root shell 后,就在上面运行了 PPTP 服务,用于充当出门在外身处非安全网络环境中时使用的 VPN 网关。家庭网络中,本来 ONT 一般就是不断电的,而且 HG8120C 上内存和处理器资源也还算充足,所以拿来做 VPN 网关很节能环保。但是 PPTP 很多年前就已经是 不安全的协议 了。终于下定决心用 OpenVPN 替换 PPTP 了,可是过程并不平坦。

由于我还有 Windows XP 客户端,所以选择了最后支持 XP 的 OpenVPN v2.3.14 版本。几经周折总算是编译成功了,其中磕跘暂且不表。配置好服务起客户端,开始进行连接测试,当客户端连接成功后,服务器却显示出了错误的信息,紧接着就退出了,到底发生了什么?

Sat May  6 18:33:38 2017 banana/192.168.1.10:48029 Assertion failed at crypto.c:173 (buf_inc_len(&work, outlen))
Sat May  6 18:33:38 2017 banana/192.168.1.10:48029 Exiting due to fatal error
Sat May  6 18:33:38 2017 banana/192.168.1.10:48029 /sbin/route del -net 10.8.0.0 netmask 255.255.255.0
Sat May  6 18:33:38 2017 banana/192.168.1.10:48029 Closing TUN/TAP interface
Sat May  6 18:33:38 2017 banana/192.168.1.10:48029 /sbin/ifconfig tun0 0.0.0.0

Assertion failed at crypto.c:173 (buf_inc_len(&work, outlen)),很明显的断言失败信息。查看源代码 crypto.c #173

167   /* Encrypt packet ID, payload */
168   ASSERT (cipher_ctx_update (ctx->cipher, BPTR (&work), &outlen, BPTR (buf), BLEN (buf)));
169   ASSERT (buf_inc_len(&work, outlen));
170
171   /* Flush the encryption buffer */
172   ASSERT (cipher_ctx_final(ctx->cipher, BPTR (&work) + outlen, &outlen));
173   ASSERT (buf_inc_len(&work, outlen));
174
175   /* For all CBC mode ciphers, check the last block is complete */
176   ASSERT (cipher_kt_mode (cipher_kt) != OPENVPN_MODE_CBC ||
177       outlen == iv_size);

跟踪进 buf_inc_len 函数所在的 buffer.h

412 static inline bool
413 buf_safe_bidir (const struct buffer *buf, int len)
414 {
415   if (buf_valid (buf) && buf_size_valid_signed (len))
416     {
417       const int newlen = buf->len + len;
418       return newlen >= 0 && buf->offset + newlen <= buf->capacity;
419     }
420   else
421     return false;
422 }
...
461 static inline bool
462 buf_inc_len (struct buffer *buf, int inc)
463 {
464   if (!buf_safe_bidir (buf, inc))
465     return false;
466   buf->len += inc;
467   return true;
468 }

在这几个函数里面加入一些诊断信息的输出,试图找到是哪个条件不满足导致的断言失败。万万没想到的是,在输出了诊断信息后,并没有引发断言失败,看起来是 观测者效应 在作祟。由于有之前的两次经验,所以马上就想到可能我又遇上了第三次工具链的 bug。马上着手检查编译器生成的汇编代码(thumb 指令) crypto.S

5250    .loc 1 173 0            ; crypto.c #173
5251    ldr r3, [sp, #80]
5252 .LBB1011:
5253 .LBB1012:
5254 .LBB1013:
5255 .LBB1014:
5256    .loc 3 230 0            ; buffer.h #230
5257    ldr r2, .L474+28        ; -1000000
5258    cmp r3, r2
5259    bge .LCB4900
5260    b   .L428   @long jump
5261 .LCB4900:
5262    ldr r0, .L474+4         ; 999999
5263    cmp r3, r0
5264    ble .LCB4903
5265    b   .L428   @long jump
5266 .LCB4903:
5267 .LBE1014:
5268 .LBE1013:
5269 .LBB1015:
5270    .loc 3 418 0            ; buffer.h #418
5271    add r8, r8, r3
5272    bpl .LCB4909
5273    b   .L428   @long jump
5274 .LCB4909:
5275    mov r1, r8
5276    add r3, r7, r1
5277    cmp r9, r3
5278    bge .LCB4913
5279    b   .L428   @long jump

直勾勾地盯着上面的汇编代码看了好长时间,并没有发现乱序优化。那还会有什么原因呢?突然发现 5271 add r8, r8, r3 之后紧跟着 5272 bpl .LCB4909 条件跳转指令,而 add 指令并不带 s 后缀(这里的判断是误打误撞,其实 thumb 指令压根就没有 s 后缀,arm 指令才有),应该不会影响标志位,故其后的条件跳转指令就达不到预期目的,和这两条汇编指令对应的 C 代码是

417       const int newlen = buf->len + len; // r3 -> len, r8 -> buf->len
418       return newlen >= 0 && ...

啊哈,就是这里的 bug。

关于 s 后缀的误打误撞

thumb add 指令有多个种类,一类指令是只能访问到 low registers (r0-r7),这类 add 指令会更新对应标志位,而能够访问到 high registers 的这类 thumb add 指令,手册上明确说明不会更新标志位。而 r8 刚好是 high register,汇编器 as 便选择了能够访问 high register 的这类 add 指令,从而标志位没能得到有效更新,才导致了上述 bug。

解决(规避)方案

bug 是 gcc 工具链导致的,使用的版本是 gcc v4.4.7,最直接的办法是更换掉有问题的工具链,但是多方面的原因,无法评估换工具链的风险,也没能在 gcc issue tracker 里找到这样的问题报告,所以不会有简单修复 gcc 的办法。故只能另辟蹊径,规避掉这个 bug。r8 引起的 bug,而 r8 缓存的是 buf->len,禁用这个缓存就可以解决,最简单粗暴的方法就是将对应 bufvolatile 修饰,gcc 便不会将结果缓存进 r8 寄存器了。

总结

发生了 bug,虽然定位了问题的根源,但是一时之间还难以从根源上解决问题,只是投机取巧避而远之,更严重的是不知道是否还有其他地方存在这同样的问题,所以选择规避问题绝非正道,等后面有时间一定得从问题的根源上解决。

标签:GCCARMASMONTOpenVPN

comments powered by Disqus