rCore Notes: Add Security Checks for sys_write

In the lab of Chapter 2, a programming exercise is proposed: add security checks for sys_write. In this article, we will analyze the security checks and implement them.

The original question is as follows, originally written in Chinese, and I will translate it into English:

In chapter 2, we implemented the first system call sys_write, which allows us to output information in user mode. However, while the OS provides services, it also has the responsibility to protect itself and other user programs from being compromised by faulty or malicious programs.

Since virtual memory hasn’t been implemented yet, it’s possible to specify a string in a user program that belongs to another program and output it, which is clearly unreasonable. Therefore, we need to add checks to sys_write:

sys_write should only be allowed to output data located within the program’s own memory space; otherwise, it should return an error.

Analysis

In the sys_write function, we need to check whether the buffer is located within the program’s own memory space. We can get the range of the current application and the user stack, then check whether the buffer is located within these two ranges.

Solution

In AppManager, the n-th application’s memory range is [app_start[n-1], app_start[n]), and current_app actually points to the next application. So the current application’s memory range is [app_start[current_app-1], app_start[current_app]). So we can get the current application’s length by app_start[current_app] - app_start[current_app-1]. In Batch OS, we load app applications from APP_BASE_ADDRESS, so the current application’s memory range is [APP_BASE_ADDRESS, APP_BASE_ADDRESS + app_start[current_app] - app_start[current_app-1]).

In USER_STACK, we can get the user stack’s top address by USER_STACK.get_sp(), because the stack grows from high to low in RISC-V, so the user stack’s range is [sp - USER_STACK_SIZE, sp).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// os/src/batch.rs

impl AppManager {
    // ... existing code
    pub fn get_current_app_range(&self) -> (usize, usize) {
        (
            APP_BASE_ADDRESS,
            APP_BASE_ADDRESS + self.app_start[self.current_app]
                - self.app_start[self.current_app - 1],
        )
    }
    // ... existing code
}


pub fn get_current_app_range() -> (usize, usize) {
    APP_MANAGER.exclusive_access().get_current_app_range()
}

pub fn get_user_stack_range() -> (usize, usize) {
    let sp = USER_STACK.get_sp();
    (sp - USER_STACK_SIZE, sp)
}

When sys_write is called, we can get the current application’s range and the user stack’s range, then check whether the buffer is located within these two ranges. Don’t forget check the end of the buffer.

By read the test cases code, we should return -1 when the buffer is not located within the current application’s range or the user stack’s range and the file descriptor is not FD_STDOUT.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// os/src/syscall/fs.rs

use crate::batch::{get_current_app_range, get_user_stack_range};

const FD_STDOUT: usize = 1;

pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
    let (app_begin_ptr, app_end_ptr) = get_current_app_range();
    let (stack_begin_ptr, stack_end_ptr) = get_user_stack_range();
    let buf_begin_ptr = buf as usize;
    let buf_end_ptr: usize = unsafe { buf.offset(len as isize) } as usize;

    if !((buf_begin_ptr >= app_begin_ptr && buf_begin_ptr < app_end_ptr)
        && (buf_end_ptr >= app_begin_ptr && buf_end_ptr < app_end_ptr))
        && !((buf_begin_ptr >= stack_begin_ptr && buf_begin_ptr < stack_end_ptr)
            && (buf_end_ptr >= stack_begin_ptr && buf_end_ptr < stack_end_ptr))
    {
        return -1;
    }

    match fd {
        FD_STDOUT => {
            let slice = unsafe { core::slice::from_raw_parts(buf, len) };
            let str = core::str::from_utf8(slice).unwrap();
            print!("{}", str);
            len as isize
        }
        _ => {
            // panic!("Unsupported fd in sys_write!");
            -1
        }
    }
}

After that, we can run make run TEST=1 to test the implementation. If the test cases is same as the expected results in test cases’ comment, the implementation is correct.

Conclusion

In this article, we analyzed the security checks for sys_write and implemented them. By checking whether the buffer is located within the current application’s range or the user stack’s range, we can prevent the program from outputting data located in other programs’ memory space. This is a basic security check for system calls, and it’s important to protect the OS and other programs from being compromised by faulty or malicious programs. If you have any questions or suggestions, please feel free to contact me by email or send private messages to me on Matrix.

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy