본문 바로가기
논문 리뷰

[OS] 논문 리뷰: Efficient Software-Based Fault Isolation

by yyjun 2025. 9. 23.

도입

현대 소프트웨어 시스템은 신뢰할 수 없는 외부 코드를 사용해야 하는 숙명을 안고 있다. Chrome의 JavaScript 엔진인 V8 엔진은 매일 수많은 웹 사이트의 스크립트를 실행하고, WebAssembly는 네이티브에 가까운 성능으로 브라우저에서 C++ 코드를 돌린다.

 

V8 엔진, WebAssembly 등 수많은 현대 소프트웨어 기술들이 직면한 공통 과제는 “어떻게 하면 신뢰할 수 없는 외부 코드를 안전하면서도 효율적으로 실행할 수 있을까?”이다.

 

1993년 12월에 발표된 “Efficient Software-Based Fault Isolation” 논문은 이 문제에 대한 혁신적인 해법을 제시한다. 놀랍게도 이 30년도 더 된 연구가 오늘날 V8 엔진, WebAssembly 등 여러 시스템 내부 격리 기술의 이론적 토대가 되었다.

 

물론 시간이 꽤 흐른 만큼 이 논문에서 제시한 방식을 현대 시스템에서 그대로 사용하지 않는다. 그렇지만 현대 시스템에서 널리 사용되는 소프트웨어 기반 격리(Software-based Fault Isolation, SFI) 기술을 최초로 제안한 기념비적인 논문이기 때문에 의미가 있다.

 

오류 격리(Fault Isolation) 기술의 필요성

이 논문에서 다루는 소프트웨어 기반 오류 격리 기술에 대해 다루기 전에 오류 격리 기술이 왜 필요한지 알아보자. 여기서 말하는 오류는 Fault를 의미한다.

 

이 글에서 다루는 논문이 출판된 1993년에도 그렇고, 이 글을 작성하고 있는 2025년에도 응용 프로그램은 외부 코드를 실행하는 경우가 많다. 예를 들어, 웹 브라우저에는 수많은 스크립트가 실행된다. 이러한 스크립트는 웹 브라우저 개발자가 만들지 않고 주로 웹 페이지 개발자가 만든다. PostgreSQL 같은 데이터베이스 시스템은 사용자 정의 함수를 지원한다. 데이터베이스의 사용자 정의 함수 역시 데이터베이스 시스템 개발자가 만드는 것이 아니라 데이터베이스 사용자가 만든다.

 

많은 응용 프로그램에서 이러한 외부 코드를 지원하는 이유는 높은 확장성과 유연성이다. 응용 프로그램을 사용하면서 필요한 기능이 있으면 직접 코드를 작성해서 추가할 수 있기 때문에 확장성과 유연성이 높아진다.

 

그렇지만 외부 소프트웨어 모듈 지원이라는 기능에는 단점도 존재한다. 바로 시스템의 안정성이 떨어지고, 데이터를 손상시킬 수 있다는 점이다. 확장 코드를 작성하는 사람을 믿을 수 없다. 확장 코드를 작성하는 사람이 문제없이 동작하는 코드를 작성한다는 보장이 없다. 코드는 사람이 작성하는 것이기 때문에 어딘가 예상치 못한 문제가 숨어 있을 수도 있고, 악의적인 사용자가 일부러 이상한 코드를 작성해서 실행시킬 수도 있다. 시스템을 만드는 사람은 이러한 문제가 발생하지 않도록 막아야 한다.

 

이러한 문제를 해결하기 위해 오류 격리(Fault Isolation) 기술이 필요하다. 오류 격리 기술은 모듈의 오류가 응용 프로그램의 데이터를 손상시키는 것을 방지하는 기술을 말한다. 말 그대로 오류를 격리시켜서, 오류가 발생하더라도 그 영향 범위를 응용 프로그램의 일부로 좁히는 기술이다.

 

신뢰할 수 없는 외부 모듈을 사용하더라도 이를 적절하게 격리할 수 있다면 시스템의 안정성을 해치지 않으면서 높은 확장성을 유지할 수 있다.

 

오류 격리(Fault isolation) 기술은 하드웨어 기반 오류 격리(Hardware-based fault isolation)와 소프트웨어 기반 오류 격리(Software-based fault isolation)로 나눌 수 있다.

 

하드웨어 기반 오류 격리

이 글에서 다루고 있는 논문 “Efficient Software-Based Fault Isolation”에서 소프트웨어 기반 오류 격리 기법을 제안하기 전까지는 모두 하드웨어 기반 오류 격리 기법을 사용하였다.

 

하드웨어 기반 오류 격리 기법은 말 그대로 하드웨어적으로 오류를 격리시킨다. 신뢰할 수 없는 외부 모듈과 시스템의 메모리 주소 공간을 아예 분리시킨다. 주소 공간을 분리시킨다는 것은 페이지 테이블 자체를 분리시킨다는 것과 같은 의미이다. 외부 모듈과 시스템은 서로 다른 페이지 테이블을 사용하기 때문에, 다른 주소 공간의 메모리를 임의로 손상시킬 수 없다.

 

단, 이렇게 하면 시스템과 외부 모듈은 메모리를 통해 바로 데이터를 주고 받을 수 없다. 대신 원격 프로시저 호출(Remote Procedure Call, RPC)을 사용해 외부 모듈과 시스템은 서로 통신할 수 있다.

 

여기서 하드웨어 기반 오류 격리 기법의 문제를 발견할 수 있다. 바로 높은 성능 비용이 발생한다는 점이다. 하드웨어 기반 오류 격리 기법에서는 시스템과 외부 모듈의 주소 공간을 분리하고 서로 RPC를 통해 데이터를 주고받는다. 주소 공간 사이의 RPC는 최소한 다음과 같은 작업이 따라온다:

  • 운영체제 커널로의 트랩
  • 호출자에서 피호출자로의 인자 복사
  • 레지스터 저장 및 복원
  • 하드웨어 주소 공간 전환
  • 사용자 레벨로의 트랩 복귀

이러한 작업들은 RPC 호출 뿐만 아니라 반환 시 다시 반복된다. 그렇기 때문에 다른 주소 공간 사이의 RPC는 같은 주소 공간 내 일반 함수 호출보다 약 100배에서 1000배 정도 느리다. 오류 격리 기법이 유용하게 사용되는 많은 경우에서, 외부 모듈과 시스템 사이 RPC는 빈번하게 발생한다. 그렇기에 RPC가 빈번하게 발생하는 많은 경우에 이러한 오버헤드는 매우 치명적이다.

 

하드웨어 기반 오류 격리 기법이 비효율적인 경우를 조금 더 자세히 살펴보면 두 가지 특성을 공통적으로 가지고 있다는 것을 알 수 있다:

  1. 하드웨어 기반 오류 격리 기법을 사용하면 전체 실행 시간의 상당 부분이 운영체제 컨텍스트 스위칭 코드에서 소모된다.
  2. 신뢰할 수 없는 코드는 극히 일부에 불과하며 대부분의 실행 시간은 신뢰할 수 있는 코드에서 소모된다.

이러한 두 가지를 만족하는 경우에는 하드웨어 기반 오류 격리 기법보다 이 논문에서 제시하는 소프트웨어 기반 오류 격리 기법이 더 효율적이다. 위 두 가지 특성 모두 높은 RPC 오버헤드와 관련이 있다. 소프트웨어 기반 오류 격리 기법은 이러한 RPC 오버헤드를 획기적으로 줄였다.

 

물론 하드웨어 기반 오류 격리 기법도 장점이 있다. 아예 주소 공간 자체를 분리하기 때문에 외부 모듈을 강하게 격리할 수 있다. 소프트웨어 기반 오류 격리 기법보다 높은 격리 수준은 하드웨어 기반 오류 격리 기법의 장점이다. 이외에도 높은 확장성, 호환성 등의 장점이 존재한다. 하드웨어 기반 오류 격리 기법 역시 현재 활발하게 사용되고 있으며, 상황에 맞는 격리 기술을 사용하는 것이 중요하다.

 

소프트웨어 기반 오류 격리(Software-based Fault Isolation, SFI)

앞서 살펴본 것처럼 주소 공간 분리는 RPC가 빈번하게 발생할 경우 비효율적이다. 그렇다면 하나의 주소 공간 내에서 오류 격리를 구현해야만 한다. 이를 위해 논문에서는 소프트웨어 기반 오류 격리(Software-based Fault Isolation, SFI) 기법을 제안한다. 이제 줄여서 SFI라고 부르겠다.

 

SFI의 세부적인 내용들을 살펴보기 전에 SFI의 동작 과정을 간단하게 알아보자. SFI를 사용하면, 먼저 신뢰할 수 없는 모듈을 해당 모듈 전용 오류 도메인(fault domain)에 로드한다. 오류 도메인은 응용 프로그램 주소 공간의 논리적으로 분리된 영역을 말한다.

 

오류 도메인으로 모듈을 로드한 이후에는, 신뢰할 수 없는 모듈의 오브젝트 코드(object code)를 수정하여 해당 모듈이 자체 오류 도메인 외부 주소로 쓰기나 점프를 수행하지 못하도록 방지한다. 별도의 SFI 오류 도메인에 격리된 모듈들은 명시적인 cross-fault-domain RPC 인터페이스를 통하지 않는 한 서로의 데이터를 수정하거나 서로의 코드를 실행할 수 없다. 즉, 오류 도메인으로 로드된 모듈은 격리된다.

SFI는 큰 틀에서 이렇게 두 단계를 거친다. 이제 SFI에 대해 조금 더 자세히 살펴보자. 논문에서는 SFI를 위해 응용 프로그램의 가상 주소 공간을 세그먼트로 분할한다. 각 세그먼트 내 모든 가상 주소는 상위 비트에 고유한 패턴을 공유하도록 나뉜다. 이 상위 비트의 고유한 패턴을 세그먼트 식별자(segment identifier)라고 부른다.

SFI의 각 오류 도메인 하나는 두 개의 세그먼트로 구성된다. 하나는 신뢰할 수 없는 모듈의 코드를 담는 세그먼트이고, 다른 하나는 그 모듈의 정적 데이터, 힙, 스택을 담는 세그먼트이다. 이를 각각 코드 세그먼트(code segment)와 데이터 세그먼트(data segment)라고 한다. 즉, 오류 도메인은 코드 세그먼트와 데이터 세그먼트로 나뉜다. 오류 도메인의 구체적인 세그먼트 주소는 로드 시점에 결정된다.

 

