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






