書いて覚えるSwift入門

第35回Meltdown and Spectre

Meltdown and Spectreの教訓

2018年最初の大ニュースといえば、本誌Software Designの読者には毎号おなじみの結城浩さん、ではなくてMeltdown and Spectre。実際に悪用するのは困難とはいえ、CPU、それも単一ではなく複数のアーキテクチャにまたがる脆弱性というのは実に稀です。

CPUというハードウェアに由来する脆弱性ゆえ、一見Swiftには関係なさそうに見えますが、しかしその根底にある原理というのはSwiftどころが現代のソフトウェアであればいたるところに存在します。それだけにMeltdownとSpectreの教訓は、プログラムする者であればすべての人が知っておくべきことだと筆者は考えます。

幸か不幸か、高級言語でありながらメモリへの直接アクセスの手段が用意されているSwiftはそれを学ぶのに向いた言語でもあります。実際に見てみましょう。

まずはTerminalからswiftを起動してREPLに入ります。

% swift
Welcome to Apple Swift version 4.0.3
(swiftlang-900.0.74.1 clang-900.0.39.2). Type
:help for assistance.

次に適当な文字列を変数に突っ込みます。

1> var str = "Hello, Swift!"
str: String = "Hello, Swift!"

その文字列が実際に格納されたアドレスを入手しておきます。

2> var addr = str.withCString{$0}
addr: UnsafePointer<Int8> = 0x0000000100202410

文字列を上書きします。

3> str = "Goodbye, Swift!"

ここであらかじめ入手しておいたアドレスを文字列として読んでみましょう。

4> String(cString:addr)
$R0: String = "Hello, Swift!"

なんということでしょう。上書き前の文字列がそのまま入っているではありませんか!

5> str
$R1: String = "Goodbye, Swift!"

念のためstrを見て見ると、確かに上書きされています。

どうしてこうなった?[1]

実は上書きなんかされていないのです。2度目の代入の際には別の場所=メモリ上に新たな文字列が作成されただけで。

データは消えず、ただ上書きされるだけ

物理的に考えると不思議なこの挙動も、論理的に考えると実に自然です。⁠データを消す」と我々はよく言いますが、データというのは実は消去不可能なのです。メモリのどこを読んでもそこには必ず0か1が入っているのですから。我々ができるのは0を1に、1を0にするだけ。では我々が「消す」と呼んでいる行為はいったいなんなのか?

場所を無視しているだけなのです。

ガベージコレクションもファイル削除も、実際には何も「消し」てないんです。⁠消す」べき領域を「上書きOK」としているだけで。だから数GB単位の動画ファイルも数十数百GB単位の仮装マシンインスタンスも一瞬で「消せる」んです。いや、消したように見えるんです。⁠上書きOKフラグ」を立てるだけなのですから。

つまり、それがどこに書かれていたかすら知ることができれば、⁠消した」はずのデータを読むことができる。まだ上書きされていなければ。

なぜそのようになっているかといえば、たいていの場合「わざわざ読めなくするまでもない」から。どうせいつか上書きされるのであれば、わざわざその場で上書きするまでもない、と。

仮に上書きで元データを読めなくしようとすると、必要な時間はざっくり元データの大きさに比例することになります。4GBの動画ファイルを消すには、4KBのテキストファイルの100万倍。8TB級のHDDいっぱいに詰まった全データを消そうとしたら1日でも終わらない!

なのでたいていの場合、不要なデータは「その場で無視していずれ上書きされるのを待つ」のが流儀だったわけです。

しかしデータというのは必要と不要の2種類だけではありません。あるプロセスには必要かつ別のプロセスからは不要どころか見えてはいけないデータというのがあるのです。たとえばブラウザのセッション。それを表示しているタブには絶対必要で、しかし別のタブやプロセスからは読めてはならない。もし読めてしまったら開いただけでユーザの秘密を盗むWebページが簡単にできてしまいます。そういう情報はどうやって扱えばいいでしょうか?

「嘘つきな」現代電脳

