安全と言われる 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 例を見ただけで包括的に確認したわけではありませんが、特に秘儀を使っているわけではなく、配列やヒープを使う直前に毎回範囲チェックをしていた、という話でした。