添加关于文件拼接的系统调用

前言

近一年来没写过博客了,从今天开始打算重操旧业。主要原因还是因为逐渐地意识到了写博客的重要性。许多知识如果没有得到有效的记录也不过是过往烟云,再加上如今课业繁重,操作系统与计算机网络两座大山,加上自己对嵌入式系统的浓厚兴趣,我必须得做点详细的文章,以此来review。

背景

本次记录源于Hdu操作系统的第一个实验作业,要求在Linux内核中添加入自己的一个系统调用,功能在备选方案中自定。

观览一圈下来,发现大部分的功能实现其实并不难,大多其实是基于kernel的一些现有函数的包装,说不上什么难度。挑来跳去,我最后选了一个关于文件拼接的题目,题目如下:

把指定文件1的内容追加到指定文件2的末尾。

初看题目乍看简单,不就是文件copy吗~开始Coding…

过程

1. 添加系统调用号

syscall_64.tbl(linuxXXX/arch/x86/entry/syscalls/syscall_64.tbl)下添加内容:

1
2
// ...
448 common append_file sys_append_file
说明
448 系统调用号
common 适用机器位数(x32/x64/common)
append_file 暴露给用户的函数名
sys_append_file 实际的函数名称

当然调用号需要基于实际情况而定(每个版本的linux都会发生变化)

2. 添加函数声明

syscalls.h(linuxXXX/include/kernel/syscalls.h)下添加内容:

1
asmlinkage long sys_append_file(const char *file1, const char *file2);

3. 添加函数定义

sys.c(linuxXXX/kernel/sys.c)中添加:

1
2
3
4
5
6
7
8
9
/**
@append_file 函数名
@target 目标文件
@src 源文件
@desc 拷贝源文件追加到目标文件后方
*/
SYSCALL_DEFINE2(append_file, const char *, target, const char *, src) {
// ...
}

Q:为什么在sys.c中添加?
A:主要是方便…实际应该在自己的.c中写,不过这种做法要写额外的makefile,不过似乎可以加快后期的编译工作?(猜想)
Q:SYSCALL_DEFINEx是什么?
A:参考Linux系统调用(syscall)原理

4. 开始我们的工作

终于,我们可以开始编写我们的函数啦!但是问题来了:我在内核态下可以使用readwriteopen之类的函数吗??开始STFW…

终于了解到了一个叫做filp_open的函数,函数原型如下:

1
struct file *filp_open(const char *filename, int flags, umode_t mode)

函数功能同open,其中mode我们可以暂且先忽略(置0),原因是其主要用作给新文件设定权限,但是我们在此不存在新文件的创建,因此无需考虑。

1
2
3
4
5
6
7
8
9
10
target_p = filp_open(target, O_WRONLY | O_APPEND, 0);
if (IS_ERR(target_p)) {
printk("open file %s error\n", target);
return -1;
}
src_p = filp_open(src, O_RDONLY, 0);
if (IS_ERR(src_p)) {
printk("open file %s error\n", src);
return -1;
}

同理我们可以看到有类似read,write的函数:

1
2
ssize_t vfs_read(struct file* filp, char __user* buffer, size_t len, loff_t* pos);
ssize_t vfs_write(struct file* filp, const char __user* buffer, size_t len, loff_t* pos);

我们参考相关文档,可以写出个大概,主要逻辑如下:

1
2
3
4
5
6
7
8
oldfs = get_fs();
// 防止用户空间地址检查
set_fs(get_ds());
while ((count = vfs_read(src, buffer, MY_BUFFER_SIZE, &src_pos)) > 0) {
vfs_write(target, buffer, count, &target_pos);
}
// 恢复
set_fs(oldfs);

此时看到这里,想必你和我一定都是一脸懵逼的…主要原因在于set_fsget_fs是什么?

又是一波STFW,好了,其实这是为了可以访问整个内核空间,避免检查。
详情请见get_fs()和set_fs()解析

噼里啪啦终于正好了,等我们兴高采烈地敲下make -j8编译后,糟糕的事情来了:

implicit declaration of function ‘get_fs’

WTF!和说好的不一样哈?原因竟然是在内核版本5.10版本后set_fs等宏被移除了!
详情请见Linux 5.10 finally ditches decades-old tool that caused security bugs

笑死,感觉自己是小丑。那此时我们就陷入迷茫了,What should we do? STFW!

1
2
ssize_t kernel_read(struct file* filp, char __user* buffer, size_t len, loff_t* pos);
ssize_t kernel_write(struct file* filp, const char __user* buffer, size_t len, loff_t* pos);

Yes!! 我们马上就要成功了

1
2
3
4
5
6
7
make -j8
make moudles_install
make install
reboot
gcc test.c -o test
./test
已杀死

已杀死??有点窒息。看一眼dmesg: unable to handle page fault for address,似乎是页面错误,这不太对啊,但是仔细想想看前面关于内存访问的操作…

没错!在filp_open函数中,我们直接使用了用户态传入的文件名指针,而该指针指向的正是用户态的内存空间,而在kernel dev中,这样的行为是禁止的!因此,我们需要使用类如copy_from_user的函数来帮助我们把文件名拷贝入内核空间!

在这里,针对字符串,我们采用的是内核下的字符串拷贝函数:

1
long strncpy_from_user (char * dst, const char __user * src, long count);

最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#define MY_BUFFER_SIZE 1024
#define NAME_LIMIT 255
SYSCALL_DEFINE2(append_file, const char *, target, const char *, src) {
char buffer[MY_BUFFER_SIZE];
char target_buffer[NAME_LIMIT];
char src_buffer[NAME_LIMIT];

struct file *target_p;
struct file *src_p;

loff_t target_pos;
loff_t src_pos;
int count;

long target_len;
long src_len;
printk("copy names...\n");
target_len = strncpy_from_user(target_buffer, target, NAME_LIMIT);
src_len = strncpy_from_user(src_buffer, src, NAME_LIMIT);
printk("target_len: %ld src_len: %ld\n", target_len, src_len);
printk("open file...\n");
target_p = filp_open(target_buffer, O_WRONLY | O_APPEND, 0);
if (IS_ERR(target_p)) {
printk("open file %s error\n", target_buffer);
return -1;
}
src_p = filp_open(src_buffer, O_RDONLY, 0);
if (IS_ERR(src_p)) {
printk("open file %s error\n", src_buffer);
return -1;
}
printk("start copy...\n");
target_pos = target_p->f_pos;
src_pos = src_p->f_pos;
// fs = get_fs();
// 防止用户空间地址检查
// set_fs(get_ds());
while ((count = kernel_read(src_p, buffer, MY_BUFFER_SIZE, &src_pos)) > 0) {
kernel_write(target_p, buffer, count, &target_pos);
}
printk("done");
// 恢复
// set_fs(fs);

filp_close(src_p, NULL);
filp_close(target_p, NULL);
printk("file colsed");
return 0;
}

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <unistd.h>

#define __NR_append_file 448

long concat_file(const char *target, const char *src) {
return syscall(__NR_append_file, target, src);
}

int main() {
printf("%d\n", concat_file("a.txt", "b.txt"));
return 0;
}

a.txt:

abc

b.txt:

efg

result(a.txt):

abcefg

参考文献

http://gityuan.com/2016/05/21/syscall/
https://stackoverflow.com/questions/1184274/read-write-files-within-a-linux-kernel-module/53917617#53917617
http://blog.chinaunix.net/uid-24237502-id-35023.html
https://www.zdnet.com/article/linux-5-10-finally-ditches-decades-old-tool-that-caused-security-bugs/

END