앞에서 말한 것처럼 SFI에서는 모듈이 오류 도메인으로 로드되면 오브젝트 코드 자체를 변환한다. 오브젝트 코드 변환을 통해 점프(jump) 명령은 코드 세그먼트로만 가도록, 저장(store) 명령은 데이터 세그먼트로만 가도록 강제할 수 있다. 이는 같은 세그먼트 내의 주소들은 상위 비트 패턴인 세그먼트 식별자가 동일하기 때문에 가능하다. 대상 주소의 상위 비트만 항상 옳은 세그먼트 식별자로 바꿔주면 대상 주소는 합법적인 세그먼트의 주소가 된다. 또는 검사 코드를 삽입해 대상 주소가 올바르지 않은 세그먼트에 속한다면 트랩을 발생시켜 올바른 대상 주소만 실행시킬 수도 있다.

 

한편, 오류 도메인과 관련해 의문이 하나 생길 수 있다. 왜 오류 도메인을 코드 세그먼트와 데이터 세그먼트로 나눌까? 이는 코드 변조를 원천 차단하기 위함이다. SFI는 단어에서도 알 수 있는 것처럼 소프트웨어를 기반으로 격리하는 방식이기 때문에 코드 변조가 가능하면 격리가 깨질 수 있다. 코드 세그먼트와 데이터 세그먼트를 나누고, 쓰기 명령은 데이터 세그먼트로만 가도록 대상 주소의 상위 비트를 항상 수정하면 코드 변조를 원천 차단할 수 있다.

 

이와 같이 명령어의 대상 주소 상위 비트를 수정해주면 올바른 세그먼트로 가도록 강제할 수 있다. 하지만 이 방법은 주소가 올바른 세그먼트에 속한다는 것은 보장하지만, 올바른 주소라는 것을 보장하지는 않는다. 상위 비트인 세그먼트 식별자가 올바르더라도 언매핑된 페이지로 접근하는 주소일 수 있다. SFI는 이것을 방지하지는 않는다. SFI는 주소가 오류 도메인을 벗어나지 않는 것만 보장한다. 명령이 언매핑된 페이지로 접근하면 페이지 폴트가 발생하는데, 이는 운영체제의 페이지 폴트 핸들러가 처리한다.

 

SFI에 대해서는 어느 정도 알아봤으니, 이제 SFI의 세부 기법에 대해 알아보자. 가장 먼저 살펴볼 기법은 세그먼트 매칭(segment matching)이다.

 

세그먼트 매칭(Segment Matching)

세그먼트 매칭에 대해 알아보기 전에 먼저 안전하지 않은 명령어(unsafe instruction)에 대해 알아보자. 안전하지 않은 명령어는 정적 검증으로는 올바른 세그먼트 내에 있음을 확인할 수 없는 주소로 점프하거나 저장하는 모든 명령어를 의미한다. 프로시저 반환을 구현하는 데 가장 일반적으로 사용되는 레지스터를 통한 점프 명렁어와 대상 주소를 저장하기 위해 레지스터를 사용하는 저장 명령어는 정적 검증이 불가능하다. 런타임에 값이 계산되기 때문이다. 이러한 명령어는 안전하지 않은 명령어다.

 

불법적인 주소의 사용을 방지하는 가장 간단한 방법은 모든 안전하지 않은 명령어 앞에 검사 코드를 삽입하는 것이다. 검사 코드는 안전하지 않은 명령어의 대상 주소가 올바른 세그먼트 식별자를 가지는지 판단한다. 만약 검사가 실패하면 삽입된 코드는 신뢰할 수 없는 모듈의 오류 도메인 외부로 시스템 에러 루틴으로 트랩을 발생시킨다. 즉, 검사 코드를 통해 불법적인 주소의 사용을 방지한다. 이러한 SFI 기법을 세그먼트 매칭(segment matching)이라고 한다.

 

일반적인 RISC 아키텍처에서는 세그먼트 매칭을 위해 4개의 명령어가 필요하다. 아래 Figure 1은 논문에서 제시한 세그먼트 매칭을 위한 의사 코드이다.

 

 

이 의사 코드를 잘 살펴보면 특이한 것을 확인할 수 있다. 총 5줄이기 때문에 앞에 나오는 4줄은 검사 코드이고, 마지막 1줄이 실행하려고 하는 안전하지 않은 명령어임을 알 수 있다. 근데 이 마지막 명령어는 target address 대신 dedicated-reg를 사용하고 있다. target address를 사용하는게 자연스러운 것 같은데 왜 dedicated-reg라는 것을 사용할까?

 

이는 신뢰할 수 없는 모듈의 코드가 검사를 건너뛰고 중간 지점으로 점프할 수 있기 때문이다. 중간 지점으로 점프하는 것 자체를 막을 수는 없다. 이 의사 코드의 첫 번째 명령어는 저장 대상 주소를 전용(dedicated) 레지스터로 복사하는 명령어다. dedicated-reg는 전용 레지스터다. 전용 레지스터의 값은 신뢰할 수 없는 모듈이 수정할 수 없다. 오직 SFI에 의해 오브젝트 코드에 추가된 코드만 전용 레지스터 값을 수정할 수 있다.

 

