Я пишу программу, требующую прав администратора (программа обновления). В какой-то момент нам нужно запустить другую программу и позволить первой завершиться. Таким образом, родительский процесс создает ответвление, отделяется от своего дочернего процесса и завершает выполнение. Под пользователем root (например, с использованием «su») все работает как положено. Под обычным пользователем ничего особенного не происходит (кроме конечно записи файлов от имени root). Но при использовании sudo, как только родительский процесс завершает свое выполнение, его дочерний процесс также умирает.
Я написал минимальную программу для устранения этой проблемы:
#include <iostream>
#include <fstream>
#include <unistd.h>
int main()
{
std::cout << "start: " << getuid() << std::endl;
if (auto pid = fork() == 0)
{
setpgid(0, 0);
std::cout << "fork has detached" << std::endl;
sleep(1);
std::ofstream test("/root/test.txt");
std::cout << "fork: " << getuid() << " " << test.is_open() << std::endl;
}
else
{
setpgid(pid, 0);
std::cout << "parent: " << getuid() << std::endl;
}
return 0;
}
Я ожидаю в качестве вывода:
$ sudo ./test_sudo
start: 0
parent: 0
fork has detached
$ fork: 0 1
Примечание: последнюю строку следует писать после подсказки.
Но с sudo я получаю только:
$ sudo ./test_sudo
start: 0
parent: 0
fork has detached
$
Если я добавлю вызов wait() в родительский элемент, дочерний элемент будет работать с sudo как и ожидалось. Итак, ребенок умирает вместе со своим родителем. Инструменты отладки, которые я пробовал, показывали только следы родительского элемента, как я могу отслеживать и отсоединенный дочерний элемент?
Почему использование sudo заставляет мою программу вести себя таким образом? Я могу обойти эту проблему в своем проекте, но меня интересует объяснение этого странного поведения.
strace --follow-forks
детям тоже покажет следы. Я предполагаю, что это не один из инструментов отладки, которые вы использовали?
Кстати, возможно, это не причина, но вы, вероятно, имели в виду if (auto pid = fork(); pid == 0)
, а не создание pid
bool.
Похоже, это поведение, специфичное для окружающей среды. Я тестировал Mac OS 12.7.4 zsh 5.8.1 (x86_64-apple-darwin21.0) и Ubuntu 20.04.6 LTS GNU bash, версия 5.0.17(1)-выпуск (x86_64-pc-linux-gnu) ) и каждый раз, когда присутствует последняя строка.
$ sudo ./a.out
start: 0
parent: 0
fork has detached
$ fork: 0 1
Примечание: последняя строка задерживается и печатается после запроса.
Оболочку, которая не пытается восстановить строку, можно назвать «более простой», «минималистической» или, может быть, «умной», но определенно не «тупой».
Да, именно этого я и ожидаю. Я добавил подсказку в свой вопрос, чтобы уточнить. Я использую Ubuntu 22.04.3 LTS GNU bash, версия 5.1.16(1)-релиз (x86_64-pc-linux-gnu), и я не могу получить последнюю строку с помощью sudo. Возможно, это зависит от некоторых настроек конфигурации.
Ответ, получивший наибольшее количество голосов, очевидно, правильный и хорошо объяснен. Не издевайтесь над моей, которая понятия не имела.
Это функция безопасности. sudo выделяет новый псевдотерминал перед выполнением указанной команды. Это сделано для того, чтобы избежать атак с использованием инъекций с терминалов, которые не контролируются напрямую sudo, см., например, рекомендации по безопасности SuSE.
В вашем случае это приводит к небольшой проблеме: ваша программа на самом деле все еще работает, но вы не увидите ее выходные данные: если вы измените финальный std::cout << ...
в дочернем процессе следующим образом:
std::ofstream log("/tmp/log");
log << "fork: " << getuid() << " " << test.is_open() << std::endl;
вы увидите в /tmp/log
, что программа все еще выполняется. Вы просто не увидели результат, потому что sudo освободил псевдотерминал после завершения родительского процесса. Итак, ваша запись std::cout
попыталась записать в уже закрытый pty, что привело к отбрасыванию вывода. (std::cout.fail()
было бы верно после строки std::cout << ...
, потому что запись действительно не удалась.)
@MaximeBeluguet Часть X11 больше связана с xauth, чем с псевдотерминалом. Но, ИМХО, проще всего просто поддерживать работу программы, пока ей все еще требуется X11, и пользователь всегда может запустить команду sudo в фоновом режиме, если не хочет, чтобы она блокировала его терминал.
Когда вы запускаете sudo
, процесс sudo
становится лидером сеанса.
Когда родительский элемент завершает работу, sudo
также завершает работу, что приводит к прекращению всех процессов (включая дочерние процессы sudo
) сеанса.
Это противоречит тому факту, что все процессы в примере выполняются до завершения. Потеряется только вывод.
@Basilevs Вы думаете, что std::cout << "fork: "...
был запущен, потерян только вывод?
да. Я думаю, что это можно доказать, перенаправив вывод sudo в файл. Тогда Sudo должен быть достаточно умен, чтобы не выделять терминал.
Также см. мой ответ, где более старые версии sudo по умолчанию ничего не завершали.
В MacOS поведение идентично sudo или нет. Последние строки:
fork: 0 0
илиfork: 501 0