copy_{to,from}_user Vs memcpy

Linux地址空間

熟悉Linux內核的開發人員都知道,Linux下的進程地址空間分爲內核空間和用戶空間,對於32bit系統來說,典型的空間劃分爲:1G(內核空間)+3G(用戶空間),對於這種劃分來說,內核空間地址範圍:0xC000 0000 ~0xFFFF FFFF,用戶空間地址範圍爲:0x0000 0000 ~ 0xBFFF FFFF。當然,爲了需要,我們可以將地址空間配置成其他方式,比如2G:2G等等。

Linux虛擬地址機制

大家知道,Linux進程中使用的地址是虛擬地址,進程在操作這些地址時,MMU通過頁表完成虛擬地址物理地址之間的映射,如果頁表miss,就觸發缺頁中斷,然後,內核通過缺頁異常處理機制分配可用的物理頁,更新關於虛擬地址和物理地址頁表項,從而完成整個虛擬地址的操作過程。

簡單明確了Linux系統關於地址空間的管理機制之後,下面,就來具體討論一下今天的主題:在內核中,如何實現內核空間和用戶空間的數據交互。

內核APIs

內核提供了用於內核空間和用戶空間數據相互拷貝的API:

  • access_ok:用於檢查用戶空間指針的有效性;
  • get_user:用於從用戶空間拷貝一個簡單的變量到內核空間,比如,1、2、4、8字節的變量。
  • put_user:用於從內核空間拷貝一個簡單的變量到用戶空間。
  • copy_to_user:用於從內核空間拷貝一個塊數據到用戶空間。
  • copy_from_user:用於從用戶空間拷貝一塊數據到內核空間。

上面這些API一般都和具體的CPU架構相關,比如,X86架構相關的實現都在/linux/arch/x86/include/asm/uaccess.h和/linux/arch/x86/lib/usercopy_32.c、usercopy_64.c這些文件中。

get/put_user與copy_(to,from}_user的區別是所拷貝的數據類型不同,前者用於簡單的數據,比如,int、long、char等,而後者用於一整塊數據的拷貝。

下面詳細的講解一下上述各個API的具體作用。

access_ok

函數的原型如下:

access_ok(type, addr, size)
  • type: VERIFY_WRITE、VERIFY_READ檢查一段地址區域是否可讀或者可寫。
  • 用於指定檢查以addr爲起始地址,長度爲size爲的用戶空間是否可讀或者可寫。
  • 如果可正常訪問,函數返回0,否則返回-EFAULT。
  • 該函數僅用於檢測用戶空間,不用於內核空間。

get/put_user

函數原型如下:
get/_user( x, ptr );
put/_user( x, ptr );

get_user和put_user用於在內核空間和用戶空間拷貝一個簡單的數據,對於像結構體這樣的數據需要使用copy_{to,from}_user。access_ok函數會檢測ptr指針的有效性,之後,通過get_user_x或者 put_user_x完成數據拷貝。相對於copy_{to,from}_user,對於小型數據的拷貝,這兩個函數的性能更好。兩個函數都是執行成功之後返回0。

copy_{to, from}_user

函數的原型如下:

copy_to_user(to, from, n)
copy_from_user(to, from, n)

兩個函數用於在內核空間和用戶空間之間拷貝數據,這個函數都是成功時返回0,失敗時返回大於0的數據,表明未能拷貝成功的字節個數。兩個函數都是通過access_ok來檢測用戶空間的地址的有效性,之後的數據拷貝實現,與CPU的體系結構有關,比如,ARM平臺下,

_copy_from_user(void *to, const void __user *from, unsigned long n)
{
	unsigned long res = n;
	might_fault();
	if (likely(access_ok(VERIFY_READ, from, n))) {
		kasan_check_write(to, n);
		res = raw_copy_from_user(to, from, n);
	}
	if (unlikely(res))
		memset(to + (n - res), 0, res);
	return res;
}

最終會調用arm平臺下的raw_copy_from_user:

`raw_copy_to_user(void __user *to, const void *from, unsigned long n)
{
#ifndef CONFIG_UACCESS_WITH_MEMCPY
	unsigned int __ua_flags;
	__ua_flags = uaccess_save_and_enable();
	n = arm_copy_to_user(to, from, n);
	uaccess_restore(__ua_flags);
	return n;
#else
	return arm_copy_to_user(to, from, n);
#endif
}`

copy_{to,from}_user Vs memcpy

還有一個經常被誤用的API,那就是,memcpy,大家可能聽過,或者看過memcpy不能用於用戶空間和內核空間之間的數據交互,那原因是什麼呢?本節將會詳細的講解copy_{to,from}_user與memcpy的區別和聯繫。

之所以在進行內核空間與用戶空間拷貝時,使用copy_{to,from}_user,主要有兩個原因:

  • copy*函數會通過access_ok檢測用戶空間地址有效性。這樣可以防止內核操作非法的地址,比如,用戶傳過來的是內核空間的地址,如果內核檢測該地址,那麼就會寫壞該地址指向的內容,造成系統崩潰。同時,也會系統安全問題,比如,黑客會故意傳入一個內核地址,並且這個地址保存着關鍵信息,比如用戶id,內核在沒有檢測的情況下,會修改地址中的內容,比如,將用戶id寫爲0,那麼黑客就可以獲取系統的最高權限。參考CVE-2017-5123內核漏洞

  • copy*函數可以在發生page fault時,進行自我修復。所謂的page fault時,是說用戶空間的地址是一個非法的地址(非棧,非堆地址),這是內核在訪問該地址時,MMU會檢測到該地址的非法性,從而產生一個異常。對於該異常,內核有兩種處理方式:

    • 向當前進行發送SIGSEGV信號,並拋出Oops信息,如果內核開啓了/proc/sys/kernel/panic_on_oops,那麼內核直接panic。
    • 處理該異常,正常返回。

    如果這時使用的是copy_{to,from}_user函數的話,其會捕獲該異常,並正常返回到用戶空間。但是如果使用的是memcpy的話,對不起,內核直接Oops或者panic。

  • 爲了更加安全,硬件加入了PAN功能(Privileged Access Never),其可以限制內核訪問用戶空間的能力,所以,訪問之前必須通過硬件指令關閉PAN,訪問完之後,開啓PAN,ARM V8架構增加了這項功能。只有copy/_{to,from}/user函數具有打開和關閉PAN的能力,所以,copy/{to,from}/_user。

所以,對於一個合格的內核開發人員,在涉及到用戶空間和內核空間數據拷貝的場景時,杜絕使用memcpy是避免出現bugs的首要注意事項。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章