LINUX.ORG.RU

Нюансы запуска процессов через system/exec

 ,


6

1

Случай #1:

Как известно, при вызове exec происходит завершение всех нитей вызывающего кроме той, что вызвала system. При этом открытые дескрипторы клонируются в новый процесс (если у них не указан флаг FD/SOCK_CLOEXEC).

Представим следующую гипотетическую ситуацию:

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

Положим, что так как эта строка может содержать переменные среды, какие-то выражения подстановки и shell-related команды и синтаксис программист, написавший эту программу посчитал, что самым оптимальным образом будет запускать эту строку с помощью команды system, которая не требует списка аргументов, как семейство exec*. Плюс, так как system «под капотом» вызывает sh -c ..., то автоматически подставятся переменные, а shell-related команды и синтаксис будут работать как нужно. Так вот, глянем исходник system, например, для glibc:

<Сначала хотел показать исходник glibc, но тут слишком дофига получается кода, поэтому сжато: clone + exec + waitpid, кстати ман говорит про fork + exec + waitpid, что не совсем то же самое>

При вызове clone/fork происходит копирование открытых дескрипторов, которые наследует процесс, который запустил system. Предположим, что кто-то передал для запуска строку, содержащую запуск программы (назовем ее evil), выполняющей следующие действия:

if (fork() != 0)
    exit(0);

Что превращает ее в отвязанного от родителя демона. Однако этот fork тоже наследует все открытые хэндлеры, что приводит нас к тому, что этот демонутый процесс evil получил копию всех открытых хендлеров супервизора, однако стал от него отвязан (system вернул 0, супервизор считает, что работа выполнена).

Теперь предположим, что это супервизор с управлением по сети. То есть супервизор открывает серверный сокет и слушает команды, которые ему приходят извне. Не будем обсуждать защищенность передачи или канала связи, всё можно сломать, суть сейчас не в этом. Суть в том, что на момент запуска system супервизор владел открытым серверным сокетом (!). Да, по совести и по правилам при его создании ему нужно было выставить SOCK_CLOEXEC, но скажем честно, кто своим серверным сокетам выставляет флаг, о котором даже в мануалах на socket из 153 строк отдано 2 (!), т.е. всего 1.3%?

Таким образом, пройдя через всю цепочку форков копия серверного сокета оказывается у демонутого evil:

super      9053 alex    3u  IPv4 11458560      0t0  TCP *:34002 (LISTEN)
super      9053 alex    4u  IPv4 11464960      0t0  TCP localhost:34002->localhost:41308 (ESTABLISHED)
evil       9257 alex    3u  IPv4 11458560      0t0  TCP *:34002 (LISTEN)

Положим, что процесс супервизора по какой-то причине завершился. Ну, например, выполнив свою работу или по ошибке. И теперь начинается самая мякотка. Ведь в системе уже открыт слушающий на порту 34002 сокет! И принадлежит он программе evil! Таким образом она может прикинуться сервером, а супервизор, в свою очередь, не сможет вернуть свой сокет, поэтому система не даст ему открыть второй сокет на том же порту без REUSEPORT:

SO_REUSEPORT (since Linux 3.9)
              Permits multiple AF_INET or AF_INET6 sockets to be bound to an identical socket address.  This option must be set on each socket (including the first socket) prior to calling bind(2) on the socket.  To prevent port hijacking, all of  the  pro‐
              cesses binding to the same address must have the same effective UID.  This option can be employed with both TCP and UDP sockets.

В общем случае, когда какая-то программа-сервер запускается и получает ALREADYINUSE то надо бы сказать мол, «не не могу работать, наверное другая копия меня уже запущена» и выключиться (ну или ожидать его освобождения).

Мы же не хотим port hijacking, верно? А вот все равно профукали. Ну и теперь evil может делать что угодно. Может получать данные от клиентов, может им даже что-то отвечать,может получать список команд для запуска и запускать вместо них что угодно, сообщая наверх что всё хорошо.

Собственно, вопрос #1: почему на серверный сокет автоматически при создании не вешается FD_CLOEXEC?

Случай #2:

Мы - супервизор, запускающий программы, забудем про сетевое взаимодействие, мы сами знаем, что нам запускать. Запускают нас от root, для того, чтобы мы могли делать какую-то очень важную вещь, ну, например, читать какой-то root-owned файл:

-rw-------  1 root root        7 июн  5 23:51 rootonly

Изобразим супервизор упрощенно:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>       
#include <sys/wait.h>
#include <errno.h>       
#include <pwd.h>


static void dowork(void) {
    int pid = fork();
    int status;
    switch (pid) {
        case -1 : exit(EXIT_FAILURE);
        case 0  :
            if (setuid(1000) == -1) {
                printf("u:%u\n", errno);
            }
            if (seteuid(1000) == -1) {
                printf("e:%u\n", errno);
            }
            execl("./hijack_evil.elf", "./hijack_evil.elf", NULL);
            printf("cannot run\n");
            exit(EXIT_FAILURE);
        default :
            wait(&status);
    }
}

int main(void) {
    FILE * fileroot = fopen("/tmp/rootonly", "r");
    printf("I am %s, #%u/%u\n", getpwuid(getuid())->pw_name, geteuid(), getuid());
    
    dowork();
    
    fclose(fileroot);
    return 0;
}
gcc hijack_root.c -o hijack_root.elf

И программу, которой нельзя давать рутовые права:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>       
#include <sys/wait.h>
#include <pwd.h>

int main(void) {
    printf("I am %s, #%u/%u\n", getpwuid(getuid())->pw_name, geteuid(), getuid());
    // my stdin is 0, stdout is 1, stderr is 2, therefore...
    char buf[1024];
    read(3, buf, 1024);
    printf("%s\n", buf);
    return 0;
}
gcc hijack_evil.c -o hijack_evil.elf

Проверяем, что просто так файл прочитать нельзя:

alex@ThinkPad-L560:~$ cat /tmp/rootonly 
cat: /tmp/rootonly: Отказано в доступе
alex@ThinkPad-L560:~$ echo $USER/$UID
alex/1000

И запускаем супервизор:

$ sudo ./hijack_root.elf 
I am root, #0/0
I am alex, #1000/1000
SECRET

При этом мы не можем ставить FD_CLOEXEC на каждый хендлер, открываемый в супервизоре, потому что он может понадобиться для целей супервизора в его форках. Соответственно вопрос #2: почему внешние программы, запускаемые через exec наследуют хендлеры? Ведь очевидно, что это может быть использовано только для «угона» файлов

★★★★★

Ответ на: комментарий от PPP328

Всё еще считаете, что можете просто так позакрывать в одном треде fd, с которыми работают все остальные?

facepalm

  1. Я кому про форк написал?
  2. Из какого стандарта ты взял attribute((destructor)) и схерали оно будет вызвано при fork или exec?

Всё еще считаете, что можете просто так позакрывать в одном треде fd, с которыми работают все остальные?

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

anonymous
()
Ответ на: комментарий от anonymous

Из какого стандарта ты взял attribute((destructor)) и схерали оно будет вызвано при fork или exec?

__attribute__((destructor)) - это gnu расширение. Которое выполняется при закрытие треда. Которое ВНЕЗАПНО вызывается когда завершается тред. И ВНЕЗАПНО при fork клонируются все треды, которые завершаются только на exec. Потому что мы только что потратили сутки для того чтобы выяснить что это именно так.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от anonymous

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

Вперед, возьмите и позакрывайте без последствий половину тредов например bittorrent.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Которое выполняется при закрытие треда. Которое ВНЕЗАПНО вызывается когда завершается тред. И ВНЕЗАПНО при fork клонируются все треды, которые завершаются только на exec. Потому что мы только что потратили сутки для того чтобы выяснить что это именно так.

Вчера ты казался умнее. Ты вот это вот всё сам проверял или прочитал где-то?

anonymous
()
Ответ на: комментарий от PPP328

Вперед, возьмите и позакрывайте без последствий половину тредов например bittorrent.

BitTorrent – это протокол. Ты предлагаешь мне закрыть половину тредов в протоколе?

anonymous
()
Ответ на: комментарий от anonymous

Я имел в виду программу.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

ты можешь вызвать fcntl на открытые ресурсы, если ты не хочешь передавать их в exec:

int fd = open(bla-bla-bla);
fcntl(fd, F_SETFD, FD_CLOEXEC);

Iron_Bug ★★★★★
()
Ответ на: комментарий от anonymous

Извините, я ошибся. Имел в виду, не потоки, а модули. Нужно больше спать.

Я имел в виду следующее: В модулях (юнитах трансляции) могут быть назначены деструкторы, с помощью аттрибута destructor, который сейчас поддерживается всеми компиляторами включая древний keil. В этих деструкторах чаще всего ведется очистка ресурсов, занятых программой. Делается это для того, чтобы не протаскивать вызов xxx_free в main, а очистить ресурсы не выходя из модуля (например синглтоны или хранилища). Так вот когда fork делает слепок процесса он действительно копирует только активный тред, но также он клонирует память всех загруженных модулей, так как на этапе работы программы они уже не делятся на юниты трансляции и копируется весь код.

При вызове exec очищается память вызванного процесса - process image замещается, что приводит к тому, что вызываются все destructor, для всех модулей (в порядке заданной очередности). Если вы до вызова exec вызвали close этих fd, то они попытаются сделать double close, либо, вариант #2 (с которым мы и столкнулись), заблокируют программу, потому что будут ожидать системного апдейта состояния сокета (потому что он склонен, но на клон система не обращает внимания потому что активен оригинал). Таким образом очищать fd перед вызовом exec - очень плохая идея. destructors могут присутствовать не только в вашем коде, но и в коде shared-библиотеки, которую вы загрузили к себе в память, а там проконтролировать, что будет при close(randomfd) невозможно.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от Iron_Bug

Эти ресурсы могут быть открыты не вами напрямую, а shared-библиотекой, которую вы используете, таким образом вы не имеете этого fd на руках, потому что он может быть скрыт за реализацией (как, например FILE, но там хотя бы есть функция fileno, но ее вызов осуждается).

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

а ты не используй что попало. думать головой надо, когда пишешь такие вещи, а не пионерские посты тут кропать.

Iron_Bug ★★★★★
()
Ответ на: комментарий от Iron_Bug

а ты не используй что попало.

Что попадает под ваш критерий «что попало»? systemd входит? Там все-все-все fd получают флаг CLOEXEC? libxml - говно? А в стандартной C++ тоже всегда-всегда можно до fd под например стримом подобраться?

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

ненужнод там на первом месте. у меня его нет, и никогда не будет. это мерзкое говно маздай.

на остальные (нормальные) библитотеки - dlclose, если они имеют свойство открывать что попало без твоего ведома. но обычно-таки библиотеки такой фигнёй не страдают и ты сам открываешь дескрипторы.

Iron_Bug ★★★★★
()
Последнее исправление: Iron_Bug (всего исправлений: 2)
Ответ на: комментарий от PPP328

При вызове exec очищается память вызванного процесса - process image замещается, что приводит к тому,

что все деструкторы потёрты и вызывать больше нечего. Деструкторы при exec не вызываются.

anonymous
()
Ответ на: комментарий от PPP328

Кстати, можешь вешать cloexec на все ненужные дескрипторы из /proc/self/fd перед exec, вместо того чтобы их close. Типа «дефолтный» cloexec. Проблема с двойным close, должна в любом случае отпасть.

anonymous
()
Ответ на: комментарий от PPP328

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

anonymous
()

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

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

Во-первых, ты не видишь разницы в словах EXEC и FORK? Конечно же можем. Во-вторых, мы можем явно закрыть все дескрипторы кроме нужных перед exec. Даже есть специальный вызов closefrom который делает это быстро. В-третьих, даже если бы этих инструментов не было, мы бы просто сохраняли все открытые дескрипторы в массив и всегда могли их закрыть через него.

slovazap ★★★★★
()
Последнее исправление: slovazap (всего исправлений: 1)
Ответ на: комментарий от slovazap

Господи, вы бы хоть тред почитали. Ваш ручной close втречает конфликт с __attribute__((destructor)), из-за чего последний в зависимости от логики валит программу или портит память

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Я вас расстрою, attribute((destructor)) - вызываются.

У меня не вызываются. Согласно докам не вызываются. Расстрой меня кодом.

