RHEL9での変更点(セキュリティ編:Part2 SELinuxのパフォーマンス向上について)

まあ、一言で言えばダイエット。

こんにちは。SIOS OSSエバンジェリスト/セキュリティ担当の面 和毅です。

まもなくRed Hat Enterprise 9(以下RHEL9)が出るという事で、RHEL9での主な変更点がどうなっているかが気になるところだと思います。

ここではRHEL9 betaを用いて、RHEL9での特にセキュリティ面での変更点を見ていきたいと思います(betaなので勿論リリース時に変更される可能性もありますが)。

今回はSELinuxのパフォーマンス改善について見ていきたいと思います。

参考にしたのは、RHEL9のリリースノートになります。

以下のような主な変更点を詳しく見ていきたいと思います(以下の項目は予定ですので必要に応じて追加・削除が発生します)。

  • SELinuxの変更
  • SCAP Contents(Compliance as Code)の変更
  • OpenSSHの変更

SELinuxの変更

SELinuxに関しては、主な変更点として

  • SELinuxで/etc/selinux/configで「SELinux=disabled」が効かなくなる(ハングします)
  • パフォーマンスの向上

が挙げられています。今回はSELinuxのパフォーマンス改善について見ていきたいと思います。

SELinuxのパフォーマンス改善

今回、SELinuxのパフォーマンス改善の内容が詳しくこちらのRED HAT BLOG (Ondrej Mosnacek氏)に載っています。このRED HAT BLOGをベースにして詳しく見ていきましょう。

まず今回の改善点ですが、大きく分けて

  • Linux Kernelでの改善点(LSMで入れてあるSELinux部分の改善点) : Linux Kernel 5.7(Fedora32で使用), 5.9で導入
  • ユーザランド(SELinux Userspace tools)での改善点 : SELinux userspace tools 3.2(Fedora 34で使用)で導入

の2つに大別できます。それぞれが入り乱れた変更になっているので、変更点は何か、というカットから詳しく見ていきます。

使用メモリの改善点

SELinuxにはポリシ内で「遷移」と呼ばれているものが二種類あります。ドメイン遷移とファイル名遷移と呼ばれるものです。この辺の構造は昔から変わっていなくて、過去(2005年ぐらい)にトレーニングで使用した(講師やってました)説明文から引っ張り出してきましたが、TEルールで言うところの

  • type_transition src_type tgt_type : process default_type ;

    src_typeのプロセスがtgt_typeのファイルを実行すると、プロセスにはdefault_typeドメインが設定される

  • type_transition src_type tgt_type : file-related-object default_type (filename);

    src_typeのプロセスがtgt_typeのディレクトリに新しいファイル関連のオブジェクト(ファイル、ディレクトリなど)を作成すると、新規オブジェクトに、default_typeが設定される

になります。「遷移」という翻訳がややこしくしちゃったんですが、日本語でのニュアンスとは微妙に異なっていて、ドメイン遷移はプロセスを起動する際のドメインの遷移を表していますが、ファイル名遷移に関しては「ターゲットで指定されているディレクトリに、新たにファイルを作成した際にデフォルトで付加するTypeをこうする」という意味になります。

Ondrej氏が着目したのが、このtype_transitionでファイル名遷移を定義しているけれども、その量が多すぎない?という点です。

(タイプ遷移、ファイル名遷移に関わらず)遷移に関するTEルールはsesearchコマンド(setools-consoleパッケージに含まれています)を用いて”sesearch -T”で見ることが出来ます。RHEL8.1で実行してみると


[root@rhel81 ~]# sesearch -T > transition_rhel81
[root@rhel81 ~]# grep -c 'device_t:' transition_rhel81
151069
[root@rhel81 ~]# grep -v -c 'device_t:' transition_rhel81
92584
[root@rhel81 ~]# wc -l transition_rhel81
243653 transition_rhel81

の様に、device_tに関するファイル名遷移だけで15万行もある事になり、ドメイン遷移・ファイル名遷移を合わせた全体の60%を占めていることになります。

このtype transition文は