즉, 안전하지 않은 명령어가 전용 레지스터를 사용하도록 오브젝트 코드를 수정하면 위험한 주소로 명령이 가지 않음을 보장할 수 있다. 왜냐하면 전용 레지스터에는 안전한 주소만 담긴다는 것을 보장할 수 있기 때문이다. 세그먼트 매칭 기법은 전용 레지스터를 사용하도록 명령을 수정하기 때문에, 검사 코드를 건너뛰더라도 격리를 달성할 수 있다.

 

세그먼트 매칭 기법은 모든 안전하지 않은 명령어 앞에 검사 코드를 삽입한다. 그렇기에 문제의 명령어를 정확히 지목할 수 있다. 이 방법은 소프트웨어 개발 과정에서 매우 유용하다. 그렇지만 이는 런타임 오버헤드가 발생하기 때문에 때에 따라서는 오버헤드를 줄이고 싶을 수 있다. 이를 위해 주소 샌드박싱(Address Sandboxing) 기법을 사용할 수 있다. SFI의 또 다른 기법인 주소 샌드박싱에 대해 알아보자.

 

주소 샌드박싱(Address Sandboxing)

주소 샌드박싱은 SFI의 또 다른 기법이다. 주소 샌드박싱은 검사 코드를 삽입하지 않는다. 그래서 세그먼트 매칭 기법과 비교했을 때 런타임 오버헤드가 작다. 대신 검사 코드가 없기 때문에 디버깅 하기가 힘들 수 있고 도메인 내부의 논리 버그는 그대로 남을 수 있다. SFI는 도메인 외부로 접근하는 것을 막는 기술이기 때문에, 도메인 내에서는 어떤 주소로 접근하던지 상관하지 않는다.

 

주소 샌드박싱은 각 안전하지 않은 명령어 앞에 대상 주소의 상위 비트를 올바른 세그먼트 식별자로 강제 설정하는 코드를 삽입하는 기법이다. 다시 말하지만, 주소 샌드박싱 기법은 불법적인 주소를 탐지하지 않는다. 오류 도메인 외부로 영향을 미치지 못하도록 차단할 뿐이다.

 

주소 샌드박싱 기법에서는 각 안전하지 않은 명령어 앞에 2개의 산술 명령이 삽입된다. 아래 Figure 2는 논문에서 제시하는 주소 샌드박싱 기법의 의사 코드이다. 세그먼트 매칭 기법에서는 4개의 명령이 삽입되었는데 절반으로 줄어든 것을 알 수 있다.

 

 

삽입된 명령은 각각 다음과 같은 역할을 갖고 있다:

  1. 대상 주소의 세그먼트 식별자에 해당하는 상위 비트를 0으로 만들고 (AND 연산) 이 결과를 전용 레지스터에 저장
  2. 전용 레지스터에 저장된 주소의 세그먼트 식별자 값을 올바른 식별자로 설정 (OR 연산)

즉, 삽입된 2줄의 명령어는 대상 주소를 강제로 올바른 세그먼트에 속하도록 바꾸는 역할을 한다. 이를 통해 오류 도메인 외부로 접근하는 것을 막는다. 하지만 앞에서 말했던 것처럼 이게 올바른 주소라는 의미는 아니다. 단지 올바른 세그먼트에 속하는 것만 보장한다.

 

또한 의사 코드 마지막 줄을 보면 전용 레지스터를 사용하도록 설정된 것을 볼 수 있다. 즉, 삽입된 2줄의 명령을 점프해도 격리가 깨지지 않는다. 왜냐하면 전용 레지스터에는 안전한 주소만 저장되기 때문이다.

 

최적화

지금까지 SFI의 세부 기법인 세그먼트 매칭 방식과 주소 샌드박싱 방식에 대해 알아 봤다. 이 두 방식 모두 약간의 오버헤드가 발생한다는 것을 보았다. 안전하지 않은 명령어마다 세그먼트 매칭 방식은 4줄, 주소 샌드박싱 방식은 2줄의 명령어가 추가된다. 이러한 오버헤드를 줄이는 최적화 방법에 대해 알아보자.

 

우선 전통적인 컴파일러 최적화를 적용할 수 있다. 루프 불변 코드 이동(loop invariant code motion), 명령어 스케줄링 최적화(instruction scheduling optimization) 기법 등을 적용할 수 있다. 이 기법들은 이 논문의 주요 관심사가 아니기 때문에 이 정도로만 소개하고 넘어가겠다.

 

전통적인 컴파일러 최적화 외에도 SFI에 특화된 최적화를 더해 오버헤드를 더욱 줄일 수 있다. 논문에서는 먼저 대상 주소를 계산하는 산술 연산을 피해 오버헤드를 줄이는 방법을 언급한다.

 

예를 들어, RISC 아키텍처에는 레지스터에 상수 오프셋을 더하여 주소를 지정하는 명령어 모드가 있다. 다음과 같은 명령을 보자: 'store value, offset(reg)' . 여기서 주소 'offset(reg)' 는 레지스터에 상수 오프셋을 더하여 계산된다. 이 store 명령을 샌드박싱하려면 세 가지 추가 명령어가 필요하다. 하나는 전용 레지스터에 'reg+offset' 을 계산하여 저장하는 명령어이고, 나머지 둘은 전용 레지스터에 저장된 주소의 세그먼트 식별자를 설정하는 샌드박싱 명령어이다. 샌드박싱에는 세그먼트 식별자에 해당하는 상위 비트를 0으로 초기화하는 AND 연산과, 올바른 세그먼트 식별자를 주입하는 OR 연산이 필요하기 때문에 두 개의 추가 명령어가 필요한 것이다.

 

논문에서는 실제 대상 주소인 'reg+off'에 샌드박싱을 적용하는 대신 레지스터 'reg' 만을 샌드박싱함으로써 명령어 하나를 절약하는 최적화 방식을 제안한다. 레지스터 'reg' 에만 샌드박싱을 적용하면 전용 레지스터에 'reg+offset' 을 계산하여 저장하는 명령어 하나가 사라지게 된다.

 

대신 reg 만 샌드박싱 하면 reg+offset 은 올바른 세그먼트를 벗어날 수 있다. 그래서 논문에서는 각 세그먼트 상단과 하단을 가드 영역으로 설정하고, 세그먼트에 인접한 가상 메모리 페이지는 매핑 해제하는 방식을 제안한다. 만약 가드 영역에 접근하면 매핑되어 있지 않기 때문에 페이지 폴트가 발생하고 운영체제가 핸들러를 통해 이 상황을 처리할 수 있다. 아래 Figure 3을 참고하자.

 

논문에서는 또 다른 최적화 기법인 스택 포인터를 전용 레지스터로 처리하는 방식을 제안하고 있다. 스택 포인터는 자주 사용되는 레지스터이다. 스택 포인터는 함수의 스택 프레임(base) 역할을 하기 때문에, 함수 스택 내부에 접근하기 위해 주소를 생성할 때마다 스택 포인터를 사용한다. 그렇기 때문에 스택 포인터를 매우 많이 사용하게 된다. 반면 스택 포인터 값을 실제로 갱신하는 일은 상대적으로 매우 적다.

 

최적화 기법을 적용하지 않은 기본 상태에서는, 이 레지스터가 안전하지 않은 레지스터에서 사용될 때마다 샌드박싱 해줘야 한다. 하지만 스택 포인터를 전용 레지스터로 처리하면 신뢰할 수 없는 모듈은 스택 포인터를 수정할 수 없다. 그렇기 때문에 신뢰할 수 있는 코드에서 스택 포인터를 설정할 때 한 번만 스택 포인터에 샌드박싱을 적용해주면 스택 포인터를 사용할 때마다 샌드박싱을 해줄 필요는 없다.

 

또한 스택 포인터가 작은 상수 값만큼 변경되고, 변경된 스택 포인터가 다음 제어 흐름 변경 명령어(점프, 리턴 등)가 실행되기 전에 load 또는 store 명령어의 주소로 사용될 때, 수정된 스택 포인터를 샌드박싱 하지 않아도 된다. 이는 가드 영역 덕분에 가능한 최적화 기법이다. 만약 수정된 스택 포인터가 가드 영역으로 이동한 경우, load 또는 store 명령에서 하드웨어 주소 폴트가 발생한다. 이 경우 운영체제가 핸들러를 통해 처리할 수 있다. SFI 특성상 오류 도메인을 벗어나는 것은 막아주지만 오류 도메인 내의 엉뚱한 주소로 가는 것은 막아주지 않는다.

 

이외에도 다양한 최적화 방법이 있지만 논문에서는 이렇게 두 가지 정도만 자세하게 다루고 있다. 이제 다음으로 넘어가 보자.

 

프로세스 자원

소프트웨어 기반 오류 격리 방식은 하드웨어 기반 오류 격리 방식과 달리 주소 공간을 분리하지 않는다. 여러 오류 도메인이 하나의 주소 공간을 공유한다. 그렇기 때문에 신뢰할 수 없는 모듈이 주소 공간의 리소스를 손상시키는 것을 방지해야 한다. 그렇지 않으면 신뢰할 수 없는 모듈 때문에 응용 프로그램의 안정성이 떨어질 수 있다.

 

이러한 문제를 해결하기 위한 한 가지 방법은 운영체제를 수정하여 오류 도메인을 인식하도록 하는 것이다. 시스템 콜이나 페이지 폴트 발생 시 커널이 프로그램 카운터의 상위 비트를 통해 현재 실행 중인 오류 도메인이 무엇인지 판단하고, 권한 정책을 적용해 리소스 접근을 제한할 수 있다. 하지만 이는 실제로 사용하기 어렵다. 왜냐하면 운영체제를 수정하는 것은 쉽지 않은데, SFI를 사용하기 위해서는 운영체제를 수정해야만 하면 실용성이 많이 떨어지기 때문이다.

 

그래서 논문의 저자들은 대체 접근 방식을 제안한다. 신뢰할 수 없는 모듈의 시스템 리소스 접근은 cross-fault-domain RPC를 통한 접근만 허용하는 방식이다. 신뢰할 수 없는 모듈이 직접 시스템 콜을 호출하여 리소스에 접근하는 대신, 신뢰할 수 있는 중재 코드(arbitration code)를 통해 리소스에 접근하는 것이다. SFI에서 신뢰할 수 없는 모듈의 시스템 콜은 자동으로 중재 코드로의 RPC 호출로 변환된다. 중재 코드는 적절한 권한 정책에 따라 리소스 접근을 제한할 수 있다. 중재 코드를 위해 별도의 오류 도메인을 할당한다.

 

