본문 바로가기

Programming

Secret of SET_FS and KERNEL_DS in Linux Kernel

디바이스 드라이버상에서 파일을 직접 읽고 쓰는것은 바람직하지 않은것으로 알려져 있지만 부득이하게 파일을 읽고쓰기 위해서는 filp_open, vfs_read 등을 이용할 수 있는데 이 과정에서 get_fs() 로 현재 fs 를 저장해놓은뒤 set_fs(KERNEL_DS) 라는 매크로 함수를 호출해 준다. 작업이 다 끝나면 저장해놨던 fs 를 복원시킨다. set_fs 는 뭐하는놈인가? 이걸 사용안하면 폴트가 나는 것일까? 예전에는 아무리 생각해봐도 이유를 알수가 없었는데, 이제야 이해가 간다. 특히 스매시더스택 레벨 30번을 풀면서 한층더 명확하게 이해하게 됬다.

한마디로 요약하자면 이유는 "유저 프로세스가 커널 메모리를 접근하지 못하게 하기 위해서" 라고 말할 수 있다.  프로세스 메모리 구조, 커널 스케줄러와 Context Switching, CPL, 세그멘테이션, 페이징 등에 대한 기본적인 이해가 없으면 저 이유를 이해하기가 어려울 것 같다. 먼저 이 문제는 CPL-0 의 커널코드가 유저 Application 을 위해서 export 된 함수 및 기능을 사용할때 발생한다. 대표적인것이 파일을 읽고 쓰는것과 같은 기능이다.  아래는 리눅스 시스템콜 테이블의 초반 일부 항목들이다.


0 sys_restart_syscall 0x00 - - - - - kernel/signal.c:2058
1 sys_exit 0x01 int error_code - - - - kernel/exit.c:1046
2 sys_fork 0x02 struct pt_regs * - - - - arch/alpha/kernel/entry.S:716
3 sys_read 0x03 unsigned int fd char __user *buf size_t count - - fs/read_write.c:391
4 sys_write 0x04 unsigned int fd const char __user *buf size_t count - - fs/read_write.c:408
5 sys_open 0x05 const char __user *filename int flags int mode - - fs/open.c:900
6 sys_close 0x06 unsigned int fd - - - - fs/open.c:969

만약 이러한 sys_open, sys_read, sys_write 와 같은 시스템콜을 커널모드에서 이용하여 파일을 읽고 쓰려는 경우 유저 application 이 이용하는것과 똑같이 했다가는 바로 세그멘테이션 폴트가 날 것이다.  그 이유는 바로 이 함수들이 파라미터로 전달받는 포인터의 가상주소 범위 때문이다. 잘 생각해보자, 일반적으로 user application 이 커널메모리영역에 접근할수 없는 이유가 무엇인가? 분명 페이지테이블상에는 커널영역에 대한 매핑이 모두 정상적으로 존재한다.  하지만 유저 어플리케이션에서 0xC0000000 와 같은 주소에 access 하면 세그멘테이션 폴트가 난다. 아래의 예를 보자.


root@ubuntu:/tmp# cat > a.c

#include <stdio.h>

int main(){

int* a = (int*)0xc0000000;

int b = *a;

printf("%d\n", b);

return 0;

}

^C

root@ubuntu:/tmp# gcc -o a a.c

root@ubuntu:/tmp# ./a

Segmentation fault (core dumped)

root@ubuntu:/tmp# 


유효한 페이지 매핑이 존재하는 커널영역의 주소에 대해서 유저 프로세스가 메모리 access 를 하려하면 segmentation fault 가 나는 이유는 페이지 테이블 엔트리의 플레그상에 user/supervisor 비트설정으로 제약을 받거나 아키텍쳐마다 다를수 있겠지만 세그멘트 레지스터와 GDT 의 설정에 기인할 것이다. 핵심은 MMU 나 CPU 의 하드웨어 관련 세팅을통해 유저 프로세스는 커널 메모리를 접근할 수 없다.


그런데 시스템콜은 상황이 다르다.  시스템콜은 이미 CPU 가 커널 모드에서 진입하여 동작함과 동시에 함수의 파라미터들을 User Process 의 코드로부터 전달받는다.  때문에 별생각없이 시스템콜을 그냥 구현해주면 유저 프로세스가 커널메모리를 전부 접근할수 있는 보안상의 문제가 발생할 것이다. 예를들어 sys_read 가 읽기 버퍼의 주소로 사용할 메모리 주소는 사실 user application 이 제공해주는 0x8049a00 과 같은 주소이다.  만약 어떤 유저 어플리케이션이 다음과 같이 read 함수를 호출하면 어떻게 될까?


