網上很多人提問爲什麼一定要copy_from_user,也有人解答。比如百度一下:
但是這裏面很多的解答沒有回答到點子上,不能真正回答這個問題。我決定寫篇文章正式回答一下這個問題,消除讀者的各種疑慮。
這個問題,我認爲需要從2個層面回答
-
第一個層次是爲什麼要拷貝,可不可以不拷貝?
-
第二個層次是爲什麼要用copy_from_user而不是直接memcpy
爲什麼要拷貝
拷貝這個事情是必須的,這個事情甚至都跟Linux都沒有什麼關係。比如Linux有個kobject結構體,kobject結構體裏面有個name指針:
truct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct kernfs_node *sd; /* sysfs directory entry */
struct kref kref;
...
};
但我們設置一個設備的名字的時候,其實就是設置device的kobject的name:
int dev_set_name(struct device *dev, const char *fmt, ...)
{
va_list vargs;
int err;
va_start(vargs, fmt);
err = kobject_set_name_vargs(&dev->kobj, fmt, vargs);
va_end(vargs);
return err;
}
驅動裏面經常要設置name,比如:
dev_set_name(&chan->dev->device, "dma%dchan%d",
device->dev_id, chan->chan_id);
但是Linux沒有傻到直接把name的指針這樣賦值:
truct device {
struct kobject kobj;
...
};
dev_set_name(struct device *dev, char *name)
{
dev->kobj.name = name_param; //假想的爛代碼
}
如果它這樣做了的話,那麼它就完蛋了,因爲驅動裏面完全可以這樣設置name:
river_func()
{
char name[100];
....
dev_set_name(dev, name);
}
傳給dev_set_name()的根本是個stack區域的臨時變量,是一個匆匆過客。而device的name對於這個device來講,必須長期存在。所以你看內核真實的代碼,是給kobject的name重新申請一份內存,然後把dev_set_name()傳給它的name拷貝進來:
int kobject_set_name_vargs(struct kobject *kobj, const char *fmt,
va_list vargs)
{
const char *s;
..
s = kvasprintf_const(GFP_KERNEL, fmt, vargs);
...
if (strchr(s, '/')) {
char *t;
t = kstrdup(s, GFP_KERNEL);
kfree_const(s);
if (!t)
return -ENOMEM;
strreplace(t, '/', '!');
s = t;
}
kfree_const(kobj->name);
kobj->name = s;
return 0;
}
這個問題在用戶空間和內核空間的交界點上是完全存在的。假設內核裏面某個驅動的xxx_write()是這麼寫的:
struct globalmem_dev {
struct cdev cdev;
unsigned char *mem;
struct mutex mutex;
};
static ssize_t globalmem_write(struct file *filp, const char __user * buf,
size_t size, loff_t * ppos)
{
struct globalmem_dev *dev = filp->private_data;
dev->mem = buf; //假想的爛代碼
return ret;
}
這樣的代碼絕對是要完蛋的,因爲dev->mem這個內核態的指針完全有可能被內核態的中斷服務程序、被workqueue的callback函數、被內核線程,或者被用戶空間的另外一個進程通過globalmem_read()去讀,但是它卻指向一個某個進程用戶空間的buffer。
在內核裏面直接使用用戶態傳過來的const char __user * buf指針,是災難性的,因爲buf的虛擬地址,只在這個進程空間是有效的,跨進程是無效的。但是調度一直在發生,中斷是存在的,workqueue是存在的,內核線程是存在的,其他進程是存在的,原先的用戶進程的buffer地址,切了個進程之後就不知道是個什麼鬼!換個進程,頁表都特碼變了,你這個buf地址還能找着人?進程1的buf地址,在下面的紅框裏面,什麼都不是!
所以內核的正確做法是,把buf拷貝到一個跨中斷、跨進程、跨workqueue、跨內核線程的長期有效的內存裏面:
struct globalmem_dev {
struct cdev cdev;
unsigned char mem[GLOBALMEM_SIZE];//長期有效
struct mutex mutex;
};
static ssize_t globalmem_write(struct file *filp, const char __user * buf,
size_t size, loff_t * ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data;
....
if (copy_from_user(dev->mem + p, buf, count))//拷貝!!
ret = -EFAULT;
else {
*ppos += count;
ret = count;
...
}
記住,對於內核而言,用戶態此刻傳入的指針只是一個匆匆過客,只是個燦爛煙花,只是個曇花一現,瞬間即逝!它甚至都沒有許諾你天長地久,隨時可能劈腿!
所以,如果一定要給個需要拷貝的理由,原因就是防止劈腿!別給我扯些有的沒的。
必須拷貝的第二個理由,可能與安全有關。比如用戶態做類似pwritev, preadv這樣的調用:
ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset);
ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt, off_t offset);
用戶傳給內核一個iov的數組,數組每個成員描述一個buffer的基地址和長度:
struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
用戶傳過來的是一個iovec的數組,裏面有每個iov的len和base(base也是指向用戶態的buffer的),傳進內核的時候,內核會對iovec的地址進行check,保證它確實每個buffer都在用戶空間,並且會把整個iovec數組拷貝到內核空間:
ssize_t import_iovec(int type, const struct iovec __user * uvector,
unsigned nr_segs, unsigned fast_segs,
struct iovec **iov, struct iov_iter *i)
{
ssize_t n;
struct iovec *p;
n = rw_copy_check_uvector(type, uvector, nr_segs, fast_segs,
*iov, &p);
...
iov_iter_init(i, type, p, nr_segs, n);
*iov = p == *iov ? NULL : p;
return n;
}
這個過程是有嚴格的安全考量的,整個iov數組會被copy_from_user(),而數組裏面的每個buf都要被access_ok的檢查:
ssize_t rw_copy_check_uvector(int type, const struct iovec __user * uvector,
unsigned long nr_segs, unsigned long fast_segs,
struct iovec *fast_pointer,
struct iovec **ret_pointer)
{
...
if (copy_from_user(iov, uvector, nr_segs*sizeof(*uvector))) {
ret = -EFAULT;
goto out;
}
...
ret = 0;
for (seg = 0; seg < nr_segs; seg++) {
void __user *buf = iov[seg].iov_base;
ssize_t len = (ssize_t)iov[seg].iov_len;
...
if (type >= 0
&& unlikely(!access_ok(buf, len))) {
ret = -EFAULT;
goto out;
}
...
}
out:
*ret_pointer = iov;
return ret;
}
access_ok(buf, len)是確保從buf開始的len長的區間,一定是位於用戶空間的,應用程序不能傳入一個內核空間的地址來傳給系統調用,這樣用戶可以通過系統調用,讓內核寫壞內核本身,造成一系列內核安全漏洞。
假設內核不把整個iov數組通過如下代碼拷貝進內核:
copy_from_user(iov, uvector, nr_segs*sizeof(*uvector))
而是直接訪問用戶態的iov,那個這個access_ok就完全失去價值了,因爲,用戶完全可以在你做access_ok檢查的時候,傳給你的是用戶態buffer,之後把iov_base的內容改成指向一個內核態的buffer去。
所以,從這個理由上來講,最開始的拷貝也是必須的。但是這個理由遠遠沒有最開始那個隨時劈腿的理由充分!
爲什麼不直接用memcpy?
這個問題主要涉及到2個層面,一個是copy_from_user()有自帶的access_ok檢查,如果用戶傳進來的buffer不屬於用戶空間而是內核空間,根本不會拷貝;二是copy_from_user()有自帶的page fault後exception修復機制。
先看第一個問題,如果代碼直接用memcpy():static ssize_t globalmem_write(struct file *filp, const char __user * buf,
size_t size, loff_t * ppos)
{
struct globalmem_dev *dev = filp->private_data;
....
memcpy(dev->mem + p, buf, count))
return ret;
}
memcpy是沒有這個檢查的,哪怕用戶傳入進來的這個buf,指向的是內核態的地址,這個拷貝也是要做的。試想,用戶做系統調用的時候,隨便可以把內核的指針傳進來,那用戶不是可以隨便爲所欲爲?比如內核的這個commit,引起了著名的安全漏洞:
CVE-2017-5123
就是因爲,作者把有access_ok的put_user改爲了沒有access_ok的unsafe_put_user。這樣,用戶如果把某個進程的uid地址傳給內核,內核unsafe_put_user的時候,不是完全可以把它的uid改爲0?
所以,你看到內核修復這個CVE的時候,是對這些地址進行了一個access_ok的:
下面我們看第二個問題,page fault的修復機制。假設用戶程序隨便胡亂傳個用戶態的地址給內核:
void main(void)
{
int fd;
fd = open("/dev/globalfifo", O_RDWR, S_IRUSR | S_IWUSR);
if (fd != -1) {
int ret = write(fd, 0x40000000, 10);//假想的代碼
if (ret < 0)
perror("write error\n");
}
}
0x40000000這個地址是用戶態的,所以access_ok是沒有問題的。但是這個地址,根本什麼有效的數據、heap、stack都不是。我特碼就是瞎寫的。
如果內核驅動用memcpy會發生什麼呢?我們會看到一段內核Oops:
用戶進程也會被kill掉:
# ./a.out
Killed
當然如果你設置了/proc/sys/kernel/panic_on_oops爲1的話,內核就不是Opps這麼簡單了,而是直接panic了。
但是如果內核用的是copy_from_user呢?內核是不會Oops的,用戶態應用程序也是不會死的,它只是收到了bad address的錯誤:
# ./a.out
write error
: Bad address
內核只是友好地提示你用戶闖進來的buffer地址0x40000000是個錯誤的地址,這個系統調用的參數是不對的,這顯然更加符合系統調用的本質。
內核針對copy_from_user,有exception fixup機制,而memcpy()是沒有的。詳細的exception修復機制見:
https://www.kernel.org/doc/Documentation/x86/exception-tables.txt
PAN
如果我們想研究地更深,硬件和軟件協同做了一個更加安全的機制,這個機制叫做PAN (Privileged Access Never) 。它可以把內核對用戶空間的buffer訪問限制在特定的代碼區間裏面。PAN可以阻止kernel直接訪問用戶,它要求訪問之前,必須在硬件上開啓訪問權限。根據ARM的spec文檔
https://static.docs.arm.com/ddi0557/ab/DDI0557A_b_armv8_1_supplement.pdf
描述:
所以,內核每次訪問用戶之前,需要修改PSATE寄存器開啓訪問權限,完事後應該再次修改PSTATE,關閉內核對用戶的訪問權限。
根據補丁:
https://patchwork.kernel.org/patch/6808781/
copy_from_user這樣的代碼,是有這個開啓和關閉的過程的。
所以,一旦你開啓了內核的PAN支持,你是不能在一個隨隨便便的位置訪問用戶空間的buffer的。