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

    文章

    1992

    浏览量

    88707
  • Linux
    +关注

    关注

    88

    文章

    11818

    浏览量

    219566

原文标题:宋宝华: Linux为什么一定要copy_from_user ?

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    yocto编译IPCF sample_user报错的原因?怎么解决?

    /s32g399avtvmcu2-fsl-linux/ipc-shm/1.0-r0/git/sample_user\' | Building app file: sample.c | aarch64-fsl-linux
    发表于 04-07 07:49

    电子人一定要学会的20种模拟电路

    很多刚接触模拟电路的同学都会有同感:原理看得懂,电路图上手就懵;公式背得熟,到实际应用就卡壳。其实模拟电路并没有想象中那么难,核心就是把最经典、最常用的基础电路吃透练熟。从电源处理到信号放大,从
    的头像 发表于 04-01 09:05 282次阅读
    电子人<b class='flag-5'>一定要</b>学会的20种模拟电路

    产品出口美国一定要 FCC 认证吗?企业必须搞清楚的合规边界

    在产品出口美国前,很多企业都会遇到同个问题:“是不是只要卖到美国,就一定要做 FCC 认证?”这个问题如果理解不清,很容易出现两种极端情况: 要么不该做却做了,增加成本;要么该做却没做,导致产品被
    的头像 发表于 02-05 15:03 685次阅读
    产品出口美国<b class='flag-5'>一定要</b> FCC 认证吗?企业必须搞清楚的合规边界

    厚德筑基自强兴业,金航标和萨科微仕强先生源于华强北的创业史!

    了深圳市金航标电子有限公司(KinghelmElectronics)与深圳市萨科微半导体有限公司(SlkorMicroSemicon),并担任总经理职。仕强先生早年
    的头像 发表于 01-22 16:40 1416次阅读
    厚德筑基自强兴业,金航标和萨科微<b class='flag-5'>宋</b>仕强先生源于华强北的创业史!

    液晶屏一定要做屏保

    液晶屏一定要做屏保,避免不可逆的显示问题,学到了。
    发表于 09-29 11:38

    为什么自动驾驶感知系统一定要注意时间同步?

    [首发于智驾最前沿微信公众号]时间同步,看似非常简单的个概念,但在自动驾驶中有着非常重要的作用。一定要明白,时间同步不是感知系统的可选项,而是多传感器系统能否正确工作的基础性约束。自动驾驶系统依赖
    的头像 发表于 09-10 09:00 978次阅读
    为什么自动驾驶感知系统<b class='flag-5'>一定要</b>注意时间同步?

    【嘉楠堪智K230开发板试用体验】编写个GPIO 的字符驱动

    value; int ret; if (count > sizeof(tmp_buf) - 1) { return -EINVAL; } if (copy_from_user
    发表于 09-07 01:03

    充电可以无线充电吗?

    无线充电利用电磁感应技术实现无线充电,兼容Qi标准,兼具便捷性与实用性,成为传统充电的替代方案。
    的头像 发表于 09-06 08:28 2172次阅读
    充电<b class='flag-5'>宝</b>可以无线充电吗?

    【重要通知】秋DFM旧版本暂停服务公告

    。 随着秋DFM多个版本的迭代升级,我们在 增强软件性能、提升分析精度和扩展功能模块等方面都取得了显著的进步 。然而,伴随着版本的不断积累,这些 旧版本在功能完整性、稳定性及用户体验方面存在一定
    发表于 09-05 13:45

    秋DFM软件丨操作教程——自定义快捷键篇

    Hi,各位秋DFM的新老粉丝们,感谢大家直以来的支持和关注呀~咱们后台的留言,小编直都有在认真记录哦!看到近期新粉丝咨询比较多的问题,是关于秋DFM软件的使用教程和操作这块。虽
    发表于 08-13 16:29

    文掌握Linux命令

    作为名运维工程师,熟练掌握Linux命令是基本功中的基本功。无论是日常工作中的系统维护,还是面试时的技术考核,Linux命令都是绕不开的核心技能。本文将从实战角度出发,系统梳理运维工程师必须掌握的
    的头像 发表于 07-22 15:23 733次阅读

    邦电子总结芯片行业十大黑话

    芯片行业中往往几个字母就能传递连串关键有效的信息,那些专业术语,浓缩了行业技术要点,涵盖在工程师们的工作日常中,今天邦博士给大家一一破译、解码你一定要了解的邦电子十大常见“黑话”。
    的头像 发表于 07-15 11:44 1718次阅读

    充电快充协议是什么

    充电快充协议是充电与设备之间实现快速充电的通信规则,它定义了电压、电流、功率等参数的传输标准,确保设备与充电高效匹配,实现安全快充。 以下是主流快充协议的详细解析: 、快充协议
    的头像 发表于 06-30 09:17 1w次阅读

    全新比亚迪PLUS登陆巴西市场

    近日,比亚迪在巴西圣保罗隆重发布全新PLUS与PLUS PREMIUM双版本,吸引了逾500位嘉宾出席现场,其中包括256位媒体记者与知名意见领袖,见证这款划时代的插电混合动力SUV闪耀登场。
    的头像 发表于 06-10 16:41 1015次阅读

    【免费工具】秋AI电路识别助手:让电路设计与分析变得轻松高效!

    电子工程师注意!还在为熬夜解析电路图崩溃?AI黑科技让电路设计与分析变得轻松高效!如果你还在为电路分析感到头疼,那么一定要试试这款超好用的工具——秋AI电路识别助手小程序!这是款由
    的头像 发表于 06-05 18:18 2927次阅读
    【免费工具】<b class='flag-5'>华</b>秋AI电路识别助手:让电路设计与分析变得轻松高效!