Skip to content

Commit 1b8b146

Browse files
ryanbreenclaude
andcommitted
fix(argv): fix double page offset bug in write_byte_to_stack
Fixed a critical bug where argc/argv was being written to the wrong physical address when setting up the stack for new processes. The bug was in write_byte_to_stack(): translate_page() returns the FULL physical address (frame base + page offset), but we were then adding page_offset again, causing the write to go to frame + 2*offset instead of frame + offset. When the process ran and read from the correct virtual address, the MMU translated to frame + offset, which was all zeros because we never wrote there. Changes: - kernel/src/process/manager.rs: Remove duplicate page_offset addition - userspace/tests/cat.rs: Use naked _start to properly capture RSP - xtask/src/main.rs: Add cat /hello.txt test to interactive tests - kernel/src/memory/stack.rs: Add stack bounds tests with proper >= check - Add exec_stack_argv_test for regression testing Fixes `cat /hello.txt` showing "missing file operand" when argc was correctly set to 2 by the kernel. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2e6e53e commit 1b8b146

File tree

9 files changed

+537
-24
lines changed

9 files changed

+537
-24
lines changed

kernel/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,10 @@ fn kernel_main_continue() -> ! {
727727
log::info!("=== EXEC TEST: exec with argv ===");
728728
test_exec::test_exec_argv();
729729

730+
// Test exec with stack-allocated argv (regression test for black_box fix)
731+
log::info!("=== EXEC TEST: exec with stack-allocated argv ===");
732+
test_exec::test_exec_stack_argv();
733+
730734
// Test getdents64 syscall for directory listing
731735
log::info!("=== FS TEST: getdents64 directory listing ===");
732736
test_exec::test_getdents();

kernel/src/memory/stack.rs

Lines changed: 302 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ impl GuardedStack {
115115
}
116116

117117
/// Find free virtual address space for stack allocation
118+
///
119+
/// This function uses a simple incrementing allocator. The bounds checking is
120+
/// performed BEFORE modifying state, ensuring that on error, the allocation
121+
/// pointer is not corrupted.
122+
///
123+
/// The bounds check uses `>=` (not `>`) because the boundary addresses are
124+
/// non-canonical: USER_STACK_REGION_END (0x8000_0000_0000) is the first
125+
/// non-canonical address in the lower half of the address space.
118126
fn find_free_virtual_space(
119127
size: usize,
120128
privilege: ThreadPrivilege,
@@ -127,26 +135,21 @@ impl GuardedStack {
127135
unsafe {
128136
match privilege {
129137
ThreadPrivilege::User => {
130-
// Check bounds BEFORE allocating to prevent overflow
131-
// USER_STACK_REGION_END is the canonical boundary (0x8000_0000_0000)
132-
// We need to ensure the entire stack fits STRICTLY BELOW the boundary
133-
// because 0x8000_0000_0000 is non-canonical
134-
let proposed_end = NEXT_USER_STACK_ADDR.saturating_add(size as u64);
135-
if proposed_end >= USER_STACK_REGION_END || proposed_end < NEXT_USER_STACK_ADDR {
136-
return Err("Out of virtual address space for user stacks");
137-
}
138+
// Use the extracted bounds check function to verify allocation is valid
139+
// This check happens BEFORE modifying NEXT_USER_STACK_ADDR
140+
let proposed_end = check_user_stack_bounds(NEXT_USER_STACK_ADDR, size as u64)?;
138141

142+
// Bounds check passed - now safe to update state
139143
let addr = VirtAddr::new(NEXT_USER_STACK_ADDR);
140144
NEXT_USER_STACK_ADDR = proposed_end;
141145
Ok(addr)
142146
}
143147
ThreadPrivilege::Kernel => {
144-
// Check bounds BEFORE allocating
145-
let proposed_end = NEXT_KERNEL_STACK_ADDR.saturating_add(size as u64);
146-
if proposed_end >= 0xFFFF_CA00_0000_0000 || proposed_end < NEXT_KERNEL_STACK_ADDR {
147-
return Err("Out of virtual address space for kernel stacks");
148-
}
148+
// Use the extracted bounds check function to verify allocation is valid
149+
// This check happens BEFORE modifying NEXT_KERNEL_STACK_ADDR
150+
let proposed_end = check_kernel_stack_bounds(NEXT_KERNEL_STACK_ADDR, size as u64)?;
149151

152+
// Bounds check passed - now safe to update state
150153
let addr = VirtAddr::new(NEXT_KERNEL_STACK_ADDR);
151154
NEXT_KERNEL_STACK_ADDR = proposed_end;
152155
Ok(addr)
@@ -259,3 +262,289 @@ pub fn is_guard_page_fault(fault_addr: VirtAddr) -> Option<&'static GuardedStack
259262
}
260263
None
261264
}
265+
266+
// ============================================================================
267+
// Bounds Check Logic - Extracted for Testing
268+
// ============================================================================
269+
//
270+
// The bounds checking logic is critical for preventing:
271+
// 1. Stack allocations that would extend into non-canonical address space
272+
// 2. Integer overflow when computing proposed_end
273+
// 3. Off-by-one errors at the boundary (>= not > to prevent allocation AT boundary)
274+
//
275+
// These helper functions extract the pure logic for unit testing.
276+
277+
/// Check if a proposed user stack allocation is valid.
278+
///
279+
/// This function encapsulates the bounds checking logic used by `find_free_virtual_space`
280+
/// for user stacks. It verifies:
281+
/// 1. The proposed_end doesn't reach or exceed USER_STACK_REGION_END
282+
/// 2. Integer overflow didn't occur (saturating_add wraparound detection)
283+
///
284+
/// # Arguments
285+
/// * `current_addr` - The current allocation pointer (where the next stack would start)
286+
/// * `size` - The size of the stack allocation (including guard page)
287+
///
288+
/// # Returns
289+
/// * `Ok(proposed_end)` - The end address if allocation would be valid
290+
/// * `Err(&'static str)` - Error message if allocation would be invalid
291+
#[inline]
292+
pub fn check_user_stack_bounds(current_addr: u64, size: u64) -> Result<u64, &'static str> {
293+
let proposed_end = current_addr.saturating_add(size);
294+
295+
// Check 1: Would the allocation extend to or past the boundary?
296+
// We use >= because USER_STACK_REGION_END (0x8000_0000_0000) is non-canonical
297+
// and we cannot allocate AT that address
298+
if proposed_end >= USER_STACK_REGION_END {
299+
return Err("Out of virtual address space for user stacks");
300+
}
301+
302+
// Check 2: Did saturating_add wrap around (overflow)?
303+
// If proposed_end < current_addr, we overflowed
304+
if proposed_end < current_addr {
305+
return Err("Out of virtual address space for user stacks");
306+
}
307+
308+
Ok(proposed_end)
309+
}
310+
311+
/// Check if a proposed kernel stack allocation is valid.
312+
///
313+
/// Similar to `check_user_stack_bounds` but for kernel stack region.
314+
/// The kernel stack region ends at 0xFFFF_CA00_0000_0000.
315+
///
316+
/// # Arguments
317+
/// * `current_addr` - The current allocation pointer
318+
/// * `size` - The size of the stack allocation
319+
///
320+
/// # Returns
321+
/// * `Ok(proposed_end)` - The end address if allocation would be valid
322+
/// * `Err(&'static str)` - Error message if allocation would be invalid
323+
#[inline]
324+
pub fn check_kernel_stack_bounds(current_addr: u64, size: u64) -> Result<u64, &'static str> {
325+
const KERNEL_STACK_REGION_END: u64 = 0xFFFF_CA00_0000_0000;
326+
327+
let proposed_end = current_addr.saturating_add(size);
328+
329+
if proposed_end >= KERNEL_STACK_REGION_END {
330+
return Err("Out of virtual address space for kernel stacks");
331+
}
332+
333+
if proposed_end < current_addr {
334+
return Err("Out of virtual address space for kernel stacks");
335+
}
336+
337+
Ok(proposed_end)
338+
}
339+
340+
// ============================================================================
341+
// Unit Tests for Stack Allocation Bounds Checking
342+
// ============================================================================
343+
344+
#[cfg(test)]
345+
mod tests {
346+
use super::*;
347+
348+
// Test constants - these mirror the actual values from layout.rs
349+
const TEST_USER_STACK_REGION_START: u64 = 0x7FFF_FF00_0000;
350+
const TEST_USER_STACK_REGION_END: u64 = 0x8000_0000_0000;
351+
const TEST_KERNEL_STACK_REGION_START: u64 = 0xFFFF_C900_0000_0000;
352+
const TEST_KERNEL_STACK_REGION_END: u64 = 0xFFFF_CA00_0000_0000;
353+
354+
// Standard stack sizes for testing
355+
const PAGE_SIZE: u64 = 4096;
356+
const STACK_64K: u64 = 64 * 1024; // 64 KiB stack
357+
const STACK_WITH_GUARD: u64 = STACK_64K + PAGE_SIZE; // Stack + guard page
358+
359+
// ========================================================================
360+
// User Stack Bounds Check Tests
361+
// ========================================================================
362+
363+
#[test]
364+
fn test_user_stack_bounds_normal_allocation() {
365+
// Normal allocation well within bounds should succeed
366+
let current = TEST_USER_STACK_REGION_START;
367+
let size = STACK_WITH_GUARD;
368+
369+
let result = check_user_stack_bounds(current, size);
370+
assert!(result.is_ok(), "Normal allocation should succeed");
371+
372+
let proposed_end = result.unwrap();
373+
assert_eq!(proposed_end, current + size);
374+
assert!(proposed_end < TEST_USER_STACK_REGION_END);
375+
}
376+
377+
#[test]
378+
fn test_user_stack_bounds_exactly_at_boundary_fails() {
379+
// Allocation that would end EXACTLY at the boundary should FAIL
380+
// because >= check means we can't allocate AT 0x8000_0000_0000
381+
let remaining_space = TEST_USER_STACK_REGION_END - TEST_USER_STACK_REGION_START;
382+
let current = TEST_USER_STACK_REGION_START;
383+
let size = remaining_space; // Would put proposed_end exactly at boundary
384+
385+
let result = check_user_stack_bounds(current, size);
386+
assert!(result.is_err(), "Allocation exactly at boundary should fail");
387+
assert_eq!(result.unwrap_err(), "Out of virtual address space for user stacks");
388+
}
389+
390+
#[test]
391+
fn test_user_stack_bounds_one_byte_before_boundary_succeeds() {
392+
// Allocation that ends one byte BEFORE the boundary should succeed
393+
let remaining_space = TEST_USER_STACK_REGION_END - TEST_USER_STACK_REGION_START;
394+
let current = TEST_USER_STACK_REGION_START;
395+
let size = remaining_space - 1; // One byte less than boundary
396+
397+
let result = check_user_stack_bounds(current, size);
398+
assert!(result.is_ok(), "Allocation one byte before boundary should succeed");
399+
400+
let proposed_end = result.unwrap();
401+
assert_eq!(proposed_end, TEST_USER_STACK_REGION_END - 1);
402+
}
403+
404+
#[test]
405+
fn test_user_stack_bounds_overflow_protection() {
406+
// Test that integer overflow is caught
407+
// Start near the end and try to allocate a huge size
408+
let current = TEST_USER_STACK_REGION_END - PAGE_SIZE; // Near the end
409+
let size = u64::MAX; // Huge size that would overflow
410+
411+
let result = check_user_stack_bounds(current, size);
412+
assert!(result.is_err(), "Overflow should be caught");
413+
}
414+
415+
#[test]
416+
fn test_user_stack_bounds_past_boundary_fails() {
417+
// Allocation that would extend past the boundary should fail
418+
let remaining_space = TEST_USER_STACK_REGION_END - TEST_USER_STACK_REGION_START;
419+
let current = TEST_USER_STACK_REGION_START;
420+
let size = remaining_space + PAGE_SIZE; // Past the boundary
421+
422+
let result = check_user_stack_bounds(current, size);
423+
assert!(result.is_err(), "Allocation past boundary should fail");
424+
}
425+
426+
#[test]
427+
fn test_user_stack_bounds_state_not_modified_on_error() {
428+
// This test verifies the SEMANTIC requirement that state is not modified on error.
429+
// Since check_user_stack_bounds is a pure function (no side effects),
430+
// this test validates that design: the function returns Result without
431+
// modifying any external state.
432+
//
433+
// The actual find_free_virtual_space function only updates NEXT_USER_STACK_ADDR
434+
// AFTER the bounds check passes, ensuring state consistency on error.
435+
436+
let current = TEST_USER_STACK_REGION_START;
437+
let size = u64::MAX; // Would overflow
438+
439+
// Call the function multiple times with invalid input
440+
let result1 = check_user_stack_bounds(current, size);
441+
let result2 = check_user_stack_bounds(current, size);
442+
443+
// Both should return the same error
444+
assert!(result1.is_err());
445+
assert!(result2.is_err());
446+
assert_eq!(result1.unwrap_err(), result2.unwrap_err());
447+
}
448+
449+
#[test]
450+
fn test_user_stack_bounds_consecutive_allocations() {
451+
// Simulate consecutive allocations until exhaustion
452+
let mut current = TEST_USER_STACK_REGION_START;
453+
let size = STACK_WITH_GUARD;
454+
let mut allocation_count = 0;
455+
456+
loop {
457+
match check_user_stack_bounds(current, size) {
458+
Ok(proposed_end) => {
459+
allocation_count += 1;
460+
current = proposed_end;
461+
// Safety: don't run forever
462+
if allocation_count > 10000 {
463+
break;
464+
}
465+
}
466+
Err(_) => break,
467+
}
468+
}
469+
470+
// Verify we made some allocations before exhaustion
471+
assert!(allocation_count > 0, "Should have made at least one allocation");
472+
473+
// Verify we stopped before or at the boundary
474+
assert!(current <= TEST_USER_STACK_REGION_END,
475+
"Current pointer should not exceed boundary");
476+
}
477+
478+
// ========================================================================
479+
// Kernel Stack Bounds Check Tests
480+
// ========================================================================
481+
482+
#[test]
483+
fn test_kernel_stack_bounds_normal_allocation() {
484+
let current = TEST_KERNEL_STACK_REGION_START;
485+
let size = STACK_WITH_GUARD;
486+
487+
let result = check_kernel_stack_bounds(current, size);
488+
assert!(result.is_ok(), "Normal kernel allocation should succeed");
489+
}
490+
491+
#[test]
492+
fn test_kernel_stack_bounds_at_boundary_fails() {
493+
let remaining_space = TEST_KERNEL_STACK_REGION_END - TEST_KERNEL_STACK_REGION_START;
494+
let current = TEST_KERNEL_STACK_REGION_START;
495+
let size = remaining_space;
496+
497+
let result = check_kernel_stack_bounds(current, size);
498+
assert!(result.is_err(), "Kernel allocation at boundary should fail");
499+
}
500+
501+
// ========================================================================
502+
// Edge Case Tests
503+
// ========================================================================
504+
505+
#[test]
506+
fn test_zero_size_allocation() {
507+
// Zero-size allocation should technically succeed (proposed_end == current)
508+
let current = TEST_USER_STACK_REGION_START;
509+
let size = 0;
510+
511+
let result = check_user_stack_bounds(current, size);
512+
assert!(result.is_ok(), "Zero-size allocation should succeed");
513+
assert_eq!(result.unwrap(), current);
514+
}
515+
516+
#[test]
517+
fn test_single_page_allocation() {
518+
let current = TEST_USER_STACK_REGION_START;
519+
let size = PAGE_SIZE;
520+
521+
let result = check_user_stack_bounds(current, size);
522+
assert!(result.is_ok());
523+
assert_eq!(result.unwrap(), current + PAGE_SIZE);
524+
}
525+
526+
#[test]
527+
fn test_max_valid_allocation() {
528+
// Maximum valid allocation: fills entire region minus 1 byte
529+
let current = TEST_USER_STACK_REGION_START;
530+
let max_size = TEST_USER_STACK_REGION_END - TEST_USER_STACK_REGION_START - 1;
531+
532+
let result = check_user_stack_bounds(current, max_size);
533+
assert!(result.is_ok(), "Max valid allocation should succeed");
534+
assert_eq!(result.unwrap(), TEST_USER_STACK_REGION_END - 1);
535+
}
536+
537+
#[test]
538+
fn test_boundary_address_is_non_canonical() {
539+
// Verify that USER_STACK_REGION_END is indeed the canonical boundary
540+
// 0x8000_0000_0000 is the first non-canonical address in the lower half
541+
assert_eq!(TEST_USER_STACK_REGION_END, 0x8000_0000_0000);
542+
543+
// Any allocation that would reach this address must fail
544+
let current = TEST_USER_STACK_REGION_END - PAGE_SIZE;
545+
let size = PAGE_SIZE; // Would end exactly at boundary
546+
547+
let result = check_user_stack_bounds(current, size);
548+
assert!(result.is_err(), "Allocation reaching boundary must fail");
549+
}
550+
}

kernel/src/process/manager.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1966,17 +1966,15 @@ impl ProcessManager {
19661966
value: u8,
19671967
) -> Result<(), &'static str> {
19681968
// Translate virtual address to physical
1969+
// NOTE: translate_page uses translate_addr which returns the FULL physical
1970+
// address including the page offset - do NOT add page_offset again!
19691971
let phys_addr = page_table.translate_page(VirtAddr::new(virt_addr))
19701972
.ok_or("Failed to translate stack address")?;
19711973

1972-
// Calculate offset within page
1973-
let page_offset = virt_addr & 0xFFF;
1974-
let phys_with_offset = phys_addr + page_offset;
1975-
19761974
// Write via direct physical memory mapping
19771975
// The kernel has a direct mapping of all physical memory
19781976
let phys_offset = crate::memory::physical_memory_offset();
1979-
let kernel_virt = phys_offset + phys_with_offset.as_u64();
1977+
let kernel_virt = phys_offset + phys_addr.as_u64();
19801978

19811979
unsafe {
19821980
core::ptr::write_volatile(kernel_virt.as_mut_ptr::<u8>(), value);

0 commit comments

Comments
 (0)