read(0, (char*)0xC0100000), 0x1000);


여기서 0xC0100000 이라는 커널 메모리주소는 시스템콜이 호출되기 이전에 access 되지 않으므로 폴트가 날 이유가 없다. 그런데 시스템콜의 본체가 이 주소를 이용할 시점에서는 이미 CPL 이 커널모드로 변경된 상태일 것이다.  따라서 sys_read 시스템콜은 사용자의 STDIN 에서 받은 입력을 문제없이 커널메모리영역인 0xC0100000 에 4096바이트만큼 덮어쓸 것이다. 이런 문제점때문에 커널에서는 User Process 한테 export 할 함수들의 메모리 주소값을 사전에 검증한다.  아까 위에서 시스템콜 테이블 엔트리들을 나열한곳에서 잘 보면 sys_read, sys_open, sys_write 의 버퍼주소에 대응되는 파라미터에 __user 라는 키워드가 붙은걸 볼 수 있다.  이는 버퍼주소에 실제 access 를 하기전에 그 주소의 range 를 확인해서 커널의 메모리 주소 영역이 아님을 확인하겠다는 의미가 된다. 

따라서 커널 디바이스드라이버에서 sys_open 을 이용해서 파일을 오픈하고자 할때, 오픈할 파일이름에 대한 포인터를 디바이스 드라이버상의 전역변수 영역에 있는걸로 줘버리면 이 포인터의 주소 범위가 Kernel 영역이기때문에 당연히 문제가 된다. 그럼 어떻게 해야하나? 디바이스 드라이버상에서 진입 Context 를 고려해서 User 메모리 영역에 메모리 할당을 동적으로 한다는건 말도 안될것이다. 가장 간단한 방법은 이 제약을 임시적으로 없애주는 것이다. 사실 이 문제를 해결하기 위한 방법으로 커널에서는 어디서부터 어디까지가 User 메모리 주소이고 Kernel 메모리 주소인지 그 경계에 대한 값을 각각 프로세스마다 따로 저장하고있다. 이 값이 기본적으로 USER_DS 라는 상수로 세팅되어 있는데(리눅스같은 1:3 스플릿인 경우 0xC0000000, 윈도우처럼 1:1 이라면 0x80000000) 이것을 변경하는 것이 바로 SET_FS 매크로의 역할이다.  사실 어셈블리코드상으로 SET_FS 매크로는 다음과 같다.


.text:0800003F 89 E0                   mov     eax, esp
.text:08000041 25 00 E0 FF FF          and     eax, 0FFFFE000h
.text:08000046 C7 45 88 00 00 00 00    mov     [ebp+var_78], 0
.text:0800004D 8D 5D 90                lea     ebx, [ebp+var_70]
.text:08000050 C7 45 8C 00 00 00 00    mov     [ebp+var_74], 0
.text:08000057 8B 70 18                mov     esi, [eax+18h]
.text:0800005A C7 40 18 FF FF FF FF    mov     dword ptr [eax+18h], 0FFFFFFFFh


ESP 의 하위 13비트를 마스킹하여 thread_info 구조체의 시작주소를 구하고(여기서 첫번째 멤버가 task_struct 의 포인터, 즉 current 이다), 거기서 0x18 오프셋의 위치에 있는 멤버가 바로 이 limit 를 나타내는 변수이다. 그 값을 KERNEL_DS 라는 상수 0xFFFFFFFF 로 세팅해주면 앞으로는 시스템콜 핸들러같은 함수들이 0xFFFFFFFF 보다 작은 범위에 있는 주소에 대해서는 문제없이 처리를 해주게 된다.  즉 커널영역의 주소에 대해서도 폴트를 내지 않고 일을 처리해주게 된다. 이것이 바로 set_fs 매크로와 KERNEL_DS, USER_DS 같은 것들을 이용하는 이유이다.




'Programming' 카테고리의 다른 글

Linux Kernel compile and update  (0) 2014.01.07
QEMU Internals  (0) 2014.01.02
Apache2 SSL Configuration  (0) 2013.11.28
Settingup ARMv7 environment with QEMU  (3) 2013.10.24
How NX is implemented in x86 Linux  (0) 2013.10.22