Rust: 安全Rust中的内存不安全问题

创建于 2020-02-17  ·  38评论  ·  资料来源: rust-lang/rust

我有一个小程序(简化了较大项目中的测试功能),将一个小数组切成薄片,并尝试访问该切片的越界元素。 使用稳定的1.41.0版本以cargo run --release运行它会打印出以下内容(在macOS 10.15和Ubuntu 19.10上进行了测试):

0 0 3 18446744073709551615
[1]    21065 segmentation fault  cargo run --release

看起来所得的切片以某种方式具有2**64 - 1长度,因此省略了边界检查,这可以预期地导致段错误。 在1.39.01.40.0 ,相同的程序会打印出我期望的样子:

0 0 3 0
thread 'main' panicked at 'index out of bounds: the len is 0 but the index is 16777216', src/main.rs:13:35
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

如果我执行以下任一操作,问题将消失:

  • 删除main()中的两个do_test(...);调用中的一个;
  • 删除for _ in 0..1 {循环;
  • for y in 0..x {循环替换for y in 0..1 { ;
  • 删除z.extend(std::iter::repeat(0).take(x));行或将其替换为z.extend(std::iter::repeat(0).take(1)); ;
  • for arr_ref in arr {循环替换let arr_ref = &arr[0]; ;
  • 指定RUSTFLAGS="-C opt-level=2" ;
  • 指定RUSTFLAGS="-C codegen-units=1"

我最好的猜测是-C opt-level=3在LLVM中启用了一个有问题的优化过程,从而导致编译错误。 对于-C opt-level=2-C opt-level=3 ,优化前的MIR( --emit mir )和LLVM IR( --emit llvm-ir -C no-prepopulate-passes )是相同的,这一事实得到了证实。

一些其他信息可能会有所帮助:

  • 我无法在Rust游乐场重现该问题(大概是因为它使用了codegen-units = 1 );
  • 我无法在具有相同1.41.0版本的Windows 10上重现该问题(不知道是什么使它与众不同);
  • cargo-bisect-rustc说回归首先发生在每晚2019-12-12 ,特别是在此commit中。 鉴于没有出现问题的1.40.0在此日期之后被释放,这对我来说似乎很可疑。

如果GitHub存储库无法正常工作,我会内联该程序(如果要在没有Cargo的情况下进行编译,请使用rustc -C opt-level=3 main.rs ):

fn do_test(x: usize) {
    let arr = vec![vec![0u8; 3]];

    let mut z = Vec::new();
    for arr_ref in arr {
        for y in 0..x {
            for _ in 0..1 {
                z.extend(std::iter::repeat(0).take(x));
                let a = y * x;
                let b = (y + 1) * x - 1;
                let slice = &arr_ref[a..b];
                eprintln!("{} {} {} {}", a, b, arr_ref.len(), slice.len());
                eprintln!("{:?}", slice[1 << 24]);
            }
        }
    }
}

fn main() {
    do_test(1);
    do_test(2);
}
A-LLVM C-bug I-unsound 💥 ICEBreaker-LLVM P-medium T-compiler regression-from-stable-to-stable

最有用的评论

LLVM IR复制器: https :

运行opt bconfused.ll -scalar-evolution -loop-idiom -scalar-evolution -indvars -S -O3 -o - | grep xprint 。 如果括号的内部是i64 -1 ,则会发生越野车优化。 如果不是,它可能没有,但是很难确定。

它似乎是由于LLVM在归纳变量简化过程中错误地将nuwadd nuw i64 %x, -1而引起的。 x是该函数的参数,而nuw表示没有无符号换行,因此这有效地断言了该参数在函数中不保证为0的位置为0。

平分(编辑:从LLVM 9到LLVM 10, @ tmiasko表示不受影响)产生此提交:

commit 58e8c793d0e43150a6452e971a32d7407a8a7401
Author: Tim Northover <[email protected]>
Date:   Mon Sep 30 07:46:52 2019 +0000

    Revert "[SCEV] add no wrap flag for SCEVAddExpr."

    This reverts r366419 because the analysis performed is within the context of
    the loop and it's only valid to add wrapping flags to "global" expressions if
    they're always correct.

    llvm-svn: 373184

看起来很有希望,因为LLVM 9.0分支Rust使用了r366419(上述提交还原的提交)。

所有38条评论

抄送@ rust-lang / compiler
@rustbot ping icebreakers-llvm

发布团队正在考虑针对Rust 1.41发布一个重要版本(我们在上周的会议中进行了简要讨论),如果我们能够尽快获得PR,我希望将其包含在其中。

嘿LLVM破冰器! 这个错误已被确认为是一个好
“突破LLVM ICE的候选人”。 如果有用,这里有一些
解决此类错误的[说明]。 也许看看?
谢谢! <3

cc @comex @DutchGhost @ hanna-kruppe @hdhoang @heyrutvik @ JOE1994 @jryans @mmilenko @nagisa @nikic @ Noah-Kennedy @SiavoshZarrasvand @spastorino @vertexclique @vgxbj

使用稳定运行1.41.0的发行版运行--release可以打印出类似的内容(在macOS 10.15和Ubuntu 19.10上测试):

不能在操场上复制它。 程序在1.41.0的发布模式下可以正常工作。

编辑:啊,你已经说过了。
该程序在Miri中也很好,因此这可能不是UB,而是编译错误。

只是添加一个数据点,我可以在最新的每晚报告中在Linux上重现它:

[andrew<strong i="6">@krusty</strong> rust-69225]$ rustc --version
rustc 1.43.0-nightly (5e7af4669 2020-02-16)

[andrew<strong i="7">@krusty</strong> rust-69225]$ cat main.rs
fn do_test(x: usize) {
    let arr = vec![vec![0u8; 3]];

    let mut z = Vec::new();
    for arr_ref in arr {
        for y in 0..x {
            for _ in 0..1 {
                z.extend(std::iter::repeat(0).take(x));
                let a = y * x;
                let b = (y + 1) * x - 1;
                let slice = &arr_ref[a..b];
                eprintln!("{} {} {} {}", a, b, arr_ref.len(), slice.len());
                eprintln!("{:?}", slice[1 << 24]);
            }
        }
    }
}

fn main() {
    do_test(1);
    do_test(2);
}

[andrew<strong i="8">@krusty</strong> rust-69225]$ rustc -C opt-level=3 main.rs

[andrew<strong i="9">@krusty</strong> rust-69225]$ ./main
0 0 3 18446744073709551615
zsh: segmentation fault (core dumped)  ./main

我能够用Rust 1.41 stable完全相同的输出重现以上内容。 Rust 1.40稳定版不会出现此问题:

$ ./main
0 0 3 0
thread 'main' panicked at 'index out of bounds: the len is 0 but the index is 16777216', main.rs:13:35
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

我认为这与@dfyz的报告完全一致,只是至少可以确认它不是特定于macOS的。

  • cargo-bisect-rustc说,回归首先发生在2019-12-12每晚,特别是在此commit中。 鉴于没有出现问题的1.40.0在此日期之后被释放,这对我来说似乎很可疑。

这是预期的。 1.40.0于2019-12-19发行,当时是基于beta分支的分支,该分支是从六周前的master分支出来的(大约在1.39.0版本发布之时)。 有关发布渠道的更多信息,请参见https://doc.rust-lang.org/book/appendix-07-nightly-rust.html

如果我不得不猜测,我会说https://github.com/rust-lang/rust/pull/67015可能是罪魁祸首。 此修复了3个代码生成问题,因此涉及到了关键代码生成代码。
抄送@ osa1 @ oli -obk

感谢您的ping。 我目前正在制作要调查的文件。 这可能是#67015引入的错误。 另一个可能是#67015刚刚发现了一个现有的错误。

我设法在Linux上每晚使用rustc重现segfault,但没有使用此config.toml生成的构建:

config.toml

[llvm]

[build]

[install]

[rust]
optimize = true
debug = true
codegen-units = 0
debug-assertions = true
debuginfo-level = 2

[target.x86_64-unknown-linux-gnu]
llvm-config = "/usr/bin/llvm-config-9"

[dist]

我每晚使用rustc检查ConstProp前后的MIR,并且MIR相同。 因此,如果这是由ConstProp引起的,那是由于库而不是该程序的生成代码中的差异引起的。

033662dfbca088937b9cdfd3d9584015b5e375b2中的回归

@rustbot修改标签:-E-needs-


@ osa1 debug-assertions = true可能是罪魁祸首。 当我尝试使用-C debug-assertions=y编译(使用普通夜间编译器)程序时,程序会慌张而不是segfault

我想我解决了! 还原a983e0590a43ed8b0f60417828efd4e79b51f494可解决此问题。 这似乎是我整天的罪魁祸首,但是我无法在工作中对其进行测试:)有人可以帮助我如何最好地针对此问题进行公关吗? 我认为最好的方法是添加一个必须失败的测试用例,但这似乎是特定于平台的,所以毕竟这不是一个好主意吗? 有什么想法吗? 谢谢!

(这已经被OP一分为二)

我设法在关闭debug-assertions的本地版本中重现了这一点(感谢@ hellow554)。

汇总中的某些PR在还原时会导致冲突,因为从那时起,我们已经rustfmt -ed一切,但我相信此问题是由于#67174引起的。

编辑:似乎我们同时发现了这个@shahn :)

@lqd是的,这是包含我上面引用的提交的问题。 那是元凶。

要添加另一个数据点,当codegen-units设置为3或更少时(假设发布配置文件为incremental=false ),问题消失了。 换句话说,当codegen-units为4或更大时,我能够重现。

LLVM跳转线程通过后,对panic_bounds_check的调用消失。 我可以从LLVM 9重现opt的问题,但不能从LLVM 10重现。

因此,我签出并构建了第1阶段rustc./x.py build -i --stage 1 ),在没有#67174的情况下重建了libstd./x.py build -i --stage 1 --keep-stage 0 src/libstd ),并使用编译了segfaulting程序四个代码单元( rustc +stage1 -C opt-level=3 -C codegen-units=4 main.rs )。 不出所料,这使段错误消失了。 如果我再次应用#67174,则段错误返回。

这意味着我现在有两个编译器,只是它们使用的标准库不同。 让我们将这些编译器称为GOOD (无segfault)和BAD (segfault)。

然后我注意到,4 _unoptimized_ *.ll通过生成的文件GOOD-C no-prepopulate-passes )是几乎相同的,通过所产生的那些BAD (唯一的区别我看到函数名称中有不同的随机ID),但是_optimized_ *.ll文件(没有-C no-prepopulate-passes )有很大的不同。 我无论如何都不是编译专家,但是由于两种情况下正在编译的程序完全相同,因此两种合适的编译器都完全相同,唯一的区别在于预编译的标准库中,我可能会涉及到LTO 。