중재 코드를 이용해 간접적으로 리소스에 접근하는 방식은 운영체제를 수정하는 방식보다 훨씬 이식성이 높다는 것을 알 수 있다.

 

데이터 공유

하드웨어 기반 오류 격리 방식에서는 신뢰할 수 없는 모듈의 주소 공간을 따로 사용한다. 주소 공간이 분리되어 있기 때문에, 페이지 테이블 항목(PTE)을 수정해 공유 메모리를 기반으로 주소 공간끼리 데이터를 공유할 수 있다. 주소 공간이 분리되어 있으니, 동일한 메모리를 가리키는 PTE라도 주소 공간마다 다르게 권한을 부여할 수 있다. 하지만 소프트웨어 기반 오류 격리(SFI) 방식에서는 이러한 표준 공유 메모리 구현을 사용할 수 없다.

 

하드웨어 기반 오류 격리 방식에서는 같은 메모리를 가리키는 PTE라도 주소 공간마다 다르게 권한을 줄 수 있다. 하지만 SFI에서는 여러 오류 도메인이 하나의 주소 공간과 한 세트의 PTE를 공유하기 때문에 도메인마다 다른 PTE 권한을 줄 수 없다. 그렇기 때문에 표준 공유 메모리 구현을 사용할 수 없다. 그렇지만, 읽기 전용 공유는 매우 쉽게 가능하다. SFI는 load(읽기) 명령어를 건드리지 않는다. 그렇기 때문에 오류 도메인이 애플리케이션 주소 공간에 매핑된 어떤 페이지든 읽어도 문제가 없다. SFI는 기본적으로 무결성은 보장하지만, 기밀성은 보장하지 않는다. 남의 메모리를 훼손하는 것은 방지하지만, 남의 메모리를 읽는 것을 금지하지는 않는다는 의미이다.

 

논문에서는 ‘Lazy Pointer Swizzling”이라 부르는 기법을 통해 추가적인 런타임 오버헤드 없이 다른 오류 도메인과 임의의 개수의 읽기-쓰기 메모리를 공유하는 방법을 제시한다. Lazy Pointer Swizzling은 서로 다른 오류 도메인이 같은 물리 메모리를 각자 자기 세그먼트 안의 같은 오프셋에 별칭(alias)으로 매핑해 두고, 포인터 값은 건드리지 않은 채 사용 시점에 샌드박싱으로 자동 정합시키는 방법을 말한다.

 

Lazy Pointer Swizzling 방식을 사용하면 하드웨어 페이지 테이블을 수정하여, 메모리의 공유 영역을 접근이 필요한 각 도메인의 세그먼트에 같은 오프셋으로 매핑한다. 즉, 공유 영역을 가상 주소 공간 내 여러 위치에 별칭 처리하지만, 각 별칭 위치는 정확히 동일한 하위 주소 비트를 가진다. 세그먼트 내의 같은 오프셋으로 매핑되었기 때문이다.

 

당연히 하드웨어 공유 메모리 방식과 마찬가지로, 각 공유 영역은 서로 다른 세그먼트 오프셋을 가져야 한다. 세그먼트 오프셋이 겹치면 안 된다. 세그먼트 오프셋으로 공유 영역을 구분하기 때문이다. 신뢰할 수 없는 오브젝트 코드가 공유 메모리에 접근할 때 샌드박싱 코드는 자동으로 공유 주소를 해당 오류 도메인의 데이터 세그먼트 내 주소로 변환한다. 이 변환은 하드웨어 변환과 정확히 동일하게 작동한다. 주소의 하위 비트는 동일하게 유지되고 상위 비트는 데이터 세그먼트 식별자로 설정된다. 이는 MMU가 가상 주소를 물리 주소로 바꾸는 과정과 정확히 동일하다.

 

운영체제가 가상 주소 별칭을 허용하는 경우에는 위와 같이 공유 메모리를 구현할 수 있다. 가상 주소 별칭은 하나의 물리 페이지를 같은 프로세스 안에서 서로 다른 둘 이상의 가상 주소로 동시에 매핑하는 것을 말한다. 만약 운영체제가 가상 주소 별칭을 허용하지 않으면 어떻게 해야 할까? 논문에서는 이러한 경우에도 읽기-쓰기 공유 메모리를 구현할 수 있는 SFI 기법인 공유 세그먼트 매칭(shared segment matching)을 소개한다.

 

공유 세그먼트 매칭 방식에서는 샌드박싱을 사용하여 세그먼트를 강제하는 대신, 해당 주소의 세그먼트 ID가 허용된 공유 세그먼트 집합에 속하는지 검사하고 통과 여부를 결정하는 방식으로 읽기-쓰기 메모리 공유를 허용한다. 검사를 위해 전용 레지스터를 사용해 비트맵을 유지한다. 이 비트맵은 오류 도메인이 접근할 수 있는 세그먼트를 나타낸다. 안전하지 않은 명령 앞에 검사 코드를 삽입하고, 비트맵을 이용하여 세그먼트 ID가 올바른지 검사한다. 앞에서 다룬 세그먼트 매칭 방식보다 하나 더 많은 명령어가 필요하다. 세그먼트 매칭 방식에서는 올바른 세그먼트 ID와 대상 주소의 세그먼트 ID가 일치하는지만 확인하면 된다. 하지만 공유 세그먼트 매칭 방식에서는 올바른 세그먼트 ID 집합에 대상 주소의 세그먼트 ID가 들어가는지 검사해야 하기 때문에 명령어가 하나 더 추가되는 것이다.

 

