1. 程式人生 > 其它 >宋寶華: Linux為什麼一定要copy_from_user ?

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

網上很多人提問為什麼一定要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\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的。