安全と言われる Rust はどのように配列の長さをチェックしているのか
Rust 言語では配列・バッファの長さチェックを行っており、既定のサイズを超えて使おうとすると実行時エラー(バッファオーバーラン)になるとのこと。配列を使う直前に毎回範囲チェックをしているのでしょうか。Rust を使うのは初めてですが、動作を確認してみます。
動作確認環境
- Windows 11 Home 21H2
- Rust 1.58.1
- WinDbg
配列を使うプログラムを作る
配列を使う Rust のプログラムを書きます。
use std::{thread, time};
fn main() {
let mut buf = [0; 4];
for i in 0..4
{
println!("i={} ", i);
if i == 0 { thread::sleep(time::Duration::from_secs(10)); }
buf[i] = i;
}
println!("buf={:?}", buf);
}
4 つの要素をもつ配列(buf[0] から buf[3] までが有効)に 0 から 3 までの値をセットし、表示するだけのプログラムです。後述の調査のため、初回ループ時は 10 秒スリープするようにしていますが、本質とは関係ありません。
最適化時の動作を調べるためリリースモードで(–release オプションを付けて)実行します。
C:\tmp\rust-tp>cargo run --release
......
i=0
i=1
i=2
i=3
buf=[0, 1, 2, 3]
配列の範囲内で使っているため、正常に実行されました。
バッファオーバーランさせる
先のプログラムを次のように改造し、ループの回数を増やします。
【変更前】
for i in 0..4
【変更後】
for i in 0..999
実行します。
C:\tmp\rust-tp>cargo run --release
......
i=0
i=1
i=2
i=3
i=4
thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 4', src\main.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\release\rust-tp.exe` (exit code: 101)
配列の要素数を超えてアクセスしたため、具体的には buf[3] までしか使えないのに buf[4] を使おうとしたため、「index out of bounds」のパニックが発生しました。境界をチェックしていることが確認できます。
デバッガで中身を追う
再度実行し、10 秒のスリープ中に WinDbg からアタッチします。
C:\tmp\rust-tp>cargo run --release
......
i=0
OS の SleepEx 関数を呼び出しているスレッドがあります。このスレッドで main 関数を実行しているようです。
ステップ実行しながら、SleepEx 関数呼び出し直後の動作を追っていきます。
;------ edx:ecx に 10 (秒) をセットしてスリープ関数を呼び出す。
mov ecx,0Ah
xor edx,edx
call rust_tp!std::thread::sleep
;------ ループのたびに rcx に 0 -> 1 -> 2 -> 3 -> 4 が入ってくる。
; 3 を超えた時点で下のほうにジャンプ。
mov rcx,qword ptr [rsp+28h]
cmp rcx,3
ja 00007ff7`f4aa125d
;------ スタック上の指定の場所に rcx の値をセット。
; rbx はループのたびに 1 -> 2 -> 3 -> 4 となり、
; 0x3E7 (= 999) 以外だったら上のほうにジャンプ
add rbx,1
mov qword ptr [rsp+rcx*8+70h],rcx
cmp rbx,3E7h
jne 00007ff7`f4aa1190
;------ ループの途中でパニックするのでここには落ちてこない
lea rax,[rsp+70h]
mov qword ptr [rsp+30h],rax
......
;------- rcx が 3 を超えた時点でここにジャンプ。
; core::panicking::panic_bounds_check を呼び出している。
; 後続の ud2(未定義命令)には戻ってこない。
00007ff7`f4aa125d:
lea r8,[00007ff7`f4abc408]
mov edx,4
call rust_tp!core::panicking::panic_bounds_check
ud2
ポイントを疑似コードで書きます。
if (3 < i) { goto パニック処理; }
buf[i] = i;
配列のアクセスの直前に範囲チェックしているだけでした。
ヒープメモリの範囲チェックについて調べる
スタック上の配列ではなく、ヒープメモリ上のバッファの範囲チェックについても調べてみます。
プログラムは次の通り。「Box::new()」でバッファを宣言しているのが新しいところです。
use std::{thread, time};
fn main() {
let mut buf = Box::new([0; 4]);
for i in 0..999
{
println!("i={} ", i);
if i == 0 { thread::sleep(time::Duration::from_secs(10)); }
buf[i] = i;
}
println!("buf={:?}", buf);
}
実行します。
C:\tmp\rust-tp>cargo run --release
......
i=0
i=1
i=2
i=3
i=4
thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 4', src\main.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\release\rust-tp.exe` (exit code: 101)
配列の時と同様、「index out of bounds」のパニックが発生しました。
再度実行し、10 秒のスリープ中に WinDbg からアタッチし、動作を調べます。
;------ edx:ecx に 10 (秒) をセットしてスリープ関数を呼び出す。
mov ecx,0Ah
xor edx,edx
call rust_tp!std::thread::sleep
;------ ループのたびに rcx に 0 -> 1 -> 2 -> 3 -> 4 が入ってくる。
; 4 以上になった時点で下のほうにジャンプ。
mov rcx,qword ptr [rbp-18h]
cmp rcx,4
jae 00007ff7`e19012b2
;------ ヒープ上の指定の場所に rcx の値をセット。
; [rbp-10h] は 00000258`87913d30 であり、
; !address 00000258`87913d30 で Usage: Heap と表示されることから
; ヒープであることがわかる。
; rbx はループのたびに 1 -> 2 -> 3 -> 4 となり、
; 0x3E7 (= 999) 以下だったら上のほうにジャンプ
add rbx,1
mov rax,qword ptr [rbp-10h]
mov qword ptr [rax+rcx*8],rcx
cmp rbx,3E7h
jb 00007ff7`e19011e0
;------ ループの途中でパニックするのでここには落ちてこない
lea rax,[rbp-10h]
mov qword ptr [rbp-28h],rax
......
;------- rcx が 4 以上になった時点でここにジャンプ。
; core::panicking::panic_bounds_check を呼び出している。
; 後続の ud2(未定義命令)には戻ってこない。
00007ff7`e19012b2:
lea r8,[00007ff7`e191c408]
mov edx,4
call rust_tp!core::panicking::panic_bounds_check (00007ff7`e191b460)
ud2
ポイントを疑似コードで書きます。
if (4 <= i) { goto パニック処理; }
buf[i] = i;
配列のとき同様、ヒープのアクセスの直前に範囲チェックしているだけでした。
ただし、なぜか、配列では「3 を超えたとき」、ヒープでは「4 以上になったとき」でチェックしていました。
おわりに
2 例を見ただけで包括的に確認したわけではありませんが、特に秘儀を使っているわけではなく、配列やヒープを使う直前に毎回範囲チェックをしていた、という話でした。