security_reclaim_label()関数

v8.4の開発サイクルで指摘された、pg_securityシステムカタログのエントリを回収する機能を実装してみた。security_reclaim_label()というSQL関数を実行すると、もう参照されていないセキュリティコンテキストを探し出して削除する。

postgres=# SELECT security_reclaim_label();
NOTICE:  secattr="unconfined_u:object_r:sepgsql_table_t:s0",     \
    secid=16589 on public.t1 was reclaimed
NOTICE:  secattr="unconfined_u:object_r:sepgsql_table_t:s0:c1",  \
    secid=16590 on public.t1 was reclaimed
NOTICE:  secattr="unconfined_u:object_r:sepgsql_table_t:s0:c2",  \
    secid=16591 on public.t1 was reclaimed
NOTICE:  secattr="unconfined_u:object_r:sepgsql_table_t:s0:c3",  \
    secid=16592 on public.t1 was reclaimed
 security_reclaim_label
                                              • -
4 (1 row)

元々、DBオブジェクトのセキュリティ属性(文字列)をタプル毎に関連付けると不必要なストレージ消費に繋がるので、個々のタプルにはSecurity IDを持たせて、そのIDでpg_securityシステムカタログ内のエントリーを参照するようにしている。

性能上の問題から参照カウンタを付ける事はできない(pg_securityへの書き込み競合)し、そもそも多数のオブジェクトがセキュリティコンテキストの文字列表現を共有する前提なので、頻繁に回収処理を行う必然性はない。

したがって、以下のような実装を採用した。

pg_security システムカタログの修正

#define SecurityRelationId        3405

CATALOG(pg_security,3405)
{
    /* OID of the table which refers the entry */
    Oid     relid;

    /* 'a' = security_acl, 'l' = security_label */
    char    seckind;

    /* Text representation of security attribute */
    text    secattr;
} FormData_pg_security;

上記のように、pg_securityシステムカタログにrelidフィールドを追加。どのテーブルがこのエントリを参照しているのかを記録する事ができる。

もはや参照されていないエントリを探すには、その探索処理の間にDBの更新処理を行ってはならないが、粒度をテーブル単位に分割する事で、ロック期間を最小限にすることができる。

security_reclaim_label()関数

security_reclaim_label()関数は内部的に以下のようなSQLを実行する。

LOCK pg_security IN SHARE MODE;

DELETE FROM pg_security WHERE relid=$1 AND relkind=$2 AND oid NOT IN
   (SELECT security_label_to_secid(security_label) FROM <table>)

ここで$1は対象テーブルのoid、$2はSECKIND_SECURITY_LABELがマッチする。
LOCK構文で、対象テーブルに対して他のトランザクションがこれを更新することを禁止する。
結果、特定テーブルから『参照されていない』セキュリティ属性はpg_securityシステムカタログから削除される。

なお、この処理は信頼済みの内部処理であるため、一時的に行レベルアクセス制御は適用されなくなる。