macOS 10.15では、execv(2)する前に実行していたプロセスの実行ファイルが削除されていると、execv(2)がENOENTを返す場合があります。どういうことかというと、
- a.outを実行
- a.out実行中にa.outファイルを削除(削除できる)
- a.outがfork(2)して親は子プロセスを待つ
- 子プロセスがexecv(2)でa.outとは別のプログラム(例えば/usr/bin/vm_stat)になる
- ENOENTエラーが返る
この動作は、発生する環境では常に発生しますが、しない場合は全く発生しないようです。現在確認できた環境だと、以下のような結果になりました。再現する場合としない場合で何が異なるのか分かっていませんが、Symantecなどのツールが入っていなくても再現するようでした。
- macOS 10.15.1 (19B2106): 0/1の環境で再現しない
- macOS 10.15.1 (19B88): 2/3の環境で再現する
- macOS 10.14.x: 今のところ一度も再現しない
これを試してみたい場合、以下のプログラムを保存して、
cc program.c ./a.out /usr/bin/vm_stat
で実行してみてください。実行するとファイルが消えるので、もう一度行いたい場合は再コンパイルが必要です。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <stdarg.h> char *argv0; static void fatal(char *fmt, ...) { va_list arg; va_start(arg, fmt); fprintf(stderr, "%s: ", argv0); vfprintf(stderr, fmt, arg); if(fmt[strlen(fmt)-1] == ':') fprintf(stderr, " %s\n", strerror(errno)); va_end(arg); exit(1); } static void run(char **args, int n) { int i, pid, status; for(i = 0; i < n; i++){ switch(pid = fork()){ case -1: fatal("child fork:"); break; case 0: if(execv(args[0], args) < 0) fatal("child exec:"); break; default: if(waitpid(pid, &status, 0) < 0) fatal("child waitpid %d:", pid); fprintf(stderr, "child %s: exit %d\n", args[0], status); break; } sleep(1); } } static void usage(void) { fprintf(stderr, "usage: %s cmd [args...]\n", argv0); exit(2); } int main(int argc, char **argv) { int pid, status; argv0 = argv[0]; if(argc <= 1) usage(); if(unlink(argv0) < 0) fatal("unlink %s:", argv); switch(pid = fork()){ case -1: fatal("fork failed:"); break; case 0: run(argv+1, 100); break; default: if(waitpid(pid, &status, 0) < 0) fatal("waitpid %d:", pid); break; } return 0; }
問題がない場合、vm_statの結果が100回出力されますが、エラーになる環境だと以下のエラーログが100回出力されます。
./a.out: child exec: No such file or directory child vm_stat: exit 256
再現する場合としない場合で、macOSの仕様変更なのか何かのバグなのか分からないので、一応フィードバックアシスタントでAppleにフィードバックを送っておきました。
2020-12-10追記
同僚氏の調査によると、
のどちらかであれば回避できるらしい。syspolicydという機能による影響みたい。