anonymous
()
Ответ на: комментарий от anonymous

Я немного некорректен. Вызовется деструктор не в случае хорошего exec, а в случае плохого exec, потому что после exec обязательно должен стоять exit, ибо после форка - это неполноценная программа, а обрубок, который нужно завершить. Так вот если вы перед exec закрыли все fd то деструктор вызовется с мусорными fd. Proof-of-concept без обработки fd:

main.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>       
#include <unistd.h>
#include <pthread.h>
#include "./file2.h"
static void * _thread(void * data) {
    int pid = fork();
    int status;
    switch (pid) {
        case -1 :
            exit(EXIT_FAILURE);
        case 0 :
            func();
            execl("/bin/echo", "/bin/echo", "echo called", NULL); // Чтобы увидеть double free надо испортить имя программы
            printf("can't run\n");
            exit(EXIT_FAILURE); // У вас нет выбора, тут нужен EXIT.
        default:
            wait(&status);
    }
}
int main(void) {
    func();
    pthread_t thread;
    pthread_create(&thread, NULL, _thread, NULL);
    sleep(5);
    return 0;
}

file2.h

#ifndef FILE2
#define FILE2
int func(void);
#endif

file2.c

#include <stdio.h>
static void _free(void) __attribute__((destructor));
static void _free(void) {
    printf("%s\n", __FUNCTION__);
}
int func(void) {
    printf("%s\n", __FUNCTION__);
}

Если нормально стартанул:

$ ./file.elf 
func
func
echo called
_free

Если плохо стартанул (плохое имя программы):

$ ./file.elf 
func
func
can't run
_free
_free

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Господи, вы бы хоть тред почитали

Я отвечал на заданный вопрос. Что вы там рассусолили в треде мне не интересно.

Ваш ручной close втречает конфликт

Вы бы об языке-то хоть базовое понятие получили прежде чем такое писать. close не может ни встретить, ни проводить никакой «конфликт», ничего свалить или испортить память, он всегда закроет дескриптор если тот валиден, а не невалидный вернёт EBADF.

`__attribute__((destructor))`

GNU расширения вы приплели по ходу действия, похоже. Тогда для начала получите базовое представление о них, например прочитайте это:

Similarly, the `destructor` attribute causes the function to be called automatically after main () has completed or exit () has been called.

чтобы не писать что они вызываются на fork, exec или тем более на неких «закрытиях потоков».

slovazap ★★★★★
()
Ответ на: комментарий от PPP328

Я немного некорректен

Ты много некорректен. После неудавшегося exec вызывают _exit.

slovazap ★★★★★
()
Ответ на: комментарий от PPP328

Вызовется деструктор не в случае хорошего exec, а в случае плохого exec, потому что после exec обязательно должен стоять exit, ибо после форка - это неполноценная программа, а обрубок, который нужно завершить.

Годно.

В линуксоманах указывается, что при повторном вызове close можно нарваться на то, что под тем же номером будет открыт новый файл.

В остальных никсах никаких предупреждений не нашёл. УБ не нашёл.

Ну и да, можно пришибить процесс быстро, решительно _close, sigkill итд.

anonymous
()
Ответ на: комментарий от anonymous

В линуксоманах указывается, что при повторном вызове close можно нарваться на то, что под тем же номером будет открыт новый файл.

Но нам на это пофиг, потому что файлы больше никто уже не открывает.

anonymous
()
Ответ на: комментарий от PPP328

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

anarquista ★★★★★
()
Ответ на: комментарий от PPP328

А можете привести пример программы (в смысле готовой), которая использует на старте хендлеры с индексом выше 2, зная что кто-то для нее их открыл, а она их унаследовала?

qmail-send prints a readable record of its activities to descriptor 0. It writes commands to qmail-lspawn, qmail- rspawn, and qmail-clean on descriptors 1, 3, and 5, and reads responses from descriptors 2, 4, and 6. qmail-send is responsible for avoiding deadlock.

monk ★★★★★
()
Ответ на: комментарий от LamerOk

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

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

чтобы уточнить нюансы появился топик лора.

Лучше б у тебя учебник появился, наконец. Может, и топиков на лоре с хабром меньше будет.

LamerOk ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.