一个F2FS文件系统Data分区损坏的真实案例

背景

某项目的一批机器,较高概率出现,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]

分析步骤

初步故障分析

  1. 首先找到打印出错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;
}
  1. 继续查找调用__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);
}
  1. 至此通过代码走读,大概确定了此问题的原因是存储的Node Page出现了数据损坏,至于损坏原因则需要对故障机进一步分析。

故障机分析

初步分析故障现象

  1. 首先找到一台可以进入adb shell的故障机,查看损坏的data。
  2. 实际中发现错误现象是,在执行ls的时候,会有错误返回,同时Kernel Log里面会出现"inconsistent node block"。
  3. 所以通过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]

通过工具进一步分析

  1. 从出错的Log中,可以获得出错文件名和nid,这些nid本身没有看出什么规律,下一步根据出错nid获取对应的物理地址,此处需要用到F2FS系统的分析工具dump.f2fs。使用dump.f2fs获取出所有出错nid对应的物理地址。
  2. 这时会发现,所有出错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
  1. 查看对应物理地址的数据,发现不仅是错误地址所在区域全部是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)
  1. 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;
  1. 查看其它为0的物理地址为什么没有导致错误文件出现,发现这些地址没有使用,所以没有导致其他文件的损坏。
bash 复制代码
XXXXXXX:/data # cat dump_nat | grep -w 199378
1|XXXXXXX:/data #
  1. 至此问题已经很清楚了,是由于磁盘上一块数据被异常修改导致了此问题的出现,那么为什么会出现这种问题呢,一定是有应用绕过文件系统,直接对裸设备进行写操作导致了这个问题的出现。后来经过确定是另外一个同学修改了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时进行修复

下面看一下这个修改的生效过程。

  1. 内存中设置标志
scss 复制代码
// 更新SBI_NEED_FSCK标志到,sbi->s_flag中
set_sbi_flag(sbi, SBI_NEED_FSCK);
  1. 在磁盘checkpoint区域设置标志
scss 复制代码
// 在更新checkpoint的时候会把这个标志转换为CP_FSCK_FLAG,写入到磁盘的checkpoint区
if (is_sbi_flag_set(sbi, SBI_NEED_FSCK))
    __set_ckpt_flags(ckpt, CP_FSCK_FLAG);
  1. 开机重启执行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;
        }
    }
  1. 执行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;
                }
        }
  1. 综上,此patch设置CP_FSCK_FLAG,可以触发fsck进行更完整的修复(最完整的是-f选项),修复了这个错误。
  2. 此类故障损坏的区域不属于整个文件系统,损坏的仅仅是文件的node,因为node是所有访问的基础,导致文件的各种访问都会出现问题。但是由于这个文件仍然存在,因此应用层无法重新创建,有些损坏的文件有可能导致Android无法正常启动,则会造成卡Log的现象。

总结

  1. 故障出现的直接原因是,data分区一段物理地址的数据被异常改写导致
  2. 修复Patch可以使fsck.f2fs执行比正常启动时候更全面的修复操作,修复此类故障,由于该项目的代码基线比较老,没有包含这个修复Patch,所以才暴露了这个问题。
  3. 此故障产生原因是出现了没有通过文件系统,直接对裸设备按照物理地址写操作,破坏了关键文件的node数据区域。实际上,如果想构造这个故障很简单,首先通过dump.f2fs获取nid对应的物理地址,然后采用dd直接对裸设备写操作:例如dd if=/dev/zero of=/dev/block/by-name/userdata bs=XXX count=XXX seek=XXX
相关推荐
二十雨辰1 小时前
[linux]docker基础
linux·运维·docker
饮浊酒2 小时前
Linux操作系统 ------(3.文本编译器Vim)
linux·vim
lihuhelihu2 小时前
第3章 CentOS系统管理
linux·运维·服务器·计算机网络·ubuntu·centos·云计算
矛取矛求2 小时前
Linux系统性能调优技巧
linux
One_Blanks2 小时前
渗透测试-Linux基础(1)
linux·运维·安全
Perishell2 小时前
无人机避障——大疆与Airsim中的角速度信息订阅获取
linux·动态规划·无人机
爱吃喵的鲤鱼2 小时前
linux进程的状态之环境变量
linux·运维·服务器·开发语言·c++
dessler3 小时前
Linux系统-ubuntu系统安装
linux·运维·云计算
荒Huang3 小时前
Linux挖矿病毒(kswapd0进程使cpu爆满)
linux·运维·服务器
hjjdebug5 小时前
linux 下 signal() 函数的用法,信号类型在哪里定义的?
linux·signal