구현과 검증

지금까지 SFI의 다양한 기법들을 알아보았다. 이제는 이걸 어떻게 구현할 수 있고, 어떻게 검증할 수 있는지 알아보자.

 

논문에서는 SFI를 구현하기 위한 두 가지 전략을 제시한다. 하나는 컴파일러 기반 전략이고, 다른 하나는 로딩 시 변환하는 전략이다. 컴파일러 기반 방식을 사용하면 컴파일러를 사용하여 신뢰할 수 없는 모듈에 대한 캡슐화된 오브젝트 코드를 생성한다. 이 방식에서 코드의 무결성은 모듈이 오류 도메인에 로드될 때 검증된다. 로딩 시 변환하는 방식을 사용하면 로드 시점에 신뢰할 수 없는 모듈의 오브젝트 코드를 직접 수정하여 격리한다. 이 방식 역시 코드의 무결성은 모듈이 오류 도메인에 로드될 때 검증된다.

 

모듈의 무결성 검증 방법에 대해 더 자세히 알아보자. 간접 점프가 존재할 경우, 코드 세그먼트의 아무 지점에서라도 실행이 시작될 수 있다. 이것은 검증의 주요 난제이다. 이를 해결하기 위해 논문에서는 안전하지 않은 영역(unsafe region)이라는 개념을 제시한다. 안전하지 않은 영역은 저장/점프 전용 레지스터에 대한 수정으로 시작된다. 그리고 다음 조건 중 하나가 충족될 때 종료된다.

  • 다음 명령어가 대상 주소를 구성하기 위해 전용 레지스터를 사용하는 저장/점프 명령어인 경우
  • 다음 명령어가 제어 전달 명령어인 경우
  • 다음 명령어의 실행이 보장되지 않는 경우
  • 코드 세그먼트에 더 이상 명령어가 없는 경우

 

모든 안전하지 않은 저장/점프 명령은 반드시 전용 레지스터를 피연산자로 사용한다. 그렇기 때문에 검증기는 안전하지 않은 영역을 분석하여 해당 영역 내에서 수정된 전용 레지스터가 영역을 벗어날 때 유효한지 확인한다. 만약 안전하지 않은 영역을 검증할 수 없다면 해당 코드는 거부된다.

 

고속 도메인 간 통신 (Low Latency Cross Fault Domain Communication)

지금까지 SFI에 대해 자세히 알아 보았다. 하지만 아직 더 알아볼 내용이 남아 있다. 오류 도메인 간 고속 통신에 대해 알아보자. SFI 그 자체도 중요하지만, 격리에 따른 비용을 줄이는 게 중요하다.

 

아래 Figure 4를 보면 신뢰할 수 있는 오류 도메인과 신뢰할 수 없는 오류 도메인 간의 cross-fault-domain RPC의 주요 구성 요소가 나와 있다.

 

이 절에서는 신뢰할 수 있는 오류 도메인과 신뢰할 수 없는 오류 도메인 사이의 cross-fault-domain RPC의 3가지 주요 측면을 집중해서 다룬다:

  1. 결함 도메인이 자체 도메인 외부의 신뢰할 수 있는 스텁 루틴을 안전하게 호출한 다음, 해당 스텁 루틴이 대상 도메인을 안전하게 호출하는 간단한 메커니즘을 설명한다. 여기서 스텁 루틴(stub routine)은 도메인 경계를 건너는 호출을 안전하게 중개하는 아주 작은 신뢰 코드를 말한다. 보통 신뢰 도메인에 상주하며 문지기 역할을 한다.
  2. 인수가 결함 도메인 간에 효율적으로 전달되는 방식을 논의한다.
  3. 오류 격리를 보장하기 위해 cross-fault-domain RPC에서 레지스터 및 기타 머신 상태가 관리되는 방식을 설명한다.

제어 흐름이 오류 도메인을 벗어나는 유일한 방법은 점프 테이블을 통하는 것뿐이다. 각 점프 테이블 항목은 대상 주소가 도메인 외부의 합법적인 진입점인 제어 전달 명령어이다. 이러한 명령어에 점프 테이블은 전용 레지스터를 사용하지 않는다. 대신 대상 주소를 직접 이용한다. 이게 가능한 이유는, 점프 테이블이 읽기 전용인 코드 세그먼트에 존재하기 때문이다. 코드 세그먼트는 신뢰할 수 있는 모듈만 수정할 수 있기 때문에, 점프 테이블 역시 신뢰할 수 있는 모듈만 수정할 수 있다. 그래서 점프 테이블은 전용 레지스터 사용에 의존하지 않는다.

 

Figure 4를 보면 call stub과 return stub이 보인다. 각 오류 도메인 쌍에 대해, 커스텀 된 call stub과 return stub이 외부에서 호출할 수 있게 공개된 프로시저마다 생성된다. 스텁은 호출자 및 피호출자 도메인 외부에서 보호되지 않은 상태로 실행된다. 스텁은 도메인 간에 cross-domain 인수를 복사하고 머신 상태를 관리하는 역할을 담당한다.

 

