QUESTION
execve는 새 process를 만드는가, 현재 process를 바꾸는가?
execve는 새 PID를 만들지 않는다. 호출한 task가 새 mm, code, stack, credentials를 받아 같은 PID로 계속 실행된다. 성공 시 이전 코드로 돌아오지 않는다는 점이 일반 함수와 가장 큰 차이다.
모든 상태가 사라지는 것도 아니다. close-on-exec가 없는 fd, current working directory, umask, 일부 signal mask와 resource limit는 남고, caught signal disposition과 address mapping은 초기화된다.
STRUCTURE
구조 그림
execve 이전
- PID 4120
- 여러 thread
- old text/data/heap/stack
- fd 0,1,2,7
- caught signal handlers
유지/판정
- PID·cwd·umask 유지
- FD_CLOEXEC 적용
- credential 재계산
- resource limit 유지
execve 이후
- PID 4120
- 호출 thread 하나
- new PT_LOAD + stack
- fd 0,1,2
- caught handler reset
execve는 새 process를 추가하지 않는다. process identity 일부는 유지하고 userspace image를 새 ELF 기준으로 교체한다.
CALL PATH
호출 흐름
exec 전후를 process identity와 process image로 나눠 본다. PID는 유지되지만 userspace pointer와 thread 구성은 전부 새 image 기준이다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| fs/exec.c | do_execveat_common(), bprm_execve() | argv/envp 복사와 binary handler 호출 |
| fs/exec.c | begin_new_exec(), setup_new_exec() | old mm을 버린 뒤 되돌릴 수 없는 구간 |
| fs/binfmt_elf.c | load_elf_binary() | ELF segment와 initial stack 생성 |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 exec_replace.c -o exec_replace01#define _GNU_SOURCE
02#include <errno.h>
03#include <fcntl.h>
04#include <stdio.h>
05#include <stdlib.h>
06#include <unistd.h>
07
08extern char **environ;
09
10int main(void)
11{
12 int fd = open("exec.log", O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
13 if (fd < 0) {
14 perror("open");
15 return 1;
16 }
17 dprintf(fd, "before exec pid=%ld\n", (long)getpid());
18
19 char *const argv[] = { "sh", "-c",
20 "printf 'after exec pid=%s\n' $$; ls -l /proc/$$/fd", NULL };
21 execve("/bin/sh", argv, environ);
22
23 int saved = errno;
24 dprintf(STDERR_FILENO, "execve failed: %d\n", saved);
25 close(fd);
26 return 1;
27}
CODE NOTES
코드 조각별 설명
O_CLOEXECopen과 FD_CLOEXEC 설정을 하나의 syscall로 묶어 다른 thread가 fork/exec하는 사이 fd가 유출되는 경쟁 조건을 없앤다.
dprintf(fdexec 전에 기록한 file 내용은 남지만 O_CLOEXEC 때문에 새 image의 fd table에는 해당 slot이 닫힌 상태로 보인다.
char *const argv[]argv[0]은 관례상 프로그램 이름이고 배열은 NULL pointer로 끝나야 한다. kernel은 문자열과 pointer 개수를 제한한다.
execve("/bin/sh"성공하면 아래 줄은 실행되지 않는다. /bin/sh의 ELF entry에서 새 userspace 흐름이 시작된다.
int saved = errno실패 직후 errno를 저장해 dprintf나 close가 값을 바꾸기 전에 원래 이유를 보존한다.
DETAILS
세부 동작
fd 상속은 allowlist로 설계한다
기본적으로 fd는 exec를 넘어간다. 모든 생성 syscall에 CLOEXEC variant를 사용하고, 명시적으로 넘길 fd만 child setup에서 dup2/dup3로 표준 번호나 약속한 번호에 배치한다.
close_range()는 큰 fd table을 정리할 때 /proc/self/fd 순회보다 race가 적다.
credential 변경은 binary metadata와 함께 일어난다
setuid/setgid bit, file capability, no_new_privs, mount nosuid, ptrace 상태가 새 effective credential 계산에 참여한다. 환경 변수만 지우는 것으로 privilege boundary가 완성되지 않는다.
secure-execution mode에서는 dynamic linker가 LD_PRELOAD 같은 환경 변수를 제한한다.
실패 가능한 구간과 복구 불가능한 구간을 나눈다
kernel은 가능한 한 binary를 검증한 뒤 old mm을 폐기한다. 그래도 교체가 시작된 뒤 치명적 오류가 나면 원래 image로 돌아갈 수 없어 signal로 종료될 수 있다.
실무에서는 parent가 child exec 성공 여부를 CLOEXEC pipe로 확인한다. exec 성공 시 pipe write end가 자동으로 닫히고, 실패 시 errno를 write한다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
linux_binprm | exec 준비부터 binary handler 완료까지 존재한다 | file, cred, argc/envc |
fd table | process에 남지만 CLOEXEC slot은 exec commit 때 닫힌다 | close_on_exec bitmap |
mm_struct | 새 image용 mm이 설치되고 이전 mm 참조가 해제된다 | VMA, arg_start/end, exe_file |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| execve가 ENOENT | binary 또는 PT_INTERP 없음 | readelf -l과 pathname 확인 |
| EACCES | directory search permission, noexec mount, file mode | namei -l, findmnt 옵션 |
| child가 잘못된 fd 보유 | CLOEXEC 누락 또는 open/fcntl race | /proc/PID/fd와 strace -f |
LAB
직접 확인
- 실행 후 /proc/새 shell PID/fd 목록에 exec.log fd가 없는지 확인한다.
- O_CLOEXEC를 제거해 같은 fd가 shell에 상속되는 차이를 비교한다.
- 전용 pipe를 만들고 child exec 실패 errno를 parent로 보내는 handshake를 구현한다.
./exec_replacestrace -f -e trace=execve,close ./exec_replacePRIMARY REFERENCES