Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 8 additions & 25 deletions components/rsext4/src/file/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,6 @@ fn remove_dentry_in_dir_block(
) -> bool {
let block_bytes = BLOCK_SIZE;
let mut offset: usize = 0;
let mut prev_off: Option<usize> = None;
let mut prev_rec_len: u16 = 0;
while offset + 8 <= block_bytes {
let inode = u32::from_le_bytes([
data[offset],
Expand All @@ -178,27 +176,14 @@ fn remove_dentry_in_dir_block(
if name_len > 0 && offset + 8 + name_len <= entry_end {
let name = &data[offset + 8..offset + 8 + name_len];
if inode != 0 && name == name_bytes {
if let Some(poff) = prev_off {
// Merge current entry's space into previous entry.
let new_len = prev_rec_len.saturating_add(rec_len);
let bytes = new_len.to_le_bytes();
data[poff + 4] = bytes[0];
data[poff + 5] = bytes[1];

// Clear current entry inode so it will be treated as free.
let zero = 0u32.to_le_bytes();
data[offset] = zero[0];
data[offset + 1] = zero[1];
data[offset + 2] = zero[2];
data[offset + 3] = zero[3];
} else {
// No previous entry in this block: mark this entry free.
let zero = 0u32.to_le_bytes();
data[offset] = zero[0];
data[offset + 1] = zero[1];
data[offset + 2] = zero[2];
data[offset + 3] = zero[3];
}
// Mark entry as deleted by zeroing inode. Do NOT merge rec_len
// into the previous entry — keeping rec_len unchanged preserves
// stable byte offsets for readdir (getdents64) across deletions.
let zero = 0u32.to_le_bytes();
data[offset] = zero[0];
data[offset + 1] = zero[1];
data[offset + 2] = zero[2];
data[offset + 3] = zero[3];
update_ext4_dirblock_csum32(
superblock,
parent_ino_num.raw(),
Expand All @@ -211,8 +196,6 @@ fn remove_dentry_in_dir_block(
if entry_end >= block_bytes {
break;
}
prev_off = Some(offset);
prev_rec_len = rec_len;
offset = entry_end;
}
false
Expand Down
43 changes: 33 additions & 10 deletions os/arceos/modules/axfs-ng/src/fs/ext4/rsext4/inode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,29 +422,52 @@ impl DirNodeOps for Inode {
let blocks = rsext4::loopfile::resolve_inode_block_allextend(fs, dev, &mut inode)
.map_err(into_vfs_err)?;

let mut idx = 0u64;
let mut byte_offset: u64 = 0;
let mut count = 0usize;
for &phys in blocks.values() {
let cached = fs
.datablock_cache
.get_or_load(dev, phys)
.map_err(into_vfs_err)?;
let data = &cached.data[..BLOCK_SIZE];
let iter = rsext4::entries::DirEntryIterator::new(data);
for (entry, _) in iter {
if entry.inode == 0 {

// Manually iterate entries, tracking byte_offset for ALL entries
// (including inode==0 deleted ones) so offset stays physical.
let mut pos = 0usize;
while pos + 8 <= data.len() {
let entry_inode =
u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
let rec_len = u16::from_le_bytes([data[pos + 4], data[pos + 5]]);
if rec_len < 8 {
break;
}
let rec_usize = rec_len as usize;
if pos + rec_usize > data.len() {
break;
}

let entry_offset = byte_offset;
byte_offset += rec_len as u64;
pos += rec_usize;

if entry_inode == 0 {
continue;
}
if entry_offset < offset {
continue;
}
if idx < offset {
idx += 1;

let name_len = data[pos - rec_usize + 6] as usize;
let file_type = data[pos - rec_usize + 7];
let name_start = pos - rec_usize + 8;
if name_len > rec_usize - 8 {
continue;
}
let name = core::str::from_utf8(entry.name)
let name = core::str::from_utf8(&data[name_start..name_start + name_len])
.map_err(|_| VfsError::InvalidData)?
.to_owned();
let node_type = dir_entry_type_to_vfs(entry.file_type);
idx += 1;
if !sink.accept(&name, entry.inode as u64, node_type, idx) {
let node_type = dir_entry_type_to_vfs(file_type);
if !sink.accept(&name, entry_inode as u64, node_type, byte_offset) {
return Ok(count);
}
count += 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)
add_executable(bug-ext4-dir-ops src/main.c)
target_compile_options(bug-ext4-dir-ops PRIVATE -Wall -Wextra -Werror)
target_compile_options(bug-ext4-dir-ops PRIVATE -Wall -Wextra -Werror -Wno-format-truncation)
install(TARGETS bug-ext4-dir-ops RUNTIME DESTINATION usr/bin)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*!
* bug-ext4-dir-ops.c
*
*
* Verifies POSIX rmdir() and rename() semantics on ext4 (rsext4),
* plus VFS dentry cache correctness after rename (stale parent bug).
*
Expand All @@ -13,8 +13,18 @@
#include <dirent.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>

/* linux_dirent64 for getdents64 syscall */
struct linux_dirent64 {
unsigned long long d_ino;
long long d_off;
unsigned short d_reclen;
unsigned char d_type;
char d_name[];
};

#define BASE "/root/bug-ext4-dir-ops-test"

/* ========== 辅助函数 ========== */
Expand Down Expand Up @@ -571,6 +581,156 @@ static void test_rename_many_files(void)
force_remove(dir);
}

/* ========== readdir offset after delete ========== */

/*
* Reproduce the rm -rf bug: read entries with getdents64 using a tiny buffer
* (1 entry per call), delete each entry immediately, then continue reading.
* On correct kernel (byte offsets), the fd position advances past the deleted
* entry and lands on the next one. On buggy kernel (logical indices), the
* index shifts after deletion and entries get skipped.
*/
static void test_readdir_offset_after_delete(void)
{
char dir[256], path[256], content[64];

snprintf(dir, sizeof(dir), "%s/readdir_del", BASE);
CHECK(mkdir(dir, 0755) == 0, "mkdir readdir_del");

/* Create 30 files */
int ok = 1;
for (int i = 0; i < 30; i++) {
snprintf(path, sizeof(path), "%s/f%02d", dir, i);
snprintf(content, sizeof(content), "%d\n", i);
if (write_file(path, content) < 0)
ok = 0;
}
CHECK(ok, "created 30 files");

/*
* Read-delete loop: tiny buffer forces 1 entry per getdents64 call.
* After reading each entry, delete it immediately. If offset tracking
* is correct, we'll see all 30 entries. If buggy, entries get skipped.
*/
int dfd = open(dir, O_RDONLY | O_DIRECTORY);
CHECK(dfd >= 0, "open dir for read-delete loop");

char buf[32]; /* minimum buffer: exactly 1 entry */
int total_deleted = 0;
int rounds = 0;

for (;;) {
int nread = syscall(SYS_getdents64, dfd, buf, sizeof(buf));
if (nread <= 0)
break;

int pos = 0;
while (pos < nread) {
struct linux_dirent64 *ent =
(struct linux_dirent64 *)(buf + pos);
if (strcmp(ent->d_name, ".") != 0 &&
strcmp(ent->d_name, "..") != 0) {
snprintf(path, sizeof(path), "%s/%s", dir, ent->d_name);
if (unlink(path) == 0)
total_deleted++;
}
pos += ent->d_reclen;
}
rounds++;
}
close(dfd);

CHECK(total_deleted == 30,
"read-delete loop: all 30 files deleted");
CHECK(rounds > 1,
"read-delete loop: needed multiple getdents calls (bug trigger)");

/* If bug exists, some files were skipped and rmdir fails */
int rmdir_ret = rmdir(dir);
CHECK_RET(rmdir_ret, 0,
"rmdir after read-delete loop succeeds (no skipped entries)");
if (rmdir_ret != 0)
force_remove(dir);
}

/*
* Variant: read-readdir-delete-readdir pattern (simulates rm -rf).
* Read all entries via getdents64, delete each batch before reading next.
*/
static void test_rm_rf_pattern(void)
{
char dir[256], path[256], content[64];

snprintf(dir, sizeof(dir), "%s/rmrf", BASE);
CHECK(mkdir(dir, 0755) == 0, "mkdir rmrf");

/* Create 30 files */
for (int i = 0; i < 30; i++) {
snprintf(path, sizeof(path), "%s/f%02d", dir, i);
snprintf(content, sizeof(content), "%d\n", i);
write_file(path, content);
}

/*
* Simulate rm -rf: open dir, read small batch, delete those files,
* repeat until empty. If offset is logical and entries are deleted
* between getdents calls, files get skipped and rmdir fails.
*/
int dfd = open(dir, O_RDONLY | O_DIRECTORY);
CHECK(dfd >= 0, "open dir for rm-rf pattern");

char buf[80];
int total_deleted = 0;
int rounds = 0;

for (;;) {
/* Read one batch */
int nread = syscall(SYS_getdents64, dfd, buf, sizeof(buf));
if (nread <= 0)
break;

/* Collect names from this batch */
char names[4][32];
int name_count = 0;
int pos = 0;
while (pos < nread && name_count < 4) {
struct linux_dirent64 *ent =
(struct linux_dirent64 *)(buf + pos);
if (strcmp(ent->d_name, ".") != 0 &&
strcmp(ent->d_name, "..") != 0) {
strncpy(names[name_count], ent->d_name, 31);
names[name_count][31] = '\0';
name_count++;
}
pos += ent->d_reclen;
}

/* Delete this batch */
for (int i = 0; i < name_count; i++) {
snprintf(path, sizeof(path), "%s/%s", dir, names[i]);
if (unlink(path) == 0)
total_deleted++;
}
rounds++;
}
close(dfd);

CHECK(total_deleted == 30,
"rm-rf pattern: all 30 files deleted");
CHECK(rounds > 1,
"rm-rf pattern: needed multiple getdents calls");

/*
* If the bug exists, some files were skipped by getdents and remain.
* rmdir should succeed only if all files were deleted.
*/
int rmdir_ret = rmdir(dir);
CHECK_RET(rmdir_ret, 0,
"rmdir after rm-rf pattern succeeds (no leftover files)");
if (rmdir_ret != 0)
force_remove(dir);
}

/* ========== main ========== */

int main(void)
Expand Down Expand Up @@ -611,6 +771,10 @@ int main(void)
test_pip_upgrade_pattern();
test_rename_many_files();

/* readdir offset correctness after deletion (rm -rf bug) */
test_readdir_offset_after_delete();
test_rm_rf_pattern();

manual_rmdir_recursive(BASE);

TEST_DONE();
Expand Down
Loading