一区二区三区三上|欧美在线视频五区|国产午夜无码在线观看视频|亚洲国产裸体网站|无码成年人影视|亚洲AV亚洲AV|成人开心激情五月|欧美性爱内射视频|超碰人人干人人上|一区二区无码三区亚洲人区久久精品

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

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

Linux閱碼場(chǎng) ? 來(lái)源:Linuxer ? 2020-07-01 14:49 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

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

但是這里面很多的解答沒(méi)有回答到點(diǎn)子上,不能真正回答這個(gè)問(wèn)題。我決定寫篇文章正式回答一下這個(gè)問(wèn)題,消除讀者的各種疑慮。

這個(gè)問(wèn)題,我認(rèn)為需要從2個(gè)層面回答

第一個(gè)層次是為什么要拷貝,可不可以不拷貝?

第二個(gè)層次是為什么要用copy_from_user而不是直接memcpy

為什么要拷貝

拷貝這個(gè)事情是必須的,這個(gè)事情甚至都跟Linux都沒(méi)有什么關(guān)系。比如Linux有個(gè)kobject結(jié)構(gòu)體,kobject結(jié)構(gòu)體里面有個(gè)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;...};

但我們?cè)O(shè)置一個(gè)設(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ū)動(dòng)里面經(jīng)常要設(shè)置name,比如:

dev_set_name(&chan->dev->device, "dma%dchan%d", device->dev_id, chan->chan_id);

但是Linux沒(méi)有傻到直接把name的指針這樣賦值:

struct device { struct kobject kobj; ...}; dev_set_name(struct device *dev, char *name){ dev->kobj.name = name_param; //假想的爛代碼}

如果它這樣做了的話,那么它就完蛋了,因?yàn)轵?qū)動(dòng)里面完全可以這樣設(shè)置name:

driver_func(){char name[100];....dev_set_name(dev, name);}

傳給dev_set_name()的根本是個(gè)stack區(qū)域的臨時(shí)變量,是一個(gè)匆匆過(guò)客。而device的name對(duì)于這個(gè)device來(lái)講,必須長(zhǎng)期存在。所以你看內(nèi)核真實(shí)的代碼,是給kobject的name重新申請(qǐng)一份內(nèi)存,然后把dev_set_name()傳給它的name拷貝進(jìn)來(lái):

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;}

這個(gè)問(wèn)題在用戶空間和內(nèi)核空間的交界點(diǎn)上是完全存在的。假設(shè)內(nèi)核里面某個(gè)驅(qū)動(dòng)的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;}

這樣的代碼絕對(duì)是要完蛋的,因?yàn)閐ev->mem這個(gè)內(nèi)核態(tài)的指針完全有可能被內(nèi)核態(tài)的中斷服務(wù)程序、被workqueue的callback函數(shù)、被內(nèi)核線程,或者被用戶空間的另外一個(gè)進(jìn)程通過(guò)globalmem_read()去讀,但是它卻指向一個(gè)某個(gè)進(jìn)程用戶空間的buffer。

在內(nèi)核里面直接使用用戶態(tài)傳過(guò)來(lái)的const char __user * buf指針,是災(zāi)難性的,因?yàn)閎uf的虛擬地址,只在這個(gè)進(jìn)程空間是有效的,跨進(jìn)程是無(wú)效的。但是調(diào)度一直在發(fā)生,中斷是存在的,workqueue是存在的,內(nèi)核線程是存在的,其他進(jìn)程是存在的,原先的用戶進(jìn)程的buffer地址,切了個(gè)進(jìn)程之后就不知道是個(gè)什么鬼!換個(gè)進(jìn)程,頁(yè)表都特碼變了,你這個(gè)buf地址還能找著人?進(jìn)程1的buf地址,在下面的紅框里面,什么都不是!

所以內(nèi)核的正確做法是,把buf拷貝到一個(gè)跨中斷、跨進(jìn)程、跨workqueue、跨內(nèi)核線程的長(zhǎng)期有效的內(nèi)存里面:

struct globalmem_dev { struct cdev cdev; unsigned char mem[GLOBALMEM_SIZE];//長(zhǎng)期有效 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; ...}

記住,對(duì)于內(nèi)核而言,用戶態(tài)此刻傳入的指針只是一個(gè)匆匆過(guò)客,只是個(gè)燦爛煙花,只是個(gè)曇花一現(xiàn),瞬間即逝!它甚至都沒(méi)有許諾你天長(zhǎng)地久,隨時(shí)可能劈腿!

所以,如果一定要給個(gè)需要拷貝的理由,原因就是防止劈腿!別給我扯些有的沒(méi)的。

必須拷貝的第二個(gè)理由,可能與安全有關(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)核一個(gè)iov的數(shù)組,數(shù)組每個(gè)成員描述一個(gè)buffer的基地址和長(zhǎng)度:

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) */};

用戶傳過(guò)來(lái)的是一個(gè)iovec的數(shù)組,里面有每個(gè)iov的len和base(base也是指向用戶態(tài)的buffer的),傳進(jìn)內(nèi)核的時(shí)候,內(nèi)核會(huì)對(duì)iovec的地址進(jìn)行check,保證它確實(shí)每個(gè)buffer都在用戶空間,并且會(huì)把整個(gè)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;}

這個(gè)過(guò)程是有嚴(yán)格的安全考量的,整個(gè)iov數(shù)組會(huì)被copy_from_user(),而數(shù)組里面的每個(gè)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開(kāi)始的len長(zhǎng)的區(qū)間,一定是位于用戶空間的,應(yīng)用程序不能傳入一個(gè)內(nèi)核空間的地址來(lái)傳給系統(tǒng)調(diào)用,這樣用戶可以通過(guò)系統(tǒng)調(diào)用,讓內(nèi)核寫壞內(nèi)核本身,造成一系列內(nèi)核安全漏洞。

假設(shè)內(nèi)核不把整個(gè)iov數(shù)組通過(guò)如下代碼拷貝進(jìn)內(nèi)核:

copy_from_user(iov, uvector, nr_segs*sizeof(*uvector))

而是直接訪問(wèn)用戶態(tài)的iov,那個(gè)這個(gè)access_ok就完全失去價(jià)值了,因?yàn)?,用戶完全可以在你做access_ok檢查的時(shí)候,傳給你的是用戶態(tài)buffer,之后把iov_base的內(nèi)容改成指向一個(gè)內(nèi)核態(tài)的buffer去。

所以,從這個(gè)理由上來(lái)講,最開(kāi)始的拷貝也是必須的。但是這個(gè)理由遠(yuǎn)遠(yuǎn)沒(méi)有最開(kāi)始那個(gè)隨時(shí)劈腿的理由充分!

為什么不直接用memcpy?

這個(gè)問(wèn)題主要涉及到2個(gè)層面,一個(gè)是copy_from_user()有自帶的access_ok檢查,如果用戶傳進(jìn)來(lái)的buffer不屬于用戶空間而是內(nèi)核空間,根本不會(huì)拷貝;二是copy_from_user()有自帶的page fault后exception修復(fù)機(jī)制。

先看第一個(gè)問(wèn)題,如果代碼直接用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是沒(méi)有這個(gè)檢查的,哪怕用戶傳入進(jìn)來(lái)的這個(gè)buf,指向的是內(nèi)核態(tài)的地址,這個(gè)拷貝也是要做的。試想,用戶做系統(tǒng)調(diào)用的時(shí)候,隨便可以把內(nèi)核的指針傳進(jìn)來(lái),那用戶不是可以隨便為所欲為?比如內(nèi)核的這個(gè)commit,引起了著名的安全漏洞:

CVE-2017-5123

就是因?yàn)?,作者把有access_ok的put_user改為了沒(méi)有access_ok的unsafe_put_user。這樣,用戶如果把某個(gè)進(jìn)程的uid地址傳給內(nèi)核,內(nèi)核unsafe_put_user的時(shí)候,不是完全可以把它的uid改為0?

所以,你看到內(nèi)核修復(fù)這個(gè)CVE的時(shí)候,是對(duì)這些地址進(jìn)行了一個(gè)access_ok的:

下面我們看第二個(gè)問(wèn)題,page fault的修復(fù)機(jī)制。假設(shè)用戶程序隨便胡亂傳個(gè)用戶態(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這個(gè)地址是用戶態(tài)的,所以access_ok是沒(méi)有問(wèn)題的。但是這個(gè)地址,根本什么有效的數(shù)據(jù)、heap、stack都不是。我特碼就是瞎寫的。

如果內(nèi)核驅(qū)動(dòng)用memcpy會(huì)發(fā)生什么呢?我們會(huì)看到一段內(nèi)核Oops:

用戶進(jìn)程也會(huì)被kill掉:

# ./a.out Killed

當(dāng)然如果你設(shè)置了/proc/sys/kernel/panic_on_oops為1的話,內(nèi)核就不是Opps這么簡(jiǎn)單了,而是直接panic了。

但是如果內(nèi)核用的是copy_from_user呢??jī)?nèi)核是不會(huì)Oops的,用戶態(tài)應(yīng)用程序也是不會(huì)死的,它只是收到了bad address的錯(cuò)誤:

# ./a.out write error: Bad address

內(nèi)核只是友好地提示你用戶闖進(jìn)來(lái)的buffer地址0x40000000是個(gè)錯(cuò)誤的地址,這個(gè)系統(tǒng)調(diào)用的參數(shù)是不對(duì)的,這顯然更加符合系統(tǒng)調(diào)用的本質(zhì)。

內(nèi)核針對(duì)copy_from_user,有exception fixup機(jī)制,而memcpy()是沒(méi)有的。詳細(xì)的exception修復(fù)機(jī)制見(jiàn):

https://www.kernel.org/doc/Documentation/x86/exception-tables.txt

PAN

如果我們想研究地更深,硬件和軟件協(xié)同做了一個(gè)更加安全的機(jī)制,這個(gè)機(jī)制叫做PAN (Privileged Access Never)。它可以把內(nèi)核對(duì)用戶空間的buffer訪問(wèn)限制在特定的代碼區(qū)間里面。PAN可以阻止kernel直接訪問(wèn)用戶,它要求訪問(wèn)之前,必須在硬件上開(kāi)啟訪問(wèn)權(quán)限。根據(jù)ARM的spec文檔

https://static.docs.arm.com/ddi0557/ab/DDI0557A_b_armv8_1_supplement.pdf

描述:

所以,內(nèi)核每次訪問(wèn)用戶之前,需要修改PSATE寄存器開(kāi)啟訪問(wèn)權(quán)限,完事后應(yīng)該再次修改PSTATE,關(guān)閉內(nèi)核對(duì)用戶的訪問(wèn)權(quán)限。

根據(jù)補(bǔ)?。?/p>

https://patchwork.kernel.org/patch/6808781/

copy_from_user這樣的代碼,是有這個(gè)開(kāi)啟和關(guān)閉的過(guò)程的。

所以,一旦你開(kāi)啟了內(nèi)核的PAN支持,你是不能在一個(gè)隨隨便便的位置訪問(wèn)用戶空間的buffer的。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 驅(qū)動(dòng)
    +關(guān)注

    關(guān)注

    12

    文章

    1915

    瀏覽量

    86861
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11508

    瀏覽量

    213624

原文標(biāo)題:宋寶華: Linux為什么一定要copy_from_user ?

