まあ、一言で言えばダイエット。
こんにちは。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ポリシ読み込みの流れを見ていきましょう。
- 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 };
となっています。
- 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
となっています。
- この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()を呼び出してポリシをロードしています。
- この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()を呼び出しています。
- 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を呼び出しています。
- 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-5. sel_fill_super()からpolicydb_read()辺りまでは、ほぼ同じ流れになります。
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 }
と定義されています。上から見ていきましょう。
- 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程度になるようです。
- 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--
となっています。少しだけ中を覗くと
- 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 };
となっています。
- この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()を用いてデータの検索を行います。
- 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の時よりも多いので解説は省きますが
- https://github.com/SELinuxProject/selinux/commit/42ae834a7428c57f7b2a9f448adf4cf991fa3487
- https://github.com/SELinuxProject/selinux/commit/8206b8cb00392aab358f4eeae38f98850438085c
でlibsepolに変更が加えられています。
Role 遷移部分の改良
ファイル名遷移の他にも、Role遷移の処理にも改良が加えられています。少し見てみましょう。
RHEL8の場合
- 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の場合
- 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=24def7bb92c19337cee26d506f87dc4eeeba7a19、https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=54b27f9287a7b3dfc85549f01fc9d292c92c68b9により、hashtab_search()とhashtab_inser()をヘッダーファイルに移動することでパフォーマンスの向上が出来ました。具体的には、こちらによると、
- ポリシのロード時間が230ms->150msになった
- linux-5.7カーネルソースを展開しchdon -Rでソースの全てのラベルを変更した所、576ms->522msになった
- “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)の変更部分に触れたいと思います。
日々のメモを更新しています。
セキュリティ関係ニュースを更新しています。個別で情報出せるようになる前の簡単な情報・リンクなんかも載せていきます。