形式検証アドベントカレンダー 10 日目の記事です。(2 本しかありませんが)
OS カーネルの実装や組み込みソフトウェアに利用できるような、C 言語とアセンブリ言語を対象とする形式検証ツールの紹介です。 出来ることと基本的な使い方くらいの説明になります。(例もショボい) 今回紹介するツールは、軽量形式手法で使われるようなソースコードと直接的な繋がりが少ない設計を吟味するためのものではなく、ソースコードとの乖離が少ないものを選びました。 また、以下に紹介するツールは研究プロジェクトであったり、メンテナンスが放棄されたものも含みます。
並行分離論理を Coq で形式化した iris を利用した検証ツール。refinedcrefinedc_sourceoshi
opam にパッケージがある。coq と iris のリポジトリを使っているので追加してからインストールする。refinedc_installationrefinedc_note1
opam repo add coq-released "https://coq.inria.fr/opam/released"
opam repo add iris-dev "https://gitlab.mpi-sws.org/iris/opam.git"
opam update
opam pin add -n -y cerberus "git+https://github.com/rems-project/cerberus.git#b60ea9a7d30dfa7f048c2b312dd86547939a035a"
opam pin add refinedc "git+https://gitlab.mpi-sws.org/iris/refinedc.git"
C のソースコードに注釈をつけたものが Coq に変換され,ある程度自動的に証明されたあと残ったものを手動で補題を立てて自動証明器に指示しつつ証明する。
プロジェクトの生成
refinedc init
swap 関数がちゃんと値を入れ替えているか検証する例
#include <refinedc.h>
[[rc::parameters("a : loc", "b : loc", "n : Z", "m : Z")]]
[[rc::args("a @ &own<n @ int<i32>>", "b @ &own<m @ int<i32>>")]]
[[rc::ensures("own a : m @ int<i32>", "own b : n @ int<i32>")]]
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
refinedc init したディレクトリで以下のようにする
refinedc check swap.c
proofs/swap 以下に Coq に変換されたコードや生成された証明がある。
この程度ならば自動で証明してくれるが、Coq に変換されるのでより複雑な対象には手動で証明することで対応できる。
モデル検査による C と C++の検査ツール。cbmccbmc_sourcecbmc_hp CProver というプロジェクトの成果物で、他の成果物はcprover_overviewに説明がある。 CBMC の他には goto 文を解析してくれるようなものもある。
C 言語の場合 C99,C89,C11 と GCC や Visual Studio のほとんどの拡張に対応していて、未定義動作などのバグの種となるミスを指摘してくれる他、ユーザーが定義した性質が実行時に満たされるかどうかを静的に解析してくれる。 (もちろん、検査器に指摘されたからと言ってバグであるとは限らない)
Win/Mac/Ubuntu に対応していますが、Docker Image が公開されているので使う。
docker run -it diffblue/cbmc:5.71.0
以下のようなフラグで検査する性質を指定できる。
以下のようなプログラムに対して検査してみる。
int main() {
char buf[3] = {'a', 'b', 'c'};
buf[3] = 'd';
}
指定したフラグでチェックされる性質(assertion)を見るには--show-properties
をつける。すると以下のように配列の境界チェックの条件が生成されていることがわかる。
cbmc --bounds-check --pointer-check cbmc_sample.c --show-properties
実際に解析を実行するには先のフラグ--show-properties
を外す
期待通りに検証が失敗している。
CBMC には、定義したアサーションに対して満たされるかどうか静的に検査する機能がある。 例えば wrap around しない減算をする以下のような関数にアサーションを加えて検査すると
cbmc cbmc_sample1.c --function uint_sub_not_wa --trace
#include <assert.h>
unsigned int uint_sub_not_wa(unsigned int a, unsigned int b)
{
assert(a >= b);
unsigned int res = a - b;
assert(res + b == a);
return res;
}
1 つ目のアサーションに対していくつか反例が上がってくる。(--trace
で反例を表示できる)
以下のようにバリデーションを加えると検証が通る。
unsigned int uint_sub_not_wa(unsigned int a, unsigned int b)
{
if (a < b) {
return 0;
}
assert(a >= b);
unsigned int res = a - b;
assert(res + b == a);
return res;
}
CBMC はループに関する性質を検証するために以下のようなコードを
while (flag) {
YOURCODE
}
if (flag) {
YOURCODE[1/flag]
if (flag) {
YOURCODE[2/flag]
if (flag) {
YOURCODE[3/flag]
}
}
}
のように解釈して(loop unrolling/unwinding)検査するため,どこまで検査するか上限を決定する必要がある。cbmc_loop_unwinding
プログラム全体の各ループに対して統一的に設定するには--unwind N
のように指定する。
例:
#include <assert.h>
int main() {
for (int i = 0; i < 100; ++i) {
assert(i < 100);
}
}
cbmc --unwind 100 cbmc_loop.c
しかし、今回の場合のようにループ回数が固定だったりすると CBMC が自動的に N を推測してくれるので、一旦--unwind
無しで実行してから手動で推測すればよい。
言わずとしれた(?)形式検証済み C コンパイラcompcert_hpだが、普通のプログラム検証にも使える。 派生物も研究・開発されており、アセンブリとの相互運用も形式検証出来るものもある。compcertm VST は CompCert の成果を使って一般のプログラム検証が出来るようにしている。vst_note1
opam で coq-released リポジトリを追加後にvst_note2
opam install coq-vst coq-compcert
Coq で設計の形式モデルを作って、その実装である C のソースコードをclightgen
で Coq に変換し設計のモデルの要求を満たしていることを証明する。
VST を使ったワークフローは
clightgen your_code.c
(出力: your_code.v
)// swap.c
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int x = 1, y = 2;
swap(&x, &y);
return x;
}
clightgen swap.c
API spec は、以下のようになる
Definition swap_spec : ident * funspec :=
DECLARE _swap
WITH sh: share, a: val, b: val, a_val: Z, b_val: Z
PRE [ tptr tint, tptr tint ]
PROP (writable_share sh)
PARAMS (a; b)
SEP (data_at sh tint (Vint (Int.repr a_val)) a; data_at sh tint (Vint (Int.repr b_val)) b)
POST [ tvoid ] (
PROP()
RETURN()
SEP (data_at sh tint (Vint (Int.repr b_val)) a; data_at sh tint (Vint (Int.repr a_val)) b)
).
Definition main_spec :=
DECLARE _main
WITH gv : globals
PRE [] main_pre prog tt gv
POST [ tint ]
PROP()
RETURN (Vint (Int.repr 2))
SEP(TT).
Lemma body_swap: semax_body Vprog Gprog f_swap swap_spec.
Proof.
start_function.
try repeat forward.
entailer!.
Qed.
start_function
はゴールを Hoare triple に変換してくれる。この時点で API spec と実装のミスマッチ(e.g. spec の返り値の型と実装の返り値の型が食い違っている)も検出してくれるforward
は、sequence rule の適用(gdb で si 連打するみたいな...)entailer!
は、分離論理のソルバ全体はこのようになる
(* swap_verify.v *)
Require Import VST.floyd.proofauto.
Require Import swap.
Instance CompSpecs : compspecs. make_compspecs prog. Defined.
Definition Vprog : varspecs. mk_varspecs prog. Defined.
Definition swap_spec : ident * funspec :=
DECLARE _swap
WITH sh: share, a: val, b: val, a_val: Z, b_val: Z
PRE [ tptr tint, tptr tint ]
PROP (writable_share sh)
PARAMS (a; b)
SEP (data_at sh tint (Vint (Int.repr a_val)) a; data_at sh tint (Vint (Int.repr b_val)) b)
POST [ tvoid ] (
PROP()
RETURN()
SEP (data_at sh tint (Vint (Int.repr b_val)) a; data_at sh tint (Vint (Int.repr a_val)) b)
).
Definition main_spec :=
DECLARE _main
WITH gv : globals
PRE [] main_pre prog tt gv
POST [ tint ]
PROP()
RETURN (Vint (Int.repr 2))
SEP(TT).
Definition Gprog := [swap_spec; main_spec].
Lemma body_swap: semax_body Vprog Gprog f_swap swap_spec.
Proof.
start_function.
try repeat forward.
entailer!.
Qed.
わりと最近に softwarefoundations の vol. 5 としてハンズオンが作られた。sf_vc
mkdir $workdir
cd $workdir
repo init -u ssh://[email protected]/seL4/verification-manifest.git
repo sync
$workdir/l4v/tools/c-parser
に移動してmake CParser
$workdir
に戻って./isabelle/bin/isabelle jedit -d ./l4v -l CParser
とすれば jedit が起動して開発の準備が完了C 言語のコードを Isabelle 上の形式言語 Simplsimplに変換して Isabelle で形式化された要求を満たしていることを検証する。
jedit を立ち上げて
./isabelle/bin/isabelle jedit -d ./l4v -l CParser CParserExample.thy
以下のようにすれば、ファイル名_global_addresses
という contextisabelle_contextに関数定義などが変換されたものが入っている。
theory CParserExample
imports CParser.CTranslation
begin
install_C_file "path/to/c_src.c"
関数はf_関数名_body_def
のような名前で変換されるので,上に付け加えて
context c_src_global_addresses
begin
thm f_your_function_body
end
などとすれば Output タブに Simpl の式が表示される
かなり自明な例
// a.c
void f(void) {}
(* CParserVerySimpleExample.thy *)
theory CParserVerySimpleExample
imports CParser.CTranslation
begin
install_C_file "a.c"
theorem (in a_global_addresses) shows "\Gamma \turnstile {| True |} PROC f() {| True |}"
apply vcg
done
Simpl の式中では元の名前で参照できる。
Frama-Cframacframac_source は、C 言語の静的解析フレームワークで、そのプラグインである WPframac_wp が Verifast や CBMC のように C 言語に注釈をつけて条件を満たすか検証する機能を持っている。
opam install frama-c
Why3 に証明器が登録されていないと言われたら、opam でインストールした場合証明器自体は入ってるはずなのでwhy3 config detect
で why3 の設定を生成すれば動くと思われる。
C 言語のコードに ACSL(ANSI/ISO C Specification Langage)という言語で注釈をつけて検査器にかける。
swap の例:
/*@
requires \valid(a);
requires \valid(b);
assigns *a;
assigns *b;
ensures *a == \old(*b);
ensures *b == \old(*a);
*/
void swap(int *a, int* b);
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
\valid(x)
はポインタ x はデリファレンスされても安全であることを表す ACSL のビルトインの述語\old(x)
は関数呼び出し前の x の値を表すframa-c -wp swap.c
で検査できる。
かなり充実したチュートリアルとして ACSL by Example がある。acsl_by_example また、プログラム検証の基本事項の解説を含むチュートリアルもあるらしい。intro_cprog_framac
C 言語を拡張して、安全にシステムプログラミングに使えることを目指しているプロジェクト。checkedccheckedc_source
といった設計思想で作られている。
Windows 向けにはバイナリ配布があるが、Linux にはないのでビルドする。 処理系は Clang をベースに作られていて、同じようにビルドできる。checkedc_compiler_build
mkdir $workdir && cd $workdir
git clone https://github.com/Microsoft/checkedc-clang src
cd src/llvm/projects/checkedc-wrapper/
git clone https://github.com/Microsoft/checkedc
cd $workdir
cmake -G Ninja -DLLVM_ENABLE_PROJECTS=clang \
-DCMAKE_INSTALL_PREFIX=./install \
-DCMAKE_BUILD_TYPE=Release \
-DLLVM_ENABLE_ASSERTIONS=ON \
-DLLVM_CCACHE_BUILD=ON \
-DLLVM_INSTALL_TOOLCHAIN_ONLY=ON \
-DLLVM_TARGETS_TO_BUILD="X86" \
-DLLVM_LIT_ARGS=-v \
./src/llvm
ninja
C 言語に対して主に以下のように拡張された言語でプログラムを記述する。
checked キーワードで境界検査付の配列が作れる e.g.
#include <stdchecked.h>↲
int out_of_bound(int a checked[2]) {
return a[3];
}
これを先程ビルドしたコンパイラに通すと以下のようにエラーになる
例は一次元だが、多次元配列もchecked[n][m]
のようにして定義できる。他に null-terminated array のための型もある。checkedc_spec
しょうもない例だが、このように書ける
#include <stdio.h>
#include <stdchecked.h>
struct BinTree _For_any(T) {
T *elm;
struct BinTree<T> *left;
struct BinTree<T> *right;
};
_For_any(T) struct BinTree<T> new_bintree(T *elm) {
struct BinTree<T> t = {
.elm = elm,
.left = NULL,
.right = NULL,
};↲
return t;
}
int main() {
int e = 1;↲
struct BinTree<int> t = new_bintree<int>(&e);
printf("%i\n", *t.elm);
}
speccheckedc_specでは、for_any
とされているが、GitHub wiki の記述では_For_any
となっていて前者はコンパイルが通らなかった。
dynamic_check
でassert
のように与えられた条件が実行時に満たされるかどうか検査できる。
dynamic_check
で与えられた条件はコンパイラが仮定として受け取るため境界検査などを省略できる場合がある。(checkedc_spec, 2.11)
既存のライブラリを問題なくリンクできる他#pragma CHECKED_SCOPE ON
で検査の適用範囲を管理できる。
また、CheckedC で既存のコードベースをコンパイルしたときも、ソースコードを書き換える必要も、勝手に意味論が変わることもないため、
既存のコードベースに CheckedC を混ぜる場合も問題なく、相互運用性がある。
C 言語に分離論理で事前・事後条件を書いて検証するツール。verifastverifast_source マルチスレッドなプログラムにも対応している。
また、しっかりしたチュートリアルverifast_tutがある。
Win/Mac/Linux に向けたリリースがある。
wget https://github.com/verifast/verifast/releases/download/nightly/verifast-21.04-118-gc2cdad9a-linux.tar.gz
tar xf verifast-21.04-118-gc2cdad9a-linux.tar.gz
./verifast-21.04-118-gc2cdad9a-linux/bin/verifast --help
独自の仕様記述言語で C のソースコード上にコメントとして事前・事後条件を書くと、自動的に証明してくれる。 以下のような単純な swap 関数を例に説明する。
void swap(int *a, int *b)
//@requires *a |-> ?x &*& *b |-> ?y;
//@ensures *a |-> y &*& *b |-> x;
{
int tmp = *a;
*a = *b;
*b = tmp;
}
?x
とあるのは ∃x. ref ↦ x という意味でポインタ変数 ref が指している値をその名前で束縛できる&*&
は分離論理における separating conjunction で両隣の式が成り立ち、且つ言及しているメモリ領域が被らないことを主張するこれを踏まえれば上の記述は, "エイリアスでないポインタ a と b にそれぞれある値 x と y を格納して swap(a, b)を呼び出せば、互いの指す値が入れ替わっている"ということを主張している。 実際に verifast を使って検証するには
path/to/verifast -shared verifast_swap.c
-shared
で main 関数を要求しないようにしている。
出力:
いくつか取り上げようと思って面倒くさくなってやめたやつ