2022年2月左右,在分析CVE-2022-24448当时的修复补丁ac795161c936 NFSv4: Handle case where the lookup of a directory fails时,发现始终都无法复现出补丁描述的问题。
发邮件Question about CVE-2022-24448问了nfs client maintainer和CVE的报告者,maintainer没回答,CVE报告者Lyu Tao给出了复现程序:
1. shell命令: mount -t nfs -o vers=4.2 $server_ip:/ /mnt/
2. c程序: fd = open("/mnt/file", O_ACCMODE|O_DIRECT|O_CREAT)
3. c程序: close(fd)
4. c程序: fd = open("/mnt/file", O_ACCMODE|O_DIRECT)
5. c程序: lseek(fd) // 空指针解引用由于nfs client maintainer始终不回答,就决定自己解决,提了一组补丁fix nfsv4 bugs of opening with O_ACCMODE flag,并最终被社区接收。
将问题补丁做了回退,修复补丁: ab0fc21bc710 Revert “NFSv4: Handle the special Linux file open access mode”后,原本的问题也暴露出来了:
1. mount -t nfs -o vers=4 $server_ip:/ /mnt/ # NFSv4.0 / NFSv4.1 / NFSv4.2 挂载
2. fd = open("/mnt/file", O_ACCMODE|O_DIRECT|O_CREAT) # 第一次打开文件,如果不存在则创建
3. close(fd)
4. fd = open("/mnt/file", O_ACCMODE|O_DIRECT) # 第二次打开文件失败修复补丁: b243874f6f95 NFSv4: fix open failure with O_ACCMODE flag。
CVE报告者Lyu Tao当时向nfs client
maintainer报告这个问题后,maintainer不知道怎么就稀里糊涂的提了一个完全不相关的补丁:
ac795161c936 NFSv4: Handle case where the lookup of a directory fails,而且这个补丁描述的所谓“问题”根本没法复现:
NFSv4:处理目录查找失败的情况
如果应用程序设置了 O_DIRECTORY 标志,并尝试打开一个普通文件,nfs_atomic_open() 会退回执行常规查找的操作。如果服务器返回了一个普通文件,我们会错误地返回一个带有未初始化打开状态的文件描述符。
解决方法是在这些情况下返回预期的 ENOTDIR 错误。
CVE报告者Lyu
Tao说的是以O_DIRECT标志打开文件,而maintainer却在说O_DIRECTORY标志,从这个事可以看出,这个nfs
client maintainer很不严谨,连问题的描述都能看错。具体可以看我和Lyu
Tao的邮件交流内容fix
nfsv4 bugs of opening with O_ACCMODE flag。
修复补丁: ab0fc21bc710 Revert “NFSv4: Handle the special Linux file open access mode”
首先,分析发生空指针解引用时的栈信息,发现是 lseek
操作时 nfs4_valid_open_stateid 函数中
struct nfs4_state *state 的值为 NULL,访问
flags 字段时出现问题,代码流程如下:
lseek
ksys_lseek
vfs_llseek
// file->f_op->llseek
nfs4_file_llseek
nfs42_proc_llseek // NFSv4.2 lseek 方法
_nfs42_proc_llseek(lock)
nfs4_set_rw_stateid(ctx=lock->open_context)
nfs4_select_rw_stateid(state=ctx->state)
nfs4_valid_open_stateid(state)
state->flags // state 的值为 NULL接着分析 struct nfs4_state *state 赋值的地方,通过跟踪
open 系统调用的过程,发现挂载后第一次使用
O_ACCMODE
标记打开文件时,struct nfs4_state *state 的值不为
NULL, 代码流程如下:
open
do_sys_open
do_sys_openat2
do_filp_open
path_openat
open_last_lookups
lookup_open
// 挂载后第一次打开文件,执行atomic_open
atomic_open
open_to_namei_flags
flag-- // if ((flag & O_ACCMODE) == 3)
// dir->i_op->atomic_open
nfs_atomic_open
ctx = create_nfs_open_context
flags_to_mode
alloc_nfs_open_context // 分配 nfs_open_context
// NFS_PROTO(dir)->open_context
nfs4_atomic_open
nfs4_do_open
_nfs4_do_open
_nfs4_open_and_get_state
state = _nfs4_opendata_to_nfs4_state
nfs4_opendata_find_nfs4_state
nfs4_get_open_state
ctx->state = state // nfs4_state 赋值, lseek 不会发生空指针解引用第二次使用 O_ACCMODE
标记打开文件时,struct nfs4_state *state 的值为
NULL, 代码流程如下:
open
do_sys_open
do_sys_openat2
do_filp_open
path_openat
open_last_lookups
lookup_open
// 挂载后第二次打开文件,返回dentry,不执行atomic_open
return dentry // if (dentry->d_inode)
do_open
vfs_open
do_dentry_open
// f->f_op->open
nfs4_file_open // 挂载后第二次打开文件,执行到这里
if ((openflags & O_ACCMODE) == 3)
nfs_open // 没有向 server 发请求
alloc_nfs_open_context
ctx->state = NULL; // 重新初始化 struct nfs4_state,值为 NULL再调用lseek():
lseek
ksys_lseek
vfs_llseek
nfs4_file_llseek // file->f_op->llseek
nfs42_proc_llseek
_nfs42_proc_llseek(lock)
nfs4_set_rw_stateid(ctx=lock->open_context)
nfs4_select_rw_stateid(state=ctx->state)
nfs4_valid_open_stateid(state)
state->flags // 空指针解引用struct nfs4_state *state 值为 NULL
的原因找到了,是因为第二次使用 O_ACCMODE
标记打开文件时,nfs4_file_open 函数中对
O_ACCMODE 标记特殊处理,调用 nfs_open, 没有向
nfs server 发送请求,struct nfs4_state *state 赋值为
NULL。
此问题是由补丁
44942b4e457b NFSv4: Handle the special Linux file open access mode
引入的,只需回退此问题补丁,即可解决空指针解引用问题。
回退问题补丁后,第二次使用 O_ACCMODE 标记打开文件时
struct nfs4_state *state 的赋值过程如下:
open
do_sys_open
do_sys_openat2
do_filp_open
path_openat
do_open
vfs_open
do_dentry_open
// f->f_op->open
nfs4_file_open // 挂载后第二次打开文件,执行到这里
if ((openflags & O_ACCMODE) == 3)
openflags--
// NFS_PROTO(dir)->open_context
nfs4_atomic_open // 执行过程和第一次打开文件时一样
nfs4_do_open
_nfs4_do_open
_nfs4_open_and_get_state
state = _nfs4_opendata_to_nfs4_state
nfs4_opendata_find_nfs4_state
nfs4_get_open_state
ctx->state = state // nfs4_state 赋值, lseek 不会发生空指针解引用修复补丁: b243874f6f95 NFSv4: fix open failure with O_ACCMODE flag
回退问题补丁
44942b4e457b NFSv4: Handle the special Linux file open access mode
后,问题补丁本想解决的问题又暴露出来: NFSv4 挂载时,第二次使用
O_ACCMODE 标记打开文件失败。
通过 tcpdump
工具抓包,观察两次打开文件时的网络数据。
nfs client 请求第一次打开文件的数据如下:
Network File System, Ops(6): SEQUENCE, PUTFH, OPEN, GETFH, ACCESS, GETATTR
...
Operations (count: 6): SEQUENCE, PUTFH, OPEN, GETFH, ACCESS, GETATTR
...
Opcode: OPEN (18)
...
share_access: OPEN4_SHARE_ACCESS_BOTH (3)
...
nfs server 回复第一次打开文件的数据如下:
Network File System, Ops(6): SEQUENCE PUTFH OPEN GETFH ACCESS GETATTR
...
Status: NFS4_OK (0)
...
nfs client 请求第二次打开文件的数据如下:
Network File System, Ops(5): SEQUENCE, PUTFH, OPEN, ACCESS, GETATTR
...
Operations (count: 5): SEQUENCE, PUTFH, OPEN, ACCESS, GETATTR
...
Opcode: OPEN (18)
...
share_access: OPEN4_SHARE_ACCESS_WANT_NO_PREFERENCE (0)
...
nfs server 回复第二次打开文件的数据如下:
Network File System, Ops(3): SEQUENCE PUTFH OPEN(NFS4ERR_BADXDR)
...
Status: NFS4ERR_BADXDR (10036)
...
通过对比两次打开文件时的抓包数据可知,第二次使用
O_ACCMODE 标记打开文件时传给 nfs server 的
share_access 为 0, nfs server 返回错误
NFS4ERR_BADXDR, nfs server 解码 share_access
过程如下:
nfsd4_decode_compound
// nfsd4_dec_ops[op->opnum](argp, &op->u)
nfsd4_decode_open
nfsd4_decode_share_access
switch (w & NFS4_SHARE_WANT_MASK)
// 合理的 share_access 为 NFS4_SHARE_ACCESS_READ(1) NFS4_SHARE_ACCESS_WRITE(2) NFS4_SHARE_ACCESS_BOTH(3)
return nfserr_bad_xdr // share_access 为 0 时,返回错误第二次使用 O_ACCMODE 标记打开文件时,nfs client 的编码
share_access 过程如下:
call_encode
rpc_xdr_encode
rpcauth_wrap_req
rpcauth_wrap_req_encode
nfs4_xdr_enc_open
encode_open
encode_openhdr
encode_share_access(xdr, arg->share_access)通过跟踪 open 系统调用的过程,第二次使用
O_ACCMODE 标记打开文件时,对 share_access
的处理过程如下:
open
do_sys_open
do_sys_openat2
do_filp_open
path_openat
open_last_lookups
lookup_open
// 第二次打开文件,返回dentry,不执行atomic_open
return dentry // if (dentry->d_inode)
do_open
vfs_open
do_dentry_open
// f->f_op->open
nfs4_file_open // 第二次打开文件执行到这里
if ((openflags & O_ACCMODE) == 3)
openflags-- // openflags == 2
nfs4_atomic_open
nfs4_do_open
_nfs4_do_open
fmode = _nfs4_ctx_to_openmode(ctx) = 0
ret = ctx->mode & (FMODE_READ|FMODE_WRITE) = 0 // ctx->mode = 32796
nfs4_opendata_alloc
// 这里 share_access 赋值为 0
p->o_arg.share_access = nfs4_map_atomic_open_share(..., fmode, ...) = 0对比第一次使用 O_ACCMODE 标记打开文件
share_access 的处理过程:
open
do_sys_open
do_sys_openat2
do_filp_open
path_openat
open_last_lookups
lookup_open
// 第一次打开文件,执行atomic_open
atomic_open
nfs_atomic_open
create_nfs_open_context
// 这里把 flags 转化为 fmode
fmode = flags_to_mode(O_ACCMODE) = FMODE_READ|FMODE_WRITE
alloc_nfs_open_context
ctx->mode = f_mode = FMODE_READ|FMODE_WRITE = 3
// NFS_PROTO(dir)->open_context
nfs4_atomic_open
nfs4_do_open
_nfs4_do_open
fmode = _nfs4_ctx_to_openmode(ctx) = 3
ret = ctx->mode & (FMODE_READ|FMODE_WRITE) = 3 // ctx->mode = 3
nfs4_opendata_alloc
// 这里 share_access 赋值为 NFS4_SHARE_ACCESS_BOTH = 3
p->o_arg.share_access = nfs4_map_atomic_open_share(..., fmode, ...) = NFS4_SHARE_ACCESS_BOTH = 3可以看出第一次和第二次使用 O_ACCMODE
标记打开文件的区别在于,第一次打开时 ctx->mode 是从
openflags 转换而来,而第二次打开时在
nfs4_file_open 函数中 ctx->mode 不是从
openflags 转换而来。
需要特别说明的是 filp->f_mode 与
ctx->mode 的作用是不一样的,filp->f_mode
用于 nfs client 判断文件是否可读可写,而 ctx->mode
是用于传给 nfs server,。当 openflags 为
O_ACCMODE 时,filp->f_mode 没有标记
FMODE_READ|FMODE_WRITE
(不可读,不可写),ctx->mode 需要标记为
FMODE_READ|FMODE_WRITE (传给 nfs server, nfs server
对文件可读可写)。
修复补丁
b243874f6f95 NFSv4: fix open failure with O_ACCMODE flag
将第二次使用 O_ACCMODE 标记打开文件时的
ctx->mode 标记为 FMODE_READ|FMODE_WRITE
传给 nfs server,即可解决此问题。
如果没有合入90cf500e338a
NFSv4: Fix return values for
nfs4_file_open()(比如某些低版本的LTS或某些公司自己维护的版本),第二次以O_ACCMODE标志打开文件会成功,具体流程如下:
open
do_sys_open
do_sys_openat2
do_filp_open
path_openat
alloc_empty_file
init_file
f->f_mode = OPEN_FMODE(flags) = 0 // 其他bit可能不为0
(flag + 1) & O_ACCMODE) = (3 + 1) & 3 = 0
open_last_lookups
lookup_open
return dentry // if (dentry->d_inode) {
do_open
vfs_open
do_dentry_open
nfs4_file_open // f->f_op->open
if ((openflags & O_ACCMODE) == 3)
openflags-- == O_RDWR
alloc_nfs_open_context(..., filp->f_mode, ...)
ctx->mode = f_mode = 0
nfs4_atomic_open // open失败
goto out_drop
err = -EOPENSTALE
return -EOPENSTALE
if (error == -EOPENSTALE) // 条件满足
error = -ESTALE
filp = path_openat // 这次open成功nfs_open_context->mode判断FMODE_EXECNFS: check FMODE_EXEC from open context mode
除了在nfs4_file_open()函数中存在“打开文件失败问题“,在其他调用alloc_nfs_open_context()的地方也存在,如果打开文件时使用O_ACCMODE标记,则nfs_open_context->mode的值在某些场景下会为0,这时_nfs4_ctx_to_openmode()得到的值就为0,再把这个值传入到struct nfs_openargs中的fmode_t fmode,向nfs
server请求时就会导致文件打开失败。解决办法是要在所有调用的alloc_nfs_open_context()的地方将传入的参数fmode_t f_mode的值统一用flags_to_mode(file->f_flags)。
通过flags_to_mode()函数的转换,确保当open
flag中含有__FMODE_EXEC时,nfs_open_context->mode中也能含有FMODE_EXEC,可以起到简化代码的作用。当我们需要判断fmode_t中是否含有FMODE_EXEC时,就不需要open
flag,一些函数的参数中也不需要传入open flag。
null-ptr-deref-test.c文件:
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int fd;
// 打开文件,O_ACCMODE 表示读写权限,O_DIRECT 表示直接 I/O,O_CREAT 表示如果文件不存在则创建
fd = open("/mnt/file", O_ACCMODE | O_DIRECT | O_CREAT);
if (fd == -1) {
perror("无法打开/创建文件");
return EXIT_FAILURE;
}
// 关闭文件
close(fd);
// 重新打开文件,O_ACCMODE 表示读写权限,O_DIRECT 表示直接 I/O
fd = open("/mnt/file", O_ACCMODE | O_DIRECT);
if (fd == -1) {
perror("无法重新打开文件");
return EXIT_FAILURE;
}
// 进行 lseek 操作(在这里只是示例,实际使用时可能需要根据具体需求进行调整)
if (lseek(fd, 0, SEEK_DATA) == -1) {
perror("lseek 操作失败");
close(fd);
return EXIT_FAILURE;
}
// 关闭文件
close(fd);
return EXIT_SUCCESS;
}open-fail-test.c文件:
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int fd;
// 打开文件,O_ACCMODE 表示读写权限,O_DIRECT 表示直接 I/O,O_CREAT 表示如果文件不存在则创建
fd = open("/mnt/file", O_ACCMODE | O_DIRECT | O_CREAT);
if (fd == -1) {
perror("无法打开/创建文件");
return EXIT_FAILURE;
}
// 关闭文件
close(fd);
// 重新打开文件,O_ACCMODE 表示读写权限,O_DIRECT 表示直接 I/O
fd = open("/mnt/file", O_ACCMODE | O_DIRECT);
if (fd == -1) {
perror("无法重新打开文件");
return EXIT_FAILURE;
}
// 关闭文件
close(fd);
return EXIT_SUCCESS;
}启动nfs server后,测试步骤:
mount -t nfs -o vers=4.2 localhost:/ /mnt/ # 既当客户端,又当服务端
gcc null-ptr-deref-test.c -o null-ptr-deref-test
gcc open-fail-test.c -o open-fail-test
./open-fail-test; echo $? # 最新的主线代码预期打开文件成功,输出0
echo 3 > /proc/sys/vm/drop_caches
./null-ptr-deref-test # 最新的主线代码预期不发生空指针解引用,不发生panicopen-fail-test程序返回值不为0时(打开失败)的情况如下:
null-ptr-deref-test程序发生panic的情况如下: