# Fil-C의 단순화한 모델

> Clean Markdown view of GeekNews topic #28696. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=28696](https://news.hada.io/topic?id=28696)
- GeekNews Markdown: [https://news.hada.io/topic/28696.md](https://news.hada.io/topic/28696.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-04-20T05:39:55+09:00
- Updated: 2026-04-20T05:39:55+09:00
- Original source: [corsix.org](https://www.corsix.org/content/simplified-model-of-fil-c)
- Points: 2
- Comments: 1

## Topic Body

- C/C++ 포인터에 **AllocationRecord 메타데이터**를 함께 붙여 추적하고, 역참조 시 **메모리 경계 검사**를 수행하는 구조
- 포인터 대입, 산술, 함수 인자 전달, 반환, `malloc`·`free` 호출까지 원래 포인터 값과 대응 메타데이터를 함께 이동하거나 **Fil-C 전용 호출**로 변환하는 방식
- 힙 메모리 안의 포인터 메타데이터는 **invisible_bytes**에 별도로 저장하며, 포인터 로드·스토어 시 값과 메타데이터를 함께 읽고 쓰고 **정렬 검사**도 추가 적용
- `filc_free`는 `visible_bytes`와 `invisible_bytes`만 해제하고 **AllocationRecord 자체는 유지**하며, 이후 정리는 가비지 컬렉터가 맡고 주소 탈출 가능성이 있는 지역 변수는 **힙 승격** 처리
- 스레드, 함수 포인터, 메모리·성능 최적화 같은 실제 구현 복잡성이 남아 있지만, 대규모 C/C++ 코드의 **메모리 안전성 검증**이나 pointer provenance의 **구체적 시스템 예시**로 활용 가능성 존재

---

### 단순화한 Fil-C 모델
- **Fil-C**는 C/C++ 코드를 메모리 안전하게 다루기 위해 포인터와 함께 **AllocationRecord*** 메타데이터를 추적하는 구조 사용
  - 실제 구현은 LLVM IR 재작성 방식이지만, 단순 모델은 C/C++ 소스 코드를 자동 변환하는 형태
  - 각 함수의 포인터형 지역 변수마다 대응하는 `AllocationRecord*` 지역 변수 추가
  - 예시로 `T1* p1`에는 `AllocationRecord* p1ar = NULL` 추가 형태
- 포인터 지역 변수에 대한 단순 대입과 계산은 원래 포인터 값과 함께 **AllocationRecord***도 같이 이동하는 방식
  - `p1 = p2`는 `p1 = p2, p1ar = p2ar`로 변환
  - `p1 = p2 + 10` 역시 `p1ar = p2ar` 동반
  - 정수에서 포인터로의 캐스트는 메타데이터를 `NULL`로 설정
  - 포인터를 정수로 바꾸는 캐스트는 그대로 유지
- 함수 인자 전달과 반환에서도 포인터와 함께 **AllocationRecord***를 추가로 전달하며, 특정 표준 라이브러리 호출은 **Fil-C 전용 함수**로 치환
  - `malloc`과 `free` 호출은 각각 `filc_malloc`, `filc_free` 형태로 변환
  - 예시로 `p1 = malloc(x); free(p1);`는 `{p1, p1ar} = filc_malloc(x); filc_free(p1, p1ar);` 형태
- `filc_malloc`은 요청한 메모리 하나만 할당하지 않고 **세 개의 할당** 수행
  - `AllocationRecord` 객체 할당
  - 실제 데이터용 `visible_bytes` 할당
  - 보이지 않는 메타데이터 저장용 `invisible_bytes`를 `calloc`으로 할당
  - `AllocationRecord`는 `visible_bytes`, `invisible_bytes`, `length` 필드 포함

### 역참조와 경계 검사
- 포인터 역참조 시 동반된 **AllocationRecord***를 사용해 **경계 검사** 수행
  - 포인터 메타데이터가 `NULL`이 아닌지 확인
  - 현재 포인터 위치와 `visible_bytes` 시작 주소의 차이를 계산
  - 오프셋이 전체 길이보다 작은지 확인
  - 남은 길이가 역참조 대상 크기보다 충분한지 확인
- 읽기와 쓰기 모두 같은 검사 절차 적용
  - `x = *p1` 전에도 검사 수행
  - `*p2 = x` 전에도 동일한 형태의 검사 수행
- 이 구조로 포인터가 가리키는 대상이 할당 범위를 벗어나는 접근 차단

### 힙 안의 포인터와 invisible_bytes
- 힙 메모리에 저장된 포인터는 지역 변수처럼 컴파일러가 직접 별도 변수로 관리할 수 없기 때문에 **invisible_bytes** 사용
  - `visible_bytes + i` 위치에 포인터가 있다면 대응하는 `AllocationRecord*`는 `invisible_bytes + i` 위치에 저장
  - 즉 `invisible_bytes`는 요소 타입이 `AllocationRecord*`인 배열처럼 동작
- 포인터 값을 메모리에서 읽거나 쓸 때는 일반 경계 검사 외에 **정렬 검사** 추가
  - 오프셋 `i`가 `sizeof(AllocationRecord*)`의 배수인지 확인
  - 이 조건이 맞아야 `invisible_bytes`를 `AllocationRecord**` 배열처럼 안전하게 접근 가능
- 포인터 로드 시 데이터 포인터와 함께 메타데이터도 함께 로드
  - `p2 = *p1`은 `p2 = *p1` 뒤에 `p2ar = *(AllocationRecord**)(p1ar->invisible_bytes + i)` 추가
- 포인터 스토어 시 포인터 값뿐 아니라 대응 메타데이터도 함께 저장
  - `*p1 = p2`는 실제 데이터 저장 후 `*(AllocationRecord**)(p1ar->invisible_bytes + i) = p2ar` 수행

### filc_free와 가비지 컬렉터
- `filc_free`는 포인터가 `NULL`이 아닐 때 **AllocationRecord**와의 일치성 검사 후 두 개의 메모리만 해제
  - `par != NULL` 확인
  - `p == par->visible_bytes` 확인
  - `visible_bytes`와 `invisible_bytes` 해제
  - 이후 `visible_bytes`, `invisible_bytes`를 `NULL`로, `length`를 0으로 변경
- `filc_malloc`이 세 개를 할당하지만 `filc_free`는 **AllocationRecord 객체 자체를 해제하지 않음**
  - 이 차이는 가비지 컬렉터가 처리
- 단순 모델에는 **stop-the-world GC**면 충분하며, 실제 Fil-C는 병렬 동시 점진 수집기 사용
  - GC는 `AllocationRecord` 객체를 따라가며 추적
  - 도달 불가능한 `AllocationRecord`를 해제 대상으로 처리
- GC는 추가로 두 가지 작업 수행
  - 도달 불가능한 `AllocationRecord`를 해제할 때 `filc_free` 호출
  - `length`가 0인 `AllocationRecord`를 가리키는 모든 포인터를 길이 0의 단일 정규 `AllocationRecord`로 변경
- 이 동작으로 `free`를 호출하지 않아도 메모리 누수로 이어지지 않음
  - GC가 자동 해제 수행
  - 다만 `free` 호출은 GC보다 더 이른 시점의 메모리 해제를 가능하게 함
- `free` 이후 해당 `AllocationRecord`는 결국 도달 불가능 상태가 되어 나중에 정리 가능

### 지역 변수 주소 탈출과 힙 승격
- GC가 존재하면 지역 변수의 주소를 안전하게 취급할 수 있는 범위 확대
  - 지역 변수의 주소가 취해졌고, 그 주소가 변수 수명 밖으로 **탈출하지 않는다는 증명**을 컴파일러가 하지 못하면 힙 할당으로 승격
- 이런 지역 변수는 스택 대신 `malloc`을 통해 할당
  - 대응하는 `free`를 별도로 삽입할 필요 없음
  - GC가 수거 담당

### Fil-C 버전 memmove
- C 표준 라이브러리의 `memmove`는 임의 메모리를 다루기 때문에 컴파일러가 그 안의 포인터 존재 여부를 알 수 없는 문제 존재
- 이를 위해 **휴리스틱** 적용
  - 임의 메모리 안의 포인터는 그 메모리 범위 안에 **완전히 포함**되어 있어야 함
  - 포인터는 올바르게 정렬되어 있어야 함
- 이 규칙 때문에 같은 8바이트 이동이라도 동작 차이 발생
  - 정렬된 8바이트를 한 번에 `memmove`하면 대응 구간의 `invisible_bytes`도 함께 이동
  - 1바이트씩 8번 나눠서 `memmove`하면 `invisible_bytes`는 이동하지 않음

### 실제 구현에서 추가되는 복잡성
- ## 스레드
  - 동시성은 **GC 복잡도**를 높이는 요소
  - `filc_free`는 즉시 메모리를 해제할 수 없음
    - 해제 중인 스레드와 다른 스레드의 동일 메모리 접근이 경쟁 상태일 수 있기 때문
  - 포인터에 대한 원자적 연산도 추가 처리가 필요
    - 기본 재작성은 포인터 로드/스토어를 두 번의 로드/스토어로 바꾸므로 원자성 파괴
- ## 함수 포인터
  - `AllocationRecord`의 추가 메타데이터로 `visible_bytes`가 일반 데이터가 아니라 **실행 코드 포인터**인지 표시
  - 함수 포인터 `p1`을 통한 호출은 `p1 == p1ar->visible_bytes` 확인과 함께 `p1ar`가 함수 포인터를 나타내는지 검사
  - 함수 포인터에 대한 타입 혼동 공격 방지를 위해 호출 ABI에서도 **타입 시그니처 검증** 필요
  - 한 가지 방법은 모든 함수가 동일한 타입 시그니처를 갖게 만드는 방식
    - 모든 인자를 구조체에 담아 메모리로 전달하는 것처럼 처리
    - ABI 경계에서는 각 함수가 그 구조체에 대응하는 단일 `AllocationRecord` 하나만 받는 형태
- ## 메모리 사용 최적화
  - `filc_malloc`이 `invisible_bytes`를 즉시 할당하지 않고 필요 시점에 지연 할당하는 방식 고려 가능
  - `AllocationRecord`와 `visible_bytes`를 하나의 할당으로 같이 배치하는 방식도 고려 가능
  - 기반 `malloc`이 각 할당 앞부분에 메타데이터를 붙인다면 그 메타데이터를 `AllocationRecord`에 넣는 방식도 고려 대상
- ## 성능 최적화
  - Fil-C의 메모리 안전성은 **성능 비용** 수반
  - 잃어버린 성능을 일부 되찾기 위한 다양한 기법 적용 여지 존재

### Fil-C 사용 시점
- 대규모 C/C++ 코드가 동작은 하는 것처럼 보여도 **메모리 안전성 검증**이 없고, 메모리 안전성을 위해 GC 도입과 큰 성능 저하를 감수할 수 있는 경우 사용 가능
  - Java, Go, Rust로 재작성하기 전까지의 임시 조치 가능성 언급
- **ASan**처럼 메모리 버그 탐지 목적으로도 Fil-C 실행 가능
  - C/C++ 코드를 Fil-C 아래에서 실행해 메모리 버그 확인 가능
- 컴파일 타임 언어와 런타임 언어가 같고 컴파일 타임 안전성이 강한 언어에서는 **안전한 컴파일 타임 평가** 용도 가능
  - 예시로 Zig 언급
  - 런타임 평가는 안전하지 않더라도 컴파일 타임 평가는 Fil-C 구성을 사용할 수 있음
- **pointer provenance**를 다루는 구체적 시스템 사례로도 의미 존재
  - `p1`과 `p2` 타입이 같을 때 `if (p1 == p2) { f(p1); }`를 `if (p1 == p2) { f(p2); }`로 바꾸는 최적화 가능성 질문 제시
  - Fil-C에서는 `f`에 전달되는 `AllocationRecord*`가 달라지므로 답은 명확히 아니라고 명시
  - 이 점에서 Fil-C는 pointer provenance를 갖는 구체적 시스템 예시 역할

## Comments



### Comment 55858

- Author: neo
- Created: 2026-04-20T05:39:57+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=47810872) 
- chibicc나 slimcc 같은 데에 **invisicaps**를 붙여보는 건 꽤 재밌는 실험이 될 것 같음  
  참조 카운팅이나 **invisible capability system** 변형도 시험해볼 여지가 있고, 약간의 간접 참조 비용 대신 메모리를 아낄 수도 있겠다는 생각임
- 나는 [filc-bazel-template](https://github.com/hsaliak/filc-bazel-template)을 만들어서 **Bazel target**으로 묶어뒀음  
  이 둘을 함께 써서 **hermetic builds**를 만들고 싶은 사람에게 도움이 되길 바라는 마음임
- 나는 이 문장의 뜻을 잘 모르겠음  
  `Upon freeing an unreachable AllocationRecord, call filc_free on it.`  
  내가 보기엔 의도한 말은, 도달 불가능한 AR을 해제하기 전에 `visible_bytes`와 `invisible_bytes` 필드가 가리키는 메모리부터 해제하라는 뜻 같음
- 나는 **Fil-C**가 지금까지 본 프로젝트 중 가장 저평가된 편이라고 느낌  
  안전성을 위해 “**rewrite it in Rust**” 하자는 말보다, 기존 C 프로그램을 완전히 메모리 안전하게 컴파일할 수 있다는 점이 더 흥미롭게 보임
  - 내가 보기엔 몇 가지를 같이 봐야 함  
    첫째, Fil-C는 **더 느리고 더 큼**. 그게 괜찮았다면 지난 10년 동안 굳이 Rust보다 Java나 C#로 먼저 갔어야 했다는 말도 가능함  
    둘째, 여전히 C를 쓰는 셈임. 기존 코드 유지보수엔 괜찮지만 새 코드를 많이 쓴다면 나는 **Rust가 훨씬 쾌적**하다고 느낌  
    셋째, Fil-C는 런타임 안전성이고, Rust는 일부를 컴파일 타임에 표현할 수 있음. 더 나아가 **WUFFS** 같은 언어는 런타임 체크 없이도 컴파일 단계에서 안전성을 증명하려고 하므로, 코드가 논리적으로 틀릴 수는 있어도 크래시나 임의 코드 실행은 막는 방향이라는 점이 다름
  - 나는 여기서 과소평가됐다고 보진 않음. 이미 관련 논의가 꽤 많이 있었음  
    [Fil-Qt: A Qt Base build with Fil-C experience](https://news.ycombinator.com/item?id=46646080), [Linux Sandboxes and Fil-C](https://news.ycombinator.com/item?id=46259064), [Ported freetype, fontconfig, harfbuzz, and graphite to Fil-C](https://news.ycombinator.com/item?id=46090009), [A Note on Fil-C](https://news.ycombinator.com/item?id=45842494), [Notes by djb on using Fil-C](https://news.ycombinator.com/item?id=45788040), [Fil-C: A memory-safe C implementation](https://news.ycombinator.com/item?id=45735877), [Fil's Unbelievable Garbage Collector](https://news.ycombinator.com/item?id=45133938) 같은 스레드들이 있었음
  - 내가 보는 Fil-C의 핵심 한계는 **runtime memory safety**라는 점임  
    여전히 메모리 안전하지 않은 코드를 쓸 수 있고, 이제는 그 결과가 취약점 대신 확실한 크래시가 된다는 의미에 가까움  
    신뢰할 수 없는 입력을 받는 웹 API 같은 걸 만든다면, 이런 문제는 결국 **denial-of-service**로 이어질 수 있어서 낫긴 해도 충분히 좋다고 보긴 어려움  
    Fil-C 작업 자체를 깎아내리려는 건 아니고, 런타임 접근에는 분명한 한계가 있다고 봄
  - 관심 가져줘서 고마움  
    다만 공정하게 말하면 Fil-C는 **Rust보다 꽤 느리고**, 메모리도 더 많이 씀  
    반면 Fil-C는 **safe dynamic linking**을 지원하고, 어떤 면에서는 Rust보다 더 엄격하게 안전하다고도 말할 수 있음  
    결국 트레이드오프이니 각자 상황에 맞게 선택하면 된다는 생각임
  - 내 느낌엔, C/C++ 프로그래머에게 자기 프로그램에 **garbage collector**를 붙일 수 있다고 말했을 때 눈이 반짝이는 경우는 드묾  
    그래서 이 아이디어가 기술적으로 흥미로워도 감성적으로는 쉽게 먹히지 않는 것 같음
- 내가 보기엔 Fil-C는 **data race** 상황에서는 메모리 안전하지 않음  
  capability와 pointer 값이 대입 중에 찢어질 수 있어서, 스레드 인터리빙이 나쁘면 잘못된 포인터로 객체에 접근해 **임의 오동작**이 날 수 있음  
  이런 한계 자체는 받아들일 수 있는데, 문제를 지적하는 사람들을 지지자들까지 나서서 강하게 몰아붙이는 분위기는 아쉽게 느껴짐
  - 내가 알기론 그 부분은 **atomic ops**를 써서 처리하고 있음  
    안타깝게도 그게 오버헤드의 큰 원인 중 하나이기도 함
- 내가 보기엔 이건 결국 **fat pointers** 계열 기법의 또 다른 변형임  
  이런 방식은 보안 보장이 충분하지 않거나, **non-fat ABI boundaries**를 넘기 어렵거나, 오버헤드가 크다는 이유로 여러 번 구현됐다가 배제된 적이 많았음
  - 하지만 요즘은 **fat pointers를 하드웨어가 직접 지원**하는 흐름이 다시 나오고 있어서, 너무 일찍 기각하는 건 아닐 수도 있음  
    게다가 filc는 단순한 fat pointer만으로 설명되지는 않는다고 봄
  - 이미 여러 플랫폼에서 **hardware memory tagging**이 제공되고 있다는 점도 같이 봐야 한다고 생각함
