# Переиспользование PID искажает атрибуцию сессий агента

Одного PID достаточно для экрана состояния. Но его недостаточно как идентичности, на которой основаны согласование, запись аудита или сессия агента с доступом к учетным данным. Операционная система переиспользует идентификаторы процессов. Если система агента принимает повторно выданный номер за того же исполнителя, она может связать более поздний процесс с согласованием, выданным более раннему.

Для такой ошибки не нужен атакующий с доступом к ядру или необычная гонка. Она возникает, когда короткоживущий агент завершается, на загруженной машине запускается достаточно процессов для повторного использования его PID, а какой-то компонент повторяет вызов, обновляет экран или находит старую строку аудита по PID. Более поздний процесс может быть обычной программой. Атрибуция все равно будет неверной, а система согласований, которая действует на основании ошибочной атрибуции, уже теряет смысл.

Исправление несложное, но требует изменить терминологию. Перестаньте называть PID идентичностью процесса. Сохраняйте зафиксированную запись о процессе: PID, время запуска, сведения о подписи кода и происхождение родителя, собранные в момент принятия решения об авторизации. После этого при каждом сравнении доказывайте, что живой процесс по-прежнему соответствует этой записи.

## PID обозначает ячейку, а не жизненный цикл процесса

Идентификатор процесса указывает на текущую запись процесса, выделенную ядром. Он не навсегда обозначает одну программу и не гарантирует глобальную уникальность во времени. После завершения процесса его PID можно выдать снова.

Это различие кажется мелким, пока система не начинает переносить прежнее согласование. Представим, что клиент агента запускается с PID 4812, просит использовать учетные данные для развертывания, и пользователь подтверждает этот запуск. Клиент завершается. Позже другой процесс получает PID 4812. Если шлюз спрашивает: «Есть ли у PID 4812 одобренная сессия?», он может найти старое согласование и связать его с новым процессом.

Опасность в том, что старый и новый процессы не обязаны работать одновременно. Ошибка часто появляется в соединении базы данных, обращении к кэшу или отложенной задаче обогащения данных. Система сохраняет согласование как `pid = 4812`, вызовы записывает так же, а затем доверяет запросу, который должен означать то же, что и раньше. Но это уже не так.

PID может быть полезен в записи для человека: по нему оператору проще изучить живую систему. Сохраняйте его. Просто не делайте его основной идентичностью для авторизации или атрибуции.

Есть и вторая сложность: `fork` и `exec` не позволяют объяснить все одной простой схемой. Fork создает новый дочерний процесс с другим PID. Exec заменяет образ программы в существующем процессе, сохраняя его PID. Если вы подтверждаете запуск оболочки, а она выполняет другую программу через exec, PID остается знакомым, но код, который отправит следующий запрос, может быть уже другим. При проектировании идентичности нужно учитывать и повторное использование PID после завершения, и замену программы в еще работающем процессе.

## Фиксируйте запись о процессе при начале сессии

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

Для шлюза агентов macOS я бы хранил такие поля как один неизменяемый снимок процесса:

- PID и эффективный идентификатор пользователя.
- Время запуска процесса с секундами и микросекундами, если платформа их предоставляет.
- Путь к исполняемому файлу на момент фиксации.
- Идентификатор подписи, сведения о команде или подписанте и хеш каталога кода, если они доступны.
- Снимок родительского процесса, включая его PID и время запуска.

Время запуска превращает PID в ссылку на конкретный жизненный цикл. Практический кортеж выглядит так:

```text
process_instance = (
  pid = 4812,
  start_time = 2026-07-22T14:03:18.482911Z,
  euid = 501
)
```

Этот кортеж отвечает на узкий вопрос: «Это тот же жизненный цикл процесса ядра, который я видел раньше?» Но он не говорит, заслуживает ли процесс доверия. Запись о подписи отвечает на другой вопрос: «Какой код macOS проверила при изучении этого процесса?» Снимок родителя отвечает еще на один: «Что создало этот процесс, согласно наблюдению в начале сессии?»

Не сводите эти вопросы к одной текстовой метке вроде `Claude Code (PID 4812)`. Для карточки согласования такая метка может подойти, но она скрывает сведения, которые понадобятся проверяющему, когда он спросит, почему конкретную сессию разрешили.

В macOS инспектор процесса может использовать `proc_pidinfo` с `PROC_PIDTBSDINFO`, чтобы получить `proc_bsdinfo`, включая `pbi_start_tvsec` и `pbi_start_tvusec`. Apple также предоставляет `start_time` процесса в модели процессов Endpoint Security. Важно не то, какой API вы выберете. Важно получить время запуска на границе принятия решения и сравнить его позже.

Небольшой помощник на C для части, связанной с жизненным циклом, может выглядеть так:

```c
#include <libproc.h>
#include <sys/proc_info.h>
#include <cstdio.h>

int read_process_lifetime(pid_t pid) {
    struct proc_bsdinfo info = {0};
    int size = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0,
                            &info, sizeof(info));
    if (size != sizeof(info)) {
        return -1;
    }

    printf("pid=%d start=%lld.%06d parent=%d uid=%d\n",
           info.pbi_pid,
           info.pbi_start_tvsec,
           info.pbi_start_tvusec,
           info.pbi_ppid,
           info.pbi_uid);
    return 0;
}
```

Форма вывода важнее языковой привязки:

```text
pid=4812 start=1784738598.482911 parent=4760 uid=501
```

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

## Подпись кода описывает код, а не один запущенный экземпляр

Сведения о подписи кода дают полезную информацию о происхождении, но сами по себе не превращаются в идентичность процесса. Идентификатор подписи может быть общим для всех выпущенных версий приложения. Идентификатор команды обозначает организацию, которая подписала код, а не конкретный исполняемый файл. Хеш каталога кода точнее описывает подписанное содержимое, но многие одновременно работающие экземпляры одного исполняемого файла все равно будут иметь один и тот же хеш.

В документации Apple по подписи кода есть важное различие, которое продукты безопасности часто стирают. Идентификатор подписи могут заявлять несколько подписантов, поэтому Apple рекомендует проверять категорию валидации, а для кода не от Apple - еще и идентификатор команды. В технической заметке Apple TN3127 также объясняется, что designated requirement объединяет идентификатор с требованиями подписи и позволяет устанавливать идентичность кода после обновлений.

Именно так и стоит на это смотреть: сведения о подписи показывают, какой исполняемый файл проверила macOS и соответствует ли он вашему требованию доверия. Они не доказывают, что это тот же процесс, который пользователь подтвердил пять минут назад.

Для авторизации сессии используйте оба уровня:

```text
same_process_lifetime:
  pid, start_time, euid all match the captured record

same_expected_code:
  captured signing requirement still validates for the live process

same_session:
  the session token refers to this captured process record, not only its PID
```

Не сравнивайте только отображаемое имя, идентификатор пакета или путь. Пути меняются. У неподписанных инструментов командной строки могут быть слабые или локальные признаки идентичности. Пользователь также может запустить копию знакомого инструмента из другого места. В карточке согласования можно показать понятную метку, но код авторизации должен сохранять проверенные сведения о подписи, на которых основана эта метка.

Распространенный плохой совет - авторизовать только по идентификатору подписи, потому что он сохраняется после обновлений приложения. Он популярен: непрерывность после обновления кажется удобной. Но для согласования отдельного запуска это неправильно. Если человек подтвердил один работающий процесс агента, последующий запуск той же подписанной программы - новый запуск и должен получить новое решение о сессии. Стабильная идентичность кода помогает сделать карточку согласования понятной. Она не дает права незаметно продлевать согласование одного запуска на все будущие экземпляры.

## Родительский PID служит подтверждением только при сохранении его жизненного цикла

Сведения о родительском процессе помогают проверяющим понять, как запустился агент. Они могут отличить клиента, запущенного из терминала, от процесса, созданного редактором, планировщиком или другим агентом. Но `ppid = 4760` подвержен той же проблеме повторного использования, что и PID дочернего процесса.

Ошибочный вариант легко заметить:

```text
session.agent_pid = 4812
session.parent_pid = 4760
```

Через три часа средство просмотра аудита находит процесс с номером 4760 и называет его родителем сессии. Исходный родитель мог завершиться задолго до этого. Теперь номер 4760 принадлежит другому процессу. Страница аудита превратила историческое утверждение в живой поиск и незаметно переписала историю.

Сохраняйте снимок родителя вместе со снимком дочернего процесса:

```text
parent_instance = (
  pid = 4760,
  start_time = 2026-07-22T14:01:02.117604Z,
  euid = 501,
  executable_path = "/usr/bin/login",
  signing_requirement = "captured evaluation",
  relationship = "observed_parent_at_session_open"
)
```

Последнее поле кажется несущественным, но оно предотвращает много неверных формулировок. Запись о родителе означает: «Это был непосредственный родитель в момент, когда система наблюдала дочерний процесс». Она не означает: «Этот родитель авторизовал дочерний процесс», «Этот родитель навсегда владеет дочерним процессом» или «Этот родитель остается текущим родителем». Для таких утверждений нужны отдельные подтверждения.

