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// NFSv4.2 lseek 方法
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 的值为 NULL state->flags
接着分析 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// if ((flag & O_ACCMODE) == 3)
flag-- // dir->i_op->atomic_open
nfs_atomic_open
ctx = create_nfs_open_context
flags_to_mode// 分配 nfs_open_context
alloc_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// nfs4_state 赋值, lseek 不会发生空指针解引用 ctx->state = state
第二次使用 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)
// 没有向 server 发请求
nfs_open
alloc_nfs_open_context// 重新初始化 struct nfs4_state,值为 NULL ctx->state = NULL;
再调用lseek()
:
lseek
ksys_lseek
vfs_llseek// file->f_op->llseek
nfs4_file_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// nfs4_state 赋值, lseek 不会发生空指针解引用 ctx->state = state
修复补丁: 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_accessswitch (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 == 2
openflags--
nfs4_atomic_open
nfs4_do_open
_nfs4_do_open0
fmode = _nfs4_ctx_to_openmode(ctx) = 0 // ctx->mode = 32796
ret = ctx->mode & (FMODE_READ|FMODE_WRITE) =
nfs4_opendata_alloc// 这里 share_access 赋值为 0
0 p->o_arg.share_access = nfs4_map_atomic_open_share(..., fmode, ...) =
对比第一次使用 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_context3
ctx->mode = f_mode = FMODE_READ|FMODE_WRITE = // NFS_PROTO(dir)->open_context
nfs4_atomic_open
nfs4_do_open
_nfs4_do_open3
fmode = _nfs4_ctx_to_openmode(ctx) = 3 // ctx->mode = 3
ret = ctx->mode & (FMODE_READ|FMODE_WRITE) =
nfs4_opendata_alloc// 这里 share_access 赋值为 NFS4_SHARE_ACCESS_BOTH = 3
3 p->o_arg.share_access = nfs4_map_atomic_open_share(..., fmode, ...) = NFS4_SHARE_ACCESS_BOTH =
可以看出第一次和第二次使用 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_file0 // 其他bit可能不为0
f->f_mode = OPEN_FMODE(flags) = 1) & O_ACCMODE) = (3 + 1) & 3 = 0
(flag +
open_last_lookups
lookup_openreturn 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-- == O_RDWR
alloc_nfs_open_context(..., filp->f_mode, ...)0
ctx->mode = f_mode = // open失败
nfs4_atomic_open goto out_drop
err = -EOPENSTALEreturn -EOPENSTALE
if (error == -EOPENSTALE) // 条件满足
error = -ESTALE// 这次open成功 filp = path_openat
nfs_open_context->mode
判断FMODE_EXEC
NFS: 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 表示如果文件不存在则创建
"/mnt/file", O_ACCMODE | O_DIRECT | O_CREAT);
fd = open(
if (fd == -1) {
"无法打开/创建文件");
perror(return EXIT_FAILURE;
}
// 关闭文件
close(fd);
// 重新打开文件,O_ACCMODE 表示读写权限,O_DIRECT 表示直接 I/O
"/mnt/file", O_ACCMODE | O_DIRECT);
fd = open(
if (fd == -1) {
"无法重新打开文件");
perror(return EXIT_FAILURE;
}
// 进行 lseek 操作(在这里只是示例,实际使用时可能需要根据具体需求进行调整)
if (lseek(fd, 0, SEEK_DATA) == -1) {
"lseek 操作失败");
perror(
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 表示如果文件不存在则创建
"/mnt/file", O_ACCMODE | O_DIRECT | O_CREAT);
fd = open(
if (fd == -1) {
"无法打开/创建文件");
perror(return EXIT_FAILURE;
}
// 关闭文件
close(fd);
// 重新打开文件,O_ACCMODE 表示读写权限,O_DIRECT 表示直接 I/O
"/mnt/file", O_ACCMODE | O_DIRECT);
fd = open(
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 # 最新的主线代码预期不发生空指针解引用,不发生panic
open-fail-test
程序返回值不为0
时(打开失败)的情况如下:
null-ptr-deref-test
程序发生panic的情况如下: