Struct Sockaddr 속편
Source: Hacker News
배경
10년간 진행된 Linux Kernel Self‑Protection Project (KSPP)의 많은 목표 중 하나는, 컴파일 시 크기를 알 수 없는 flexible array member의 경우에도 모든 배열 참조를 경계 검사할 수 있게 하는 것입니다. 커널에서 특히 까다로운 flexible array member는 실제로는 그렇게 선언되지도 않았습니다. 거의 정확히 1년 전, LWN은 네트워킹 서브시스템에서 많이 사용되는 sockaddr 구조체의 안전성을 높이려는 노력을 살펴보았습니다. 1년이 지난 지금도 Kees Cook은 이 작업을 마무리할 방법을 찾고 있습니다.
struct sockaddr 문제
전통적으로 struct sockaddr는 다음과 같이 정의됩니다:
struct sockaddr {
short sa_family;
char sa_data[14];
};
sa_data 필드는 1980년대 초 BSD Unix에서 처음 정의될 때 네트워크 주소를 담기에 충분했지만, 현재는 충분하지 않습니다. 그 결과, 커널과 사용자 공간 모두에서 실제로는 더 큰 구조체를 가리키는 struct sockaddr 포인터가 많이 사용됩니다. 즉, sa_data가 선언되지 않았음에도 불구하고 flexible array member처럼 취급되고 있는 것입니다.
struct sockaddr의 널리 퍼진 사용은 구조체 내 배열 멤버 사용을 더 잘 검사하려는 여러 시도에 큰 장애물이 되었습니다.
지난 해 말, 커널의 많은 부분이 struct sockaddr_storage(구현은 struct __kernel_sockaddr_storage)를 사용하도록 변경되었습니다. 이 구조체는 알려진 모든 네트워크 주소를 담을 수 있을 만큼 큰 데이터 배열을 가지고 있습니다. struct sockaddr 정의를 바꿔 sa_data를 명시적인 flexible array member로 만들려는 시도가 있었지만, 다음과 같은 문제에 부딪혔습니다:
- 커널 곳곳에서
struct sockaddr를 다른 구조체 안에 포함시킵니다. - 대부분의 경우
sa_data는 flexible array member로 취급되지 않습니다. - 개발자들은
struct sockaddr를 포함 구조체의 어디에든 자유롭게 삽입했으며, 종종 끝이 아닙니다.
sa_data를 flexible array member로 재정의하면, 컴파일러는 실제 구조체 크기를 알 수 없게 됩니다. 따라서 struct sockaddr를 포함하는 구조체를 배치할 수 없게 되고, 커널 빌드 시 수만 개의 경고가 발생합니다. 커널 개발자는 경고 폭탄보다 배열 오버플로우 위험을 감수하는 편을 택하기 때문에 이 작업은 중단되었습니다.
제안된 해결책: struct sockaddr_unsized
한 가지 해결책은 임베디드 struct sockaddr 필드를 struct sockaddr_storage로 교체하는 것이지만, 이는 불필요한 메모리 사용을 늘려서 호응을 얻기 어렵습니다.
대신 Cook은 **패치 시리즈**를 통해 새로운 sockaddr 변형을 도입하고 있습니다:
struct sockaddr_unsized {
__kernel_sa_family_t sa_family; /* 주소 패밀리, AF_xxx */
char sa_data[]; /* flexible 주소 데이터 */
};
이 구조체는 sa_data 크기가 유연해야 하지만 실제 크기도 알려져 있는 내부 네트워크 서브시스템 인터페이스에서 사용됩니다. 예를 들어, struct proto_ops의 bind() 메서드는 다음과 같이 정의됩니다:
int (*bind) (struct socket *sock,
struct sockaddr *myaddr,
int sockaddr_len);
myaddr의 타입을 struct sockaddr_unsized * 로 바꿀 수 있는데, sockaddr_len이 sa_data 배열의 실제 크기를 제공하기 때문입니다. Cook의 패치 시리즈는 이러한 교체를 다수 수행하여 네트워킹 서브시스템에서 가변 크기의 sockaddr 구조체 사용을 없앱니다. 이로써 sa_data 14바이트 배열을 초과해 읽는 struct sockaddr 사용이 사라지고, struct sockaddr를 기존의 고전적인 비‑flexible 정의로 되돌릴 수 있게 됩니다. 이제 해당 구조체를 사용하는 코드에 배열 경계 검사를 적용할 수 있습니다.
전망
이 변경만으로도 수많은 경고가 사라지며, 이는 좋은 마무리 지점으로 여겨집니다. 하지만 여전히 sockaddr_unsized 구조체는 치명적인 오버플로우 위험을 내포하고 있습니다. 상황이 정리되면 이 구조체들에 대한 경계 검사 구현이 다음 과제가 될 것입니다.
패치 세트에서 언급된 한 가지 접근법은 sa_data_len 필드를 추가해 sa_data 배열의 길이를 구조체에 포함시키는 것입니다. 이렇게 하면 counted_by() 어노테이션을 사용해 필드 간 관계를 문서화하기 쉬워지고, 컴파일러가 자동으로 경계 검사를 삽입할 수 있게 됩니다.
Rust로 새로운 코드를 작성할 수 있다는 점은 커널의 메모리 안전 버그를 줄이는 데 큰 기대를 주지만, 커널에는 당분간 사라지지 않을 방대한 C 코드가 존재합니다. 그 코드를 더 안전하게 만들 수 있는 모든 시도는 환영받을 것입니다. struct sockaddr의 다양한 변형은 다소 어리석어 보일 수 있지만, 40년 전 정의된 API에 약간의 안전성을 도입하는 과정의 일부입니다. 10년간의 KSPP 작업으로 커널은 더 안전해졌지만, 아직 해야 할 일은 많이 남아 있습니다.