Иногда отсутствует PTRACE_EVENT_VFORK при запуске ptrace

Извините, что я не могу опубликовать код, чтобы воспроизвести это. Моя проблема в том, что я не знаю, как отладить эту проблему.

Я использую ptrace с PTRACE_O_TRACEFORK | PTRACE_O_TRACEEXEC | PTRACE_O_TRACEVFORK | PTRACE_O_TRACEVFORKDONE | PTRACE_O_TRACECLONE для отслеживания процесса и его детей (и дочерних детей). Механизм очень похож на strace, но имеет несколько иные цели, поскольку я просто отслеживаю файлы, которые читаются или изменяются.

Мой код (написанный на C) отлично работает на Debian wheezy и Debian jessie на архитектуре x86-64 (а также менее проверен на i386). Когда я пытаюсь скомпилировать и запустить на виртуальной машине Ubuntu Precise x86-64 (которая использует ядро ​​3.2.0), я столкнулся с трудностями.

На машине Precise я иногда обнаруживаю, что я не получаю PTRACE_EVENT_VFORK сразу после вызова vfork, но вместо этого начинаю принимать события (пару событий SIGSTOP и несколько системных вызовов) без когда-либо получая событие PTRACE_EVENT_VFORK. Я не вижу ничего подозрительного в выполняемых системных вызовах, и поведение не предсказуемо.

Я не уверен, что попытаться уменьшить это до минимального случая ошибки, и я действительно не имею ни малейшего представления о том, что может пойти не так, никогда прежде не наблюдал такого поведения отсутствующих событий. Вполне возможно, что различие не является ядром, а скорее инструментами построения, которые я отслеживаю (что представляет собой комбинацию python + gcc).

Любые предложения?

Ответ 1

Недавно я работал над чем-то похожим. Я подозреваю, что вы давно решили свою проблему или сдались, но напишите здесь ответ для потомков.

Различные события, которые вы регистрируете с помощью PTRACE_SETOPTIONS, генерируют сообщения, отличные от обычных событий ptrace. Но нормальные события все еще сгенерированы. Одним из обычных событий является то, что новый разветвленный процесс начинает останавливаться и должен быть продолжен из трассировщика.

Это означает, что если вы зарегистрировали события, которые вы смотрите с помощью PTRACE_O_TRACEFORK (или VFORK) waitpid, вы будете запускать дважды для одного и того же процесса после вилки.

Один будет со статусом, который:

WIFSTOPPED(status) && (WSTOPSIG(status) & 0xff == SIGSTOP)

Другой будет:

WIFSTOPPED(status) && (WSTOPSIG(status) & 0xff == 0) &&
    ((status >> 16) == PTRACE_EVENT_FORK) /* or VFORK */

Кажется, нет гарантии от ядра, в каком порядке они будут поступать. Я обнаружил, что он близок к 50/50 в моей системе.

Чтобы справиться с этим, мой код выглядит примерно так:

static void
proc_register(struct magic *pwi, pid_t pid, bool fork) {
    /*
     * When a new process starts two things happen:
     *  - We get a wait with STOPPED, SIGTRAP, PTRACE_EVENT_{CLONE,FORK,VFORK}
     *  - We get a wait with STOPPED, SIGSTOP
     *
     * Those can come in any order, so to get the proc in the right
     * state this function should be called twice on every new proc. If
     * it called with fork first, we set the state to NEW_FORKED, if
     * it called with STOP first, we set NEW_STOPPED. Then when the
     * other call comes, we set the state to TRACED and continue the
     * process.
     */
    if ((p = find_proc(pwi, pid)) == NULL) {
            p = calloc(1, sizeof(*p));
            p->pid = pid;
            TAILQ_INSERT_TAIL(&pwi->procs, p, list);
            if (fork) {
                    p->state = NEW_FORKED;
            } else {
                    p->state = NEW_STOPPED;
            }
    } else {
            assert((fork && p->state == NEW_STOPPED) || (!fork && p->state == NEW_FORKED));
            p->state = TRACED;
            int flags = PTRACE_O_TRACEEXEC|PTRACE_O_TRACEEXIT|PTRACE_O_TRACEFORK|PTRACE_O_TRACEVFORK;

            if (ptrace(PTRACE_SETOPTIONS, pid, NULL, flags))
                    err(1, "ptrace(SETOPTIONS, %d)", pid);
            if (ptrace(PTRACE_CONT, pid, NULL, signal) == -1)
                    err(1, "ptrace(CONT, %d, %d)", pid, signal);
    }
}
[...]
    pid = waitpid(-1, &status, __WALL);
    if (WIFSTOPPED(status) && (WSTOPSIG(status) & 0xff == SIGSTOP)) {
            proc_register(magic, pid, false);
    } else if (WIFSTOPPED(status) && (WSTOPSIG(status) & 0xff == 0) && ((status >> 16) == PTRACE_EVENT_FORK)) {
            proc_register(magic, pid, true);
    } else {
            /* ... */
    }

Ключом к выполнению этой работы было не отправлять PTRACE_CONT, пока мы не получим оба события. Когда я выяснял, как это работает, я слишком много отправлял PTRACE_CONT, и ядро ​​с радостью принимало их, что иногда приводило к тому, что мои процессы выходили задолго до того, как PTRACE_EVENT_FORK прибыл. Это затруднило отладку.

N.B. Я не нашел никакой документации об этом или что-либо, говоря, что так оно и должно быть. Я только узнал, что это заставляет работать так, как сегодня. YMMV.

Ответ 2

Я сталкивался с этой страницей несколько раз (по разным причинам). Если трассировщик отслеживает множество трассировок, и там много событий (например, когда для SECCOMP установлено значение RET_TRACE). waitpid(-1,...) возможно, не лучшая вещь, чтобы ждать каких-либо трассировок, потому что может быть много трассировок, изменяющих состояния, особенно в системах SMP (кто еще все еще использует систему UP), что означает, что может тонны событий, прибывающих за очень короткий промежуток времени, и OP был прав, события могут быть не в порядке: какое-то событие или сигнал может произойти даже до PTRACE_EVENT_FORK.

Тем не менее, это не тот случай (никаких событий не в порядке), когда трассировщик вызывает waitpid(specific_pid_greater_than_zero,...): мы tracee ТОЛЬКО определенную tracee. Конечно, модель вашей программы может выглядеть не так элегантно/просто, вам даже может понадобиться отслеживать состояния трассировки (блокировка или нет), и она решает, когда/какую трассировку продолжить (PTRACE_CONT), но с бонусом не беспокоиться о хакерских способах обрабатывать неупорядоченные события (также вряд ли, чтобы понять это правильно).