ARR38-C. ライブラリ関数が無効なポインタを生成しないことを保証する
配列やオブジェクトに変更を加える C ライブラリ関数は、少なくとも 2 つの引数をとる。1 つは配列またはオブジェクトへのポインタで、もう 1 つは操作する要素の数またはバイト数を示す整数である。これらの関数に不適切な引数を与えると、引数のポインタはオブジェクトを指さないか、オブジェクトの終端を超えた場所を指し、結果として未定義の動作が生じる可能性がある。
このルールでは、ポインタの要素数(element count)はポインタが指すオブジェクトのサイズとする。このサイズは、アクセスすることが有効な要素の数として表現される。
次のコードについて考えてみる。
int arr[5]; int *p = arr; unsigned char *p2 = (unsigned char *)arr; unsigned char *p3 = arr + 2; void *p4 = arr;
ポインタ p
の要素数は、sizeof(arr) / sizeof(arr[0])
つまり 5
である。ポインタ p2
の要素数は、sizeof(int) == 4
である処理系においては sizeof(arr)
つまり 20
である。同様の処理系において、p3
の要素数は12
である。これはp3
が配列arr
の先頭から2つ先の要素を指すためである。p4
の要素カウントは、void *
ではなく unsigned char *
として解釈されるため、p2
の要素数と同じである。
ポインタと整数を引数にとるライブラリ関数
以下に挙げる標準ライブラリ関数は、1つのポインタ引数と1つのサイズ引数をとる。関数には、少なくともサイズ引数で指定される要素数で構成される有効なメモリオブジェクトをポインタが指していなければならない、という制約が伴う。
fgets() |
fgetws() |
mbstowcs() 1 |
wcstombs() 1 |
mbrtoc16() 2 |
mbrtoc32() 2 |
mbsrtowcs() 1 |
wcsrtombs() 1 |
mbtowc() 2 |
mbrtowc() 1 |
mblen() |
mbrlen() |
memchr() |
wmemchr() |
memset() |
wmemset() |
strftime() |
wcsftime() |
strxfrm()1 |
wcsxfrm()1 |
strncat()2 |
wcsncat()2 |
snprintf() |
vsnprintf() |
swprintf() |
vswprintf() |
setvbuf() |
tmpnam_s() |
snprintf_s() |
sprintf_s() |
vsnprintf_s() |
vsprintf_s() |
gets_s() |
getenv_s() |
wctomb_s() |
mbstowcs_s()3 |
wcstombs_s()3 |
memcpy_s()3 |
memmove_s()3 |
strncpy_s()3 |
strncat_s()3 |
strtok_s()2 |
strerror_s() |
strnlen_s() |
asctime_s() |
ctime_s() |
snwprintf_s() |
swprintf_s() |
vsnwprintf_s() |
vswprintf_s() |
wcsncpy_s()3 |
wmemcpy_s()3 |
wmemmove_s()3 |
wcsncat_s()3 |
wcstok_s()2 |
wcsnlen_s() |
wcrtomb_s() |
mbsrtowcs_s()3 |
wcsrtombs_s()3 |
memset_s()4 |
1 2つのポインタと1つの整数を引数にとる。整数は、入力元バッファではなく、出力先バッファの要素数のみを指定する。
2 2つのポインタと1つの整数を引数にとる。整数は、出力先バッファではなく、入力元バッファの要素数のみを指定する。
3 2つのポインタと2つの整数を引数にとる。整数はいずれもポインタの要素数に対応する。
4 1つのポインタと2つのサイズに関する整数を引数にとる。第一の整数引数は、バッファの空きバイト数を指定する。第二の整数引数はバッファに書込むバイト数を指定する。
ポインタと整数を引数にとる関数の呼出しにおいて、与えるサイズはポインタの要素数より小さくなくてはならない。
違反コード (要素カウント)
次に示す違反コード例では、間違った要素数を使って wemcpy()
を呼び出している。sizeof
演算子はサイズをバイト数で返すが、memcpy()
は wchar_t *
に基づいた要素数を使っている。
#include <string.h> #include <wchar.h> static const char str[] = "Hello world"; static const wchar_t w_str[] = L"Hello world"; void func(void) { char buffer[32]; wchar_t w_buffer[32]; memcpy(buffer, str, sizeof(str)); wmemcpy(w_buffer, w_str, sizeof(w_str)); }
適合コード (要素カウント)
ポインタが指す領域に対する処理する関数を使うときは、関数が想定する要素数に基づいてサイズを表さなくてはならない。たとえば、memcpy()
が期待する要素数はvoid *
の単位で表現され、一方、wmemcpy()
が期待する要素数はwchar_t *
の単位で表現される。下記のコードでは、sizeof
演算子を使わず、文字列中の要素数を返す呼出しを行っており、コピー関数が期待する要素数に合っている。
#include <string.h> #include <wchar.h> static const char str[] = "Hello world"; static const wchar_t w_str[] = L"Hello world"; void func(void) { char buffer[32]; wchar_t w_buffer[32]; memcpy(buffer, str, strlen(str) + 1); wmemcpy(w_buffer, w_str, wcslen(w_str) + 1); }
違反コード (ポインタ + 整数)
次に示す違反コード例では、割り当てられたメモリよりも大きなバイト数をn
に代入し、この値を memset()
に渡している。
#include <stdlib.h> #include <string.h> void f1(size_t nchars) { char *p = (char *)malloc(nchars); /* ... */ const size_t n = nchars + 1; /* ... */ memset(p, 0, n); }
適合コード (ポインタ + 整数)
この適合コードは、n
の値がポインタ p
が指す動的メモリのサイズを超えないことを保証している。
#include <stdlib.h> #include <string.h> void f1(size_t nchars) { char *p = (char *)malloc(nchars); /* ... */ const size_t n = nchars; /* ... */ memset(p, 0, n); }
違反コード (ポインタ + 整数)
次に示す違反コード例において、配列a
の要素数はARR_SIZE
個の要素である。memset()
はバイト数を (引数として) 期待しているため、配列のサイズは sizeof(float)
ではなく sizeof(int)
を使って間違って計算されている。そのため、sizeof(int) != sizeof(float)
であるような処理系では不正なポインタが生じてしまう。
#include <string.h> void f2() { const size_t ARR_SIZE = 4; float a[ARR_SIZE]; const size_t n = sizeof(int) * ARR_SIZE; void *p = a; memset(p, 0, n); }
適合コード (ポインタ + 整数)
次に示す適合コードでは、memset()
が要求する要素数は適切に計算されている。
#include <string.h> void f2() { const size_t ARR_SIZE = 4; float a[ARR_SIZE]; const size_t n = sizeof(a); void *p = a; memset(p, 0, n); }
2つのポインタと1つの整数を引数にとるライブラリ関数
次の表に示すライブラリ関数は、2つのポインタ引数と1つのサイズ引数をとる。関数には、少なくともサイズ引数で指定される要素数で構成される有効なメモリオブジェクトをいずれのポインタも指していなければならない、という制約が伴う。
memcpy() |
wmemcpy() |
memmove() |
wmemmove() |
strncpy() |
wcsncpy() |
memcmp() |
wmemcmp() |
strncmp() |
wcsncmp() |
strcpy_s() |
wcscpy_s() |
strcat_s() |
wcscat_s() |
2つのポインタと整数を引数にとる関数呼出しにおいて、引数に与えられたサイズがいずれかのポインタの要素数よりも大きくてはならない。
違反コード (2つのポインタ + 1つの整数)
この違反コード例では、n
の値を正しく計算しておらず、p
が参照するオブジェクトの終端を超えて書込みが発生する可能性があるため、診断メッセージが出力される。
#include <string.h> void f4(char p[], const char *q) { const size_t n = sizeof(p); if ((memcpy(p, q, n)) == p) { /* ... */ } }
このコードはARR01-C. 配列のサイズを求めるときに sizeof 演算子をポインタに適用しないにも違反している。
適合コード (2つのポインタ + 1つの整数)
この適合コードでは n
が文字配列のサイズと等しいことを保証している。
#include <string.h> void f4(char p[], const char *q, size_t size_p) { const size_t n = size_p; if ((memcpy(p, q, n)) == p) { /* ... */ } }
1つのポインタと2つの整数を引数にとるライブラリ関数
次の表に示すライブラリ関数は、1つのポインタ引数と2つのサイズ引数をとる。関数には、2つのサイズ引数の積で表されるバイト数を少なくとも含む有効なメモリオブジェクトをポインタが指していなければならない、という制約が伴う。
bsearch() |
bsearch_s() |
qsort() |
qsort_s() |
fread() |
fwrite() |
1つのポインタを2つの整数を引数にとる関数呼出しにおいて、第一の整数は各オブジェクトに必要なバイト数を、第二の整数は配列の要素数を表す。2つの整数の積は、unsigend char *
として表されるポインタの要素数より大きくてはならない。
違反コード (1つのポインタ + 2つの整数)
この違反コード例では、struct obj
型のオブジェクトを可変個割り当てている。関数では、「INT30-C. 符号無し整数の演算結果がラップアラウンドしないようにする」に従い、整数のラップアラウンドが発生しないようにnum_objs
が十分小さいことを確認している。詰め物の存在を考慮し、struct obj
のサイズは8バイトであると想定している。しかし、詰め物はターゲットとなるアーキテクチャやコンパイラの設定に依存するため、オブジェクトのサイズが間違った値になり、結果として、要素数も間違った値になる可能性がある。
#include <stdint.h> #include <stdio.h> struct obj { char c; int i; }; void func(FILE *f, struct obj *objs, size_t num_objs) { const size_t obj_size = 8; if (num_objs > (SIZE_MAX / obj_size) || num_objs != fwrite(objs, obj_size, num_objs, f)) { /* エラー処理 */ } }
適合コード (1つのポインタ + 2つの整数)
この適合コードではsizeof
演算子を使って正しくオブジェクトのサイズを計算し、num_objs
を使って要素数を表している。
#include <stdint.h> #include <stdio.h> struct obj { char c; int i; }; void func(FILE *f, struct obj *objs, size_t num_objs) { if (num_objs > (SIZE_MAX / sizeof(*objs)) || num_objs != fwrite(objs, sizeof(*objs), num_objs, f)) { /* エラー処理 */ } }
違反コード (1つのポインタ + 2つの整数)
この違反コードでは、関数f()
の中でfread()
を呼び出し、各size
バイトのwchar_t
型オブジェクトをnitems
個、要素数BUFSIZ
の配列wbuf
に読み込んでいる。しかし、nitems
の値を計算する式において、wchar_t
のサイズがchar
のサイズとは異なり1より大きい可能性があることを考慮してない。そのため、fread()
はwbuf
の終端を超えたポインタを使い、配列の外に値を書込む可能性がある。この操作は未定義の動作109につながり、バッファオーバーフローをもたらすことが想定される。Common Weakness Enumeration データベースでは、このプログラミングエラーは、CWE-121, "Access of memory location after end of buffer" および CWE-805, "Buffer access with incorrect length value" として取り上げられている。
#include <stddef.h> #include <stdio.h> void f(FILE *file) { wchar_t wbuf[BUFSIZ]; const size_t size = sizeof(*wbuf); const size_t nitems = sizeof(wbuf); size_t nread; nread = fread(wbuf, size, nitems, file); }
適合コード (1つのポインタ + 2つの整数)
この適合コードでは、fread()
がファイルから読み取る要素の上限を正しく計算している。
#include <stddef.h> #include <stdio.h> void f(FILE *file) { wchar_t wbuf[BUFSIZ]; const size_t size = sizeof(*wbuf); const size_t nitems = sizeof(wbuf) / size; size_t nread; nread = fread(wbuf, size, nitems, file); }
リスク評価
呼び出されるライブラリ関数によっては、攻撃者がヒープもしくはスタックバッファオーバーフローの脆弱性を利用して任意のコードを実行できる可能性がある。
ルール |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
ARR38-C |
高 |
高 |
中 |
P18 |
L1 |
自動検出(最新の情報はこちら)
ツール |
バージョン |
チェッカー |
説明 |
---|---|---|---|
PRQA QA-C | 8.1 | 2931 | 実装済み |
関連するガイドライン
C セキュアコーディングスタンダード | API00-C. 関数のなかで引数を検証する ARR01-C. 配列のサイズを求めるときに sizeof 演算子をポインタに適用しない INT30-C. 符号無し整数の演算結果がラップアラウンドしないようにする |
ISO/IEC TS 17961 | Forming invalid pointers by library functions [libptr] |
MITRE CWE | CWE-121, Stack-based buffer overflow CWE-805, Buffer access with incorrect length value |
参考資料
[ISO/IEC TS 17961] | Programming Languages, Their Environments and System Software Interfaces |
翻訳元
これは以下のページを翻訳したものです。
ARR38-C. Guarantee that library functions do not form invalid pointers (revision 95)