type_transition abrt_handle_event_t device_t:blk_file fixed_disk_device_t bcache9;
type_transition abrt_handle_event_t device_t:blk_file fixed_disk_device_t dm-0;
type_transition abrt_handle_event_t device_t:blk_file fixed_disk_device_t dm-1;
...
type_transition cinder_api_t device_t:blk_file removable_device_t sr3;
type_transition cinder_api_t device_t:blk_file removable_device_t sr4;
...

等と、ほぼdevice_tタイプが振られているディレクトリに対しての定義になっています。

しかし、単純にこれらのルールを削除してしまうとリグレッションが発生してしまう可能性があります。そこでOndrej氏はここの処理を削除する代わりにメモリ内でのストレージを最適化してメモリ使用量を減らせないかと考え、新たにLinux Kernel/User Spaceの双方を書き換えました。

ポリシ内を精査し、(tgt_type, file-related-object,name)をセットにして見てみると、このセットはほとんど同じdefault_typeに紐付いていることがわかりました。実際、type_transitionのルール数を見てみると


[root@rhel81 ~]# cat transition_rhel81 |sed s/":"/" "/g|sed s/";"//g |awk '{print "("$3","$4","$6"),"$5}'|sort|more
(NetworkManager_etc_t,dir,),NetworkManager_etc_rw_t
(NetworkManager_etc_t,file,),NetworkManager_etc_rw_t
(NetworkManager_exec_t,process,),NetworkManager_t
(NetworkManager_exec_t,process,),NetworkManager_t
(NetworkManager_exec_t,process,),NetworkManager_t
(NetworkManager_exec_t,process,),NetworkManager_t
(NetworkManager_exec_t,process,),NetworkManager_t
(NetworkManager_exec_t,process,),NetworkManager_t
(NetworkManager_exec_t,process,),NetworkManager_t
(NetworkManager_exec_t,process,),NetworkManager_t
(NetworkManager_exec_t,process,),NetworkManager_t
(NetworkManager_exec_t,process,),NetworkManager_t
(NetworkManager_exec_t,process,),NetworkManager_t
(NetworkManager_initrc_exec_t,process,),initrc_t
(NetworkManager_initrc_exec_t,process,),initrc_t
(NetworkManager_initrc_exec_t,process,),initrc_t
(NetworkManager_initrc_exec_t,process,),initrc_t
(NetworkManager_initrc_exec_t,process,),initrc_t
(NetworkManager_initrc_exec_t,process,),initrc_t
(NetworkManager_initrc_exec_t,process,),initrc_t
(NetworkManager_initrc_exec_t,process,),initrc_t
--snip--
(zoneminder_script_exec_t,process,[),zoneminder_script_t
(zoneminder_script_exec_t,process,[),zoneminder_script_t
(zos_remote_exec_t,process,),zos_remote_t
(zos_remote_exec_t,process,),zos_remote_t
(zos_remote_exec_t,process,),zos_remote_t
(zos_remote_exec_t,process,),zos_remote_t
(zos_remote_exec_t,process,),zos_remote_t
(zos_remote_exec_t,process,),zos_remote_t
(zos_remote_exec_t,process,),zos_remote_t
(zos_remote_exec_t,process,),zos_remote_t
(zos_remote_exec_t,process,),zos_remote_t
(zos_remote_exec_t,process,),zos_remote_t
[root@rhel81 ~]# cat transition_rhel81 |sed s/":"/" "/g|sed s/";"//g |awk '{print "("$3","$4","$6"),"$5}'|sort|wc
243653  243653 10389647
[root@rhel81 ~]# cat transition_rhel81 |sed s/":"/" "/g|sed s/";"//g |awk '{print "("$3","$4","$6"),"$5}'|sort|uniq|wc
6237    6237  243453

の様になり、行数で見てみると24万行以上あったものが重複を数えないと6300行未満に収まることがわかります。

そこで、kernel/selinux/ss/policydb.hファイルで


RHEL8の場合)
95 struct filename_trans {
96         u32 stype;              /* current process */
97         u32 ttype;              /* parent dir context */
98         u16 tclass;             /* class of new object */
99         const char *name;       /* last path component */
100 };

をキーとしてdefault_typeに結びつけていたものを


RHEL9の場合)
94 struct filename_trans_key {
95         u32 ttype;              /* parent dir context */
96         u16 tclass;             /* class of new object */
97         const char *name;       /* last path component */
98 };
99