Если у вас есть события процессов Endpoint Security, структура `es_process_t` предоставляет исходный PID родителя, а также `parent_audit_token` и `responsible_audit_token`, время запуска и сведения о подписи. Когда API предоставляет audit token, он предпочтительнее обычного PID, потому что сохраняет больше контекста. Но объект процесса из события все равно нужно считать зафиксированным наблюдением. Не заменяйте его последующим поиском по PID и не выдавайте это за эквивалент.

В системах без Endpoint Security используйте доступные API процессов, получайте сведения как можно быстрее и явно показывайте неопределенность. Родитель может завершиться между чтением данных дочернего процесса и чтением данных родителя. Не изображайте уверенность там, где произошла гонка. Пометьте родителя как недоступного или неполного, сохраните идентичность дочернего процесса, которую удалось получить, и не заявляйте о связи, которую вы не смогли наблюдать.

## Повторно проверяйте процесс перед действием в одобренной сессии

При согласовании сессии нужны два момента работы с идентичностью: фиксация при открытии сессии и повторная проверка при использовании полномочий. Первая защищает аудиторский след. Вторая защищает следующее действие.

Представим такую последовательность:

1. Процесс агента открывает сессию, и вы фиксируете PID 4812, время запуска, сведения о подписанте и родителе.
2. Человек подтверждает именно этот запуск агента.
3. Процесс завершается, пока токен сессии остается в памяти или локальном IPC-соединении.
4. Более поздний процесс получает PID 4812 и предъявляет устаревший токен или находит старую связь из-за ошибки.
5. Перед выполнением вызова с учетными данными шлюз проверяет живой процесс.

При последней проверке PID может совпасть. Время запуска - нет. Это несовпадение должно закрыть сессию. Не исправляйте запись новым временем, не создавайте незаметно заменяющую сессию и не приписывайте действие прежнему процессу.

Проверка также должна завершаться отказом, если получить сведения не удалось. Если код не может прочитать информацию о процессе, потому что процесс завершился, изменились права или операционная система вернула неполные данные, нельзя установить, что запрос поступил от подтвержденного процесса. Правильный ответ - потребовать новую сессию.

Сравнение должно быть узким и буквальным. Не используйте неточные признаки вроде «та же команда», «тот же рабочий каталог» или «то же окно терминала». Они помогают человеку понять контекст, но не выдерживают случайной или намеренной неоднозначности. Процесс может сменить рабочий каталог, а два независимых процесса могут выбрать одно имя команды.

Кэшировать нужно решение об авторизации, связанное с зафиксированным экземпляром процесса. Неправильное место для кэша - таблица соответствий PID и состояния согласования. Такая таблица пройдет тесты на спокойном ноутбуке и сломается при частой смене процессов, поэтому обнаружить проблему после выпуска особенно трудно.

## Моделируйте сессии как неизменяемые события, а не изменяемые строки процессов

Журнал сессии должен сохранять то, что шлюз наблюдал в каждый момент. Изменяемая строка, которая всегда говорит «PID 4812 активен», не может ответить, какие поля пришли от исходного агента, от заменившего его процесса или из позднего фонового обновления.

Используйте события только для добавления с явными ссылками. Структура может быть простой:

```json
{
  "event_type": "session_authorized",
  "session_id": "sess_7d9f",
  "process_instance": {
    "pid": 4812,
    "start_time": "2026-07-22T14:03:18.482911Z",
    "euid": 501,
    "signing_id": "com.example.agent",
    "team_id": "A1B2C3D4E5",
    "cdhash": "captured-code-directory-hash"
  },
  "parent_instance": {
    "pid": 4760,
    "start_time": "2026-07-22T14:01:02.117604Z"
  },
  "approval": {
    "scope": "this process run",
    "decision": "approved"
  }
}
```

Следующее событие вызова ссылается на `sess_7d9f` и записывает собственное время, запрошенный канал, цель действия и результат. Оно не копирует только `pid: 4812` в надежде, что аналитик восстановит остальное. Если повторная проверка не пройдена, запишите отдельное событие отказа с наблюдаемым несовпадением.

```json
{
  "event_type": "action_denied",
  "session_id": "sess_7d9f",
  "reason": "process_start_time_mismatch",
  "captured_pid": 4812,
  "captured_start_time": "2026-07-22T14:03:18.482911Z",
  "observed_start_time": "2026-07-22T15:47:09.031882Z"
}
```

Так проверяющий получает конкретные сведения: номер процесса был переиспользован, шлюз заметил несовпадение и отклонил запрос. Без обоих временных значений событие лишь говорит, что что-то не получилось. Этого недостаточно, когда нужно отличить программную ошибку от попытки получить чужие полномочия.

Разделение Sallyport на журнал Sessions и журнал Activity здесь полезно: авторизация сессии и отдельные действия отвечают на разные вопросы аудита. Оба журнала должны сохранять снимок процесса, существовавший в момент решения о сессии, а не полагаться на один PID при последующем отображении или просмотре.

## Карточка согласования должна показывать происхождение, не создавая ложной уверенности

Пользователь не может принять хорошее решение по диалогу, в котором написано только «Агент хочет получить доступ». Ему нужен достаточный контекст, чтобы узнать вызывающую сторону, но не стоит перегружать карточку точными на вид полями, которые почти ничего не говорят.

На первом месте укажите проверенного владельца подписи кода. Добавьте имя или путь к исполняемому файлу, если это помогает. Показывайте связь с родителем простыми словами, например «запущен подписанным терминальным приложением» или «запущен помощником редактора», только если у вас есть подтверждающие данные. PID показывайте как сведения для диагностики, а не как заявленную идентичность.

Не представляйте цепочку родителей как цепочку сертификатов. Родство процессов - это наблюдение операционной системы в определенный момент. Подписанный родитель может запустить неподписанного ребенка. Ожидаемый родитель может быть оболочкой, которая затем выполнит другую программу через exec. Карточка должна ясно показывать, кто запрашивает сессию сейчас, а сведения о родителе давать лишь как контекст.

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

Одно согласование должно означать один наблюдаемый запуск. Если пользователям нужно более широкое решение о доверии, создайте для этого отдельную явную функцию с понятной областью действия, например требованием к подписанному коду и указанным сроком. Не добавляйте такую область незаметно в согласование сессии только потому, что в реализации оказался удобный кэш PID.

## Проверяйте ошибку, а не обычное поведение процессов

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

Не нужно ждать, пока macOS естественным образом повторно выдаст конкретный PID. Спрячьте слой инспекции процессов за интерфейсом и передавайте ему управляемые снимки. Хороший тест содержит одобренную запись и более позднее наблюдение с тем же PID, но другим временем запуска:

```text
captured: pid=4812 start=1784738598.482911 signer=team-A
observed: pid=4812 start=1784744029.031882 signer=team-B
expected: deny with process_start_time_mismatch
```

Добавьте второй сценарий: PID и время запуска совпадают, но требование к подписи не проходит. Добавьте еще один, в котором дочерний процесс остается жив, но exec меняет сведения о его исполняемом файле. Такие тесты показывают, что код авторизации соединяет правильные поля, а не просто проверяют, что инспекция процесса возвращает данные.

Отдельно протестируйте представление аудита. Создайте старую сессию с родительским PID, смоделируйте завершение родителя, а затем предоставьте другой процесс с тем же PID. Историческая сессия в интерфейсе должна сохранить исходные поля родителя. Она не должна заменять их именем из текущей таблицы процессов.

Наконец, проверьте отмену и ошибки инспекции. Процесс может исчезнуть за короткий промежуток между приемом IPC-запроса и чтением его данных. Код должен записать понятную причину отказа и потребовать от вызывающей стороны создать новую сессию. Системы, которые превращают такие сбои в приблизительные совпадения, создают именно ту неоднозначность, которую должна была устранить модель идентичности.

## Полезный инвариант прост и строг

Каждое привилегированное действие агента должно ссылаться на сессию, связанную с одним зафиксированным жизненным циклом процесса. Живой запрос должен доказать, что пришел из того же жизненного цикла, а его исполняемый файл по-прежнему соответствует одобренной идентичности кода. Переиспользованный PID не проходит первую проверку. Другой бинарный файл за неизменившимся PID не проходит вторую.

Этот инвариант возвращает сведения о процессе на свои места. Время запуска различает жизненные циклы. Данные о подписанте идентифицируют проверенный код. Сведения о родителе показывают происхождение. Идентификатор сессии хранит решение человека. Ни одно из этих полей не может заменить остальные.

Sallyport может сделать это решение видимым, показывая при авторизации сессии владельца подписи кода процесса и сохраняя отдельные записи о запусках и вызовах. Но реализация все равно должна запрещать удобному номеру, каким бы знакомым он ни выглядел в журнале, подменять процесс, который получил согласование.

В следующий раз, когда увидите таблицу с ключом `pid`, спросите, что произойдет после завершения процесса. Если ответ звучит как «мы снова найдем его», это не хранилище идентичности сессий. Исправьте это до того, как обычное событие жизненного цикла процесса превратится в ошибку авторизации.