确实,如果我通过了-Z thinlto=no-C lto=no-C lto=yes-C lto=thin (目前我不确定,但是我猜所有形式-C lto中的(与ThinLTO(默认情况下使用))到BAD有所不同,段错误再次消失。

LTO在这里似乎有问题吗?

我已经阅读了测试用例。 我已经阅读了正在还原的提交。 我仍然不知道发生了什么。 什么坏了?

什么坏了?

我不相信在这一点上任何人都可以肯定地说出到底是什么破了,但是我的初步分析是这样(请注意以下内容,我不在Rust团队或LLVM团队中,可以做的就是修改编译器,并盯着LLVM IR):

  • 我们从标准库的Layout::repeat()一行中删除了溢出检查,最终导致了这种内存不安全的情况。 从数学上讲,在此处使用未经检查的加法应该是绝对安全的-此函数中的注释(以及Layout::pad_to_align()的注释)说明了原因;
  • 我的代码示例演示了该问题,甚至没有调用此函数,但是它显式地使用Vec ,它隐式地使用Vec::reserve_internal() ,而后者依次调用Layout::repeat()
  • Layout::repeat()被标记为#[inline] ,显然GOODBAD之间的唯一相关区别是此函数是否内联到do_test()或不。 例如,恢复溢出检查将禁止内联并解决该问题; 删除#[inline]属性会产生相同的效果; 禁用LTO会禁用库函数的内联,并再次解决该问题。

如果这是真的(再次,我不确定以上任何一项内容是100%肯定),这意味着在进行内联后,某些恶意LLVM传递或传递的组合会错误地优化IR。 这就是我目前正在尝试调查的内容,但遗憾的是,这并不容易(至少对于像我这样的非LLVM-ICE破坏者而言),因为IR在GOODBAD之间的差异适中大。 它的确看起来像错误的版本忽略了panic_bounds_check ,但是我仍然不确定为什么。

另外,受@tmiasko的评论启发,我尝试使用不同的LLVM版本编译rustc,似乎LLVM 10rc1修复了该问题(我尝试的最新错误LLVM版本是9.0.1)。 不过,这似乎不应该归咎于“跳转线程”传递,因为我看不到9.0.1和10rc1之间的任何相关提交。

如果还原Layout::repeat()更改仅掩盖了一个不相关的错误的特定出现的征兆,还原真的是正确的做法吗?

如果还原Layout :: repeat()更改仅隐藏了一个不相关的错误的特定出现的征兆,那么还原真的是正确的做法吗?

我认为,在以下情况下可能还可以:

  • 零钱已寄出
  • 它使错误更易于触发,影响了许多用户
  • 正确修复将花费很长时间

如果这些保持不变,那么我想我会撤回更改,发布一个次要版本以解除对用户的阻止(即使该错误仍然存​​在,即使没有更改,一切也可以正常工作),然后专注于实际错误。

我记得在另一个编译器中实际上是这样做的。 我们还原了发布分支中的更改,但未对master分支进行更改(这不是一个好习惯,这会在以后引起问题),并发布了一个新的次要版本。 然后修复实际的错误。

无论如何,只要对漏洞进行优先级排序和修复,并且使漏洞更易于触发的提交本身不是漏洞修复,那么我现在看不到任何还原问题。

问题是,此错误真的很容易触发吗? 到目前为止,我们已经收到一份报告,该报告中包含一些测试案例,其中尝试进一步最小化(例如展开看似微不足道的for _ in 0..1循环)无法重现。

我认为该错误不容易触发,似乎我倒霉。

无论如何,我真的很感谢@shahn在还原Layout::new()更改时遇到的麻烦,但是在这种情况下,IMO还原它是不正确的。 我的推理(除了@SimonSapin所说的):

  • 删除Layout::repeat()溢出检查允许LLVM在发行版本中内联Vec::reserve() 。 在某些情况下,它可能会带来不错的性能提升(当然应该对此进行衡量);
  • 实际上, libcore/alloc.rsLayout::pad_to_align() )中的前一个函数使用相同的未检查加法模式,并使用完全相同的注释说明了什么使之成为可能。 恢复的溢出检查Layout::repeat()但不是在Layout::pad_to_align()似乎真的怪我;
  • 如果真的有人在这个问题上受阻(我绝对不是),那么还有许多其他解决方法都不需要更改stdlib(例如,禁用ThinLTO,更改优化级别,减少代码生成单元的数量)。