の様にし、これとstypeのリストを組み合わせてdefault_typeに結びつけるという方法で、無駄なメモリの消費量を抑えることに成功しました。この為にこのコミットで、Linux Kernel内での表現方法を変更しています。

————————————–

Linux Kernel内部の違い

ざっくりと(本当にざっくりと)簡単に、Linux Kernel内での処理の違いをRHEL8とRHEL9bで見てみましょう。

RHEL8の場合

以下で、RHEL8の場合のSELinuxポリシ読み込みの流れを見ていきましょう。

  1. SELinuxのポリシはselinuxfsを通じてバイナリファイルからkernelに読み込ませます。selinux_files構造体の定義がsecurity/selinux/selinuxfs.cで定義されており
    
    1896 static int sel_fill_super(struct super_block *sb, void *data, int silent)
    1897 {
    1898         struct selinux_fs_info *fsi;
    1899         int ret;
    1900         struct dentry *dentry;
    1901         struct inode *inode;
    1902         struct inode_security_struct *isec;
    1903
    1904         static const struct tree_descr selinux_files[] = {
    1905                 [SEL_LOAD] = {"load", &sel_load_ops, S_IRUSR|S_IWUSR},
    1906                 [SEL_ENFORCE] = {"enforce", &sel_enforce_ops, S_IRUGO|S_IWUSR},
    1907                 [SEL_CONTEXT] = {"context", &transaction_ops, S_IRUGO|S_IWUGO},
    1908                 [SEL_ACCESS] = {"access", &transaction_ops, S_IRUGO|S_IWUGO},
    1909                 [SEL_CREATE] = {"create", &transaction_ops, S_IRUGO|S_IWUGO},
    1910                 [SEL_RELABEL] = {"relabel", &transaction_ops, S_IRUGO|S_IWUGO},
    1911                 [SEL_USER] = {"user", &transaction_ops, S_IRUGO|S_IWUGO},
    1912                 [SEL_POLICYVERS] = {"policyvers", &sel_policyvers_ops, S_IRUGO},
    1913                 [SEL_COMMIT_BOOLS] = {"commit_pending_bools", &sel_commit_bools_ops, S_IWUSR},
    1914                 [SEL_MLS] = {"mls", &sel_mls_ops, S_IRUGO},
    1915                 [SEL_DISABLE] = {"disable", &sel_disable_ops, S_IWUSR},
    1916                 [SEL_MEMBER] = {"member", &transaction_ops, S_IRUGO|S_IWUGO},
    1917                 [SEL_CHECKREQPROT] = {"checkreqprot", &sel_checkreqprot_ops, S_IRUGO|S_IWUSR},
    1918                 [SEL_REJECT_UNKNOWN] = {"reject_unknown", &sel_handle_unknown_ops, S_IRUGO},
    1919                 [SEL_DENY_UNKNOWN] = {"deny_unknown", &sel_handle_unknown_ops, S_IRUGO},
    1920                 [SEL_STATUS] = {"status", &sel_handle_status_ops, S_IRUGO},
    1921                 [SEL_POLICY] = {"policy", &sel_policy_ops, S_IRUGO},
    1922                 [SEL_VALIDATE_TRANS] = {"validatetrans", &sel_transition_ops,
    1923                                         S_IWUGO},
    1924                 /* last one */ {""}
    1925         };
    

    となっています。

  2. sel_load_opsの定義もsecurity/selinux/selinuxfs.c中で定義されており
    
    588 static const struct file_operations sel_load_ops = {
    589         .write          = sel_write_load,
    590         .llseek         = generic_file_llseek,
    591 };
    592
    

    となっています。

  3. このsel_write_load()も同様にsecurity/selinux/selinuxfs.cで
    
    531 static ssize_t sel_write_load(struct file *file, const char __user *buf,
    532                               size_t count, loff_t *ppos)
    533
    534 {
    535         struct selinux_fs_info *fsi = file_inode(file)->i_sb->s_fs_info;
    536         ssize_t length;
    537         void *data = NULL;
    --snip--
    565         length = security_load_policy(fsi->state, data, count);
    566         if (length) {
    567                 pr_warn_ratelimited("SELinux: failed to load policy\n");
    568                 goto out;
    569         }
    --snip--
    

    として定義されていて、security_load_policy()を呼び出してポリシをロードしています。

  4. このsecurity_load_policy()はsecurity/selinux/ss/services.c中で
    
    2082 /**
    2083  * security_load_policy - Load a security policy configuration.
    2084  * @data: binary policy data
    2085  * @len: length of data in bytes
    2086  *
    2087  * Load a new set of security policy configuration data,
    2088  * validate it and convert the SID table as necessary.
    2089  * This function will flush the access vector cache after
    2090  * loading the new policy.
    2091  */
    2092 int security_load_policy(struct selinux_state *state, void *data, size_t len)
    2093 {
    2094         struct policydb *policydb;
    2095         struct sidtab *oldsidtab, *newsidtab;
    2096         struct policydb *oldpolicydb, *newpolicydb;
    2097         struct selinux_mapping *oldmapping;
    2098         struct selinux_map newmap;
    2099         struct sidtab_convert_params convert_params;
    2100         struct convert_context_args args;
    2101         u32 seqno;
    2102         int rc = 0;
    2103         struct policy_file file = { data, len }, *fp = &file;
    --snip--
    2119
    2120         if (!state->initialized) {
    2121                 rc = policydb_read(policydb, fp);
    2122                 if (rc) {
    2123                         kfree(newsidtab);
    2124                         goto out;
    2125                 }
    2126
    --snip--
    

    として定義されており、policydb_read()を呼び出しています。

  5. policydb_read()はsecurity/selinux/ss/policydb.c中で定義されており
    
    2298  * Read the configuration data from a policy database binary
    2299  * representation file into a policy database structure.
    2300  */
    2301 int policydb_read(struct policydb *p, void *fp)
    2302 {
    2303         struct role_allow *ra, *lra;
    2304         struct role_trans *tr, *ltr;
    2305         int i, j, rc;
    --snip--
    2519
    2520         rc = filename_trans_read(p, fp);
    2521         if (rc)
    2522                 goto bad;
    2523
    --snip--
    

    としてfilename_trans_readを呼び出しています。

  6. filename_trans_read()は
    
    1920 static int filename_trans_read(struct policydb *p, void *fp)
    1921 {
    1922         struct filename_trans *ft;
    1923         struct filename_trans_datum *otype;
    1924         char *name;
    1925         u32 nel, len;
    1926         __le32 buf[4];
    1927         int rc, i;
    --snip--
    1937         for (i = 0; i < nel; i++) {
    1938                 otype = NULL;
    1939                 name = NULL;
    1940
    1941                 rc = -ENOMEM;
    1942                 ft = kzalloc(sizeof(*ft), GFP_KERNEL);
    1943                 if (!ft)
    1944                         goto out;
    

    として定義されており、filename_transのサイズでメモリをアロケートします。

RHEL9bの場合

一方、RHEL9bの場合のSELinuxポリシ読み込みの流れを見ていきます。

  1. 1-5. sel_fill_super()からpolicydb_read()辺りまでは、ほぼ同じ流れになります。
  2. filename_trans_read()は

    
    2062 static int filename_trans_read(struct policydb *p, void *fp)
    2063 {
    2064         u32 nel;
    2065         __le32 buf[1];
    2066         int rc, i;
    2067
    2068         if (p->policyvers < POLICYDB_VERSION_FILENAME_TRANS)
    2069                 return 0;
    2070
    2071         rc = next_entry(buf, fp, sizeof(u32));
    2072         if (rc)
    2073                 return rc;
    2074         nel = le32_to_cpu(buf[0]);
    2075
    2076         if (p->policyvers < POLICYDB_VERSION_COMP_FTRANS) {
    2077                 p->compat_filename_trans_count = nel;
    2078
    2079                 rc = hashtab_init(&p->filename_trans, (1 << 11));
    2080                 if (rc)
    2081                         return rc;
    2082
    2083                 for (i = 0; i < nel; i++) {
    2084                         rc = filename_trans_read_helper_compat(p, fp);
    2085                         if (rc)
    2086                                 return rc;
    2087                 }
    2088         } else {
    2089                 rc = hashtab_init(&p->filename_trans, nel);
    2090                 if (rc)
    2091                         return rc;
    2092
    2093                 for (i = 0; i < nel; i++) {
    2094                         rc = filename_trans_read_helper(p, fp);
    2095                         if (rc)
    2096                                 return rc;
    2097                 }
    2098         }
    2099         hash_eval(&p->filename_trans, "filenametr");
    2100         return 0;
    2101 }
    

    と定義されています。上から見ていきましょう。

  3. hastab_init()でハッシュテーブルを初期化しています。1 << 11は100000000000 (2048)になります。hashtab_init()はsecurity/selinux/ss/hashtab.cで定義されており
    
    /*
    * Here we simply round the number of elements up to the nearest power of two.
    * I tried also other options like rounding down or rounding to the closest
    * power of two (up or down based on which is closer), but I was unable to
    * find any significant difference in lookup/insert performance that would
    * justify switching to a different (less intuitive) formula. It could be that
    * a different formula is actually more optimal, but any future changes here
    * should be supported with performance/memory usage data.
    *
    * The total memory used by the htable arrays (only) with Fedora policy loaded
    * is approximately 163 KB at the time of writing.
    */
    static u32 hashtab_compute_size(u32 nel)
    {
    return nel == 0 ? 0 : roundup_pow_of_two(nel);
    }
    int hashtab_init(struct hashtab *h, u32 nel_hint)
    {
    u32 size = hashtab_compute_size(nel_hint);
    /* should already be zeroed, but better be safe */
    h->nel = 0;
    h->size = 0;
    h->htable = NULL;
    if (size) {
    h->htable = kcalloc(size, sizeof(*h->htable), GFP_KERNEL);
    if (!h->htable)
    return -ENOMEM;
    h->size = size;
    }
    return 0;
    }
    

    となっています。こちらのhashtab_initにより引数nel_hintで渡されたサイズのメモリを割当てています。ここのコメントに書かれている通り、Fedoraのポリシをロードしてもメモリの使用量は163KB程度になるようです。

  4. filename_trans_read()の次の処理を見ると、ハッシュテーブルを初期化(メモリを割り当て)後にfilename_trans_read_helper_compat(p,fp)を呼び出しています。この関数はsecurity/selinux/ss/policydb.cで定義されており
    
    1889 static int filename_trans_read_helper_compat(struct policydb *p, void *fp)
    1890 {
    1891         struct filename_trans_key key, *ft = NULL;
    1892         struct filename_trans_datum *last, *datum = NULL;
    1893         char *name = NULL;
    1894         u32 len, stype, otype;
    1895         __le32 buf[4];
    1896         int rc;
    1897
    1898         /* length of the path component string */
    1899         rc = next_entry(buf, fp, sizeof(u32));
    1900         if (rc)
    1901                 return rc;
    1902         len = le32_to_cpu(buf[0]);
    1903
    1904         /* path component string */
    1905         rc = str_read(&name, GFP_KERNEL, fp, len);
    1906         if (rc)
    1907                 return rc;
    1908
    1909         rc = next_entry(buf, fp, sizeof(u32) * 4);
    1910         if (rc)
    1911                 goto out;
    1912
    1913         stype = le32_to_cpu(buf[0]);
    1914         key.ttype = le32_to_cpu(buf[1]);
    1915         key.tclass = le32_to_cpu(buf[2]);
    1916         key.name = name;
    1917
    1918         otype = le32_to_cpu(buf[3]);
    1919
    1920         last = NULL;
    1921         datum = policydb_filenametr_search(p, &key);
    1922         while (datum) {
    1923                 if (unlikely(ebitmap_get_bit(&datum->stypes, stype - 1))) {
    1924                         /* conflicting/duplicate rules are ignored */
    1925                         datum = NULL;
    1926                         goto out;
    1927                 }
    1928                 if (likely(datum->otype == otype))
    1929                         break;
    1930                 last = datum;
    1931                 datum = datum->next;
    1932         }
    1933         if (!datum) {
    1934                 rc = -ENOMEM;
    1935                 datum = kmalloc(sizeof(*datum), GFP_KERNEL);
    1936                 if (!datum)
    1937                         goto out;
    --snip--
    

    となっています。少しだけ中を覗くと

  5. filename_trans_key構造体はsecurity/selinux/ss/policydb.hで定義されており
    
    94 struct filename_trans_key {
    95         u32 ttype;              /* parent dir context */
    96         u16 tclass;             /* class of new object */
    97         const char *name;       /* last path component */
    98 };
    

    になっています。また、filename_trans_datum構造体も同じファイルで定義されており

    
    100 struct filename_trans_datum {
    101         struct ebitmap stypes;  /* bitmap of source types for this otype */
    102         u32 otype;              /* resulting type of new object */
    103         struct filename_trans_datum *next;      /* record for next otype*/
    104 };
    

    となっています。

  6. このfilename_trans_keyを用いて、
    
    452 struct filename_trans_datum *policydb_filenametr_search(
    453         struct policydb *p, struct filename_trans_key *key)
    454 {
    455         return hashtab_search(&p->filename_trans, key, filenametr_key_params);
    456 }
    

    の様にhashtable_search()を用いてデータの検索を行います。

  7. hashtab_search()はsecurity/selinux/ss/hashtab.hで定義されており
    
    90 /*
    91  * Searches for the entry with the specified key in the hash table.
    92  *
    93  * Returns NULL if no entry has the specified key or
    94  * the datum of the entry otherwise.
    95  */
    96 static inline void *hashtab_search(struct hashtab *h, const void *key,
    97                                    struct hashtab_key_params key_params)
    98 {
    99         u32 hvalue;
    100         struct hashtab_node *cur;
    101
    102         if (!h->size)
    103                 return NULL;
    104
    105         hvalue = key_params.hash(key) & (h->size - 1);
    106         cur = h->htable[hvalue];
    107         while (cur) {
    108                 int cmp = key_params.cmp(key, cur->key);
    109
    110                 if (cmp == 0)
    111                         return cur->datum;
    112                 if (cmp < 0)
    113                         break;
    114                 cur = cur->next;
    115         }
    116         return NULL;
    117 }
    

    の様にハッシュテーブルをkeyを用いて検索し、データ列(datum)を返します。

この様に、RHEL8では単純に広く構造体を取ってfilename_trans_read()関数でロードしていましたが、RHEL9ではハッシュテーブルを最適化してキーを使用しデータをロードする様になっています。

UserLandでの違い

次に、UserLandの方(libsepol)の違いを見てみましょう。

UserLandの場合には、Linux Kernel側で見た様なハッシュテーブルをkeyで検索する、という形式に変えたためにバイナリポリシの構造を合わせたものになります。こちらは説明としては冗長になるのと、コード変更の量がLinux Kernelの時よりも多いので解説は省きますが

でlibsepolに変更が加えられています。

Role 遷移部分の改良

ファイル名遷移の他にも、Role遷移の処理にも改良が加えられています。少し見てみましょう。

RHEL8の場合

  1. Role遷移の設定はsecurity/selinux/ss/policydb.c中のpolicydb_read()関数でバイナリポリシを読んでpolicydb()構造体に格納しており
    
    2297 /*
    2298  * Read the configuration data from a policy database binary
    2299  * representation file into a policy database structure.
    2300  */
    2301 int policydb_read(struct policydb *p, void *fp)
    2302 {
    2303         struct role_allow *ra, *lra;
    2304         struct role_trans *tr, *ltr;
    2305         int i, j, rc;
    2306         __le32 buf[4];
    2307         u32 len, nprim, nel;
    --snip--
    2454         rc = next_entry(buf, fp, sizeof(u32));
    2455         if (rc)
    2456                 goto bad;
    2457         nel = le32_to_cpu(buf[0]);
    2458         ltr = NULL;
    2459         for (i = 0; i < nel; i++) {
    2460                 rc = -ENOMEM;
    2461                 tr = kzalloc(sizeof(*tr), GFP_KERNEL);
    2462                 if (!tr)
    2463                         goto bad;
    2464                 if (ltr)
    2465                         ltr->next = tr;
    2466                 else
    2467                         p->role_tr = tr;
    2468                 rc = next_entry(buf, fp, sizeof(u32)*3);
    2469                 if (rc)
    2470                         goto bad;
    2471
    2472                 rc = -EINVAL;
    2473                 tr->role = le32_to_cpu(buf[0]);
    2474                 tr->type = le32_to_cpu(buf[1]);
    2475                 tr->new_role = le32_to_cpu(buf[2]);
    2476                 if (p->policyvers >= POLICYDB_VERSION_ROLETRANS) {
    2477                         rc = next_entry(buf, fp, sizeof(u32));
    2478                         if (rc)
    2479                                 goto bad;
    2480                         tr->tclass = le32_to_cpu(buf[0]);
    2481                 } else
    2482                         tr->tclass = p->process_class;
    2483
    2484                 rc = -EINVAL;
    2485                 if (!policydb_role_isvalid(p, tr->role) ||
    2486                     !policydb_type_isvalid(p, tr->type) ||
    2487                     !policydb_class_isvalid(p, tr->tclass) ||
    2488                     !policydb_role_isvalid(p, tr->new_role))
    2489                         goto bad;
    2490                 ltr = tr;
    2491         }
    2492
    2493         rc = next_entry(buf, fp, sizeof(u32));
    --snip--
    

    のようにpolicydb中のlistを一つずつ確認しているのがわかります。

RHEL9bの場合

  1. RHEL8の時と同じくpolicydb_read()関数を見てみると
    
    2398 /*
    2399  * Read the configuration data from a policy database binary
    2400  * representation file into a policy database structure.
    2401  */
    2402 int policydb_read(struct policydb *p, void *fp)
    2403 {
    2404         struct role_allow *ra, *lra;
    2405         struct role_trans_key *rtk = NULL;
    2406         struct role_trans_datum *rtd = NULL;
    2407         int i, j, rc;
    2408         __le32 buf[4];
    2409         u32 len, nprim, nel, perm;
    --snip--
    2572         rc = hashtab_init(&p->role_tr, nel);
    2573         if (rc)
    2574                 goto bad;
    2575         for (i = 0; i < nel; i++) {
    2576                 rc = -ENOMEM;
    2577                 rtk = kmalloc(sizeof(*rtk), GFP_KERNEL);
    2578                 if (!rtk)
    2579                         goto bad;
    2580
    2581                 rc = -ENOMEM;
    2582                 rtd = kmalloc(sizeof(*rtd), GFP_KERNEL);
    2583                 if (!rtd)
    2584                         goto bad;
    2585
    2586                 rc = next_entry(buf, fp, sizeof(u32)*3);
    2587                 if (rc)
    2588                         goto bad;
    2589
    2590                 rtk->role = le32_to_cpu(buf[0]);
    2591                 rtk->type = le32_to_cpu(buf[1]);
    2592                 rtd->new_role = le32_to_cpu(buf[2]);
    2593                 if (p->policyvers >= POLICYDB_VERSION_ROLETRANS) {
    2594                         rc = next_entry(buf, fp, sizeof(u32));
    2595                         if (rc)
    2596                                 goto bad;
    2597                         rtk->tclass = le32_to_cpu(buf[0]);
    2598                 } else
    2599                         rtk->tclass = p->process_class;
    2600
    2601                 rc = -EINVAL;
    2602                 if (!policydb_role_isvalid(p, rtk->role) ||
    2603                     !policydb_type_isvalid(p, rtk->type) ||
    --snip--
    

    の様にファイル名遷移の時と同じ様にハッシュテーブルを利用してRole遷移も処理していることがわかります。

他にもいくつかのソースに修正が加えられていますが、基本的にはハッシュテーブルを利用した処理に変更する様になっています。これにより、Role遷移時の検索時間がRHEL8の時のおよそ50%で済むことになり、パフォーマンスの改善に繋がりました。

ハッシュテーブルのインライン化

更にhttps://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24def7bb92c19337cee26d506f87dc4eeeba7a19https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=54b27f9287a7b3dfc85549f01fc9d292c92c68b9により、hashtab_search()とhashtab_inser()をヘッダーファイルに移動することでパフォーマンスの向上が出来ました。具体的には、こちらによると、

  1. ポリシのロード時間が230ms->150msになった
  2. linux-5.7カーネルソースを展開しchdon -Rでソースの全てのラベルを変更した所、576ms->522msになった
  3. “stress-ng –msg 1 –msg-ops 10000000″を実行した所、13.95s -> 12.41sになった

と、スピードの向上に寄与しています。

その他

“semodule -B”でポリシモジュールを作成する部分の実装で、不要になったコードを削除したり、頻繁に呼び出されるebitmap_cardinality()のキャッシュを実装することで、”semodule -NB”の実行時間を8.0s -> 6.3sに縮めたとの事です。

計測

ここで、実際にSELinuxのパフォーマンス改善がどの程度現れているかを、実際に測ってみてみましょう。

  • 環境:VMWare Guest (2 CPU, 4GB Memory, 40GB SCSI)
  • OS: RHEL 8.5 , RHEL 9b
  • インストール:双方ベースのみ。UnixBenchで使用するphp等をインストールしたのみで、デフォルトのインストール

この環境で、vmstatでメモリの使用率、UnixBenchでSystem Benchmark Index Scoreを計測します。正確を期するために、UnixBenchは10回計測しています。また、計測の度にシステムは一度シャットダウンし、再度立ち上げ後(OS起動時には様々なサービスが立ち上がるため)5分間待った後に計測を行っています。

vmstatの結果

OS起動後に5分間待った後に、vmstatで10秒おきに30分間計測を行いました。freeの値をグラフに示しています(単位はMBになります)。

図を見てわかるとおり、RHEL 8.5のSELinux有効・無効のfreeの間隔(つまり凡そSELinuxが使用しているメモリ)に比べて、RHEL9bの有効・無効の間隔のほうが狭くなっていることがわかります。差の部分を数値化してグラフにしてみましょう。

図を見てわかるとおり、RHEL8.5の場合にはfreeの差分(凡そSELinuxが使用しているメモリ)が80MB近辺だったのに対し、RHEL9bの場合には20-30MBとなっています。

このことから、SELinuxを有効にした際のメモリの消費量はRHEL9bによりかなり改善されていることがわかります。

unixbenchの結果

OS起動後に5分間待った後に、UnixBenchで10回計測を行いました。「System Benchmark Index Score(以下Score)」の値で、SELinux有効の際と無効の際の差分(無効-有効)をグラフに示しています。

図を見てわかるとおり、RHEL 8.5のSELinux有効・無効でのScoreの差分に関してはばらつきがあるものの、RHEL8.5に比べて特段RHEL9bが良くなっている様には見えません。それもそのはずで、上記で説明した通り、今回の改善の主眼は

  • SELinuxを有効にした際のLinux Kernelが使用するメモリ量の改善
  • semodule -NBなど、ポリシをコンパイルする際の時間の改善

にあったため、それ以外の処理速度に関しては特に改良が加えられていないためです。

以上から、今回のRHEL9bでのSELinuxのパフォーマンス改善に関しては

メモリの使用率は改善があったが、それで通常のスピードが凄く上がったわけではない(つまりダイエットした人が常時素早く動けるようになるわけではないのと同じこと)

ということがわかりました。でも

小さいことは良いことだから、ね。Android(SELinux有効だから)とか組み込み系は恩恵めっちゃあると思うし。

次回予告

今回でRHEL9上のSELinuxの変更点は終わりになります。次回はSCAP(Compliance As Code)の変更部分に触れたいと思います。

日々のメモを更新しています。

セキュリティ関係ニュースを更新しています。個別で情報出せるようになる前の簡単な情報・リンクなんかも載せていきます。



タイトルとURLをコピーしました