Android、Java和Python中的TimSort错误
1.1在Java中重现TimSort错误
1.2 TimSort如何工作(原则上)?
1.3 TimSort错误的演练 证明TimSort的正确性
2.1验证系统KeY
2.2修复及其正式规范
2.3分析KeY的产量 建议修复Python和Android/Java Timsort错误
3.1错误的Python merge_collapse函数
3.2修正的Python merge_collapse函数
3.3 Java/Android merge_collapse函数不正确
3.4修正的Java/Android merge_collapse函数 结论-我们可以学到什么?
1.Android、Java和Python中的TimSort错误
1.1在Java中重现TimSort错误
git克隆 https://github.com/abtools/java-timsort-bug.git 光盘java-timsort-bug javac*.java语言 java测试TimSort 67108864
线程“main”java.lang.ArrayIndexOutOfBoundsException异常:40 位于java.util。 推送运行(TimSort.java:413) 在java.util。 排序(TimSort.java:240) 位于java.util。 Arrays.sort(Arrays.java:1438) 位于TestTimSort.main(TestTimSport.java:18)
1.2 TimSort如何工作(原则上)?
runLen(运行长度) [n-2] runLen(运行长度) [n-1]+ runLen(运行长度) 【n】 runLen(运行长度) [n-1]> runLen(运行长度) 【n】
1.3 TimSort错误的演练
私有的 空隙 合并折叠(){ 虽然 (堆栈大小>1){ int n=堆栈大小-2; 如果 (n>0&&runLen[n-1]<=runLen[n]+runLen[n+1]){ 如果 (运行长度[n-1]<运行长度[n+1]) n--; 合并At(n); } 其他的 如果 (runLen[n]<=运行长度[n+1]){ 合并At(n); } 其他的 { 中断;// 已建立不变量 } } }
120, 80, 25, 20, 30
120, 80, 45, 30
2.证明TimSort的(in)正确性
2.1验证系统KeY
/*@ 私有不变量 @runBase.length==runLen.length&&runBase!= runLen; @*/
/*@ 私有normal_behavior @ 需要 @n>=最小值; @ 确保 @\结果>=最小值/2; @*/ 私有的 静止的 整数/*@ 纯净的 @*/最小运行长度(int n){ 断言 n>=0; 整数r=0; //如果任何1位移位,则变为1 /*@ 循环_变量 n>=最小值/2&&r>=0&&r<=1; @ 减少 n; @ 可转让的 \什么都没有 ; @*/ 虽然 (n>=最小值){ r|=(n&1); n>>=1; } 返回 n+r; }
/*@ 私有不变量 @ ( \对于所有人 整数i; 0<=i&&i<堆叠尺寸-4; @运行长度[i]>运行长度[i+1]+运行长度[i+2])) @*/
2.2修复及其正式规范
/*@ 需要 @堆栈大小>0&& @runLen[stackSize-4]>runLen[stackSize-3]+runLen-[stackSize-2] @&&runLen[stackSize-3]>runLen[stackSize-2]; @ 确保 @ ( \对于所有人 整数i; 0<=i&&i<stackSize-2; @运行长度[i]>运行长度[i+1]+运行长度[i+2]) @&&runLen[stackSize-2]>运行长度[stackSize-1] @*/ 私有的 空隙 合并折叠()
私有的 空隙 新建合并折叠(){ 虽然 (堆栈大小>1){ int n=堆叠大小-2; 如果 (n>0&&runLen[n-1]<=runLen[n]+runLen-[n+1]|| n-1>0&&runLen[n-2]<=运行长度[n]+运行长度[n-1]){ 如果 (运行时间[n-1]<运行时间[n+1]) n--; } 其他的 如果 (n<0||runLen[n]>runLen[n+1]){ 打破 ; // 已建立不变量 } 合并At(n); } }
/*@ 循环_变量 @ ( \对于所有人 整数i; 0<=i&&i<堆叠尺寸-4; @运行长度[i]>运行长度[i+1]+运行长度[i+2]) @&&runLen[stackSize-4]>运行长度[stackSize-3]) @*/
2.3分析KeY的产量
i<stackSize-4:来自循环不变量 i=stackSize-4:从n>1开始==>runLen[n-2]>runLen[n-1]+runLen[n] i=stackSize-3:从n>0=>runLen[n-1]>runLen[n]+runLen[n+1] i=stackSize-2:从n开始>=0==>runLen[n]>runLen[n+1]
3.建议修复Python和Android/Java Timsort错误
3.1错误的Python merge_collapse函数
/*MergeState的最大条目数 *pending运行堆栈。 *这足以对大小约为的数组进行排序 *32*phi**MAX_MERGE_PENDING(最大值) *其中φ=1.618。 85足够大了, *适用于阵列 带有2**64个元素。 */ #定义MAX_MERGE_PENDING 85 合并_折叠 (合并状态 * 毫秒) { 结构 s_slice(切片) * 第页 = 毫秒 -> 未决; 断言(ms); 虽然 (毫秒 -> n个 > 1 ) { Py_size_t编号 = 毫秒 -> n个 - 2 ; 如果 (n) > 0 && p[数字] -1 ].len(长度) <= p[n].长度 + p[数字] +1个 ].len){ 如果 (p[n -1 ].len(长度) < p[数字] +1个 ].len) -- n; 如果 (合并_ at(ms,n) < 0 ) 返回 -1 ; } 其他的 如果 (p[n].len <= p[数字] +1个 ].len){ 如果 (合并_ at(ms,n) < 0 ) 返回 -1 ; } 其他的 打破 ; } 返回 0 ; }
3.2修正的Python merge_collapse函数
合并_崩溃 (合并状态 * 毫秒) { 结构 s_slice(切片) * 第页 = 毫秒 -> 悬而未决的; 断言(ms); 虽然 (毫秒 -> n个 > 1 ) { Py_size_t编号 = 毫秒 -> n个 - 2 ; 如果 (n) > 0 && p[数字] -1 ].len(长度) <= p[n].长度 + p[数字] +1个 ].len(长度) ||(n-1>0&&p[n -2 ].len(长度) <= p[n].长度 + p[数字] -1 ].len)){ 如果 (p[n -1 ].len(长度) < p[数字] +1个 ].len) -- n; 如果 (合并_ at(ms,n) < 0 ) 返回 -1 ; } 其他的 如果 (p[n].len) <= p[数字] +1个 ].len){ 如果 (合并_ at(ms,n) < 0 ) 返回 -1 ; } 其他的 打破 ; } 返回 0 ; }
3.3 Java/Android merge_collapse函数不正确
私有void mergeCollapse(){ while(堆栈大小>1){ int n=堆栈大小-2; 如果(n>0&&runLen[n-1]<=runLen[n]+runLen-[n+1]) { if(运行长度[n-1]<运行长度[n+1]) n--; 合并At(n); }else if(runLen[n]<=runLen[n+1]){ 合并At(n); }其他{ 中断;// 已建立不变量 } } }
3.4修正的Java/Android merge_collapse函数
private void newMergeCollapse(){ while(堆栈大小>1){ int n=堆栈大小-2; if((n>=1&&runLen[n-1]<=runLen[n]+runLen-[n+1]) ||(n>=2&&runLen[n-2]<=runLen[n]+runLen-1]) { if(运行长度[n-1]<运行长度[n+1]) n--; }else if(runLen[n]>运行长度[n+1]){ 中断;// 已建立不变量 } 合并At(n); } }
4.结论-我们可以学到什么
正式方法通常被从业者归类为不相关和/或不可行。 事实并非如此:我们发现并修复了每天数十亿用户使用的软件中的一个错误。 如我们的分析所示,在没有正式分析和验证工具帮助的情况下,几乎不可能找到并修复此错误。 它在Java和Python的核心库例程中已经存在多年了。 早期出现的潜在错误本应得到修复,但实际上只会降低其发生的可能性。 尽管该错误本身不太可能发生,但很容易看出它如何用于攻击。 主流编程语言核心库的其他部分可能存在更多未检测到的错误。 难道我们不应该在它们造成伤害或被利用之前找到它们吗? Java开发人员社区对我们的报告的反应有些令人失望:他们没有使用我们的mergeCollapse()的固定版本(并且经过验证!),而是选择“充分”增加分配的runLen。 如我们所示,这是不必要的。 因此,无论是谁使用java.utils。 Collection.sort()被强制过度分配空间。 考虑到使用这样一个中心例程的程序运行数量之大,这将导致相当大的能量浪费。至于我们的解决方案没有被采用的原因,我们只能猜测:也许JDK维护人员没有仔细阅读我们的报告,因此不信任和理解我们的修复。 毕竟,开放Java是一项社区工作,主要由时间有限的志愿者推动。