Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

macOS 10.15 Catalinaでexecvが失敗する

macOS 10.15では、execv(2)する前に実行していたプロセスの実行ファイルが削除されていると、execv(2)ENOENTを返す場合があります。どういうことかというと、

  1. a.outを実行
  2. a.out実行中にa.outファイルを削除(削除できる)
  3. a.outfork(2)して親は子プロセスを待つ
  4. 子プロセスがexecv(2)a.outとは別のプログラム(例えば/usr/bin/vm_stat)になる
  5. 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追記

同僚氏の調査によると、

  • posix_spawnやvforkなら発生しない
  • システム環境設定→セキュリティとプライバシー→プライバシータブ→デベロッパツールの「ターミナル」にチェックを入れる

のどちらかであれば回避できるらしい。syspolicydという機能による影響みたい。