전체 흐름
execve()는 새 task를 만들지 않는다. 같은 current가 기존 사용자 이미지에서 시스템 호출로 들어와 인자·환경 문자열을 준비하고, 성공 시 새 사용자 이미지로 돌아간다. 준비 중인 bprm->mm은 exec_mmap() 전에 현재 task의 주소 공간으로 공개되지 않는다.
do_execveat_common() L1888–1970은 argc/envc를 세고 bprm_stack_limits()와 copy_strings()를 호출한다. 첫 대표 코드 상자는 이 두 helper의 경계를 발췌한다.
소스 코드 위치
Linux v6.6의 고정 원문
fs/exec.c가 syscall 준비, linux_binprm, binary handler 호출 및 begin_new_exec()의 경계를 보여 준다. ELF 형식의 상세 적재는 fs/binfmt_elf.c, 구조체 계약은 include/linux/binfmts.h와 함께 확인한다.
GitHub 원본:
대표 코드
bprm_stack_limits() 끝과 copy_strings() 시작
*
* In the case of argc = 0, make sure there is space for adding a
* empty string (which will bump argc to 1), to ensure confused
* userspace programs don't start processing from argv[1], thinking
* argc can never be 0, to keep them from walking envp by accident.
* See do_execveat_common().
*/
ptr_size = (max(bprm->argc, 1) + bprm->envc) * sizeof(void *);
if (limit <= ptr_size)
return -E2BIG;
limit -= ptr_size;
bprm->argmin = bprm->p - limit;
return 0;
}
/*
* 'copy_strings()' copies argument/environment strings from the old
* processes's memory to the new process's stack. The call to get_user_pages()
* ensures the destination page is created and not swapped out.
*/
static int copy_strings(int argc, struct user_arg_ptr argv,
struct linux_binprm *bprm)
{
struct page *kmapped_page = NULL;
char *kaddr = NULL;
unsigned long kpos = 0;
int ret;
while (argc-- > 0) {
const char __user *str;
int len;
unsigned long pos;
ret = -EFAULT;
str = get_user_arg_ptr(argv, argc);
if (IS_ERR(str))
goto out;
len = strnlen_user(str, MAX_ARG_STRLEN);
읽기 기준: L502–516은 bprm_stack_limits()의 끝이며 L518–541은 copy_strings()의 시작이다. do_execveat_common() 본문은 L1888에서 시작한다.
bprm_execve() 진입 (L1812)
}
/*
* sys_execve() executes a new program.
*/
static int bprm_execve(struct linux_binprm *bprm,
int fd, struct filename *filename, int flags)
{
struct file *file;
int retval;
retval = prepare_bprm_creds(bprm);
if (retval)
return retval;
/*
* Check for unsafe execution states before exec_binprm(), which
* will call back into begin_new_exec(), into bprm_creds_from_file(),
* where setuid-ness is evaluated.
*/
check_unsafe_exec(bprm);
current->in_execve = 1;
sched_mm_cid_before_execve(current);
file = do_open_execat(fd, filename, flags);
retval = PTR_ERR(file);
if (IS_ERR(file))
goto out_unmark;
sched_exec();
bprm->file = file;
/*
* Record that a name derived from an O_CLOEXEC fd will be
* inaccessible after exec. This allows the code in exec to
* choose to fail when the executable is not mmaped into the
* interpreter and an open file descriptor is not passed to
* the interpreter. This makes for a better user experience
* than having the interpreter start and then immediately fail
* when it finds the executable is inaccessible.
읽기 기준: L1807은 앞 함수의 닫는 중괄호이고 L1812부터 bprm_execve()가 시작한다. 이 앞부분만으로 no-return 이후 오류를 판단할 수 없으므로 L1856–1885와 begin_new_exec()도 읽는다.
함수별 분석
do_execveat_common()은 linux_binprm을 할당해 argc/envc와 스택 한계를 확인하고 파일명·환경·인자를 임시 스택 페이지에 복사한다. bprm_execve()는 파일·자격 증명을 준비하고 exec_binprm()으로 형식 탐색을 넘긴다.
begin_new_exec()는 binary handler 내부에서 호출된다. ELF의 load_elf_binary() L1000–1003가 그 예이며, handler가 성공 반환한 뒤 호출되는 별도 단계가 아니다. search_binary_handler() L1717–1760은 -ENOEXEC일 때만 다른 형식을 탐색하고, point_of_no_return 뒤에는 재시도하지 않는다. exec_binprm() L1764–1807은 interpreter를 바꾸면 제한된 깊이로 다시 탐색한다.
| 함수 | 실제 역할 | 실패/다음 소비자 |
|---|---|---|
do_execveat_common L1888–1970 | bprm 할당, arg/env 수와 용량 확인, 문자열 복사 | 준비 실패는 free_bprm() 뒤 errno 반환; 성공 시 bprm_execve() |
bprm_execve L1812–1886 | cred 준비, 실행 파일 열기, exec_binprm() 호출 | 경계 뒤 오류에는 필요 시 fatal signal 설정 |
exec_binprm / search_binary_handler | ELF/script 등 handler와 interpreter를 탐색 | handler가 성공 경로에서 begin_new_exec() 호출 |
구조체와 필드
준비 객체와 현재 task에 설치된 객체를 분리해서 읽는다. linux_binprm은 경계 이전의 입력·새 mm 계획을 보유하며, current->mm의 교체는 exec_mmap()에서 일어난다.
| 객체/필드 | 생산 지점 | 읽는 기준 |
|---|---|---|
current / task_struct | 기존 호출 task | 새 task를 할당하는 경로가 아니다. thread-group 조정은 de_thread()에서 일어날 수 있다. |
linux_binprm의 argc/envc/p/argmin | do_execveat_common(), bprm_stack_limits() | 문자열 수·스택 하한과 역방향 복사 위치를 결정 |
bprm->mm와 argv/envp 페이지 | alloc_bprm() 및 문자열 복사 | 새 이미지의 준비 공간. exec_mmap() 전에는 현재 task의 mm이 아님 |
bprm->point_of_no_return | begin_new_exec() L1259 | 이후 오류가 기존 userspace로 돌아갈 수 없는 경계 |
current->mm / creds | exec_mmap() / 후속 commit_creds() | 실제 새 주소 공간과 자격 증명 반영을 각 시점별로 구분 |
실행 단계와 상태 변화
begin_new_exec() L1259가 exec_mmap() L1296보다 먼저 no-return 경계를 세운다. 따라서 "새 mm 설치 뒤에야 되돌릴 수 없다"는 순서는 v6.6 코드와 반대다.
bprm_creds_from_file()은 경계 전이다. 경계 뒤 실패는 C 내부에서 음수 값을 전파할 수 있어도 기존 사용자 이미지로 errno를 돌려주지 않도록 fatal signal을 보장한다.
| 위치 | 상태 변화 | 오류의 의미 |
|---|---|---|
| 인자 준비 / 파일 열기 / L1252 | 새 bprm·mm과 creds를 준비, 기존 current->mm 유지 | 실패하면 정리 후 기존 userspace에 errno 반환 가능 |
| L1259 | point_of_no_return = true | 이후 실패는 기존 이미지로 돌아갈 수 없음 |
| L1264–1285 | de_thread(), 파일 테이블 분리, 새 mm의 실행 파일 지정 | mm 교체 전이라도 이미 fatal 경계 뒤 |
| L1296 / L1013–1027 | exec_mmap()이 새 mm을 current에 설치 | 이 단계 자체도 오류 가능; 경계는 이미 지나 있음 |
| L1382 / L1803 | 후속 자격 증명 commit / 성공 후 sched_process_exec | tracepoint는 성공 관찰 지점이지 모든 오류의 경계 표식이 아님 |
불변 조건과 실패 사례
| 검토할 조건 | v6.6 코드상 이유 | 실패 해석 |
|---|---|---|
| 새 task 생성으로 설명하지 않기 | 동일한 current가 새 사용자 이미지로 바뀐다. | PID만 보고 새 process라고 추론하면 de_thread()와 mm 교체를 혼동한다. |
| 인자 문자열과 pointer 공간 함께 계산 | bprm_stack_limits()는 argc=0에도 빈 argv[0] 자리와 pointer 배열을 고려한다. | -E2BIG, -EFAULT 등은 handler 진입 전에도 발생할 수 있다. |
| handler 재시도 경계 준수 | search_binary_handler()는 point_of_no_return 또는 retval != -ENOEXEC면 반환한다. | 경계 뒤에 다른 handler로 정상 fallback한다고 그리면 안 된다. |
| 경계 뒤 오류를 fatal로 처리 | bprm_execve() L1877–1878은 이미 fatal signal이 없을 때 SIGSEGV를 강제한다. | 오래된 사용자 이미지가 일반 errno를 받아 계속 실행하는 경로가 아니다. |
계측과 검증
각 관찰 수단은 서로 다른 시점을 보여 준다. tracefs의 전역 tracer/filter나 trace_pipe는 공유 상태를 바꾸거나 다른 관찰자의 데이터를 소비할 수 있으므로 아래 예시는 읽기 전용 기능 확인만 한다.
| 관찰 수단 | 보이는 범위 | 보이지 않는 것 |
|---|---|---|
syscalls:sys_enter_execve / execveat | 호출 진입과 인자 pointer; 해당 event가 빌드에 있을 때 | 문자열 복사·handler·mm 교체의 성공 여부 |
syscalls:sys_exit_execve | 제공·활성화된 경우 syscall 반환값 | 내부 실패 단계의 위치; no-return 이후 기존 image 복귀 여부를 단독 판정할 수 없음 |
sched:sched_process_exec | exec_binprm() L1803의 성공 후 사건 | 실패한 호출·준비 중 단계 |
function trace / do_execveat_common | 별도 격리 계측을 구성하면 함수 진입·소요 시간 | 호출 관찰만으로 경계·handler 성공을 단정할 수 없음 |
/proc/<pid>/maps | 읽는 순간 해당 task의 주소 공간 snapshot | 과거 실패와 함수 내부 진행 단계 |
# tracefs가 이미 제공된 환경에서만 조회한다.
test -r /sys/kernel/tracing/available_events && grep -E '^(sched:sched_process_exec|syscalls:sys_(enter|exit)_execve(at)?)$' /sys/kernel/tracing/available_events
test -r /sys/kernel/tracing/events/sched/sched_process_exec/format && cat /sys/kernel/tracing/events/sched/sched_process_exec/format추가 확인 사항
- 해당 보드의 v6.6 빌드에서 등록된
linux_binfmt목록과 interpreter rewrite 조건은 무엇인가? - 실패가 L1259 앞인지 뒤인지 어떤 함수 반환·fatal signal 관찰로 구분할 수 있는가?
- 같은 task가 exec에 성공한 뒤 새 mm과 credentials가 각각 언제 보이는지 어떻게 검증할 것인가?
- tracefs 이벤트가 실제 빌드에 있는지 읽기 전용으로 확인한 뒤, 실험용 격리 trace instance가 필요한가?