現代のコンピュータは、この問題も次のように解決しています。

  • プログラムをカーネルプロセスとユーザプロセスに分ける
  • ユーザプロセスから読み書きできるのはレジスタとメモリだけで、他プロセスを含めほかには何も存在しないふりをしている
  • ユーザプロセスがレジスタとメモリ以外の何かにアクセスしたいときには、カーネルにお願いする(システムコール⁠⁠。カーネルにやってほしいことをレジスタとメモリに書いたうえで、CPUをカーネルプロセスに明け渡す
  • CPUを明け渡されたカーネルプロセスはプロセスのお願いを実行したあと、その結果をレジスタとメモリに書いてCPUをユーザプロセスに戻す
  • CPUにはカーネルにしか実行できない(特権)命令が用意されており、ユーザプロセスがそれを実行しようとするとエラーになる。もう少し厳密には、エラーであることを示すデータをレジスタとメモリに書いてからCPUをユーザプロセスに戻す

これを1秒間に何千回と繰り返すことで、何十何百、場合によっては何千というプロセスが同時に動いているように人間には見えるのですが、実際のところユーザ専用CPUもカーネル専用CPUも存在せず、その場その場で「今の自分はユーザ」⁠今の自分はカーネル」という「演技」をしているに過ぎません。そしてどちらも同じ舞台=CPUを使っているのです。

もし「幕間」=コンテキスト・スイッチで前の演者が自分の小道具を片付け忘れたら?

当然、次の演者にそれが渡ってしまいます。

「えいえい! おこった?」

こういう問題が存在すること自体はずっと以前――本誌Software Design創刊以前――から知られていましたが、なぜそれが今頃になって問題になったのでしょう?

コードから見えるCPUと実際のCPUの乖離が大きくなったのが一番の原因でしょう。今回問題となったのは、投機的実行とキャッシュ(cache)です。Raspberry Piがこの脆弱性を免れたのも、この2つが貧弱だったおかげ。

投機的実行の好例は、ポプテピピック(アニメ第1話)⁠。ポプ子の「えいえい! おこった?」にピピ美が「おこってないよ♥」と答えるというやりとりを二度繰り返したあと、ポプ子が寸止めしたのにピピ美が「おこっ」まで言ってしまいますが、ピピ美はまさに投機的実行していたわけです。

「とりあえずやっておいて、不要だったらやらなかったことにする」というのが投機的実行なのですが、問題は「やらなかったことにする」部分。今までは「やった結果を無視する」だけで十分だと思われていたのが、やった結果を別の誰かが盗み見てしまうという問題が今回顕在化したわけです。ピピ美の「おこっ」ですね。ちなみに原作第1話ではポプ子の寸止めはなかったのですが、これが投機的実行の成功例なのか否かは不明です。

次にキャッシュ。⁠プロセスにはレジスタとメモリしか見えない」と前述しましたが、かつては実際のプロセッサもそうでした。しかし現在のCPUでは、レジスタとメモリの間に何段階ものキャッシュが立ちはだかっています。なぜそうなったかといえば、演算回路の速度向上にメモリのアクセス速度が追いつかなかったから。

そのメモリより外部記憶はさらに遅く、その外部記憶よりネットはさらに遅い。結果外部記憶はメモリにキャッシュされ、ネットの向こうのWebコンテンツが外部記憶にキャッシュされ、そのおかげでネット社会が成立しているのですが、実に興味深いことにCPUのキャッシュはずっとプログラマから隠蔽されてきました。プログラムで挙動をコントロールすることは基本できないのです。アドレス0xdeadbeefの内容がどこにあるかは神ならぬCPUぞ知るなのです。

そしてCPUと同様、キャッシュもまたプロセスたちによって使いまわされます。使いまわされる以上、前のプロセスが片付け忘れたデータは次のプロセスが読めてしまいます。

これを防ぐには「キャッシュにもフラグを付けてアクセスコントロールする」「きちんと上書きして後片付けする」かのどちらかなのですが、前述のとおりキャッシュはプログラムからは見えない。それがMeltdownとSpectreを実際に悪用するのが困難になっているのと同時にデバッグも難しくしています。とくに「フラグを付ける」のはCPUにしかできない仕事で、その機能がないCPUに対してソフトウェア側でできることは「上書きして片付ける」しかなく、脆弱性対策を施すと3割もパフォーマンスが落ちる事例が出るのもうなづけます。

余談ですがmacOSはv10.13.2でMeltdownの、v10.13.3でSpectreの対策が施されたのですが、パフォーマンス低下が数%で済んでいるのはサポート対象のMacのほとんどが「フラグが立てられる」⁠正確にはPCIDをサポートしている)CPUを搭載しているからだと推測されます。

「計算機科学に難問は二つだけ。名前付けとキャッシュ無効化だ」とはPhil Karltonの名言ですが、プログラマが直接関与できないキャッシュにまでそれが及ぶとは……。

ソフトウェアではデザインしきれない時代

プログラマから見えるCPUが30年前からほとんど進歩していないことを我々は呆れるべきなのか褒めるべきなのか。そこにはキャッシュも存在しなければ、SSDも「Macintosh HD」のまま。それがないスマホとて、一皮剥けばただのUnixマシン。なぜそうなったか100人に尋ねたら100万通りの答えが返ってきそうですが、筆者の答えはプログラマがハードウェアに「甘えていたから⁠⁠。今までと同じようにプログラムすれば、新しいハードウェアでも同じように動く……とくにx86は生みの親のIntelの挑戦すらはねのけて今に至りました。その牙城を攻略しつつあるARMすら、その点においては50歩100歩であることをMeltdownとSpectreは示しました。高性能化時代の覇者Wintel、省電力化時代の覇者ARMスマホの次の時代の覇者は誰なのか? それが誰であれ、セキュア化がその鍵となるのは確かだと断言しておきます。Coincheckに預けたビットコインにかけて;-)

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」⁠使いたい」⁠発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