安全なハンドル

ポインタ

C では言語がオブジェクト指向を直接には支援してくれないが、構造体を生成する関数とその構造体を操作する関数を組にして使うような形式にすることはよくあって、擬似的なオブジェクト指向と言えるだろう。

例えばごく簡単な例として整数を格納する構造体を生成する関数と構造体に格納された整数を表示する関数で表すとこういう要領だ。

// foo.h
struct foo {
  int n;
};

struct foo* make_foo(int n);
void foo_print(struct foo* obj);
// foo.c
#include <stdio.h>
#include <stdlib.h>
#include "foo.h"

struct foo* make_foo(int n) {
  struct foo* obj = malloc(sizeof(struct foo));
  if(obj) obj->n = n;
  return obj;
}

void foo_print(struct foo* obj) {
  printf("%d\n", obj->n);
}

だが、これには問題もある。 構造体の型を厳密に記述するとなるとヘッダファイルに構造体の定義を書かざるを得ない。 アクセス制御のない C では構造体のメンバにアクセスすることを禁止できないのだ。 以下のように、あたりまえに構造体を書換えることが出来てしまう。

//test.c
#include <assert.h>
#include "foo.h"

int main(void) {
  struct foo* obj=make_foo(1);
  assert(obj);
  obj->n=2; // 構造体の内容を直接変更できる
  foo_print(obj);
  return 0;
}

ここからはライブラリが用意した方法以外でオブジェクトの内容に触れることが出来ないようにする方法を考えてみることにする。 前提として、ライブラリのソースコードを改変することは出来ないものとしよう。 改変を許すならさすがにどうとでも出来てしまうので。

void*

では構造体の型をヘッダに書かずに済ませることは出来ないだろうかと考えるとまず思い浮かぶのは void* にキャストするという方法だ。

// foo.h
void* make_foo(int n);
void foo_print(void* obj);
// foo.c
#include <stdio.h>
#include <stdlib.h>
#include "foo.h"

struct foo {
  int n;
};

void* make_foo(int n) {
  struct foo* obj = malloc(sizeof(struct foo));
  if(obj) obj->n = n;
  return (void*) obj;
}

void foo_print(void* obj) {
  printf("%d\n", ((struct foo*)obj)->n);
}

だが、構造体の定義はヘッダファイルに書いていなくとも調べればすぐにわかることだ。 あらためて定義した上でキャストすることは出来る。

//test.c
#include <assert.h>
#include "foo.h"

struct foo {
  int n;
};

int main(void) {
  void* obj=make_foo(1);
  assert(obj);
  ((struct foo*)obj)->n=2; // キャストすれば同じこと
  foo_print(obj);
  return 0;
}

フィルタ

しばしば使われるのは、ポインタと適当な値の排他的論理和をとったものを返してハンドルとして使うというものだ。

// foo.h
intptr_t make_foo(int n);
void foo_print(intptr_t obj);
// foo.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "foo.h"

struct foo {
  int n;
};

#define mask 0xdeadbeaf

intptr_t make_foo(int n) {
  struct foo* obj = malloc(sizeof(struct foo));
  if(obj) obj->n = n;
  return (intptr_t) obj^mask;
}

void foo_print(intptr_t obj) {
  printf("%d\n", ((struct foo*)(obj^mask))->n);
}

返される値はもはやポインタではなく、指す先はデタラメだ。 排他的論理和をとるのに使う値はプロセスごとに生成するとよいだろう。 プロセス ID のようなものがある環境ならそれを使えば充分かもしれない。 だが、その値の生成方法が知れればオブジェクトのアドレスは容易に知れてしまうので充分な隠蔽とは云えない。

//test.c
#include <stdint.h>
#include "foo.h"

struct foo {
  int n;
};

int main(void) {
  intptr_t handle=make_foo(1);
  ((struct foo*)(handle^0xdeadbeaf))->n=2; // 隠しきれない!
  foo_print(handle);
  return 0;
}

テーブルとインデックス

それではいっそのことポインタを管理するのはライブラリ側でやって、返すのはテーブルのインデックスでよいのではないか。

// foo.h
int make_foo(int n);
void foo_print(int handle);
void foo_close(int handle);
// foo.c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "foo.h"

struct foo {
  int n;
};

static struct foo* table[10]={};
static const int table_size = sizeof(table)/sizeof(struct foo*);

int make_foo(int n) {
  struct foo* obj = malloc(sizeof(struct foo));
  assert(obj);
  obj->n = n;
  int i;
  for(i=0; i<table_size; i++) if(!table[i]) break;
  if(i<table_size) { table[i]=obj; return i; }
  else { free(obj); obj=NULL; return -1; }
}

void foo_close(int handle) {
  assert(table[handle]);
  free(table[handle]);
  table[handle]=NULL;
}

void foo_print(int handle) {
  assert(table[handle]);
  printf("%d\n", table[handle]->n);
}

この例では問題点の説明のためにオブジェクトを解放する関数も用意した。 問題となるのはハンドルの使い回しである。 ハンドルがテーブルのインデックスである以上、クローズしたハンドルが以下のように使い回されることがありうる。

//test.c
#include <stdint.h>
#include "foo.h"

struct foo {
  int n;
};

int main(void) {
  int handle1=make_foo(1);
  foo_print(handle1);
  foo_close(handle1);
  int handle2=make_foo(2);
  foo_print(handle1); // 解放したはずのハンドルなのに!
  return 0;
}

カウンタ

ハンドルの使い回しの問題を解決するためには、ハンドルが使い回されるたびにカウンタを増加させていく方法で対処できる。

テーブルのインデックスをハンドルの上位バイトに、カウンタをハンドルの下位バイトに格納する方法で書いてみた。

// foo.h
int make_foo(int n);
void foo_print(int handle);
void foo_close(int handle);
// foo.c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "foo.h"

struct foo {
  int n;
};

static struct {int count; struct foo* obj;} table[10]={};
static const int table_size = 10;

int make_foo(int n) {
  struct foo* obj = malloc(sizeof(struct foo));
  assert(obj);
  obj->n = n;
  int i;
  for(i=0; i<table_size; i++) if(!table[i].obj) break;
  if(i<table_size) {
    table[i].obj=obj;
    return (i<<16)|(table[i].count);
  }
  else { free(obj); obj=NULL; return -1; }
}

void foo_close(int handle) {
  int index=handle>>16;
  assert(table[index].obj);
  assert(table[index].count==(handle&0xffff));
  table[index].count++;
  free(table[index].obj);
  table[index].obj=NULL;
}

void foo_print(int handle) {
  int index=handle>>16;
  assert(table[index].obj);
  assert(table[index].count==(handle&0xffff));
  printf("%d\n", table[index].obj->n);
}

これならクローズされたハンドルを使おうとすれば検出することが可能だ。

//test.c
#include <stdint.h>
#include "foo.h"

struct foo {
  int n;
};

void table_print(void);

int main(void) {
  int handle1=make_foo(1);
  foo_print(handle1);
  foo_close(handle1);
  int handle2=make_foo(2);
  foo_print(handle2);
  foo_print(handle1); // クローズされているので assert する!
  return 0;
}

もちろん、通常はここまで厳格に検査する必要はない。 しかし、プラグインを追加することで拡張する種類のソフトウェアでは何らかの方法でハンドルの妥当性を検証する方法を用意しておかなければ脆弱性を生むことになりかねないということもあるだろう。 バイナリレベルでは型も役に立たない。

Document ID: a0edf0c7b3d133ff8a6b6c8778fea403