PostgreSQLビルトイン関数の定義

PostgreSQLにはCREATE FUNCTION命令という関数を定義するSQL構文があるが、そんなことしなくても使えるビルトイン関数というのが数多くある。
以下は、PostgreSQLソースコードを修正してビルトイン関数を追加するためのメモ。
[手順]
1. 関数の実体を実装する
返り値の型はDatumで、引数はPG_FUNCTION_ARGSというマクロを使用して定義する。
関数がコールされる時には、FunctionCallInfoDataオブジェクトへのポインタが渡されるが、別のマクロを利用して簡単に引数を参照できるようになっている。

2.src/include/pg_proc.h にエントリを追加する
手順1で実装した関数をSQLから呼び出せるよう、『関数一覧表(pg_procシステムカタログ)』に登録する。
DATA(insert 1242 ( boolin .... )); のように、初期化プロセス(initdbコマンドによって実行される)でpg_procシステムカタログに追加するデータが並んでいるので、ここに先ほど追加した関数のエントリを書き加える。
各データ要素の意味はPostgreSQL ドキュメント pg_procシステムカタログに載っているので、関数の性質に応じてパラメータを調節する。

3.再ビルド&再インストール&データベースクラスタの構築
PostgreSQLを再ビルドする。この時、一旦 make clean しないといけない。
なぜなら、make中で実行されるスクリプトが pg_proc.h を元に backend/utils/fmgrtab.c というファイルを生成するが、これはpg_proc.hを更新しても再作成されないから(Makefileのバグ?)。

以下は、実際に SE-PgSQL 向けに SELinuxパーミッションチェックを行うSQL関数 selinux_check_perm() をビルトイン関数として追加した例。

[関数の実装]
Datum
selinux_check_perm(PG_FUNCTION_ARGS)
{
    text                *tselcon = PG_GETARG_TEXT_P(0);
    security_class_t    objcls = PG_GETARG_UINT32(1);
    access_vector_t     av = PG_GETARG_UINT32(2);
    security_context_t  tcon;
    security_id_t       tsid;
    int                 rc;

    if (PG_ARGISNULL(0) || PG_ARGISNULL(1) || PG_ARGISNULL(2))
        PG_RETURN_BOOL(false);
    
    /* text 型の引数を'\0'終端文字列に変換 */
    tcon = DatumGetCString(DirectFunctionCall1(textout,
                                 PointerGetDatum(tselcon)));
    /* Security Context → SID への変換 */
    if (avc_context_to_sid(tcon, &tsid))
        selerror("could not obtain SID of '%s'", tcon);

    /* 許可された操作かどうかをチェック */
    rc = avc_has_perm_noaudit(SelinuxClientSid, tsid, objcls, av, 
                              &AvcEntryRef, NULL);
    sidput(tsid);

    PG_RETURN_BOOL(rc == 0);
}
<||
||>[pg_proc.hへのエントリの追加]
DATA(insert OID = 2711 ( selinux_check_perm PGNSP PGUID 12 f f f f
                         s 3 16 "25 23 23" _null_ _null_ _null_
                         selinux_check_perm - _null_ ));
(※) 実際には全部一行で書く

特に、以下の要素に注意してパラメータを設定
proisstrict(7番目) ... True だと、引数がNULLの時には関数が呼び出されずにNULLを返却すると仮定される。しかし、true/falseの2値関数としたいので、ここは false に設定。
provolatile(9番目) ... i (= 同一引数に対し常に同じ値を返す), s (=セッション内では、同一引数に対し常に同じ値を返す), v (=常に異なった値を返す可能性がある)、のいずれか。最適化に使われるっぽい。
pronargs(10番目) ... 関数の引数の数
prorettype(11番目) ... 関数の返り値の型(pg_typeシステムカタログに記載)のOID。bool型なので16
proallargtypes(12番目) ... 関数の引数の型。text, int4, int4 なので、順にそれらのOIDを並べる。

[実行結果]
[実行結果]
postgres=# select * from drink
      where selinux_check_perm(security_context, 55, 16);
 id | name  | price |        security_context
----+-------+-------+---------------------------------
  1 | water |   110 | user_u:object_r:unconfined_t:s0
  2 | coke  |   120 | user_u:object_r:unconfined_t:s0
  3 | juice |   120 | user_u:object_r:unconfined_t:s0
  4 | beer  |   240 | user_u:object_r:unconfined_t:s0
  5 | wine  |   360 | user_u:object_r:unconfined_t:s0
(5 rows)

postgres=#

このように、SQL文の中で関数として利用できる。
SE-PgSQLでは、SELECT/UPDATE/DELETE文を発行した時に、selinux_check_perm()関数を勝手に where 句の中に埋め込んで、アクセス権のない行を結果セットから取り除く。ユーザ視点では、それらの行はそもそも存在しないように見える。