也许是在本地抛出包含不变量的防御性断言作为先决条件,以便它对特定的细节感到恐慌以追捕这种特定的边缘情况或某些调试器? 我敢打赌,这是在某些条件下进行的未经检查的计算。

然后,当它被跟踪时(如我刚刚学习的LLVM中的某个地方,ty @dyfz),回归测试用例将非常棒,因此不会再次发生。 🙏

LLVM IR复制器: https :

运行opt bconfused.ll -scalar-evolution -loop-idiom -scalar-evolution -indvars -S -O3 -o - | grep xprint 。 如果括号的内部是i64 -1 ,则会发生越野车优化。 如果不是,它可能没有,但是很难确定。

它似乎是由于LLVM在归纳变量简化过程中错误地将nuwadd nuw i64 %x, -1而引起的。 x是该函数的参数,而nuw表示没有无符号换行,因此这有效地断言了该参数在函数中不保证为0的位置为0。

平分(编辑:从LLVM 9到LLVM 10, @ tmiasko表示不受影响)产生此提交:

commit 58e8c793d0e43150a6452e971a32d7407a8a7401
Author: Tim Northover <[email protected]>
Date:   Mon Sep 30 07:46:52 2019 +0000

    Revert "[SCEV] add no wrap flag for SCEVAddExpr."

    This reverts r366419 because the analysis performed is within the context of
    the loop and it's only valid to add wrapping flags to "global" expressions if
    they're always correct.

    llvm-svn: 373184

