access_ok
access_ok()は参照メモリが、アクセス可能領域かのチェックを行います。返り値が0だとエラーです。実務運用はユーザ空間のメモリを、参照できるかどうかを行います。current_thread_info()->addr_limit.segに参照可能なリミットが設定され、それを超えるメモリは参照できません。システム依存ですが、ユーザー空間は0x00000000-0xbfffffff、カーネル空間は0xc0000000‐0xffffffffです。
従って、current_thread_info()->addr_limit.seg=0xC0000000なら、ユーザー空間メモリで、カーネル空間メモリはエラーとなります。current_thread_info()->addr_limit.seg=0xffffffffで、ユーザ/カーネル空間に関係なく全領域のメモリはOKです。
デフォルトではプロセスは、current_thread_info()->addr_limit.seg=0xC0000000とし、ユーザ空間メモリとやり取りするデバイスドライバ等は、カーネル空間アドレスでコールする事による脆弱性が発生する故、そのような関数には、access_ok()により、ユーザ空間のメモリかどうかチェックを行っています。
なお、lkmからそのような関数をコールする場合、current_thread_info()->addr_limit.seg=0xFFFFFFFFとする事で、カーネル空間メモリを介してのコールが可能となります。
従って、current_thread_info()->addr_limit.seg=0xC0000000なら、ユーザー空間メモリで、カーネル空間メモリはエラーとなります。current_thread_info()->addr_limit.seg=0xffffffffで、ユーザ/カーネル空間に関係なく全領域のメモリはOKです。
デフォルトではプロセスは、current_thread_info()->addr_limit.seg=0xC0000000とし、ユーザ空間メモリとやり取りするデバイスドライバ等は、カーネル空間アドレスでコールする事による脆弱性が発生する故、そのような関数には、access_ok()により、ユーザ空間のメモリかどうかチェックを行っています。
なお、lkmからそのような関数をコールする場合、current_thread_info()->addr_limit.seg=0xFFFFFFFFとする事で、カーネル空間メモリを介してのコールが可能となります。
検証サンプル
#include <linux/module.h> #include <linux/pagemap.h> MODULE_LICENSE("GPL"); static int __init hoge_init(void) { char buf[32]; mm_segment_t old_fs, a; int ret; printk("buff addr:%p\n", buf); old_fs = get_fs(); ret = access_ok(VERIFY_WRITE, buf, sizeof(buf)) ; printk("addr limit1:%lx\n", old_fs.seg); printk("ret access1:%d\n", ret); set_fs(get_ds()); a = get_fs(); ret = access_ok(VERIFY_WRITE, buf, sizeof(buf)) ; printk("addr limit2:%lx\n", a.seg); printk("ret access2:%d\n", ret); return -1; } module_init(hoge_init)
実効結果
[root@localhost lkm]# insmod access_ok.ko insmod: error inserting 'access_ok.ko': -1 Operation not permitted [root@localhost lkm]# dmesg : [ 4247.615172] buff addr:dde77ec0 [ 4247.615177] addr limit1:c0000000 [ 4247.615179] ret access1:0 <-アクセス不可 [ 4247.615181] addr limit2:ffffffff [ 4247.615183] ret access2:1 <-アクセス可
実装
アーキテクチャ依存で、以下はx86での実装です。access_ok()が0ならエラーですが、実態は__range_not_ok(addr, size) == 0とし、__range_not_ok()が0でないならエラーとなります。typedef struct { unsigned long seg; } mm_segment_t; #define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET) #define __PAGE_OFFSET _AC(CONFIG_PAGE_OFFSET, UL) #define CONFIG_PAGE_OFFSET 0xC0000000 #define _AC(X,Y) X #define TASK_SIZE PAGE_OFFSET #define TASK_SIZE_MAX TASK_SIZE #define MAKE_MM_SEG(s) ((mm_segment_t) { (s) }) #define KERNEL_DS MAKE_MM_SEG(-1UL) #define USER_DS MAKE_MM_SEG(TASK_SIZE_MAX) #define get_ds() (KERNEL_DS) #define get_fs() (current_thread_info()->addr_limit) #define set_fs(x) (current_thread_info()->addr_limit = (x)) #define set_fs(x) (current_thread_info()->addr_limit = (x)) #define access_ok(type, addr, size) (likely(__range_not_ok(addr, size) == 0)) #define __range_not_ok(addr, size) \ ({ \ unsigned long flag, roksum; \ __chk_user_ptr(addr); \ asm("add %3,%1 ; sbb %0,%0 ; cmp %1,%4 ; sbb $0,%0" \ : "=&r" (flag), "=r" (roksum) \ : "1" (addr), "g" ((long)(size)), \ "rm" (current_thread_info()->addr_limit.seg)); \ flag; \ })__range_not_okの拡張インラインアセンブラの展開は以下の通りです。sbbはキャリアフラグも考慮したsubで、sbb %0,%0は、flag=0ですが、addr+sizeが32/64ビット(システム依存)をオーバフロしていれば、flag=-1で、sbb $0,%0はcurrent_thread_info()->addr_limit.seを超えていれば、flag - 0 - 1となります。問題なければflag=0で、そうでないならflag=-1/-2となります。
"1" (addr): roksum = addr add %3,%1 : roksum = roksum + size; sbb %0,%0 : flag = flag - flag - CF cmp %1,%4 : roksum < current_thread_info()->addr_limit.seg sbb $0,%0 : flag = flag - 0 - CF
追記
__chk_user_ptr()は、__CHECKER__なら、externとし、そうでないなら(void)0とします。これはコンパイル時、インラインのaccess_okの引数が__userとなっているかのチェック故かと思います。extern void __chk_user_ptr()は、実装されておらず、推測ですが、parseコマンドで__chk_user_ptr()を削除するか、retのみのr__chk_user_ptrのシンボルとして実装するかと思います。たぶん削除されるのでは。と思いますが・・・。(ソース内に関数/シンボルセクションでの__chk_user_ptr掛かる実装は無いようです。)#ifdef __CHECKER__ # define __user __attribute__((noderef, address_space(1))) # define __kernel __attribute__((address_space(0))) # define __safe __attribute__((safe)) # define __force __attribute__((force)) # define __nocast __attribute__((nocast)) # define __iomem __attribute__((noderef, address_space(2))) : extern void __chk_user_ptr(const volatile void __user *); extern void __chk_io_ptr(const volatile void __iomem *); #else # define __user # define __kernel # define __iomem # define __chk_user_ptr(x) (void)0 # define __chk_io_ptr(x) (void)0 : #endif