削除されたファイルの状態について、CentOS 7.9 と Oracle Linux 7.9 (WSL2) で挙動が異なっていたので調べました。
ファイル削除時のディレクトリエントリの状態
CentOS 7.9の場合
ext4ファイルシステムになっているパーティションをマウントして、サンプルファイルをいくつか作成します。
[root@localhost ~]# mkfs.ext4 /dev/sdb1
(略)
[root@localhost ~]# mount /dev/sdb1 /mnt/test/
[root@localhost ~]# date > /mnt/test/a.txt
[root@localhost ~]# date > /mnt/test/b.txt
[root@localhost ~]# date > /mnt/test/c.txt
debugfsで該当ディレクトリ(/)のブロック番号を確認し、block_dump
でブロックデータをダンプします。
[root@localhost ~]# debugfs /dev/sdb1
debugfs 1.42.9 (28-Dec-2013)
debugfs: ls
2 (12) . 2 (12) .. 11 (20) lost+found 12 (16) a.txt
13 (16) b.txt 14 (4020) c.txt
debugfs: stat /
Inode: 2 Type: directory Mode: 0755 Flags: 0x80000
Generation: 0 Version: 0x00000000:00000003
User: 0 Group: 0 Size: 4096
File ACL: 0 Directory ACL: 0
Links: 3 Blockcount: 8
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x6519f801:5bf0c4bc -- Mon Oct 2 07:51:45 2023
atime: 0x6519f7e8:00000000 -- Mon Oct 2 07:51:20 2023
mtime: 0x6519f801:5bf0c4bc -- Mon Oct 2 07:51:45 2023
crtime: 0x6519f7e8:00000000 -- Mon Oct 2 07:51:20 2023
Size of extra inode fields: 28
EXTENTS:
(0):8737
debugfs: block_dump 8737
0000 0200 0000 0c00 0102 2e00 0000 0200 0000 ................
0020 0c00 0202 2e2e 0000 0b00 0000 1400 0a02 ................
0040 6c6f 7374 2b66 6f75 6e64 0000 0c00 0000 lost+found......
0060 1000 0501 612e 7478 7400 0000 0d00 0000 ....a.txt.......
0100 1000 0501 622e 7478 7400 0000 0e00 0000 ....b.txt.......
0120 b40f 0501 632e 7478 7400 0000 0000 0000 ....c.txt.......
0140 0000 0000 0000 0000 0000 0000 0000 0000 ................
*
debugfs: quit
0075~0114(8進数)の16バイトがb.txtのエントリーとなります。
その後b.txtのみを削除し、再度同じブロックの状態を確認します。
[root@localhost ~]# rm /mnt/test/b.txt
rm: 通常ファイル `/mnt/test/b.txt' を削除しますか? y
[root@localhost ~]# debugfs /dev/sdb1
debugfs 1.42.9 (28-Dec-2013)
debugfs: ls -d
2 (12) . 2 (12) .. 11 (20) lost+found 12 (32) a.txt
<13> (16) b.txt 14 (4020) c.txt
debugfs: block_dump 8737
0000 0200 0000 0c00 0102 2e00 0000 0200 0000 ................
0020 0c00 0202 2e2e 0000 0b00 0000 1400 0a02 ................
0040 6c6f 7374 2b66 6f75 6e64 0000 0c00 0000 lost+found......
0060 2000 0501 612e 7478 7400 0000 0d00 0000 ...a.txt.......
0100 1000 0501 622e 7478 7400 0000 0e00 0000 ....b.txt.......
0120 b40f 0501 632e 7478 7400 0000 0000 0000 ....c.txt.......
0140 0000 0000 0000 0000 0000 0000 0000 0000 ................
*
ls -d
の結果として削除された<13> (16) b.txt
が表示されています。13が削除されたiノード番号を表し、16が該当ファイルのエントリー長を表しています。
ダンプデータにもb.txtエントリーのデータがそのまま残っていますが、ひとつ前のエントリa.txtのデータ長(0060~0061の2バイト、リトルエンディアン)が16バイトから32バイトに変わっており(0x0010→0x0020)、b.txtの領域だったところがa.txtの一部として扱われています。ls
の結果でも12 (16) a.txt
から12 (32) a.txt
に変わっています。
Oracle Linux 7.9(WSL2)の場合
こちらでも同じ操作を実行し、ファイル削除前の状態は以下の通りです。
[root@DESKTOP-HE7J8K0 ~]# debugfs /dev/sdd1
debugfs 1.42.9 (28-Dec-2013)
debugfs: ls -d
2 (12) . 2 (12) .. 11 (20) lost+found 12 (16) a.txt
13 (16) b.txt 14 (4020) c.txt
debugfs: stat /
Inode: 2 Type: directory Mode: 0755 Flags: 0x80000
(略)
EXTENTS:
(0):8737
debugfs: block_dump 8737
0000 0200 0000 0c00 0102 2e00 0000 0200 0000 ................
0020 0c00 0202 2e2e 0000 0b00 0000 1400 0a02 ................
0040 6c6f 7374 2b66 6f75 6e64 0000 0c00 0000 lost+found......
0060 1000 0501 612e 7478 7400 0000 0d00 0000 ....a.txt.......
0100 1000 0501 622e 7478 7400 0000 0e00 0000 ....b.txt.......
0120 b40f 0501 632e 7478 7400 0000 0000 0000 ....c.txt.......
0140 0000 0000 0000 0000 0000 0000 0000 0000 ................
*
ここまでの状況は同じで、このあとb.txtを削除します。
[root@DESKTOP-HE7J8K0 ~]# rm /mnt/test/b.txt
rm: remove regular file '/mnt/test/b.txt'? y
[root@DESKTOP-HE7J8K0 ~]# debugfs /dev/sdd1
debugfs 1.42.9 (28-Dec-2013)
debugfs: ls -d
2 (12) . 2 (12) .. 11 (20) lost+found 12 (32) a.txt
14 (4020) c.txt
debugfs: block_dump 8737
0000 0200 0000 0c00 0102 2e00 0000 0200 0000 ................
0020 0c00 0202 2e2e 0000 0b00 0000 1400 0a02 ................
0040 6c6f 7374 2b66 6f75 6e64 0000 0c00 0000 lost+found......
0060 2000 0501 612e 7478 7400 0000 0000 0000 ...a.txt.......
0100 0000 0000 0000 0000 0000 0000 0e00 0000 ................
0120 b40f 0501 632e 7478 7400 0000 0000 0000 ....c.txt.......
0140 0000 0000 0000 0000 0000 0000 0000 0000 ................
*
こちらではls -d
の結果にb.txtは表示されず、ダンプデータのb.txtがあった個所はゼロ埋めさえてきれいに消されてしまっています。このブロックデータから消されたファイル名を元にiノード番号を調べるようなことはできません。
ソースコード確認
Linux Kernelの該当処理を確認します。それぞれのディストリビューションで使用しているカーネルバージョンは以下の通りです。
CentOS 7.9の場合:
[root@localhost ~]# uname -a
Linux localhost.localdomain 3.10.0-1160.el7.x86_64 #1 SMP Mon Oct 19 16:18:59 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
Oracle Linux 7.9(WSL2)の場合:
[root@DESKTOP-HE7J8K0 ~]# uname -a
Linux DESKTOP-HE7J8K0 5.15.90.1-microsoft-standard-WSL2 #1 SMP Fri Jan 27 02:56:13 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
Oracle Linuxは使用できるカーネルは Red Hat Compatible Kernel(RHCK)と Unbreakable Enterprise Kernel(UEK)の2種類あり、以下を見ると7.9のUEKは5.4.17のようなので、今回試した Windows WSL2 はさらに異なっています。
カーネルの処理
構造体ext4_dir_entry_2への削除操作は fs/ext4/namei.c の ext4_generic_delete_entry() で行われています。
Oracle Linux 7.9(kernel 5.15.90.1)の場合は以下のようになっています。
https://elixir.bootlin.com/linux/v5.15.90/source/fs/ext4/namei.c#L2640
/*
* ext4_generic_delete_entry deletes a directory entry by merging it
* with the previous entry
*/
int ext4_generic_delete_entry(struct inode *dir,
struct ext4_dir_entry_2 *de_del,
struct buffer_head *bh,
void *entry_buf,
int buf_size,
int csum_size)
{
struct ext4_dir_entry_2 *de, *pde;
unsigned int blocksize = dir->i_sb->s_blocksize;
int i;
i = 0;
pde = NULL;
de = (struct ext4_dir_entry_2 *)entry_buf;
while (i < buf_size - csum_size) {
if (ext4_check_dir_entry(dir, NULL, de, bh,
entry_buf, buf_size, i))
return -EFSCORRUPTED;
if (de == de_del) {
if (pde) {
pde->rec_len = ext4_rec_len_to_disk(
ext4_rec_len_from_disk(pde->rec_len,
blocksize) +
ext4_rec_len_from_disk(de->rec_len,
blocksize),
blocksize);
/* wipe entire dir_entry */
memset(de, 0, ext4_rec_len_from_disk(de->rec_len,
blocksize));
} else {
/* wipe dir_entry excluding the rec_len field */
de->inode = 0;
memset(&de->name_len, 0,
ext4_rec_len_from_disk(de->rec_len,
blocksize) -
offsetof(struct ext4_dir_entry_2,
name_len));
}
inode_inc_iversion(dir);
return 0;
}
i += ext4_rec_len_from_disk(de->rec_len, blocksize);
pde = de;
de = ext4_next_entry(de, blocksize);
}
return -ENOENT;
}
ループ処理でディレクトリエントリを順に処理していますが、途中で "wipe entire dir_entry" とコメントがあるところで該当領域を0で埋めていることがわかります。
いつ変更されたのか
ChangeLog-5.13によると、commit id 6c0912739699d8e4b6a87086401bf3ad3c59502d としてkernel 5.13.0 以降でmemsetでゼロ埋めするような変更が fs/ext4/namei.c に加えられています。
$ git show 6c0912739699d8e4b6a87086401bf3ad3c59502d
commit 6c0912739699d8e4b6a87086401bf3ad3c59502d
Author: Leah Rumancik <leah.rumancik@gmail.com>
Date: Thu Apr 22 18:08:34 2021 +0000
ext4: wipe ext4_dir_entry2 upon file deletion
Upon file deletion, zero out all fields in ext4_dir_entry2 besides rec_len.
In case sensitive data is stored in filenames, this ensures no potentially
sensitive data is left in the directory entry upon deletion. Also, wipe
these fields upon moving a directory entry during the conversion to an
htree and when splitting htree nodes.
The data wiped may still exist in the journal, but there are future
commits planned to address this.
Signed-off-by: Leah Rumancik <leah.rumancik@gmail.com>
Link: https://lore.kernel.org/r/20210422180834.2242353-1-leah.rumancik@gmail.com
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index 680fc3211cbf..8b46a17a85c1 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
(略)
@@ -2577,15 +2585,27 @@ int ext4_generic_delete_entry(struct inode *dir,
entry_buf, buf_size, i))
return -EFSCORRUPTED;
if (de == de_del) {
- if (pde)
+ if (pde) {
pde->rec_len = ext4_rec_len_to_disk(
ext4_rec_len_from_disk(pde->rec_len,
blocksize) +
ext4_rec_len_from_disk(de->rec_len,
blocksize),
blocksize);
- else
+
+ /* wipe entire dir_entry */
+ memset(de, 0, ext4_rec_len_from_disk(de->rec_len,
+ blocksize));
+ } else {
+ /* wipe dir_entry excluding the rec_len field */
de->inode = 0;
+ memset(&de->name_len, 0,
+ ext4_rec_len_from_disk(de->rec_len,
+ blocksize) -
+ offsetof(struct ext4_dir_entry_2,
+ name_len));
+ }
+
inode_inc_iversion(dir);
return 0;
}
まとめ
変更されたのがkernel 5.13.0なので、CentOS 7.9のkernel 3.10.0ではまだゼロ埋めされない処理になっています。
Oracle LinuxはいわゆるRHELクローンで、RHELおよびCentOSと100%アプリケーション・バイナリ互換性があると謳っていますが、当然カーネルが違えば内部の処理も異なってくるので注意が必要です。
この5.13.0で適用された変更はファイルを適切に削除する(削除されたファイルの痕跡を残さない)という意味では正しいと思われますが、誤削除したファイルをなんとか復旧させたい人にとっては手がかりがひとつなくなったことになります。
参考
- ファイル削除時のディレクトリブロックをダンプしての挙動確認:
Understanding EXT4 (Part 6): Directories - 構造体ext4_dir_entry_2の詳細:
Ext4 Disk Layout - Directory Entries