看起来很有希望,因为LLVM 9.0分支Rust使用了r366419(上述提交还原的提交)。

T编译器分类:P介质,基于以下情况摘要

pnkfelix:#69225的其余工作似乎是1.修复LLVM(通过挑选其58e8c793d0e43150a6452e971a32d7407a8a7401或升级到LLVM 10),然后2.阅读PR#67174。
pnkfelix:但是这些都不是我的优先事项。
pnkfelix:至少,此LLVM错误似乎没有比其他LLVM代码生成错误更好或更糟。 我想这就是@simulacrum所说的。

更新:在PR中尝试升级到LLVM 10#67759

更新2:盲目地选择它们的还原提交可能是不明智的,因为我们可能出于某种原因选择了原始选择,因此还原可能会产生意想不到的下游影响。 至少,我们不应该在不了解后果的情况下进行尝试(鉴于升级到LLVM 10的努力,我们可能根本不应该尝试进行还原,因为这在很大程度上浪费了工作...)

最初的承诺是精心挑选的吗? 至少从@comex '的注释对我来说还不清楚(“包含在Rust的LLVM 9.0分支中”)也可能意味着它只是LLVM 9.0的一部分)。

有问题的提交是一个非常局部且很小的更改,它向函数调用中添加了一个参数,字面意思是it is safe [in this case] to add SCEV::FlagNSW (从代码中判断,新参数也可以是SCEV::FlagNUW ),因此,我认为这很可能是导致优化失败的原因。 我可以确认删除此参数(即,将(void)getAddRecExpr(getAddExpr(StartVal, Accum, Flags), Accum, L, Flags);更改(void)getAddRecExpr(getAddExpr(StartVal, Accum), Accum, L, Flags); )可以解决此问题。