스텁은 신뢰할 수 있기 때문에 호출 인자를 대상 도메인으로 직접 복사할 수 있다. 주소 공간의 전통적인 RPC 구현은 일반적으로 데이터를 전송하기 위해 3번의 복사 작업을 수행한다.

  1. 호출 인자들을 메시지로 마샬링
    마샬링(marshalling): 한 객체의 메모리에서 표현 방식을 저장 또는 전송에 적합한 다른 데이터 형식으로 변환하는 과정
  2. 커널이 메시지를 대상 주소 공간으로 복사
  3. 호출 받는 측에서 인수를 디마샬링

하지만 LRPC(Lightweight Remote Procedure Call)는 호출자와 피호출자가 공유 버퍼를 통해 통신하도록 함으로써 도메인 간 데이터 전달에 단 한 번의 복사만 사용한다. LRPC는 같은 기계에서 서로 다른 주소 공간/보호 도메인 사이의 호출을 아주 싸게 만들기 위해 제안된 RPC이다. 공유 버퍼를 쓰면 호출자와 피호출자가 같은 물리 페이지를 함께 매핑하기 때문에 인자를 한 번만 공유 버퍼로 복사하면 된다. 복사한 뒤에는 그냥 읽어서 사용하면 된다.

 

스텁은 도메인 간 인수 복사 뿐만 아니라 머신 상태 관리도 담당한다. 머신 상태(machine state) 관리는 도메인 경계를 넘나드는 동안 CPU가 가지고 있는 모든 실행 컨텍스트를 안전하게 저장/전환/복원하는 일을 말한다. Cross-domain 호출 시, 호출자가 향후 사용할 가능성이 있고 피호출자에 의해 수정될 수 있는 모든 레지스터는 보호되어야 한다. 즉, 따로 저장되어야 한다. 스텁은 아키텍처 규약(convention) 상 프로시저 호출 간 보존되어야 하는 최소한의 레지스터만 저장한다.

 

이때 적용할 수 있는 최적화 수단으로는, 피호출자 측 도메인에 보존되어야 하는 레지스터를 수정하는 명령어가 존재하지 않을 경우, 해당 레지스터 지정을 생략하는 기법이 있다.

 

레지스터 저장 및 복원 외에도, 스텁은 실행 스택 전환, 사용 중인 SFI 기법에 맞는 올바른 레지스터 컨텍스트 설정, 모든 전용 레지스터 검증 업무를 담당한다. 머신 상태 관리의 전반을 담당한다고 볼 수 있다.

 

도메인 내부에서 치명적 오류(e.g. 주소 위반)나 무한 루프와 같은 문제가 발생해도 시스템은 견고해야 한다. 망가지지 않고 안전하게 회복되어야 한다는 뜻이다. 먼저 치명적 오류를 처리하는 방법을 알아보자. 논문에서 제시하는 프로토타입에서는 신뢰할 수 없는 오류 도메인의 코드에서 치명적 오류가 발생하면 UNIX 시그널이 발생한다. 그러면 운영체제의 시그널 핸들러가 이를 잡고 현재 진행 중인 cross-domain 호출을 즉시 종료한다. 이후에 호출자 도메인에 에러 코드나 예외 반환 등을 통해 오류를 통지한다.

 

다음은 무한 루프 등으로 인해 지나치게 오래 걸리는 호출에 대한 처리 방법을 알아보자. 애플리케이션의 모든 도메인이 동일한 OS 스레드 하나를 공유할 때, 한 도메인이 무한 루프에 빠지면 애플리케이션 전체가 멈춘다. 그렇기 때문에 SFI에서는 무한 루프나 지나치게 오래 걸리는 호출이 문제가 된다. 이러한 문제를 해결하기 위해 신뢰 모듈은 타이머 기능을 사용하여 주기적으로 인터럽트를 걸고, 타임아웃된 호출을 강제 종료한다.

 

마치며

지금까지 “Efficient Software-Based Fault Isolation” 논문의 내용을 살펴봤다. 소프트웨어 기반 오류 격리는 하드웨어 기반 격리 방식의 오버헤드가 높아지는 상황에 효율적으로 오류 격리를 달성하기 위한 기술이다. 소프트웨어 기반 오류 격리를 구현하는 세부 기법들과 다양한 최적화 방법들도 살펴봤다. 또한 스텁을 이용해 두 오류 도메인끼리 빠르게 통신하는 원리도 살펴보았다. 이상으로 글을 마치겠다.

 

참고 문헌

  • [1] Efficient Software-Based Fault Isolation
  • [2] Empirical Security Analysis of Software-based Fault Isolation through Controlled Fault Injection
  • [3] Mechanising and verifying the WebAssembly specification
  • [4] Bringing the Web up to Speed with WebAssembly
  • [5] Comparison Of Fault Detection And Isolation Methods: A Review
  • [6] Going beyond the Limits of SFI: Flexible and Secure Hardware-Assisted In-Process Isolation with HFI
  • [7] The RISC Concept - A Survey of Implementations
  • [8] https://ko.wikipedia.org/wiki/마샬링_(컴퓨터_과학)

'논문 리뷰' 카테고리의 다른 글

[System] 논문 리뷰: The Tail at Scale  (3) 2025.08.23