@@ -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+ }
0 commit comments