Переиспользование PID искажает атрибуцию сессий агента
Переиспользование PID может связать действия агента не с тем процессом. Узнайте, как безопаснее строить сессии агентов macOS с учетом времени запуска, проверки подписи, снимка родителя и строгой повторной валидации.

Одного 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 в ссылку на конкретный жизненный цикл. Практический кортеж выглядит так:
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 для части, связанной с жизненным циклом, может выглядеть так:
#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;
}
Форма вывода важнее языковой привязки:
pid=4812 start=1784738598.482911 parent=4760 uid=501
Если при следующем чтении тот же PID возвращается с другим временем запуска, перед вами другой процесс. Отклоните совпадение сессии, даже если всем удобным слоям системы хочется считать этот номер знакомым.
Подпись кода описывает код, а не один запущенный экземпляр
Сведения о подписи кода дают полезную информацию о происхождении, но сами по себе не превращаются в идентичность процесса. Идентификатор подписи может быть общим для всех выпущенных версий приложения. Идентификатор команды обозначает организацию, которая подписала код, а не конкретный исполняемый файл. Хеш каталога кода точнее описывает подписанное содержимое, но многие одновременно работающие экземпляры одного исполняемого файла все равно будут иметь один и тот же хеш.
В документации Apple по подписи кода есть важное различие, которое продукты безопасности часто стирают. Идентификатор подписи могут заявлять несколько подписантов, поэтому Apple рекомендует проверять категорию валидации, а для кода не от Apple - еще и идентификатор команды. В технической заметке Apple TN3127 также объясняется, что designated requirement объединяет идентификатор с требованиями подписи и позволяет устанавливать идентичность кода после обновлений.
Именно так и стоит на это смотреть: сведения о подписи показывают, какой исполняемый файл проверила macOS и соответствует ли он вашему требованию доверия. Они не доказывают, что это тот же процесс, который пользователь подтвердил пять минут назад.
Для авторизации сессии используйте оба уровня:
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 дочернего процесса.
Ошибочный вариант легко заметить:
session.agent_pid = 4812
session.parent_pid = 4760
Через три часа средство просмотра аудита находит процесс с номером 4760 и называет его родителем сессии. Исходный родитель мог завершиться задолго до этого. Теперь номер 4760 принадлежит другому процессу. Страница аудита превратила историческое утверждение в живой поиск и незаметно переписала историю.
Сохраняйте снимок родителя вместе со снимком дочернего процесса:
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 процессов, получайте сведения как можно быстрее и явно показывайте неопределенность. Родитель может завершиться между чтением данных дочернего процесса и чтением данных родителя. Не изображайте уверенность там, где произошла гонка. Пометьте родителя как недоступного или неполного, сохраните идентичность дочернего процесса, которую удалось получить, и не заявляйте о связи, которую вы не смогли наблюдать.
Повторно проверяйте процесс перед действием в одобренной сессии
При согласовании сессии нужны два момента работы с идентичностью: фиксация при открытии сессии и повторная проверка при использовании полномочий. Первая защищает аудиторский след. Вторая защищает следующее действие.
Представим такую последовательность:
- Процесс агента открывает сессию, и вы фиксируете PID 4812, время запуска, сведения о подписанте и родителе.
- Человек подтверждает именно этот запуск агента.
- Процесс завершается, пока токен сессии остается в памяти или локальном IPC-соединении.
- Более поздний процесс получает PID 4812 и предъявляет устаревший токен или находит старую связь из-за ошибки.
- Перед выполнением вызова с учетными данными шлюз проверяет живой процесс.
При последней проверке PID может совпасть. Время запуска - нет. Это несовпадение должно закрыть сессию. Не исправляйте запись новым временем, не создавайте незаметно заменяющую сессию и не приписывайте действие прежнему процессу.
Проверка также должна завершаться отказом, если получить сведения не удалось. Если код не может прочитать информацию о процессе, потому что процесс завершился, изменились права или операционная система вернула неполные данные, нельзя установить, что запрос поступил от подтвержденного процесса. Правильный ответ - потребовать новую сессию.
Сравнение должно быть узким и буквальным. Не используйте неточные признаки вроде «та же команда», «тот же рабочий каталог» или «то же окно терминала». Они помогают человеку понять контекст, но не выдерживают случайной или намеренной неоднозначности. Процесс может сменить рабочий каталог, а два независимых процесса могут выбрать одно имя команды.
Кэшировать нужно решение об авторизации, связанное с зафиксированным экземпляром процесса. Неправильное место для кэша - таблица соответствий PID и состояния согласования. Такая таблица пройдет тесты на спокойном ноутбуке и сломается при частой смене процессов, поэтому обнаружить проблему после выпуска особенно трудно.
Моделируйте сессии как неизменяемые события, а не изменяемые строки процессов
Журнал сессии должен сохранять то, что шлюз наблюдал в каждый момент. Изменяемая строка, которая всегда говорит «PID 4812 активен», не может ответить, какие поля пришли от исходного агента, от заменившего его процесса или из позднего фонового обновления.
Используйте события только для добавления с явными ссылками. Структура может быть простой:
{
"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 в надежде, что аналитик восстановит остальное. Если повторная проверка не пройдена, запишите отдельное событие отказа с наблюдаемым несовпадением.
{
"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, но другим временем запуска:
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, спросите, что произойдет после завершения процесса. Если ответ звучит как «мы снова найдем его», это не хранилище идентичности сессий. Исправьте это до того, как обычное событие жизненного цикла процесса превратится в ошибку авторизации.
Вопросы и ответы
Почему идентификатор процесса не является уникальной идентичностью процесса?
PID - это ячейка в таблице процессов операционной системы, а не постоянный идентификатор. После завершения одного процесса macOS может назначить тот же номер другому. Запись вида «PID 8421» после достаточного количества запусков может указывать уже не на тот процесс.
Что нужно хранить вместе с PID, чтобы безопасно идентифицировать процесс агента?
Как минимум храните PID вместе со временем запуска процесса. Для системы управления агентами также записывайте идентификатор пользователя, путь к исполняемому файлу на момент подключения, сведения о подписи и снимок родительского процесса. Каждое поле помогает обнаружить свой способ, которым обычный PID может ввести в заблуждение.
Достаточно ли PID и времени запуска для авторизации агента?
Нет. Время запуска различает два жизненных цикла, которым случайно достался один PID, но не показывает, заслуживает ли исполняемый файл доверия. Дополните его проверенными сведениями о подписи, а родительский процесс сохраняйте как дополнительное подтверждение происхождения.
Можно ли доверять одному идентификатору подписи macOS?
Идентификатор подписи кода обозначает код в определенной области подписи, но сам по себе не доказывает, что процесс тот, который вы хотели подтвердить. Надежная проверка учитывает подписанта или команду и проверяет требование к коду. Для криминалистических записей сохраняйте также хеш каталога кода, если платформа его предоставляет.
Как записывать родительский процесс, не допуская ошибок из-за переиспользования PID?
Снимайте сведения о родителе в момент подключения дочернего процесса или при наблюдении события exec. Не ищите родительский PID позже, считая результат исторической истиной. Родительские PID могут переиспользоваться так же, как PID дочерних процессов.
Как не допустить, чтобы переиспользованный PID подтвердил не того агента?
Рассматривайте идентичность процесса как входные данные для авторизации, а не как подпись для отображения. Непосредственно перед выдачей сессии снова проверьте живой процесс, сравните его время запуска и сведения о подписи с сохраненным снимком и отклоните запрос, если процесс завершился или изменился.
Решает ли Endpoint Security задачу атрибуции процессов в macOS?
Endpoint Security может помочь, но не заменяет запись об идентичности с учетом жизненного цикла процесса. Endpoint Security предоставляет время запуска, данные аудита, сведения о подписи кода и данные о родительском аудите в структурах процесса. Для него также нужны соответствующие права и операционная поддержка, которые требуются не каждому настольному приложению.
Чем cdhash отличается от идентичности процесса?
Нет. Хеш каталога кода идентифицирует содержимое подписанного кода, а идентичность процесса относится к одному запущенному экземпляру этого кода. Десять одновременно работающих процессов могут иметь один и тот же хеш, а один PID со временем может относиться к разным процессам.
Как журналу аудита моделировать сессии агентов и завершение процессов?
Используйте неизменяемые события, а не одну изменяемую строку сессии, которую постоянно перезаписывают. Сохраняйте зафиксированную идентичность при согласовании сессии, добавляйте записи о вызовах со ссылкой на эту идентичность и записывайте отзыв или завершение отдельными последующими событиями. Так проверяющий увидит последовательность событий, а не догадку по текущему состоянию.
Может ли exec изменить подтвержденный процесс агента, не меняя его PID?
Fork может сохранить PID, а exec заменить образ программы в этом процессе. Поэтому PID и даже связь с родителем могут остаться прежними, пока исполняемый файл и подписант изменились. Проверяйте идентичность в момент подключения агента и еще раз перед использованием долгоживущей авторизации.