← System Programming DUJINLABS.COM

Signal / Event / IPC · Linux userspace / kernel ABI

Unix socket과 SCM_RIGHTS

Unix domain socket이 byte뿐 아니라 열린 file 참조를 다른 process의 fd table에 설치하는 과정을 control message 단위로 설명합니다.

Series
24 / 37
Build
cc -std=c17 -Wall -Wextra -O2 send_fd.c -o send_fd
Run
./send_fd
Kernel
Linux 6.18.37 LTS

fd 번호를 다른 process에 보내면 같은 파일을 공유할 수 있는가?

process A의 fd 번호 7을 정수로 보내도 process B의 fd table 7은 전혀 다른 의미다. SCM_RIGHTS는 kernel이 source fd를 struct file 참조로 바꾸고 receiver의 새 fd slot에 설치해 같은 open file description을 전달한다.

stream Unix socket에서는 control message와 함께 최소 한 byte의 normal data를 보내는 관례가 안전하다. receiver는 cmsghdr level/type/length를 모두 검증하고 예상하지 않은 fd를 즉시 닫아야 한다.

구조 그림

그림 1. SCM_RIGHTS가 전달하는 file reference

Sender fd table

  • fd 5
  • FD_CLOEXEC
  • sendmsg control

Socket message

  • SCM_RIGHTS
  • scm_fp_list
  • struct file ref +1

Shared struct file

  • f_pos=12
  • O_RDONLY
  • inode /etc/hostname

Receiver fd table

  • new fd 8
  • MSG_CMSG_CLOEXEC
  • same f_pos/f_flags

sender의 fd 숫자 5가 receiver의 fd 숫자 8로 그대로 복사되는 것이 아니다. 둘이 같은 struct file을 참조하게 된다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
sender fd struct file lookup
sendmsg cmsghdr SCM_RIGHTS
socket queue file reference 보관
recvmsg receiver fd install
shared file offset/status flag 공유

전달되는 것은 번호가 아니라 open file description 참조다. receiver가 받는 숫자는 새로 배정되지만 file offset과 status flag는 sender와 공유할 수 있다.

그림 3. 커널 내부에서 지나가는 주요 지점
scm_send fd array 검증
unix_scm_to_skb file refs attach
socket queue skb 수명
scm_recv new fd reserve
fd_install receiver table

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

Linux 6.18.37 LTS 소스 위치

glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.

파일함수·구조체여기서 볼 것
net/core/scm.c scm_fp_copy(), scm_detach_fds() fd를 file reference array로 복사하고 receiver에 설치
net/unix/af_unix.c unix_stream_sendmsg(), unix_stream_read_generic() control message를 Unix socket queue와 함께 전달
fs/file.c get_unused_fd_flags(), fd_install() receiver fd slot 할당

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 send_fd.c -o send_fd
01#define _GNU_SOURCE
02#include <fcntl.h>
03#include <stdio.h>
04#include <string.h>
05#include <sys/socket.h>
06#include <unistd.h>
07
08int main(void)
09{
10    int pair[2];
11    if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, pair) < 0)
12        return 1;
13    int source = open("/etc/hostname", O_RDONLY | O_CLOEXEC);
14    if (source < 0)
15        return 1;
16
17    char marker = 'F';
18    struct iovec iov = { .iov_base = &marker, .iov_len = 1 };
19    char control[CMSG_SPACE(sizeof(int))];
20    memset(control, 0, sizeof(control));
21    struct msghdr out = { .msg_iov = &iov, .msg_iovlen = 1,
22                          .msg_control = control, .msg_controllen = sizeof(control) };
23    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&out);
24    cmsg->cmsg_level = SOL_SOCKET;
25    cmsg->cmsg_type = SCM_RIGHTS;
26    cmsg->cmsg_len = CMSG_LEN(sizeof(int));
27    memcpy(CMSG_DATA(cmsg), &source, sizeof(source));
28    if (sendmsg(pair[0], &out, 0) < 0)
29        return 1;
30
31    char in_control[CMSG_SPACE(sizeof(int))];
32    struct msghdr in = { .msg_iov = &iov, .msg_iovlen = 1,
33                         .msg_control = in_control, .msg_controllen = sizeof(in_control) };
34    if (recvmsg(pair[1], &in, MSG_CMSG_CLOEXEC) < 0)
35        return 1;
36    int received = -1;
37    cmsg = CMSG_FIRSTHDR(&in);
38    if (cmsg && cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS)
39        memcpy(&received, CMSG_DATA(cmsg), sizeof(received));
40    char text[128];
41    ssize_t n = read(received, text, sizeof(text));
42    if (n > 0)
43        write(STDOUT_FILENO, text, (size_t)n);
44    close(received); close(source); close(pair[0]); close(pair[1]);
45    return n < 0;
46}

