Let's encryptのバグはRustで実装していたら防げたの?
という記事を見かけたので、ではNimではどうなのかと思い書いてみました。
はじめに
Let's encryptのバグの原因はポインタに起因する実装ミスでした。
「Nimはいいぞ」と言うためだけにNimで実装した場合を検証してみます。
以下引用
原因はなんだった?
詳しくは
https://jovi0608.hatenablog.com/entry/2020/03/09/094737
のステキなまとめを見たほうがいいのですが、シンプルにすると、このような実装です。
fail.gofunc main() { var out []*int for i := 0; i < 3; i++ { out = append(out, &i) } fmt.Println("Values:", *out[0], *out[1], *out[2]) fmt.Println("Addresses:", out[0], out[1], out[2]) }
ValuesもAddressesも[0]~[2]で同じ値が表示されます。
ループカウンタを値渡しではなく、
参照渡しをして保管してしまったことが要因です。
です。
実際にどのような動きをするのか、Go Playgroundで試してみましょう
Go言語では変数名に「&」をつけることで、変数のアドレスを示します。同じことをNimで行うには、変数名を「addr」という関数を通してアドレスを取り出す必要があります。
Goの場合
var a = "abc"
fmt.Println(&a)
>> 0x40c138
Nimの場合
var a = "abc"
echo a.addr.repr
>> ptr 0x5596f66b2c40 --> 0x7fc2daa98058"abc"
Nimではechoで表示できるのはstring型だけです。「addr」はポインタ型を返す関数であり、ポインタ型はstring型ではないために、「repr」を付けて明示的に文字列表現を取り出す必要があります。
簡易的なソースでチェック
では問題のソースを何も考えずにNimで書いてみましょう。
import strformat
var output:seq[int]
for i in 0..2:
output.add(i)
echo &"Values: {output[0]}, {output[1]}, {output[2]}"
echo &"Address: {output[0].addr.repr}, {output[1].addr.repr}, {output[2].addr.repr}"
>> Values: 0, 1, 2
>> Address: ptr 0x7f20a35d4058 --> 0
, ptr 0x7f20a35d4060 --> 1
, ptr 0x7f20a35d4068 --> 2
非常に素直に、期待通りの結果になっていますね。
Nimではシステムプログラミングもできるため、明示的なメモリ管理やポインタへのアクセスの機能も提供されていますが、アプリケーション開発においてはポインタのことは全く考える必要がありません。PHPやPythonやJavaScriptと同じように素直に開発できます。
では少し頭を使ってNimで書いてみます。今回はGoのソースと同じように明示的にアドレスを渡します。
import strformat
var output:seq[int]
for i in 0..2:
output.add(i.unsafeAddr()) # アドレスを渡す
^^^^^^^^^^^^^^^^^^^
echo &"Values: {output[0].ptr}, {output[1].ptr}, {output[2].ptr}"
echo &"Address: {output[0].addr.repr}, {output[1].addr.repr}, {output[2].addr.repr}"
コンパイルエラーになります。なぜでしょうか。
答えは「output」の中身の型がint型で定義されているからです。int型で中身が定義された配列に、ポインタ型を追加することはできません。安全ですね!
最後に無理やり今回のような事故を起こしてみようとすると、このようになります。
import strformat
var output = newSeq[pointer](0) # 内部がポインタ型の配列を宣言する
for i in 0..2:
output.add(i.unsafeAddr()) # アドレスを渡す
echo &"Values: {cast[ptr int](output[0])[]}, {cast[ptr int](output[1])[]}, {cast[ptr int](output[2])[]}"
echo &"Address: {output[0].repr}, {output[1].repr}, {output[2].repr}"
>> Values: 2, 2, 2
>> Address: 0x5587019bef70
, 0x5587019bef70
, 0x5587019bef70
cast[ptr int](output[0])[]
ってこんなことを書かないとポインタから値を取り出せないんですねー。いくらなんでもこんなことを書いてる時点で、バグに気づくはずです。
実際に問題が起きたソースでチェック
さて、問題が起きた実際のコードを見てみましょう
1.func modelToAuthzPB(am *authzModel) (*corepb.Authorization, error) {
2. expires := am.Expires.UTC().UnixNano()
3. id := fmt.Sprintf("%d", am.ID)
4. status := uintToStatus[am.Status]
5 pb := &corepb.Authorization{
6. Id: &id,
7. Status: &status,
8. Identifier: &am.IdentifierValue,
9. RegistrationID: &am.RegistrationID,
10. Expires: &expires,
11. }
12. (snip)
13. return pb, nil
14.}
15.
16. // authzModelMapToPB converts a mapping of domain name to authzModels into a
17. // protobuf authorizations map
18. func authzModelMapToPB(m map[string]authzModel) (*sapb.Authorizations, error) {
19. resp := &sapb.Authorizations{}
20. for k, v := range m {
21. // Make a copy of k because it will be reassigned with each loop.
22. kCopy := k
23. authzPB, err := modelToAuthzPB(&v)
24. if err != nil {
25. return nil, err
26. }
27. resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{Domain: &kCopy, Authz: authzPB})
28. }
29. return resp, nil
30.}
23行目のココです
authzPB, err := modelToAuthzPB(&v)
20行目の for k, v := range m {
から渡されたv
をmodelToAuthzPB
関数に参照渡ししています。Go言語では関数の引数には参照(アドレス)を渡すことは普通なので違和感ないかもしれませんが、正しくは22行目のように
kCopy := k
vCopy := v
authzPB, err := modelToAuthzPB(&vCopy)
と一度ローカル変数に代入した上でその参照を渡さなくてはいけません。これこそ今回の問題のキモかと思います。
この問題をRustでは以下のように解決していました。
good.rustfn main() { let mut out:Vec<&i32> = vec![]; for i in 0..3{ out.push(&i); } println!("{:?}", out); }
コンパイル時に、このようなエラーが出ます。
6 | out.push(&i); | --- ^^ borrowed value does not live long enough | | | borrow later used here
Rustは生存期間を厳密に検証してくれるので、とても安全です!
Nimでは引数に普通に値を渡すだけで、自動的にそれを参照渡しとしてくれます。
問題の箇所も、ローカル変数への代入というのはいりません。PHPやPythonなどスクリプト言語と同じ感覚で書いてOKなのです。
for key, val in m.pairs:
let authzPB = modelToAuthzPB(val)
カンタンですね
Nimはいいぞ
Go言語では普段のアプリケーション開発でも常に参照とポインタのことを意識する必要があります。関数定義で引数はアドレスで受け取り、関数の実装でアドレスから値を取り出して使うということが一般的なGoのコーディングスタイルかと思います。そのために参照を使うべきでない場面でも参照を使ってしまい、今回のような事故が起きてしまったのではないかと思います。
特に今回の事故では、ループから取り出した値を関数の引数に渡すという処理だったために、余計に難しかったと思います。
Nimでは
- アプリケーション開発ではポインタや参照のことを意識する必要はない
- 関数定義では自動的に参照渡しになってくれる(わざわざ引数にアドレスを渡さなくてよい)
- 関数内部の引数は自動的に参照から値を取り出してくれる(わざわざアドレスから値を取り出さなくてよい)
- コンパイル時に値渡しか参照渡しか厳密に型チェックしてくれる
によって安全にプログラミングができます。
Nimに興味を持った人は、こちらも読んでくださいね!