文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評(píng)論

    相關(guān)推薦
    熱點(diǎn)推薦

    如何在Linux內(nèi)核5.18版本之后和64位架構(gòu)中從內(nèi)核空間調(diào)用ioctl?

    (\'EFAULT\' / Bad Address)。 存在函數(shù) \'copy_from_user()\', ...那簡(jiǎn)直是失敗的。 在代碼和幾個(gè)論壇中搜索,我找到了有關(guān)“set_fs()”舞蹈刪除
    發(fā)表于 04-02 06:06

    嵌入式學(xué)習(xí)-飛凌嵌入式ElfBoard ELF 1板卡-內(nèi)核空間與用戶空間的數(shù)據(jù)拷貝之獲取用戶空間數(shù)據(jù)

    ; BUFFER_SIZE) length = BUFFER_SIZE - *offset; if (copy_from_user(user_buffer + *offset, buffer
    發(fā)表于 03-22 09:25

    飛凌嵌入式ElfBoard ELF 1板卡-內(nèi)核空間與用戶空間的數(shù)據(jù)拷貝之獲取用戶空間數(shù)據(jù)

    ; BUFFER_SIZE) length = BUFFER_SIZE - *offset; if (copy_from_user(user_buffer + *offset, buffer
    發(fā)表于 03-21 13:58

    嵌入式學(xué)習(xí)-飛凌嵌入式ElfBoard ELF 1板卡-內(nèi)核空間與用戶空間的數(shù)據(jù)拷貝之?dāng)?shù)據(jù)拷貝介紹

    空間之間進(jìn)行數(shù)據(jù)傳輸時(shí),需要進(jìn)行數(shù)據(jù)拷貝操作。Linux內(nèi)核提供了幾種方法來(lái)實(shí)現(xiàn)內(nèi)核空間與用戶空間之間的數(shù)據(jù)拷貝。copy_to_user()和copy_from_user()這兩個(gè)函數(shù)用于在內(nèi)核空間
    發(fā)表于 03-20 11:50

    飛凌嵌入式ElfBoard ELF 1板卡-內(nèi)核空間與用戶空間的數(shù)據(jù)拷貝之?dāng)?shù)據(jù)拷貝介紹

    空間與用戶空間之間的數(shù)據(jù)拷貝。copy_to_user()和copy_from_user()這兩個(gè)函數(shù)用于在內(nèi)核空間和用戶空間之間進(jìn)行數(shù)據(jù)拷貝。copy_to_user()函數(shù)用于將數(shù)據(jù)從內(nèi)核空間復(fù)制到
    發(fā)表于 03-19 08:55

    嵌入式學(xué)習(xí)-飛凌嵌入式ElfBoard ELF 1板卡-字符驅(qū)動(dòng)之字符驅(qū)動(dòng)框架描述

    copy_from_user函數(shù)在內(nèi)核緩沖區(qū)和用戶空間之間傳輸數(shù)據(jù)。 同步與互斥機(jī)制:驅(qū)動(dòng)程序可能需要使用信號(hào)量、互斥鎖或自旋鎖等同步機(jī)制,以確保多個(gè)進(jìn)程或線程之間的數(shù)據(jù)安全性和致性。 錯(cuò)誤處理:驅(qū)動(dòng)程序需要正確處理各種錯(cuò)誤情況,包括參數(shù)校驗(yàn)、資源分配失敗、設(shè)備訪問(wèn)錯(cuò)
    發(fā)表于 03-17 14:05

    USB組合設(shè)備的配置描述符里一定要用IAD描述符嗎?

    USB組合設(shè)備的配置描述符里一定要用IAD描述符嗎
    發(fā)表于 03-11 06:41

    DMD全局復(fù)位是否一定要求加載所有行的數(shù)據(jù)?

    1.DMD全局復(fù)位是否一定要求加載所有行的數(shù)據(jù)?可否指定某段的行數(shù)據(jù)進(jìn)行變化,然后申請(qǐng)全局復(fù)位,沒(méi)有數(shù)據(jù)變化的行保持原先數(shù)據(jù)。 2.指定某段的行數(shù)據(jù)變化,DVALID信號(hào)應(yīng)該如何控制??刂?/div>
    發(fā)表于 02-26 08:04

    獨(dú)立站一定要買服務(wù)器嗎?

    獨(dú)立站一定要買服務(wù)器嗎?在考慮獨(dú)立站是否需要購(gòu)買服務(wù)器時(shí),首先要明確的是,服務(wù)器的存在對(duì)于網(wǎng)站的穩(wěn)定運(yùn)行至關(guān)重要。服務(wù)器的主要工作是處理用戶發(fā)送的訪問(wèn)請(qǐng)求,并將所需數(shù)據(jù)以網(wǎng)頁(yè)形式展示給用戶。對(duì)于獨(dú)立
    的頭像 發(fā)表于 01-06 18:17 ?324次閱讀

    AFE4900的SEN引腳一定要拉低或拉高嗎?可以浮空嗎?

    AFE4900的SEN引腳,一定要拉低或拉高嗎?可以浮空嗎?
    發(fā)表于 12-09 07:18

    AD8338ACPZ輸出后,一定要接慮波嗎?

    請(qǐng)問(wèn)下,AD8338ACPZ輸出后,一定要接慮波嗎? 可以直接輸出AD給MCU嗎? 還有,這個(gè)芯片還正常量產(chǎn)嗎?
    發(fā)表于 12-03 08:34

    DDC112U的clk和conv信號(hào)一定要同步嘛,不同步會(huì)不會(huì)有問(wèn)題?

    DDC112U的clk和conv信號(hào)一定要同步嘛,不同步會(huì)不會(huì)有問(wèn)題
    發(fā)表于 11-18 08:03

    終于知道為什么一定要預(yù)埋HDMI線了

    ] | | ---------------------------------------------------------------------------------------- |蕞近,大數(shù)據(jù)給我推送了篇文章,講的是家裝時(shí)一定要預(yù)埋HDMI線,我在想大家在裝
    的頭像 發(fā)表于 10-24 15:25 ?1203次閱讀

    運(yùn)放的輸入端為什么一定要有直流通路?

    運(yùn)放的輸入端為什么一定要有直流通路?就是接個(gè)電阻到地呢?之前用的VCA822有自激振蕩,用戶手冊(cè)中說(shuō)要在輸入端接電阻到地,后來(lái)用的TL3016的比較器,輸出波形明顯的雜波,發(fā)現(xiàn)輸入端接對(duì)地電阻后波形就變好了...我想請(qǐng)問(wèn)這樣的方式有什么理論依據(jù)呢?
    發(fā)表于 09-19 06:04

    4G模組無(wú)法正常聯(lián)網(wǎng)?一定要記得考慮SIM卡的問(wèn)題!

    當(dāng)大家在調(diào)試4G模組但卻無(wú)法正常聯(lián)網(wǎng)時(shí), 大多數(shù)人的第反應(yīng)是這4G模組一定有什么問(wèn)題吧? 幾乎沒(méi)有人會(huì)認(rèn)為是流量卡(SIM卡)的問(wèn)題,一定要記得考慮SIM卡。
    的頭像 發(fā)表于 08-12 15:37 ?3894次閱讀
    4G模組無(wú)法正常聯(lián)網(wǎng)?<b class='flag-5'>一定要</b>記得考慮SIM卡的問(wèn)題!