脆弱性の温床 strncpy をLinuxカーネルから削除――C言語の文字列コピー関数50年の歴史
ある夜、サーバーが乗っ取られた理由
1988年11月2日の夜、計算機センターのUNIXサーバーが悲鳴を上げていた。
サーバーのコンソールに入ってみると応答が異常に遅い。システム負荷を表示してみると、今まで見たことがないような高負荷が表示されている。
「なんだこれは?どうなってるんだ?」
プロセス一覧を表示してみると、root権限でシェルが無数に走っている!?
とにかく変なプロセスを止めようとkillコマンドを打っていくが、止めても止めてもプロセスが湧いてくる…
そのとき別の大学の計算機センターの友人から電話がかかってきた。あっちでも同じ状況らしい。
「fingerd(フィンガーデーモン)のポートから何か入ってきてるぞ!」
確認するとTCP 79番ポートのコネクションで埋め尽くされている…
これでは、いくらプロセスを止めても外から流れ込んできてキリがない…
インターネット史上最初の大規模ワーム「Morris Worm」による大規模感染のとき、現場はこのような困惑と混乱に見舞われたようです。このとき使われた脆弱性は、fingerdに存在したバッファオーバーフロー(Buffer Overflow)。その根本原因は、gets()やstrcpy()に共通する「宛先バッファの大きさをインターフェースに含めない」という関数仕様でした。
それから38年。2026年6月20日、Linuxカーネル7.2のマージウィンドウで、静かに、しかし決定的な変更がマージされました。
strncpy()――1970年代からずっと「strcpy()より安全」と信じられてきた関数――が、カーネルのソースコードから完全に消えた
6年、362個のパッチ、無数のレビュー。なぜこれほどの時間がかかったのか。そもそもstrncpyはstrcpyの何を直そうとして生まれ、なぜ結局「危険」の烙印を押されて追放されたのか。
この記事は、C言語の文字列コピー関数を巡る、50年以上にわたる「安全性への果てしない格闘」の物語です。
第1章:なぜstrcpyはこれほど長く危険であり続けたのか
多くの入門書は「strcpyはバッファオーバーフローを起こすから危険」と説明しています。では、なぜそれが「危険」で済まず「業界最大級の脆弱性の温床」と呼ばれるまでになったのでしょうか。
それは、境界チェックゼロという設計によるものです。
void foo(char* bar) {
char c[12];
strcpy(c, bar); // barの長さを一切確認しない
}
strcpy(dst, src)は、srcが\0(NUL文字)に到達するまで、dstのサイズなど気にせず無条件にバイトをコピーし続けます。
問題はメモリのレイアウトにあります。スタック上では、ローカル変数のすぐ後ろに関数のリターンアドレスが置かれるのが一般的です。そこで、攻撃者が意図的に長い文字列を送り込むと、次のようなことが起こります。
- バッファが溢れる
- リターンアドレスが上書きされる
- 関数の
returnで、攻撃者が用意したシェルコードへジャンプする - 任意のコードが実行される――権限昇格の完成です
これが「スタックスマッシング」と呼ばれる、C言語における最も古典的で最も再現性の高い攻撃パターンです。1988年のMorris Wormも、その後数十年に渡るCVE(既知の脆弱性)の山も、根っこをたどれば同じ構造にたどり着きます。
第2章:「安全にしたはず」のstrncpyはなぜ別の罠だったのか
「strcpyが危ないなら、長さnを渡すstrncpyを使えばいい」――これは多くのC入門書が推奨してきた「ベストプラクティス」でした。
しかし現実にはstrncpyもまた、Linuxカーネルのドキュメントで「actively dangerous(積極的に危険)」と名指しされる存在になってしまいました。
何が起きたのでしょうか。
実は、驚くべきことにstrncpyは安全な文字列コピーのために設計された関数ではありませんでした。
strncpyはstrcpynとして作られ、その後strncpyと改名されて、Version 7 Unix(1979年)のCライブラリに収録されました。
もともとの用途は、ディレクトリエントリや会計レコードのような、固定長かつゼロ埋めされた名前フィールドを扱うことでした。
重要なのは、これは可変長の文字列を「安全なNUL終端文字列としてコピーする関数」ではなく、固定幅「フィールド」を埋めるための関数だった、という点です。
この生い立ちが、以下の奇妙な仕様を生みました。
| ソース文字列の長さ |
strncpy(dst, src, n)の挙動 |
|---|---|
nより短い |
コピー後、残りのバイトを全て\0で埋める(ゼロパディング) |
n以上 |
nバイトちょうどコピーし、NUL終端を書かない
|
後者の仕様が致命的でした。
char temp[5];
strncpy(temp, argv[1], 5); // argv[1]が5文字以上ならNULが付かない
printf("Password: %s\n", temp); // %sはNULまで読み続ける→隣のメモリまで読んでしまう
文字列を扱うprintfの%sやstrlen、strcmpは「文字列はいつかNULで終わる」ことを前提に動きます。NULが付いていないtempを渡せば、関数はバッファの外側のメモリを読み続けてしまいます。これは書き込みオーバーフローではなく、読み取りオーバーリードという別種の脆弱性――スタックやヒープの隣接領域の情報漏洩――を引き起こします。
さらに、ソースが短い場合の「余った領域を全部ゼロで埋める」という仕様も、カーネルのような高頻度実行パスでは無視できない性能上の無駄として批判されてきました。
つまりstrncpyは、本来の目的ではない「安全コピー関数」として誤用されることで、「NUL終端の欠落」という新しい罠を生み出してしまった関数だったのです。
第3章:そもそもなぜCの文字列は「終端」に頼るのか
ここまで読んで、モダンな言語しか知らない読者はこう思うでしょう。
「そもそも文字列に長さを持たせればよかったのでは?」――鋭い指摘です。
実はこれこそがC言語の設計の根源に横たわる問題なのです。
C言語の生みの親であるDennis Ritchie自身が書き残した論文 "The Development of the C Language" に、その答えがあります。
文字列の最後に終端文字を置くという設計は、実はC自身ではなく、その前身であるB言語(1969年)の時代に選択されました。さらにその前のBCPLでは、文字列の先頭バイトに文字数(カウント)を格納する方式を採っていました。Ritchieはこの変更についてこう記しています。
"This change was made partially to avoid the limitation on the length of a string caused by holding the count in an 8- or 9-bit slot, and partly because maintaining the count seemed... less convenient than using a terminator."
要するに「8〜9ビットのカウント枠では文字列長に上限ができてしまう」「カウントを保守し続けるより終端文字を使う方が実用上便利」という判断です。
そしてもう一つ、より本質的な設計判断があります。それは「配列の情報は変換されてポインタになる(array decay)」というCの文法規則です。
char arr[10]という配列は、式の中では単なるchar*――先頭要素へのポインタ――に変換されます。この瞬間、配列が持っていた「サイズ」という情報はコンパイル時に消え去ります。
言語仕様としては、関数に配列を渡した瞬間、呼び出された側は「これが何バイトあるか」を知るすべが無いのです。
ですから、必要な場合は別の引数として渡すなど個別の設計が必要になります。
当時のPDP-7のような資源制約の大きい環境では、先頭に長さを置く方式、終端文字を置く方式、文字コードの一部を終端フラグに使う方式など、複数の選択肢がありました。終端文字方式は唯一の正解ではありませんが、B言語の開発者にとっては、当時の制約下で十分に合理的な設計上の選択だったのです。
つまり、終端文字方式は「欠陥」ではなく、1969年という時代における「妥当な設計上の選択」でした。問題は、この設計上の選択が半世紀以上経った現代の巨大で複雑なソフトウェアにもそのまま引き継がれてしまったことにあります。
第4章:モダンな言語はなぜこの罠にハマらないのか
PythonやJavaでコードを書くとき、バッファオーバーフローの心配をしないのはなぜなのでしょうか。
ここが、Cしか知らない世代とモダンな言語しか知らない世代の間の断絶ポイントです。
「文字列の長さを気にしたことがない」という感覚は、偶然ではなく設計の賜物なのです。
PythonやJavaの文字列は、単なるメモリ上の文字の羅列ではなく、「型」として実装されています。このようなイメージです。
Cのchar*(実態)
┌──────────────┐
│ 0x7fff1234 │ ← ただのアドレスの数値
└──────────────┘
↓ そのアドレスを見に行くと…
h e l l o \0 [??? 次の変数 ???]
↑ ここで止まれなければ隣を読み続ける
Pythonのstrオブジェクト(内部イメージ)
┌──────────────┐
│ type: str │ ← 型情報
│ length: 5 │ ← 長さを保持(strlenが要らない)
│ data: "hello"│ ← 実データ
└──────────────┘
つまり、長さの管理が言語機能として保証されているのです。
C++のstd::stringも同じ発想で、内部的に(pointer, length, capacity)の三要素を保持します。長さを常に保持しているため、strlenのようなO(n)の走査は不要です。
ただし、C++のstd::string自体が全ての範囲外アクセスを自動的に防ぐわけではなく、範囲チェックが必要な場面ではat()を使う、あるいはより安全なAPI設計を選ぶ必要があります。
さらに一歩進んだのがRustです。
RustのStringと&strは、単に長さを持つだけでなく、所有権とライフタイムをコンパイル時に型システムでチェックします。
&strは「ポインタ+長さ」のファットポインタとして実装され、借用チェッカーによって「参照先のデータが生きている間しか使えない」ことをコンパイル時に強制します。
実行前に、C言語なら実行時にしか(あるいは実行してさえ)発覚しないバグがコンパイル時に弾かれるのです。
Cの文字列関数が50年以上格闘してきた「長さをどう伝えるか」「終端をどう保証するか」という問題を、モダンな言語は「型システムに組み込む」ことで対策したのです。
ただし、完全に安心できるわけではありません。たとえばCPythonやOpenJDKのような主要実装の内部にはC/C++で書かれた部分があり、そこではまだstrncpyなどの文字列処理関数は現役です。言語仕様としては安心でも、その下の実装レイヤーには「Cの文字列問題」は残り続けています。
第5章:なぜ「Cを直す」のではなく「関数を作り直す」道を選んだのか
ここまで読めば、「じゃあCの文字列をファットポインタに変えればいいじゃないか」と思うのは自然な発想です。char*という型そのものの実行時表現を、ポインタと長さのペアに拡張する、という方法です。しかし、それは実現していませんし、おそらく今後もされません。
そこにはABI互換という巨大な足かせがあるからです。
Cのarray decayとNUL終端の慣習は、単なる言語仕様の問題ではありません。
OSのシステムコール、動的リンクライブラリのインターフェース、他言語との相互運用(FFI)――この50年間に書かれたほぼ全てのソフトウェアのバイナリインターフェース(ABI)――が、NUL終端char*を前提として設計されています。
もし言語仕様を変えてchar*を(ptr, length)のペアに変えれば、既存のコンパイル済みのCコード資産が軒並み動かなくなります。ソースコードを修正して再ビルドできるものは何とかなりますが、バイナリでのみで提供されているたくさんのクローズドソースライブラリは対処が不可能です。
では別の方法で、ポインタと長さがペアとなった文字列型を新たに追加することも考えられます。実際、この発想で作られた文字列操作ライブラリも存在します。しかし、これもデファクトスタンダードにはなりそうにありません。OSのシステムコールをはじめとする既存インターフェースの大半がNULL終端のchar*を前提としているままなので、境界を越えるたびに変換が必要になってしまうからです。
だからこそ、C標準化委員会もLinuxカーネルコミュニティも、「言語を変える」のではなく「関数のAPI仕様を明示的にする」という現実的な道を選びました。
C11のAnnex Kではstrcpy_s(dst, dstsz, src)のように、デスティネーションのサイズを必須引数にする関数群が追加されました。ただしglibcではオプション実装扱いにとどまり、広く普及したとは言えません。
Linuxカーネルが選んだのはstrscpy()という独自関数です。
ssize_t strscpy(char *dest, const char *src, size_t count);
strncpyやstrlcpyとの違いを整理すると、その設計思想の進化がよく見えます。
| 観点 | strncpy |
strlcpy |
strscpy |
|---|---|---|---|
| NUL終端保証 | されない | される | される |
| 無駄なゼロパディング | あり | なし | なし |
| srcの全体を読むリスク | なし | あり(危険) | なし |
| 切り捨ての検出 | 困難 | 可能だが煩雑 |
-E2BIGで明示的 |
strscpyは切り捨て時に-E2BIGを返すため、呼び出し側が戻り値を確認すれば切り捨てを明確に検出できます。少なくとも、strncpyのように危険な状態を成功と区別できず「うっかり素通りする」ことを防ぐことができます。
Linuxカーネルコミュニティは2019年頃にstrncpyを公式の非推奨リストに載せてから、実に6年をかけて約362個のパッチで全ての利用箇所を精査し、用途に応じてstrscpy()・strscpy_pad()・strtomem_pad()・memcpy()・memcpy_and_pad()へと個別に置き換えていきました。
単純な一括置換ができなかったのは、「NUL終端文字列として使いたかったのか」「固定幅フィールドとして使いたかったのか」という元々のstrncpyの二重の出自が、現場のコードにそのまま反映されていたからです。
そして2026年6月20日、Linux 7.2でstrncpyのAPIそのものがソースツリーから削除されました。
以後、誰かがうっかりLinuxカーネルにstrncpyを呼び出すコードを書いても、ビルドで検出されます。半世紀続いた口約束が、ようやく強制力を持つルールに置き換わった瞬間でした。
そして、C標準化委員会にも動きがあります。WG14のN3935提案では、C標準そのものからstrncpyを削除しようとしています。
※Removing strncpy
終章:1969年の選択から生まれた一つの罠を、2026年に返し終えるまで
strcpyからstrncpyへ、そしてstrscpyによる廃止へ――この流れを貫くのは、たった一つのシンプルな真実です。
Cの文字列は、文字列自身に長さの情報が含まれていない。文字列の終端は、プログラマとメモリの間の「口約束」で成り立ってきた。
その口約束は、1969年当時の資源制約の大きい環境では合理的な選択でした。
しかし、その選択がそのまま50年以上引き継がれ、Morris Wormから2026年のLinuxカーネルまで、無数のバグと脆弱性と、そして「6年・362パッチ」という気の遠くなる修復作業を発生させました。
PythonやJavaやRustが「最初から文字列に長さを持たせる」という当たり前を実現できたのは、彼らがCが背負った50年分の教訓の上に立って言語仕様を設計したからに他なりません。
それでもなお、処理系にはCの文字列関数が残っており、Cが背負ってきた50年分の課題は、今も生き続けています。
strncpyの廃止は、単なる一つの関数の引退のニュースではありません。
それは、半世紀前の合理的な妥協が、現代の安全基準に追いつくまでにどれだけの時間と労力を要するかを教えてくれる、生きた技術史の一断面なのです。
最後に、この問題のこれからについてです。Linuxカーネルのソースツリーからstrncpyを排除したことは、C言語の文字列問題そのものの解決ではありません。同様のAPIは依然としてLinuxカーネル内に残っていますし、ユーザーアプリケーション側ではstrncpyも有効です。これは一つの節目であって、まだまだ長い返済の道のりは続きます。
※当初の記事で、strncpyの作者がDoug Gwyn氏であると誤記していました。また、訂正で同氏がstrcpynがstrncpyと改名された理由を解説したと記載しましたが、これも別人の記事を取り違えたものでした。お詫びして訂正いたします。WG14でC言語の国際標準規格に携わったDoug Gwyn氏は、strn*関数が主にNUL終端なしに固定幅にファイル名を格納するために使わるものである旨を、2006年10月20日にcomp.std.cに下記のように投稿されています。
The strn* functions were of use mainly for tightly packed structures such as the PDP-11 Unix directory entry, where a 14-character filename was stored without null terminator.
※B言語の終端文字'*e'は、Brian Kernighanの公式チュートリアル文書では「ASCII EOT(End of Transmission、値004)」と説明されています。一方、Thinkage社の「B Language Reference Manual」では「ASCII NULL(000、文字列エスケープシーケンス'*e')」と説明されています。当初の記事では、B言語の文字列がNUL終端であると記載していましたが、NULと特定せず、文字列の最後に終端文字を置く方式と修正しています。
Discussion
という説明の後で
とありますが、
strncpyは安全な文字列コピーのために設計された関数ではなかったという説明と「書き込みオーバーフロー」を防ぐ代わりにという説明は矛盾している気がします。strcpyとstrncpyがVersion 7 Unixで同時に登場したことを考えると「新しい罠を生み出してしまった」というのも時期的に正しくない気がしますがどうでしょうか。ご指摘通り、矛盾しています。本文を修正させていただきました。
時期につきましては、strcpyをもとにstrcpynが作られ、それがstrncpyに改名されたようです。strcpyの脆弱性対策でstrcpynが作られたわけではありませんが、strncpyが「安全コピー関数」として誤用されることで、「NUL終端の欠落」という新しい罠を生み出してしまったと思われますので、そのような記載に修正させていただきました。
とありますが氏の経歴が書かれている を見ると1977年は学生で、1983年からUS Army Research Laboratoryに在籍されてる方のようですが、AT&Tベル研にいた経歴は見当たらないのですが当時としてOSSでもないUNIXの開発に氏が関わっていたというのは本当の話でしょうか?
ご指摘ありがとうございます。Doug Gwyn氏は
strcpynがstrncpyと改名された理由を解説しておられますが、この関数を作成されたという事実は確認できませんでしたので、記載を修正しました。GoogleのAIに相談すると や ( https://web.archive.org/web/20260107090921/https://groups.google.com/g/net.unix-wizards/c/G4XS2RcFuAU/m/-id_254AeiIJ )
をソースとして挙げてくるのですが、内容を確認するとstrcpynがstrncpyに改名された経緯に関する投稿は確認できますがDoug Gwyn氏による投稿ではなく、Doug Gwyn氏による投稿も確認できるのですが話題として違う話を投稿されているようです。
Doug Gwyn氏がstrncpyについてstrcpynから改名された理由を解説されていることが確認できる一次ソースがありましたらお教えください。
ご指摘いただいたとおり別人の記事を取り違えたものでした。お詫びして訂正いたします。WG14でC言語の国際標準規格に携わったDoug Gwyn氏は、strn*関数が主にNUL終端なしに固定幅にファイル名を格納するために使われるものである旨を、comp.std.cに下記のように投稿されていました。
に
とありますが、最初期のUNIXが7bitの文字コードであるASCIIにべったり依存していてマルチバイト文字や国際化についてはなんも考慮されていなかった事実とPDP-7では1文字に9ビットを割り当てていたことと併せて、当時よく見られた方法である文字列の終端の文字のMSBを1にセットする方法は選択肢の内だったのではないかと思うのですがどうでしょうか。
この方法であれば文字列を表すのに終端の1バイトは不要となる分メモリの使用効率は良く、終端に
0を使用する方法を「最適解」というのは言い過ぎな気がします。ご指摘通り、最適解は言い過ぎでした。B言語の開発者が、先頭に長さを置く方式、終端文字を置く方式、文字コードの一部を終端フラグに使う方式など、複数の選択肢からNUL終端を選んだ、という表現に修正させていただきました。
に
とありますが、関数に配列を渡すのと同時に要素数やバイト数を渡すことは普通に行われる方法でありこの記事で取り上げられてるstrncpyもそのようなデザインとなっていますね。
「関数に配列を渡した瞬間、呼び出された側は「これが何バイトあるか」を一切知る術が無いのです。」というのは無駄に不安をあおる表現になっていると思います。
「一切知る術が無い」は大げさすぎる表現でした。必要な場合は別の引数として渡すことで対策されていますので、言語仕様としては知るすべがない、という表現に修正させていただきました。
の中で
とありますが、Pythonのリファレンス実装的な存在であるCPythonやJavaのOpenJDKのJITコンパイラのソースを見るとstrncpyも使われており7.2以前のLinuxカーネルと状況は変わらないことが分かります。PythonやJavaの言語レベルでのみ着目して「バッファオーバーフローの心配をしない」と断ずるべきではないと思いますがどうでしょうか。
終章に
と説明するのも適当ではないと思います。
ご指摘通り、根本から消し去れているわけではなく、下の実装レイヤーには「Cの文字列問題」は残り続けていますので、完全に安心できるわけではないという内容を記載しました。
1969年というのはB言語を起源に持つC言語の0で終端する文字列のことを言われていて、2026年というのはLinux 7.2での
strncpyの根絶を言われてるのだと思います。Linux 7.2で
strncpyが根絶してもなおC言語の0で終端する文字列の問題が解決したわけではないため大げさな話をされてると思います。Linux 7.2の kernel/ 以下だけを見ても strncpyこそ見当たらないもののバッファオーバーフローを引き起こす危険性のある strcpyやstrcatやsprintf、文字列を切り取ってしまう危険性のある snprintf もいまだ使われおり、あるいはユーザープログラムや各種ライブラリからもこれらの関数が根絶した事実もないため依然としてC言語の文字列の危険性が問題として存在している状況に変わりはありません。
ご指摘通り、strncpyによる一つの罠が対策されただけですので、ミスリードを起こす表現になってしまっていました。これは一つの節目であって、まだまだ長い返済の道のりは続くという内容に修正させていただきました。
と説明されてますが恐らくはABIとAPIを混同されていると思います。
文字列長とフレキシブル配列メンバ による文字列を表す構造体を定義し
それを使用する文字列APIを用意しそちらへの移行を行うということはABIを変更せず当たり前に可能であり、古いAPIを使用する既存のCコード資産が軒並み動かなくなるということはありません。
時期を見て危険なAPIは廃止するのが良いでしょう。
C言語の言語仕様を変えて
char*を(ptr, length)のペアに変えた場合は、ABI互換の問題が発生すると思われます。一方、ご指摘のようにポインタと長さがペアとなった文字列型を新たに追加する場合は、ABI互換の問題は発生しませんので、この両方を記載するようにしました。と説明されていますが、A TUTORIAL INTRODUCTION TO THE LANGUAGE Bを見ると
となっており、終端が
0x04であるASCIIのEOTと説明されていることを考えるとNUL終端とは異なるのではないでしょうか。ご指摘ありがとうございます。
B言語の文字列の終端文字
'*e'については、C言語の直接の起源に近いUnix/PDP系のB言語を記述したKernighanのチュートリアルで「ASCII EOT(End of Transmission、値004)」と説明されており、B言語の文字列をNUL終端とする記載は不適切でした。一方で、Thinkage社の「B Language Reference Manual」には、'*e'を「ASCII NUL(000)」とする記述もあり、資料間で終端文字の値に差異があることから、記事ではB言語の文字列についての記載を「文字列の最後に終端文字を置く方式」と修正しました。Thinkage社の「B Language Reference Manual」とは以下のものだと思いますが
版権表示が
となっておりベル研でのB言語の開発からは期間がだいぶ経ってからのものであることが分かります。
と記載があるものなのでベル研B言語の仕様の参考にはするべき資料ではないと思います。
という説明ですが、高級言語で文字列を扱えるのはCOBOLやPL/I、LISPが先行してたと思いますし、それ以前のアセンブリ言語の時代でも文字列を扱うプログラムはあったと思います。終端を表す方法も文字列長を使用する方法もB言語やBCPLの発明ではないのでは?