女人自慰AV免费观看内涵网,日韩国产剧情在线观看网址,神马电影网特片网,最新一级电影欧美,在线观看亚洲欧美日韩,黄色视频在线播放免费观看,ABO涨奶期羡澄,第一导航fulione,美女主播操b

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

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

Linux閱碼場 ? 來源:Linuxer ? 2020-07-01 14:49 ? 次閱讀

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

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

這個問題,我認為需要從2個層面回答

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

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

為什么要拷貝

拷貝這個事情是必須的,這個事情甚至都跟Linux都沒有什么關系。比如Linux有個kobject結構體,kobject結構體里面有個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;...};

但我們設置一個設備的名字的時候,其實就是設置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的指針這樣賦值:

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

如果它這樣做了的話,那么它就完蛋了,因為驅動里面完全可以這樣設置name:

driver_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){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;}

這個問題在用戶空間和內核空間的交界點上是完全存在的。假設內核里面某個驅動的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) {intret=write(fd,0x40000000,10);//假想的代碼 if (ret < 0) perror("write error "); }}

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的。

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 驅動
    +關注

    關注

    12

    文章

    1901

    瀏覽量

    86547
  • Linux
    +關注

    關注

    87

    文章

    11465

    瀏覽量

    212840

原文標題:宋寶華: Linux為什么一定要copy_from_user ?

文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦
    熱點推薦

    如何在Linux內核5.18版本之后和64位架構中從內核空間調用ioctl?

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

    嵌入式學習-飛凌嵌入式ElfBoard ELF 1板卡-內核空間與用戶空間的數據拷貝之獲取用戶空間數據

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

    飛凌嵌入式ElfBoard ELF 1板卡-內核空間與用戶空間的數據拷貝之獲取用戶空間數據

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

    嵌入式學習-飛凌嵌入式ElfBoard ELF 1板卡-內核空間與用戶空間的數據拷貝之數據拷貝介紹

    空間之間進行數據傳輸時,需要進行數據拷貝操作。Linux內核提供了幾種方法來實現內核空間與用戶空間之間的數據拷貝。copy_to_user()和copy_from_user()這兩個函數用于在內核空間
    發表于 03-20 11:50

    飛凌嵌入式ElfBoard ELF 1板卡-內核空間與用戶空間的數據拷貝之數據拷貝介紹

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

    嵌入式學習-飛凌嵌入式ElfBoard ELF 1板卡-字符驅動之字符驅動框架描述

    copy_from_user函數在內核緩沖區和用戶空間之間傳輸數據。 同步與互斥機制:驅動程序可能需要使用信號量、互斥鎖或自旋鎖等同步機制,以確保多個進程或線程之間的數據安全性和致性。 錯誤處理:驅動程序需要正確處理各種錯誤情況,包括參數校驗、資源分配失敗、設備訪問錯
    發表于 03-17 14:05

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

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

    DMD全局復位是否一定要求加載所有行的數據?

    1.DMD全局復位是否一定要求加載所有行的數據?可否指定某段的行數據進行變化,然后申請全局復位,沒有數據變化的行保持原先數據。 2.指定某段的行數據變化,DVALID信號應該如何控制。控制
    發表于 02-26 08:04

    獨立站一定要買服務器嗎?

    獨立站一定要買服務器嗎?在考慮獨立站是否需要購買服務器時,首先要明確的是,服務器的存在對于網站的穩定運行至關重要。服務器的主要工作是處理用戶發送的訪問請求,并將所需數據以網頁形式展示給用戶。對于獨立
    的頭像 發表于 01-06 18:17 ?301次閱讀

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

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

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

    請問下,AD8338ACPZ輸出后,一定要接慮波嗎? 可以直接輸出AD給MCU嗎? 還有,這個芯片還正常量產嗎?
    發表于 12-03 08:34

    DDC112U的clk和conv信號一定要同步嘛,不同步會不會有問題?

    DDC112U的clk和conv信號一定要同步嘛,不同步會不會有問題
    發表于 11-18 08:03

    終于知道為什么一定要預埋HDMI線了

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

    運放的輸入端為什么一定要有直流通路?

    運放的輸入端為什么一定要有直流通路?就是接個電阻到地呢?之前用的VCA822有自激振蕩,用戶手冊中說要在輸入端接電阻到地,后來用的TL3016的比較器,輸出波形明顯的雜波,發現輸入端接對地電阻后波形就變好了...我想請問這樣的方式有什么理論依據呢?
    發表于 09-19 06:04

    4G模組無法正常聯網?一定要記得考慮SIM卡的問題!

    當大家在調試4G模組但卻無法正常聯網時, 大多數人的第反應是這4G模組一定有什么問題吧? 幾乎沒有人會認為是流量卡(SIM卡)的問題,一定要記得考慮SIM卡。
    的頭像 發表于 08-12 15:37 ?3601次閱讀
    4G模組無法正常聯網?<b class='flag-5'>一定要</b>記得考慮SIM卡的問題!