三行で頼む1
- realloc(ptr, 0)がfree(ptr)と等価だったのは過去の話
- reallocの第二引数は絶対に0にするな
- ISO CもPOSIXも信用するな
これはC言語 Advent Calendar 2016の14日目の記事です。
話の枕
私がことあるごとに言っている持ちネタとして、bzero関数を削除するなってのがあります。bzero関数はISO Cの標準関数ではありませんが、X/Openという古の規格団体が定義していたもので、現在でもUNIX系OSでは普通に使えます。これは、指定されたメモリブロックを単純にゼロバイトでクリアするための関数です。たとえば次のようにして使います:
#include <strings.h>
char buf[42];
bzero(buf, sizeof (buf));
標準C言語に親しんでいる人間ならば分かると思いますが、これはより汎用的な関数であるところのmemsetを使って次のように書くことができます:
#include <string.h>
char buf[42];
memset(buf, 0, sizeof (buf));
「じゃあbzeroなんて要らないんじゃないの?」という意見もまあもっともなのですが、しかしながら、memset関数の99%はゼロクリアのために使われると言っても過言ではないのではないかと思うほど、この関数の第二引数は0のことが多いと思います。しかも、memset関数の第二引数と第三引数は非常に紛らわしいため、しばしば取り違えられるバグの温床となります。
したがって、私なんぞはゼロクリア専用の関数を用意しておいてもバチは当たらないと思うのですが、そういう意見をrejectする時に必ず使われる印籠のような言葉があります。それが直交性という考え方です。つまり、標準関数としては、機能が重複しない(=直交した)ミニマルな関数セットだけを用意して、プログラマにはその組み合わせであらゆることを実現させようという考え方です。これに従えば、「既にmemset関数でできることのために、新しく関数を用意する必要はないのだ」ということですね。POSIXもそういう思想に毒されている部分があるため、哀れbzero関数はPOSIX:2004で廃止予定とされ、POSIX:2008で葬り去られたというわけです。まあ、あんまり名前も良くなかったし、<strings.h>なんてVAX時代の遺物みたいなヘッダファイルをインクルードしないといけなかったしで、仕方がないかなあ、という側面もありますが。一方その頃Win32はZeroMemoryマクロを使い続けた。
でも、直交性に拘るのであれば、既にISO Cには直交性を欠いた冗長な関数群が存在していました。それは、malloc/realloc/calloc/freeです。これらは、ISO C:1995(C95)まではすべてreallocとmemsetの組み合わせで表現可能でした:
ptr = realloc(NULL, size); /* malloc(size) と等価 */
realloc(ptr, 0); /* free(ptr) と等価 */
/* 以下はcalloc(memb, size)と等価(ただし掛け算のオーバーフロー時の振る舞いを除く) */
ptr = realloc(NULL, memb*size);
if (ptr)
memset(ptr, 0, memb*size);
したがって、mallocもcallocもfreeも冗長な存在だったのです。
realloc(ptr, 0)の変遷
まあ、ここまでは話の枕ということで、別にbzeroも直交性もどうでもよくて、本題はreallocの仕様についてです。上で書いたreallocの万能性は、実は過去のものです。現行規格であるISO C:2011(C11)や、その一つ前のISO C:1999(C99)へと厳格に準拠したreallocには、freeと等価な機能はありません。
非NULLなptrに対するrealloc(ptr, 0)の結果には次のような変遷があります:
C90/C95
realloc(ptr, 0)はNULLを返し、ptrは解放される。
C99/C11
realloc(ptr, 0)は次のうちのいずれかとなる:
- NULLが返される。この時、ptrは解放されず有効なポインタとして残っている。
- 有効なポインタが返される。それがptrと等しいこともある。
現在のドラフト
realloc(ptr, 0)は次のうちのいずれかとなる:
- NULLが返される。この時、ptrが解放されず有効なポインタとして残るか、それとも解放されるかは実装依存である。
- 有効なポインタが返される。それがptrと等しいこともある。
C90/C95とC99/C11は完全に矛盾しており、現在のドラフトは、この両方を折衷したものとなっております。結局、この二度の変更は、両方とも互換性を破壊しています。つまり、
- C90/C95 → C99/C11 realloc(ptr, 0)がfree(ptr)と等価であると期待している(=正しくC90/C95に準拠した)プログラムが破壊される。
- C99/C11 → 現在のドラフト NULLが返ってきたときにptrが有効なポインタとして残っていることに期待して律儀にfree(ptr)を呼ぶ(=正しくC99/C11に準拠した)プログラムが破壊される。
困ったことですね。
ま、そもそもreallocをfreeの代わりに使うなよって話ではあるのですが、古くからC言語を使っている人間には、realloc(ptr, 0)がfree(ptr)と等価だというのは半ば常識化していた割に、それがC99で破壊されたということは意外と知られてないのではないかと思います。
POSIXはどうか
POSIXでも現行のPOSIX:2008ではC99と同様の規定になっています。一方、POSIX:2004ではC95と同様となっていました。しかしながら、POSIX:2004のNormative ReferenceとされているのはC99ですので、これは単純なPOSIX:2004のミスですね。
世の中の実装はどうか
もともとの仕様が「realloc(ptr, 0)はfree(ptr)と等価」ですから、1999年ごろまでのrealloc実装のほとんどはそういう仕様だったと思いますが、C99において互換性のない変更がされてしまっているので、ここで世の中の実装も「過去の互換性を守る」か「C99を満たす」かの二者択一を迫られることになりました。現行の規格ドラフトに変更されるきっかけとなったDefect Report #400では、次の3つの実装が紹介されています(ptrが非NULLの場合のみを抜粋、errnoはISO C的にはあんまり意味が無いのでこれも省略):
- AIX : realloc(ptr, 0)はNULLを返し、ptrを解放する
- BSD : realloc(ptr, 0)はNULLを返し、ptrはそのままにする
- glibc : realloc(ptr, 0)はNULLを返し、ptrを解放する
この三つの実装ではすべてNULLを返しますが、AIXとglibcはC90/C95の、BSDはC99/C11の仕様に従っています。BSDがC99に追従しているのは意外ですが、これはおそらくjemallocがスクラッチから書き起こされたときにC99に厳格に準拠したのだと思います。現在のドラフトは、実装が混在しているこの現状を追認する形で定められたようです。
結論
というわけで、reallocの第二引数に0を渡してはいけません。C99以降の現在の世界では、前節で述べた通り、実装によって仕様が分裂しているため、メモリリークしないようにするポータブルな方法が存在しないからです。
そもそも、reallocというのは使い方を間違えやすい関数で、
- 渡したptrと戻り値が違う場合の扱いをミスする
- 意図的ではなく、うっかり第二引数に0を渡してしまう
- NULLが返ってきた場合の扱いをミスする
というバグを良く見かけます。結果として、良くてDoS、悪くて任意コード実行の脆弱性の温床になってきたりしてきました。reallocを使うときは十分に注意しましょうね。
余談1: 規格の文言の変遷
C90/C95
ちょっと手元にISO C:1990/1995の規格書がないため、「標準Cライブラリ ANSI/ISO/JIS C規格(P.J.プラウガー著・福富寛、門倉明彦、清水恵介訳)」からreallocの定義を引用します(この本の「C規格における定義」セクションは、JIS C:1993の規格書からの転載となっているので、内容に関してはISO C:1990/1995と等価です):
7.10.3.4 realloc 関数
形式
#include <stdlib.h>
void *realloc(void *ptr, size_t size);
機能
realloc関数は、ptrが指すオブジェクトの大きさを、sizeが示す大きさに変更する。新しい大きさと古い大きさの小さい方までのオブジェクトの内容は、変更しない。新しい大きさのほうが大きいとき、オブジェクトの新しく割り付けられた部分の値は、不定とする。ptrが空ポインタのとき、realloc関数は、指定された大きさでのmalloc関数と同じ動作をする。それ以外の場合、ptrがcalloc関数、malloc関数もしくはrealloc関数の呼出しによって以前に返されたポインタと一致しないとき、または領域がfree関数もしくはrealloc関数の呼出しによって解放されているとき、その動作は未定義とする。領域の割付けができなかったとき、ptrが指すオブジェクトは変化しない。sizeが0でかつptrが空ポインタでないとき、ptrが指すオブジェクトを開放する。
返却値
realloc関数は、空ポインタまたは割り付けた領域へのポインタのどちらかを返す。ただし、割り付けた領域は、以前の位置と同じであるとは限らない。
本文中、私が太字で強調した部分が、今回の話題に関係するところです。
C99/C11
ANSI/ISO/IEC/9899-1999からの引用です:
7.20.3 Memory management functions
(中略)
If the size of the space requested is zero, the behavior is implementationdefined: either a null pointer is returned, or the behavior is as if the size were some nonzero value, except that the returned pointer shall not be used to access an object.
(中略)
7.20.3.4 The realloc function
Synopsis
#include <stdlib.h>
void *realloc(void *ptr, size_t size);
Description
The realloc function deallocates the old object pointed to by ptr and returns a pointer to a new object that has the size specified by size. The contents of the new object shall be the same as that of the old object prior to deallocation, up to the lesser of the new and old sizes. Any bytes in the new object beyond the size of the old object have indeterminate values.
If ptr is a null pointer, the realloc function behaves like the malloc function for the specified size. Otherwise, if ptr does not match a pointer earlier returned by the calloc, malloc, or realloc function, or if the space has been deallocated by a call to the free or realloc function, the behavior is undefined. If memory for the new object cannot be allocated, the old object is not deallocated and its value is unchanged.
Returns
The realloc function returns a pointer to the new object (which may have the same value as a pointer to the old object), or a null pointer if the new object could not be allocated.
本文中、私が太字で強調した部分が、今回の話題に関係するところです。
現在のドラフト
先ほど紹介したDR#400のように変更されます:
7.22.3 Memory management functions
7.22.3.5 The realloc function
If memory for the new object cannot be allocated, the old object is not deallocated and its value is unchanged.
↓
If size is non-zero and memory for the new object is not allocated, the old object is not deallocated. If size is zero and memory for the new object is not allocated, it is implementation-defined whether the old object is deallocated. If the old object is not deallocated, its value shall be unchanged.
The realloc function returns a pointer to the new object (which may have the same value as a pointer to the old object), or a null pointer if the new object could not be allocated.
↓
The realloc function returns a pointer to the new object (which may have the same value as a pointer to the old object), or a null pointer if the new object has not been allocated.
7.31 Future library directions
7.31.12 General utilities <stdlib.h>
Invoking realloc with a size argument equal to zero is an obsolescent feature.
変更点を太字で強調しておきました。過去に注釈なしに行った非互換な変更を「obsolescent」で済ませたよこの子。
余談2: C99で何が起こったのか
現在のC Standard Committeeの議論の内容は比較的よく公開されているため、たとえば先ほどのDR#400がどのような議論を経て決定されたのかは2つの議事録(N1603(PDF), N1642(PDF))を見ると分かります。しかし、C99のころの議事録はもっと大雑把で、しかもほとんどドラフトが公開されていないため、いつ、どの段階で変更が行われたのかは良く分からない部分があります。
私が調べた限り、次のことが分かりました:
-
1998年8月3日のドラフト(N843)では、まだ
If the realloc function returns a null pointer when size is zero and ptr is not a null pointer, the object it pointed to has been freed.
の文言がある。 - 1998年10月5-9日のSanta Cruzにおける会合の議事録に、「realloc rewording」の文字がある。(Status of approved proposals for C9X, FCD (Pre-Portland)(N858))
この「rewording」で、「rewording」では済まない変更が行われたのかもしれません。
問題は、この変更が意図的なのか、それとも何らかの間違いなのかということなのですが、C99のAnnex J (informative) Portability issuesという章で示されている「unspecified behavior」の列挙の中に、次の一文があります:
The amount of storage allocated by a successful call to the calloc, malloc, or realloc function when 0 bytes was requested (7.20.3).
ここは規格本体ではなく単なる参考情報ですが、わざわざこのように明記してあるということは、sizeが0の場合の動作が一つに定まらない(=freeと等価だったC95までとは明確に非互換である)ことを承知の上で、あのような規格本文にしたというわけです **2016-12-14追記:**コメントでC90規格からの引用をしていただいてますが、何故かC90にも同様の記述があったので、この記述をもって「意図的である」とする根拠が全く無くなりました。というかC90の校正もガバガバやんけ。
しかしながら一方で、C言語の標準化委員会の出しているRationale for International Standard — Programming Languages — C Revision 5.10 April-2003 (PDF)という文書には、規格書とは矛盾したことが書いてあります:
If the first argument is not null, and the second argument is 0, then the call frees the memory pointed to by the first argument, and a null argument may be returned; C99 is consistent with the policy of not allowing zero-sized objects.
当然、Rationaleには規格としての効力はないので、規格本文の方が優先されますが、せっかくRationaleを用意しておきながら、「なぜそういう変更を行ったのか」が書かれていないのみならず、規格と矛盾したことが書かれているのは困ったものですね。