背景
某项目的一批机器,较高概率出现,Data分区损坏问题,开机卡Log,重启无法恢复。开机错误Log中出现"inconsistent node block"字样,不同机器损坏文件不一致,损坏文件数量也不一致。重启fsck无法修复此类错误,一些损坏文件是影响Android启动的关键文件,导致设备卡Log。项目的平台是Android 10,内核版本是4.19, Data分区是F2FS文件系统。
出错Log示例
scss
[ 5.681615] (4)[ 1643|Binder:1244_3 ] F2FS-fs (sda16): inconsistent node block, nid:3510, node_footer[nid:0,ino:0,ofs:0,cpver:0,blkaddr:0]
分析步骤
初步故障分析
- 首先找到打印出错Log的代码,在__get_node_page函数里,出错原因是,满足了这个条件,nid != nid_of_node(page),即磁盘上对应nid的Node page读出来的值和预期的nid不符。
scss
static struct page *__get_node_page(struct f2fs_sb_info *sbi, pgoff_t nid,
struct page *parent, int start)
{
struct page *page;
int err;
if (!nid)
return ERR_PTR(-ENOENT);
if (f2fs_check_nid_range(sbi, nid))
return ERR_PTR(-EINVAL);
repeat:
page = f2fs_grab_cache_page(NODE_MAPPING(sbi), nid, false);
if (!page)
return ERR_PTR(-ENOMEM);
err = read_node_page(page, 0);
if (err < 0) {
f2fs_put_page(page, 1);
return ERR_PTR(err);
} else if (err == LOCKED_PAGE) {
err = 0;
goto page_hit;
}
if (parent)
f2fs_ra_node_pages(parent, start + 1, MAX_RA_NODE);
lock_page(page);
if (unlikely(page->mapping != NODE_MAPPING(sbi))) {
f2fs_put_page(page, 1);
goto repeat;
}
if (unlikely(!PageUptodate(page))) {
err = -EIO;
goto out_err;
}
if (!f2fs_inode_chksum_verify(sbi, page)) {
err = -EFSBADCRC;
goto out_err;
}
page_hit:
if(unlikely(nid != nid_of_node(page))) {
** f2fs_msg(sbi->sb, KERN_WARNING, "inconsistent node block, "**
"nid:%lu, node_footer[nid:%u,ino:%u,ofs:%u,cpver:%llu,blkaddr:%u]",
nid, nid_of_node(page), ino_of_node(page),
ofs_of_node(page), cpver_of_node(page),
next_blkaddr_of_node(page));
err = -EINVAL;
out_err:
ClearPageUptodate(page);
f2fs_put_page(page, 1);
return ERR_PTR(err);
}
return page;
}
- 继续查找调用__get_node_page的位置,f2fs_get_node_page和f2fs_get_node_page_ra是调用函数,函数的功能就是从磁盘读取一个node page,f2fs_get_node_page是高频调用函数,在读,写等操作中都会调用到。
c
struct page *f2fs_get_node_page(struct f2fs_sb_info *sbi, pgoff_t nid)
{
return __get_node_page(sbi, nid, NULL, 0);
}
struct page *f2fs_get_node_page_ra(struct page *parent, int start)
{
struct f2fs_sb_info *sbi = F2FS_P_SB(parent);
nid_t nid = get_nid(parent, start, false);
return __get_node_page(sbi, nid, parent, start);
}
- 至此通过代码走读,大概确定了此问题的原因是存储的Node Page出现了数据损坏,至于损坏原因则需要对故障机进一步分析。
故障机分析
初步分析故障现象
- 首先找到一台可以进入adb shell的故障机,查看损坏的data。
- 实际中发现错误现象是,在执行ls的时候,会有错误返回,同时Kernel Log里面会出现"inconsistent node block"。
- 所以通过ls遍历/data分区下所有文件,获得损坏的所有文件信息。
ini
损坏文件总计9个
XXXXXXX:/data # ls -l -i -R | grep -i inval
ls: ./data/com.android.providers.media/databases: Invalid argument
ls: ./data/com.pico.browser/shared_prefs: Invalid argument
ls: ./data/com.pico.browser/app_chrome: Invalid argument
ls: ./data/com.pico.browser/app_webview: Invalid argument
ls: ./data/com.pico.browser/cache/WebView: Invalid argument
ls: ./system_ce/0/shortcut_service: Invalid argument
ls: ./user_de/0/com.pvr.vrshell/code_cache: Invalid argument
ls: ./user_de/0/os.teatracker/shared_prefs: Invalid argument
ls: ./vendor/wifi/wpa: Invalid argument
对应的出错Log
[ 6469.564870] (6)[29965|ls ] F2FS-fs (sda16): inconsistent node block, nid:4449, node_footer[nid:0,ino:0,ofs:0,cpver:0,blkaddr:0]
[ 6469.588465] (7)[29965|ls ] F2FS-fs (sda16): inconsistent node block, nid:4813, node_footer[nid:0,ino:0,ofs:0,cpver:0,blkaddr:0]
[ 6469.588493] (7)[29965|ls ] F2FS-fs (sda16): inconsistent node block, nid:5341, node_footer[nid:0,ino:0,ofs:0,cpver:0,blkaddr:0]
[ 6469.588512] (7)[29965|ls ] F2FS-fs (sda16): inconsistent node block, nid:5362, node_footer[nid:0,ino:0,ofs:0,cpver:0,blkaddr:0]
[ 6469.588949] (7)[29965|ls ] F2FS-fs (sda16): inconsistent node block, nid:5363, node_footer[nid:0,ino:0,ofs:0,cpver:0,blkaddr:0]
[ 6469.711036] (7)[29965|ls ] F2FS-fs (sda16): inconsistent node block, nid:4810, node_footer[nid:0,ino:0,ofs:0,cpver:0,blkaddr:0]
[ 6469.723568] (7)[29965|ls ] F2FS-fs (sda16): inconsistent node block, nid:3067, node_footer[nid:0,ino:0,ofs:0,cpver:0,blkaddr:0]
[ 6469.725820] (7)[29965|ls ] F2FS-fs (sda16): inconsistent node block, nid:3506, node_footer[nid:0,ino:0,ofs:0,cpver:0,blkaddr:0]
[ 6469.729115] (7)[29965|ls ] F2FS-fs (sda16): inconsistent node block, nid:166, node_footer[nid:0,ino:0,ofs:0,cpver:0,blkaddr:0]
通过工具进一步分析
- 从出错的Log中,可以获得出错文件名和nid,这些nid本身没有看出什么规律,下一步根据出错nid获取对应的物理地址,此处需要用到F2FS系统的分析工具dump.f2fs。使用dump.f2fs获取出所有出错nid对应的物理地址。
- 这时会发现,所有出错nid对应的物理地址基本上是很连续的。
vbnet
XXXXXXX:/data # ./dump.f2fs -n 0~-1 /dev/block/by-name/userdata
XXXXXXX:/data # cat dump_nat | grep -w 166
nid: 166 ino: 166 offset: 0 blkaddr: 199390 pack:2
可得所有错误nid对应的物理地址,可以看到这些地址很连续
nid:166, blkaddr:199390
nid:3067, blkaddr:199391
nid:3506, blkaddr:199393
nid:4449, blkaddr:199394
nid:4810, blkaddr:199395
nid:4813, blkaddr:199396
nid:5341, blkaddr:199397
nid:5362, blkaddr:199398
nid:5363, blkaddr:199399
- 查看对应物理地址的数据,发现不仅是错误地址所在区域全部是0,前后也有一段是0,总计116KB
yaml
XXXXXXX:/data # ./busybox hexdump -s 0x30ad2000 -n 118784 /dev/block/by-name/userdata
30ad2000 0000 0000 0000 0000 0000 0000 0000 0000
*
30aef000
//整个区域都是0
0x30ad2000(199378) ~ 0x30aef000(199407)
- f2fs_node的存储结构是4KB的最后24个字节是node_footer,现在整个4KB都是0,所以打印的错误Log是node_footer[nid:0,ino:0,ofs:0,cpver:0,blkaddr:0]
arduino
// f2fs_node size:4096
struct f2fs_node {
union {
struct f2fs_inode i; //size:4072
struct direct_node dn; //size:4072
struct indirect_node in; //size:4072
};
struct node_footer footer; // footer用于记录node的类型
} __packed;
struct node_footer {
__le32 nid; /* node id */
__le32 ino; /* inode nunmber */
__le32 flag; /* include cold/fsync/dentry marks and offset */
__le64 cp_ver; /* checkpoint version */
__le32 next_blkaddr; /* next node page block address */
} __packed;
- 查看其它为0的物理地址为什么没有导致错误文件出现,发现这些地址没有使用,所以没有导致其他文件的损坏。
bash
XXXXXXX:/data # cat dump_nat | grep -w 199378
1|XXXXXXX:/data #
- 至此问题已经很清楚了,是由于磁盘上一块数据被异常修改导致了此问题的出现,那么为什么会出现这种问题呢,一定是有应用绕过文件系统,直接对裸设备进行写操作导致了这个问题的出现。后来经过确定是另外一个同学修改了Linux的KSWAP的代码存在Bug,KSWAP就是在内核里绕过文件系统,直接对data分区的物理地址直接读写,出错的原因是因为在某些条件下写的物理地址错了,正好写到了Node page的区域。
故障修复
虽然找到了引起问题的原因,但是针对这类问题为什么fsck.f2fs没有修复呢?于是上网找了一下发现针对这类故障开源社区已经有了解决方案
lore.kernel.org/linux-f2fs-...
yaml
* [f2fs-dev] [PATCH] f2fs: set SBI_NEED_FSCK flag when inconsistent node block found@ 2021-09-18 12:46 Weichao Guo via Linux-f2fs-devel
2021-09-18 22:34 ` Chao Yu0 siblings, 1 reply; 2+ messages in thread
From: Weichao Guo via Linux-f2fs-devel @ 2021-09-18 12:46 UTC (permalink / raw)
To: jaegeuk, chao; +Cc: omega, linux-f2fs-devel
Inconsistent node block will cause a file fail to open or read,
which could make the user process crashes or stucks. Let's mark
SBI_NEED_FSCK flag to trigger a fix at next fsck time. After
unlinking the corrupted file, the user process could regenerate
a new one and work correctly.
Signed-off-by: Weichao Guo <guoweichao@oppo.com>
---
fs/f2fs/node.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/fs/f2fs/node.c b/fs/f2fs/node.c
index e863136..556fcd8 100644
--- a/fs/f2fs/node.c
+++ b/fs/f2fs/node.c
@@ -1443,6 +1443,7 @@ static struct page *__get_node_page(struct f2fs_sb_info *sbi, pgoff_t nid,
nid, nid_of_node(page), ino_of_node(page),
ofs_of_node(page), cpver_of_node(page),
next_blkaddr_of_node(page));
+ set_sbi_flag(sbi, SBI_NEED_FSCK);
err = -EINVAL;
out_err:
ClearPageUptodate(page);
--
2.7.4
此问题的修改方法是在发现这个错误的时候设置一个需要执行fsck标志,下一次启动执行fsck时进行修复
下面看一下这个修改的生效过程。
- 内存中设置标志
scss
// 更新SBI_NEED_FSCK标志到,sbi->s_flag中
set_sbi_flag(sbi, SBI_NEED_FSCK);
- 在磁盘checkpoint区域设置标志
scss
// 在更新checkpoint的时候会把这个标志转换为CP_FSCK_FLAG,写入到磁盘的checkpoint区
if (is_sbi_flag_set(sbi, SBI_NEED_FSCK))
__set_ckpt_flags(ckpt, CP_FSCK_FLAG);
- 开机重启执行fs_mgr,在挂载文件系统前检查,调用fsck
scss
// 在正常开机,挂载文件系统前的检查f2fs文件系统的流程,走的流程是 fsck.f2fs -a选项,只进行快速修复
// 经确认无法修复此类故障类型。
// -a check/fix potential corruption, reported by f2fs
// -f check/fix entire partition
} else if (is_f2fs(fs_type)) {
const char* f2fs_fsck_argv[] = {F2FS_FSCK_BIN, "-a", blk_device.c_str()};
const char* f2fs_fsck_forced_argv[] = {F2FS_FSCK_BIN, "-f", blk_device.c_str()};
if (should_force_check(*fs_stat)) {
LINFO << "Running " << F2FS_FSCK_BIN << " -f " << realpath(blk_device);
ret = android_fork_execvp_ext(
ARRAY_SIZE(f2fs_fsck_forced_argv), const_cast<char**>(f2fs_fsck_forced_argv), &status,
true, LOG_KLOG | LOG_FILE, true, const_cast<char*>(FSCK_LOG_FILE), nullptr, 0);
} else {
** // 正常开机都会走到这个分支**
** LINFO << "Running " << F2FS_FSCK_BIN << " -a " << realpath(blk_device);**
ret = android_fork_execvp_ext(
ARRAY_SIZE(f2fs_fsck_argv), const_cast<char**>(f2fs_fsck_argv), &status, true,
LOG_KLOG | LOG_FILE, true, const_cast<char*>(FSCK_LOG_FILE), nullptr, 0);
}
if (ret < 0) {
/* No need to check for error in fork, we can't really handle it now */
LERROR << "Failed trying to run " << F2FS_FSCK_BIN;
}
}
- 执行fsck,满足fsck -a && CP_FSCK_FLAG条件,设置内部修复标志fix_on,进行更完整的修复,当然执行时间也会长一些。
scss
//在执行fsck的时候,会根据CP_FSCK_FLAG标志,设置c.fix_on为1,这样会对磁盘进行完整扫描,修复错误
if (c.auto_fix || c.preen_mode) {
u32 flag = get_cp(ckpt_flags);
**if (flag & CP_FSCK_FLAG ||**
flag & CP_QUOTA_NEED_FSCK_FLAG ||
(exist_qf_ino(sb) && (flag & CP_ERROR_FLAG))) {
c.fix_on = 1;
} else if (!c.preen_mode) {
print_cp_state(flag);
return 1;
}
}
- 综上,此patch设置CP_FSCK_FLAG,可以触发fsck进行更完整的修复(最完整的是-f选项),修复了这个错误。
- 此类故障损坏的区域不属于整个文件系统,损坏的仅仅是文件的node,因为node是所有访问的基础,导致文件的各种访问都会出现问题。但是由于这个文件仍然存在,因此应用层无法重新创建,有些损坏的文件有可能导致Android无法正常启动,则会造成卡Log的现象。
总结
- 故障出现的直接原因是,data分区一段物理地址的数据被异常改写导致
- 修复Patch可以使fsck.f2fs执行比正常启动时候更全面的修复操作,修复此类故障,由于该项目的代码基线比较老,没有包含这个修复Patch,所以才暴露了这个问题。
- 此故障产生原因是出现了没有通过文件系统,直接对裸设备按照物理地址写操作,破坏了关键文件的node数据区域。实际上,如果想构造这个故障很简单,首先通过dump.f2fs获取nid对应的物理地址,然后采用dd直接对裸设备写操作:例如dd if=/dev/zero of=/dev/block/by-name/userdata bs=XXX count=XXX seek=XXX