Skip to content

Commit 6c4f8f0

Browse files
authored
Merge pull request #90 from ryanbreen/feature/exec-from-filesystem
feat(exec): enable loading ELF binaries from ext2 filesystem
2 parents 6635fd5 + 53dac45 commit 6c4f8f0

File tree

12 files changed

+1313
-7
lines changed

12 files changed

+1313
-7
lines changed

kernel/src/fs/ext2/block_group.rs

Lines changed: 524 additions & 3 deletions
Large diffs are not rendered by default.

kernel/src/fs/ext2/mod.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,26 @@ impl Ext2Fs {
293293
return Err("Not a regular file");
294294
}
295295

296-
// For now, just set size to 0 and clear block pointers
297-
// A full implementation would also free the data blocks in the block bitmap
296+
// Free all allocated data blocks before clearing pointers
297+
// This prevents block leaks where blocks remain marked "in use" but are unreachable
298+
let i_block = inode.i_block;
299+
300+
// Free direct blocks (0-11)
301+
for i in 0..12 {
302+
if i_block[i] != 0 {
303+
let _ = block_group::free_block(
304+
self.device.as_ref(),
305+
i_block[i],
306+
&self.superblock,
307+
&mut self.block_groups,
308+
);
309+
}
310+
}
311+
312+
// TODO: Free indirect blocks (single, double, triple) for large files
313+
// For now, just handle direct blocks which covers files up to 12KB (1KB blocks)
314+
// or 48KB (4KB blocks)
315+
298316
inode.i_size = 0;
299317
inode.i_dir_acl = 0; // Clear high bits of size
300318
inode.i_blocks = 0;

kernel/src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,14 @@ fn kernel_main_continue() -> ! {
838838
log::info!("=== FS TEST: cwd syscalls (getcwd, chdir) ===");
839839
test_exec::test_cwd();
840840

841+
// Test exec from ext2 filesystem
842+
log::info!("=== FS TEST: exec from ext2 filesystem ===");
843+
test_exec::test_exec_from_ext2();
844+
845+
// Test filesystem block allocation (regression test for truncate/alloc bugs)
846+
log::info!("=== FS TEST: block allocation regression test ===");
847+
test_exec::test_fs_block_alloc();
848+
841849
// Test Rust std library support
842850
log::info!("=== STD TEST: Rust std library support ===");
843851
test_exec::test_hello_std_real();

kernel/src/syscall/handlers.rs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,50 @@ pub fn sys_exec_with_frame(
11421142
})
11431143
}
11441144

1145+
/// Load ELF binary from ext2 filesystem path.
1146+
///
1147+
/// Returns the file content as Vec<u8> on success, or an errno on failure.
1148+
///
1149+
/// NOTE: This function intentionally has NO logging to avoid timing overhead.
1150+
/// It's called on every exec syscall, and serial I/O causes CI timing issues.
1151+
#[cfg(feature = "testing")]
1152+
fn load_elf_from_ext2(path: &str) -> Result<Vec<u8>, i32> {
1153+
use super::errno::{EACCES, EIO, ENOENT, ENOTDIR};
1154+
use crate::fs::ext2;
1155+
1156+
// Get ext2 filesystem
1157+
let fs_guard = ext2::root_fs();
1158+
let fs = fs_guard.as_ref().ok_or(EIO)?;
1159+
1160+
// Resolve path to inode number
1161+
let inode_num = fs.resolve_path(path).map_err(|e| {
1162+
if e.contains("not found") {
1163+
ENOENT
1164+
} else {
1165+
EIO
1166+
}
1167+
})?;
1168+
1169+
// Read inode metadata
1170+
let inode = fs.read_inode(inode_num).map_err(|_| EIO)?;
1171+
1172+
// Check it's a regular file (not directory)
1173+
if inode.is_dir() {
1174+
return Err(ENOTDIR);
1175+
}
1176+
1177+
// Check execute permission (S_IXUSR = 0o100)
1178+
let perms = inode.permissions();
1179+
if (perms & 0o100) == 0 {
1180+
return Err(EACCES);
1181+
}
1182+
1183+
// Read file content
1184+
let data = fs.read_file_content(&inode).map_err(|_| EIO)?;
1185+
1186+
Ok(data)
1187+
}
1188+
11451189
/// sys_execv_with_frame - Replace the current process with a new program (with argv support)
11461190
///
11471191
/// This is the extended implementation that supports passing command-line arguments.
@@ -1268,8 +1312,24 @@ pub fn sys_execv_with_frame(
12681312

12691313
#[cfg(feature = "testing")]
12701314
{
1271-
// Load the binary from the test disk by name
1272-
let elf_vec = crate::userspace_test::get_test_binary(program_name);
1315+
// Try to load from ext2 filesystem first, fall back to test disk
1316+
let elf_vec = if program_name.contains('/') {
1317+
// Path-like name: load from ext2 filesystem
1318+
match load_elf_from_ext2(program_name) {
1319+
Ok(data) => data,
1320+
Err(errno) => return SyscallResult::Err(errno as u64),
1321+
}
1322+
} else {
1323+
// Bare name: try ext2 /bin/ first, then fall back to test disk
1324+
let bin_path = alloc::format!("/bin/{}", program_name);
1325+
match load_elf_from_ext2(&bin_path) {
1326+
Ok(data) => data,
1327+
Err(_) => {
1328+
// Fall back to test disk for compatibility
1329+
crate::userspace_test::get_test_binary(program_name)
1330+
}
1331+
}
1332+
};
12731333
let boxed_slice = elf_vec.into_boxed_slice();
12741334
let elf_data = Box::leak(boxed_slice) as &'static [u8];
12751335
let name_string = alloc::string::String::from(program_name);

kernel/src/test_exec.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,6 +2033,63 @@ pub fn test_cwd() {
20332033
}
20342034
}
20352035

2036+
/// Test exec from ext2 filesystem
2037+
pub fn test_exec_from_ext2() {
2038+
log::info!("Testing exec from ext2 filesystem");
2039+
2040+
#[cfg(feature = "testing")]
2041+
let exec_ext2_elf_buf = crate::userspace_test::get_test_binary("exec_from_ext2_test");
2042+
#[cfg(feature = "testing")]
2043+
let exec_ext2_elf: &[u8] = &exec_ext2_elf_buf;
2044+
#[cfg(not(feature = "testing"))]
2045+
let exec_ext2_elf = &create_hello_world_elf();
2046+
2047+
match crate::process::creation::create_user_process(
2048+
String::from("exec_from_ext2_test"),
2049+
exec_ext2_elf,
2050+
) {
2051+
Ok(pid) => {
2052+
log::info!("Created exec_from_ext2_test process with PID {:?}", pid);
2053+
log::info!("Exec ext2 test: process scheduled for execution.");
2054+
log::info!(" -> Userspace will emit EXEC_EXT2_TEST_PASSED marker if successful");
2055+
}
2056+
Err(e) => {
2057+
log::error!("Failed to create exec_from_ext2_test process: {}", e);
2058+
log::error!("Exec ext2 test cannot run without valid userspace process");
2059+
}
2060+
}
2061+
}
2062+
2063+
/// Test filesystem block allocation (regression test for s_first_data_block offset bug)
2064+
/// This tests:
2065+
/// - truncate_file() properly frees blocks (not just clears pointers)
2066+
/// - Multi-file operations don't corrupt other files' data blocks
2067+
pub fn test_fs_block_alloc() {
2068+
log::info!("Testing filesystem block allocation (regression test)");
2069+
2070+
#[cfg(feature = "testing")]
2071+
let fs_block_alloc_test_elf_buf = crate::userspace_test::get_test_binary("fs_block_alloc_test");
2072+
#[cfg(feature = "testing")]
2073+
let fs_block_alloc_test_elf: &[u8] = &fs_block_alloc_test_elf_buf;
2074+
#[cfg(not(feature = "testing"))]
2075+
let fs_block_alloc_test_elf = &create_hello_world_elf();
2076+
2077+
match crate::process::creation::create_user_process(
2078+
String::from("fs_block_alloc_test"),
2079+
fs_block_alloc_test_elf,
2080+
) {
2081+
Ok(pid) => {
2082+
log::info!("Created fs_block_alloc_test process with PID {:?}", pid);
2083+
log::info!("Block alloc test: process scheduled for execution.");
2084+
log::info!(" -> Userspace will emit BLOCK_ALLOC_TEST_PASSED marker if successful");
2085+
}
2086+
Err(e) => {
2087+
log::error!("Failed to create fs_block_alloc_test process: {}", e);
2088+
log::error!("Block alloc test cannot run without valid userspace process");
2089+
}
2090+
}
2091+
}
2092+
20362093
/// Test Rust std library support via hello_std_real
20372094
pub fn test_hello_std_real() {
20382095
log::info!("Testing Rust std library support (hello_std_real)");

testdata/ext2.img

0 Bytes
Binary file not shown.

userspace/tests/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ path = "hello_time.rs"
1414
name = "hello_world"
1515
path = "hello_world.rs"
1616

17+
[[bin]]
18+
name = "simple_exit"
19+
path = "simple_exit.rs"
20+
1721
[[bin]]
1822
name = "counter"
1923
path = "counter.rs"
@@ -334,3 +338,11 @@ path = "dns_test.rs"
334338
[[bin]]
335339
name = "http_test"
336340
path = "http_test.rs"
341+
342+
[[bin]]
343+
name = "exec_from_ext2_test"
344+
path = "exec_from_ext2_test.rs"
345+
346+
[[bin]]
347+
name = "fs_block_alloc_test"
348+
path = "fs_block_alloc_test.rs"

userspace/tests/build.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ echo ""
3131
# List binaries being built
3232
BINARIES=(
3333
"hello_world"
34+
"simple_exit"
3435
"hello_time"
3536
"counter"
3637
"spinner"
@@ -102,6 +103,8 @@ BINARIES=(
102103
"cat"
103104
"exec_argv_test"
104105
"exec_stack_argv_test"
106+
"exec_from_ext2_test"
107+
"fs_block_alloc_test"
105108
"shell_pipe_test"
106109
# Coreutils
107110
"ls_cmd"

0 commit comments

Comments
 (0)