而且,这个有问题的提交不是挑剔的。 这真是倒霉—看起来还原是在9.0.0创建之后发生的,因此上游9.0.0仍然具有令人反感的参数。 由于某种原因,该还原也没有被反向移植到9.0.1。 10.0.0-rc1和更高版本具有还原功能。

这里的评论解释了为什么实际上在这里添加nswnuw是不安全的。 与LLVM开发人员讨论此问题可能是一个好主意,但是我认为选择恢复将解决此问题,并且由于它很小且自成一体,因此不会有任何意想不到的影响。

PS对@comex造成此问题的根源大加赞赏。 很棒的工作。

FWIW我可以确认https://github.com/llvm/llvm-project/commit/58e8c793d0e43150a6452e971a32d7407a8a7401可以安全选择,这是一个保守的更改。 如果您对SCEV nowrap标志的问题所在的更多上下文感兴趣,请参阅https://lists.llvm.org/pipermail/llvm-dev/2019-September/135195.html

我想我什至在恢复#67174之后也找到了重现此问题的方法。 这是一个稍长但仍安全的程序,该程序使用最新的#67174夜间版本在Windows,Linux和macOS上可靠地进行了段错误恢复:

fn do_test(x: usize) {
    let mut arr = vec![vec![0u8; 3]];

    let mut z = vec![0];
    for arr_ref in arr.iter_mut() {
        for y in 0..x {
            for _ in 0..1 {
                z.reserve_exact(x);
                let iterator = std::iter::repeat(0).take(x);
                let mut cnt = 0;
                iterator.for_each(|_| {
                    z[0] = 0;
                    cnt += 1;
                });
                let a = y * x;
                let b = (y + 1) * x - 1;
                let slice = &mut arr_ref[a..b];
                slice[1 << 24] += 1;
            }
        }
    }
}

fn main() {
    do_test(1);
    do_test(2);
}

视窗:

PS> rustup run nightly rustc --version
rustc 1.43.0-nightly (6d0e58bff 2020-02-23)
PS> rustup run nightly cargo run --release
    Finished release [optimized] target(s) in 0.01s
     Running `target\release\rust-segfault.exe`
error: process didn't exit successfully: `target\release\rust-segfault.exe` (exit code: 0xc0000005, STATUS_ACCESS_VIOLATION)

Linux:

$ rustup run nightly rustc --version
rustc 1.43.0-nightly (6d0e58bff 2020-02-23)
$ rustup run nightly cargo run --release
    Finished release [optimized] target(s) in 1.13s
     Running `target/release/rust-segfault`
