운영체제 기본/보호
약속을 지키지 않는 프로그램
다음 C 코드는 유효한 객체를 가리키지 않는 포인터로 값을 쓰려는 버그다.
*(int *)NULL = 1;
NULL에 int *라는 형식을 붙여도 값을 저장할 공간이 생기지는 않는다. C에서는 널 포인터 역참조의 동작을 정의하지 않는다. 따라서 이 코드가 반드시 특정 기계 명령을 실행하거나 정해진 오류로 종료한다고 보장할 수는 없다.[1]
실제 쓰기 명령이 실행되는 경우, 일반적인 메모리 보호 환경에서는 대상의 매핑과 쓰기 권한을 검사한다. 유효한 매핑이나 권한이 없으면 접근이 차단된다. 포인터 값을 만들어 냈다고 다른 프로세스의 메모리에 접근할 권한까지 얻는 것은 아니다.[2]
반대로 프로그램들이 같은 실제 메모리를 자유롭게 건드릴 수 있다면, 잘못된 포인터 쓰기가 다른 프로그램의 데이터나 운영체제의 관리 정보에 도달할 수 있다. NULL에 쓰는 이 한 줄이 반드시 다른 프로세스를 덮어쓴다는 뜻은 아니다. 잘못된 주소에 대한 쓰기를 누가, 어디서 막는가가 보호의 문제다. 올바른 코드를 작성하라는 약속만으로는 실수나 악의적인 접근을 차단할 수 없다.
보호는 프로그램이 접근할 수 있는 자원과 허용된 연산에 경계를 두고 이를 강제하는 일이다. 파일이나 주소 공간이라는 추상화를 제공하더라도 프로그램이 그 아래의 저장장치나 다른 주소 공간에 직접 접근할 수 있다면 경계는 무너진다. 따라서 자원을 관리하는 운영체제에는 요청을 거부할 수단과, 프로그램이 그 수단을 우회하지 못하게 하는 구조가 함께 필요하다.
보호 규칙 자체도 보호해야 한다. 프로그램이 자신의 접근 권한을 기록한 메모리를 마음대로 수정할 수 있다면 권한 검사는 의미가 없어진다. 규칙을 집행하는 코드와 상태를 보호 대상과 분리해야 한다. [3]
실행 권한, 주소, 자원의 경계
하드웨어의 권한 모드를 사용하는 운영체제에서는 일반 프로그램을 제한된 권한으로 실행하고, 메모리 매핑이나 장치 제어처럼 시스템 전체에 영향을 주는 동작은 커널이 담당한다. 사용자 모드에서도 산술 연산과 허용된 메모리 접근은 CPU가 직접 실행한다. 모든 명령을 커널이 대신 실행하면 보호를 위해 지나치게 큰 비용을 치르게 된다.
주소 공간은 접근 가능한 메모리의 범위를 나눈다. MMU를 사용하는 시스템에서는 운영체제가 주소 변환과 접근 권한을 설정하고, 하드웨어가 메모리 접근 때 이를 검사한다. 프로그램이 다른 주소를 계산했다고 해서 해당 메모리에 접근할 권한까지 생기지는 않는다. 잘못된 접근은 예외를 일으킬 수 있고, 운영체제는 이를 처리하거나 해당 프로그램의 실행을 중단한다.
자원별 권한은 별도로 판단한다. 사용자 모드라는 이유만으로 모든 파일 접근이 금지되는 것은 아니며, 어떤 파일을 읽을 수 있다고 해서 수정도 허용되는 것은 아니다. CPU 권한 모드는 어떤 종류의 동작을 직접 실행할 수 있는지, 주소 공간은 어떤 메모리에 접근할 수 있는지, 자원 권한은 어떤 대상에 어떤 연산을 허용할지를 나눈다. [4]
경계를 넘는 요청도 검증해야 한다
격리된 프로그램도 파일을 읽고 다른 프로그램과 통신해야 한다. 시스템 호출은 프로그램이 정해진 진입점을 통해 커널에 작업을 요청하는 경로다. 하드웨어가 제어를 커널로 넘기면 커널 코드가 요청을 처리하고 결과를 돌려준다. 진입·복귀 과정은 인터럽트와 제어권을 참조한다. 호출자는 요청할 작업과 인자를 고를 수 있지만 커널에서 실행할 임의의 명령을 지정할 수는 없다.
예를 들어 데이터를 읽어 달라는 요청에는 대상, 결과를 저장할 주소, 최대 길이가 들어간다. 커널은 그 대상을 읽을 권한이 있는지와 결과를 돌려줄 메모리 범위가 유효한지 확인해야 한다. 주소와 길이를 합산할 때 정수 범위를 넘는 경우도 고려해야 한다. 호출자가 넘긴 숫자나 포인터를 신뢰하면 커널이 자신의 권한으로 금지된 접근을 대신 수행할 수 있다. [5]
검사 시점과 사용 시점 사이의 변화도 문제다. 다른 스레드가 인자가 담긴 메모리를 바꿀 수 있다면, 검사한 내용과 실제 처리한 내용이 달라질 수 있다. 필요한 값을 커널의 관리 영역에 복사하거나, 대상의 수명을 유지하고 동기화하는 등 처리 과정 내내 가정이 유지되도록 만들어야 한다.
격리와 협력의 비용
경계를 넘을 때는 실행 상태를 보존하고 복원하며 인자와 권한을 확인하는 비용이 든다. 데이터를 복사해야 할 수도 있다. 다만 커널에 진입했다고 반드시 다른 스레드로 문맥 교환하는 것은 아니다. 요청을 바로 끝내면 같은 스레드가 돌아오고, 기다려야 한다면 다른 스레드에 CPU를 넘길 수 있다.
이미지 처리 기능을 별도 프로세스에 두면 그 기능의 잘못된 메모리 쓰기가 편집기의 메모리를 직접 훼손하는 일을 막을 수 있다. 대신 큰 이미지와 결과를 주고받는 통신 비용이 생긴다. 공유 메모리로 복사를 줄이면 같은 영역에 대한 동기화와 변경 권한을 관리해야 한다. 경계를 나누는 단위와 데이터를 교환하는 방식은 함께 설계해야 한다.
최소 권한 원칙은 각 구성 요소에 필요한 자원과 연산만 허용하여 오류나 침해의 피해 범위를 줄인다. 그러나 허용된 출력 파일에 잘못된 결과를 쓰는 것까지 막아 주지는 않는다. 보호는 프로그램의 정확성, 공유 데이터의 동시성, 장애 후 복구를 모두 보장하는 말이 아니다. 어떤 실패를 경계 안에 가두고 어떤 협력을 허용할지 정하는 설계다.
참고 문헌
- ↑ C11 위원회 초안 N1570, 6.5.3.2 문단 4와 각주 102.
- ↑ Linux 커널 문서: Page Tables, 주소 변환과 페이지 접근 권한.
- ↑ Jerome H. Saltzer·Michael D. Schroeder, The Protection of Information in Computer Systems (1975), §I.
- ↑ xv6: Operating system organization.
- ↑ xv6: Traps and system calls.