一个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_footernid: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
相关推荐
且从容.23 分钟前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落2 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨2 小时前
WSL2的安装与使用
linux·wsl
微小的鱼2 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng2 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮3 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
Linux-lucky15 小时前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu
禁默15 小时前
启元 Q1 二次开发实践:Ubuntu 部署与仿真验证,从 PrimeConsole 到 ROS 2 Humble + RViz
linux·运维·ubuntu
skywalk816316 小时前
把 去也网站 从 Ubuntu 搬到 FreeBSD:一次 Service Jail + nginx 的部署实录
linux·运维·nginx·ubuntu·freebsd
爱莉希雅&&&18 小时前
GitLab 完整学习笔记
linux·运维·服务器