網(wǎng)上很多人提問為什么一定要copy_from_user,也有人解答。比如百度一下:

但是這里面很多的解答沒有回答到點(diǎn)子上,不能真正回答這個問題。我決定寫篇文章正式回答一下這個問題,消除讀者的各種疑慮。
這個問題,我認(rèn)為需要從2個層面回答
第一個層次是為什么要拷貝,可不可以不拷貝?
第二個層次是為什么要用copy_from_user而不是直接memcpy
為什么要拷貝
拷貝這個事情是必須的,這個事情甚至都跟Linux都沒有什么關(guān)系。比如Linux有個kobject結(jié)構(gòu)體,kobject結(jié)構(gòu)體里面有個name指針:
struct 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;...};
但我們設(shè)置一個設(shè)備的名字的時候,其實(shí)就是設(shè)置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;}
驅(qū)動里面經(jīng)常要設(shè)置name,比如:
dev_set_name(&chan->dev->device, "dma%dchan%d", device->dev_id, chan->chan_id);
但是Linux沒有傻到直接把name的指針這樣賦值:
struct device { struct kobject kobj; ...}; dev_set_name(struct device *dev, char *name){ dev->kobj.name = name_param; //假想的爛代碼}
如果它這樣做了的話,那么它就完蛋了,因?yàn)轵?qū)動里面完全可以這樣設(shè)置name:
driver_func(){char name[100];....dev_set_name(dev, name);}
傳給dev_set_name()的根本是個stack區(qū)域的臨時變量,是一個匆匆過客。而device的name對于這個device來講,必須長期存在。所以你看內(nèi)核真實(shí)的代碼,是給kobject的name重新申請一份內(nèi)存,然后把dev_set_name()傳給它的name拷貝進(jìn)來:
int kobject_set_name_vargs(struct kobject *kobj, const char *fmt, va_list vargs){constchar*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;}
這個問題在用戶空間和內(nèi)核空間的交界點(diǎn)上是完全存在的。假設(shè)內(nèi)核里面某個驅(qū)動的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;}
這樣的代碼絕對是要完蛋的,因?yàn)閐ev->mem這個內(nèi)核態(tài)的指針完全有可能被內(nèi)核態(tài)的中斷服務(wù)程序、被workqueue的callback函數(shù)、被內(nèi)核線程,或者被用戶空間的另外一個進(jìn)程通過globalmem_read()去讀,但是它卻指向一個某個進(jìn)程用戶空間的buffer。
在內(nèi)核里面直接使用用戶態(tài)傳過來的const char __user * buf指針,是災(zāi)難性的,因?yàn)閎uf的虛擬地址,只在這個進(jìn)程空間是有效的,跨進(jìn)程是無效的。但是調(diào)度一直在發(fā)生,中斷是存在的,workqueue是存在的,內(nèi)核線程是存在的,其他進(jìn)程是存在的,原先的用戶進(jìn)程的buffer地址,切了個進(jìn)程之后就不知道是個什么鬼!換個進(jìn)程,頁表都特碼變了,你這個buf地址還能找著人?進(jìn)程1的buf地址,在下面的紅框里面,什么都不是!

所以內(nèi)核的正確做法是,把buf拷貝到一個跨中斷、跨進(jìn)程、跨workqueue、跨內(nèi)核線程的長期有效的內(nèi)存里面:
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; ...}
記住,對于內(nèi)核而言,用戶態(tài)此刻傳入的指針只是一個匆匆過客,只是個燦爛煙花,只是個曇花一現(xiàn),瞬間即逝!它甚至都沒有許諾你天長地久,隨時可能劈腿!
所以,如果一定要給個需要拷貝的理由,原因就是防止劈腿!別給我扯些有的沒的。
必須拷貝的第二個理由,可能與安全有關(guān)。比如用戶態(tài)做類似pwritev, preadv這樣的調(diào)用:
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);
用戶傳給內(nèi)核一個iov的數(shù)組,數(shù)組每個成員描述一個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的數(shù)組,里面有每個iov的len和base(base也是指向用戶態(tài)的buffer的),傳進(jìn)內(nèi)核的時候,內(nèi)核會對iovec的地址進(jìn)行check,保證它確實(shí)每個buffer都在用戶空間,并且會把整個iovec數(shù)組拷貝到內(nèi)核空間:
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;}
這個過程是有嚴(yán)格的安全考量的,整個iov數(shù)組會被copy_from_user(),而數(shù)組里面的每個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長的區(qū)間,一定是位于用戶空間的,應(yīng)用程序不能傳入一個內(nèi)核空間的地址來傳給系統(tǒng)調(diào)用,這樣用戶可以通過系統(tǒng)調(diào)用,讓內(nèi)核寫壞內(nèi)核本身,造成一系列內(nèi)核安全漏洞。
假設(shè)內(nèi)核不把整個iov數(shù)組通過如下代碼拷貝進(jìn)內(nèi)核:
copy_from_user(iov, uvector, nr_segs*sizeof(*uvector))
而是直接訪問用戶態(tài)的iov,那個這個access_ok就完全失去價值了,因?yàn)?,用戶完全可以在你做access_ok檢查的時候,傳給你的是用戶態(tài)buffer,之后把iov_base的內(nèi)容改成指向一個內(nèi)核態(tài)的buffer去。
所以,從這個理由上來講,最開始的拷貝也是必須的。但是這個理由遠(yuǎn)遠(yuǎn)沒有最開始那個隨時劈腿的理由充分!
為什么不直接用memcpy?
這個問題主要涉及到2個層面,一個是copy_from_user()有自帶的access_ok檢查,如果用戶傳進(jìn)來的buffer不屬于用戶空間而是內(nèi)核空間,根本不會拷貝;二是copy_from_user()有自帶的page fault后exception修復(fù)機(jī)制。
先看第一個問題,如果代碼直接用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是沒有這個檢查的,哪怕用戶傳入進(jìn)來的這個buf,指向的是內(nèi)核態(tài)的地址,這個拷貝也是要做的。試想,用戶做系統(tǒng)調(diào)用的時候,隨便可以把內(nèi)核的指針傳進(jìn)來,那用戶不是可以隨便為所欲為?比如內(nèi)核的這個commit,引起了著名的安全漏洞:
CVE-2017-5123

就是因?yàn)?,作者把有access_ok的put_user改為了沒有access_ok的unsafe_put_user。這樣,用戶如果把某個進(jìn)程的uid地址傳給內(nèi)核,內(nèi)核unsafe_put_user的時候,不是完全可以把它的uid改為0?
所以,你看到內(nèi)核修復(fù)這個CVE的時候,是對這些地址進(jìn)行了一個access_ok的:

下面我們看第二個問題,page fault的修復(fù)機(jī)制。假設(shè)用戶程序隨便胡亂傳個用戶態(tài)的地址給內(nèi)核:
void main(void){ int fd; fd = open("/dev/globalfifo", O_RDWR, S_IRUSR | S_IWUSR); if (fd != -1) {intret=write(fd,0x40000000,10);//假想的代碼 if (ret < 0) perror("write error "); }}
0x40000000這個地址是用戶態(tài)的,所以access_ok是沒有問題的。但是這個地址,根本什么有效的數(shù)據(jù)、heap、stack都不是。我特碼就是瞎寫的。
如果內(nèi)核驅(qū)動用memcpy會發(fā)生什么呢?我們會看到一段內(nèi)核Oops:

用戶進(jìn)程也會被kill掉:
# ./a.out Killed
當(dāng)然如果你設(shè)置了/proc/sys/kernel/panic_on_oops為1的話,內(nèi)核就不是Opps這么簡單了,而是直接panic了。
但是如果內(nèi)核用的是copy_from_user呢?內(nèi)核是不會Oops的,用戶態(tài)應(yīng)用程序也是不會死的,它只是收到了bad address的錯誤:
# ./a.out write error: Bad address
內(nèi)核只是友好地提示你用戶闖進(jìn)來的buffer地址0x40000000是個錯誤的地址,這個系統(tǒng)調(diào)用的參數(shù)是不對的,這顯然更加符合系統(tǒng)調(diào)用的本質(zhì)。
內(nèi)核針對copy_from_user,有exception fixup機(jī)制,而memcpy()是沒有的。詳細(xì)的exception修復(fù)機(jī)制見:
https://www.kernel.org/doc/Documentation/x86/exception-tables.txt
PAN
如果我們想研究地更深,硬件和軟件協(xié)同做了一個更加安全的機(jī)制,這個機(jī)制叫做PAN (Privileged Access Never)。它可以把內(nèi)核對用戶空間的buffer訪問限制在特定的代碼區(qū)間里面。PAN可以阻止kernel直接訪問用戶,它要求訪問之前,必須在硬件上開啟訪問權(quán)限。根據(jù)ARM的spec文檔
https://static.docs.arm.com/ddi0557/ab/DDI0557A_b_armv8_1_supplement.pdf
描述:

所以,內(nèi)核每次訪問用戶之前,需要修改PSATE寄存器開啟訪問權(quán)限,完事后應(yīng)該再次修改PSTATE,關(guān)閉內(nèi)核對用戶的訪問權(quán)限。
根據(jù)補(bǔ)丁:
https://patchwork.kernel.org/patch/6808781/

copy_from_user這樣的代碼,是有這個開啟和關(guān)閉的過程的。
所以,一旦你開啟了內(nèi)核的PAN支持,你是不能在一個隨隨便便的位置訪問用戶空間的buffer的。
-
驅(qū)動
+關(guān)注
關(guān)注
12文章
1926瀏覽量
88066 -
Linux
+關(guān)注
關(guān)注
88文章
11590瀏覽量
217383
原文標(biāo)題:宋寶華: Linux為什么一定要copy_from_user ?
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄

宋寶華: Linux為什么一定要copy_from_user ?
評論