Segmentation fault (core dumped)

苹果系统:

λ rustup run nightly rustc --version
rustc 1.43.0-nightly (6d0e58bff 2020-02-23)
λ rustup run nightly cargo run --release
    Finished release [optimized] target(s) in 0.01s
     Running `target/release/rust-segfault`
[1]    24331 segmentation fault  rustup run nightly cargo run --release

这个程序不依赖于CODEGEN单元的数量,所以它出现segfaults在操场上,(上稳定,β和夜间)。 我还通过编译来自与LLVM 9链接的master的rustc (还原了#67174)来复制此代码。

潜在的LLVM错误仍然是相同的,因此升级到LLVM 10或挑选LLVM修复程序会使该段错误消失。

我真希望我能了解正在发生的事情。 看起来确实由于多余的nuw而取消了边界检查,这来自错误地缓存的SCEV值(就像在C程序中链接到@nikic的线程cnt变量)都会导致LLVM IR看起来非常不同,并使问题消失。

我的印象是1.41.1刚刚在#69359中完成(我的时机不好),因此在这一点上没有什么可以做的。 它是至少一个好主意,更新注释Layout::repeat()与LLVM问题的更详细的解释? 如果是这样,我可以发送公关。

我的印象是1.41.1刚刚在#69359中完成(我的时机不好),因此在这一点上没有什么可以做的。

如果我们在1.41.1中包含的补丁实际上没有解决问题,则应重新考虑是否要向后移植新的修复程序并重建发行版。 在发布团队会议上达成了共识,即向后移植LLVM修复程序,但我个人认为,另一个新的PoC可能需要对该主题进行另一次讨论。

抄送@ Mark-Simulacrum @ rust-lang / release

@dfyz,我们将尝试向后移植LLVM修复程序以获取1.41.1的另一个版本,同时我们等待有关实际发布该版本的共识。

FWIW,对我而言,新的复制器在稳定的1.38.0和更早版本上可以按预期工作( index out of bounds ),但在1.39.0和更高版本上存在段错误。 LLVM在1.38和1.39之间并没有很大差异(https://github.com/rust-lang/llvm-project/compare/71fe7ec06b85f612fc0e4eb4134c7a7d0f23fac5...8adf9bdccfefb8d03f0e8db3b012fb41da1580a4可以,但Rust可以是任何差异)一路走来。

新的复制器在稳定的1.38.0上可以按预期工作(索引超出范围)

我(偶然地)发现在1.38.0上设置-C codegen-units=1会重现段错误。 1.37.0对我来说似乎很安全(我尝试过的所有选项组合都不会产生段错误)。

不用理会1.37.0使用LLVM 8。
奇怪的是,介于1.37.0和1.38.0之间的LLVM IR差异(带有-C codegen-units=1 )只是一行:

- %71 = icmp eq {}* %70, null
+ %71 = icmp ule {}* %70, null

(其中%70是从<core::slice::IterMut<T> as core::iter::traits::iterator::Iterator>::next()的结果得出的)

仅此一项就足以欺骗LLVM将可怕的nuwadd nuw i64 %x, -1

1.37.0对我来说似乎很安全(我尝试过的所有选项组合都不会产生段错误)。

那是使用LLVM 8,所以应该完全不应该指责SCEV更改。

那是使用LLVM 8

我的错,不好意思,我很困惑(我很高兴将它简化为单行差异,我什至没有检查LLVM版本)。

我们使用精心挑选的LLVM修复程序准备了新的1.41.1工件。 您可以使用以下方法在本地进行测试:

RUSTUP_DIST_SERVER=https://dev-static.rust-lang.org rustup update stable

ping https://github.com/rust-lang/rust/issues/69225#issuecomment -586941455

[triagebot]该问题已成功解决,而没有经过ping通的编译器团队的任何参与。
很好。

1.41.1已经发布,我想是时候该解决这个问题了。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

SharplEr picture SharplEr  ·  3评论

dtolnay picture dtolnay  ·  3评论

behnam picture behnam  ·  3评论

dwrensha picture dwrensha  ·  3评论

cuviper picture cuviper  ·  3评论