코드 조각별 설명

실제 코드 11행SOCK_SEQPACKET | SOCK_CLOEXEC

message boundary를 보존하는 local socket pair를 만들고 두 endpoint 모두 exec 상속을 막는다.

실제 코드 19행CMSG_SPACE(sizeof(int))

cmsghdr와 payload 정렬 padding까지 포함한 control buffer 크기를 계산한다. CMSG_LEN과 용도가 다르다.

실제 코드 25행SCM_RIGHTS

payload의 int 값을 receiver에 그대로 복사하는 것이 아니라 kernel이 해당 fd들의 struct file 참조를 전달한다.

실제 코드 34행MSG_CMSG_CLOEXEC

받아서 설치되는 모든 fd에 close-on-exec를 원자적으로 설정해 recvmsg와 fcntl 사이 유출 경쟁을 막는다.

실제 코드 37행CMSG_FIRSTHDR(&in)

실제 코드에서는 모든 control message를 CMSG_NXTHDR로 순회하고 MSG_CTRUNC, 길이, 예상 fd 개수를 검증해야 한다.

세부 동작

01

control buffer truncation은 resource leak로 이어진다

buffer가 작으면 MSG_CTRUNC가 설정될 수 있다. 전달된 fd 일부가 설치됐는지와 kernel 처리 규칙을 확인하고, protocol 위반 시 받은 모든 fd를 닫는다.

수신 개수 상한을 두지 않으면 peer가 fd table limit을 소모하게 만들 수 있다.

02

credential 전달과 결합할 수 있다

SO_PASSCRED/SCM_CREDENTIALS 또는 SO_PEERCRED로 local peer의 PID/UID/GID를 확인할 수 있다. pathname socket permission만으로 장기 connection의 모든 authorization을 대신하지 않는다.

PID namespace와 credential 변경 가능성을 고려해 접속 시점 권한 모델을 정한다.

03

공유 offset이 의도인지 확인한다

SCM_RIGHTS로 받은 fd는 dup와 마찬가지로 같은 open file description을 참조하므로 read offset과 O_NONBLOCK/O_APPEND가 공유된다. 독립 offset이 필요하면 receiver가 pathname/handle을 바탕으로 별도 open해야 한다.

전달 뒤 sender가 close해도 receiver 참조가 남아 file object는 유지된다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
scm_fp_listsendmsg에서 file refs를 모아 socket message 수명 동안 유지한다count, struct file array
cmsghdrsend/recv syscall 동안 user control buffer에 존재한다level, type, length
received fdrecvmsg가 receiver fd table에 설치하고 close에서 해제된다CLOEXEC, shared f_pos/f_flags

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
EMFILE로 recv 실패receiver fd limit 부족RLIMIT_NOFILE과 받은 개수 제한
예상치 못한 fd leakMSG_CTRUNC/validation/error cleanup 누락/proc/PID/fd와 close path
읽기 offset이 움직임sender와 open description 공유fdinfo pos와 protocol 의도

직접 확인

  1. sender가 source를 close한 직후에도 receiver가 읽을 수 있는지 확인한다.
  2. received fd와 source fd에서 번갈아 한 byte씩 읽어 공유 file offset을 확인한다.
  3. SCM_CREDENTIALS를 추가해 sender pid/uid/gid와 SO_PEERCRED 결과를 비교한다.
실행./send_fd
추적strace -e trace=socketpair,openat,sendmsg,recvmsg,read,close ./send_fd

원문