Обычно исполняемый файл не может быть перезаписан, пока процесс запускается из этого файла.Есть ли возможность обойти / сломать этот замок?
Да.Не перезаписывайте исполняемый файл;замените его.
То есть вы сохраняете новый исполняемый файл под временным именем в том же каталоге (или в любом месте той же файловой системы - должен быть на том же монтируемом!), а затем либо rename()
или link()
временный файл поверх исполняемого файла.
В сценарии оболочки вы можете использовать mv -f newbinary oldbinary
, если оба значения newbinary
и oldbinary
в той же файловой системе и смонтировать.В сценарии Bash вы можете использовать что-то вроде
#!/bin/bash
BINDIR=/usr/bin
# Autoremoved work directory
Work="$(mktemp -d)" || exit 1
trap "cd / ; rm -rf '$Work'" EXIT
# ... Check if new binaries available ...
# Otherwise: exit 0
# ... Download new binaries under "$Work/" ...
# Copy 'executable' to $BINDIR, under a temporary name
tempbin="executable.$PID-$RANDOM$RANDOM$RANDOM"
if ! mv -f "$Work/executable" "$BINDIR/$tempbin" ; then
# Failed
exit 1
elif ! mv -f "$BINDIR/$tempbin" "$BINDIR/executable" ; then
# Failed
exit 1
fi
# Successfully replced.
exit 0
. Это работает во всех системах POSIXy, поскольку имя файла полностью отделено от inode , который определяет его содержимое, режим доступа, владельца., временные метки и т. д.
На практике ядро будет сохранять старый inode до тех пор, пока есть исполняемые файлы, на которых он выполняется, или если какой-либо процесс его откроет.Однако имя файла сразу будет указывать на новый индекс с новым исполняемым содержимым.Таким образом, по сути, переименование / ссылка просто изменяет то, на какой индекс ссылается имя файла.Именно поэтому временный файл должен находиться в той же файловой системе (с тем же монтированием).
Цель - процесс (долго выполняющаяся задача) должен обновляться с минимальным временем простоя.
Это обычная дыра в безопасности, позволяющая процессу изменить себя.Обычно это даже не разрешается в системах POSIXy, если только процесс не выполняется с привилегиями суперпользователя (например, как root
или в Linux с возможностью CAP_FOWNER
).Вы НЕ хотите этого делать.
(Только потому, что это обычно делается, например, с веб-материалами PHP, это не делает его нормальным или безопасным. Если это так, то мы должны согласитьсяэтот экскремент имеет приятный вкус, потому что есть миллиарды мух и навозных жуков, которые так думают. Если вы посмотрите, вы обнаружите, что у таких веб-сервисов ALL были серьезные проблемы с безопасностью, некоторые из которых непосредственно связаны с этиммеханизм обновления. Некоторые сопровождающие упомянутого пакета утверждают, что проблемы во время обновлений, такие как атаки типа «человек посередине», являются ошибкой пользователей, а не их. Конечно, они ошибаются.)
Вместо этого у вас должна быть отдельная привилегированная служба, которая периодически проверяет наличие обновлений и, при обнаружении, извлекает новую версию, используя вышеуказанный метод замены.В простейшем случае это можно просто запустить из cron или подобного.
Если ваши пользователи действительно этого хотят, вы можете создать минимальный демон C, который периодически проверяет, доступна ли новая версия.Вы можете получать его по определенному адресу дейтаграммы домена Unix, чтобы ваш исполняемый файл мог отправить ему один символ (независимо от того, от какого пользователя он запускается), чтобы демон обновления тут же проверил (если он не проверил)достаточно недавно).По сути, он просто будет ждать (скажем, используя select()
) достаточно времени, чтобы пройти, или конкретного запроса для проверки.Когда придет время, он запустит сценарий оболочки, чтобы проверить, доступен ли новый исполняемый файл (скажем, с использованием popen()
и т. Д .; типичное расположение для сохранения таких сценариев - /usr/lib/yourservice/
).Если скрипт отвечает, что доступна новая версия, запустите другой скрипт, чтобы загрузить и заменить двоичный файл.Если процесс получает сигнал SIGHUP
, выполните проверку немедленно;если он получает сигнал SIGTERM
, выйдите.Таким образом, он может быть запущен как служба и не потреблять много ресурсов при запуске.
В вашем долговременном исполняемом файле, если он находится в точке, где он может заменить себя более новой версией, используйтеstat()
на /proc/self/exe
и argv[0]
, чтобы проверить, имеют ли они одинаковые st_dev
и st_ino
.Если они этого не делают, то служба обновлений предоставила более новую версию исполняемого файла, и ваша служба может запустить
if (argv[0][0] == '/')
execv(argv[0], argv);
else
execvp(argv[0], argv);
или, если вы определите абсолютный путь к вашему исполняемому файлу во время компиляции, скажем, exepath
тогда
execvp(exepath, argv);
, чтобы заменить себя более новой версией.
Обратите внимание, что такой процесс должен закрыть все открытые дескрипторы файлов (кроме стандартных потоков; 0, 1 и 2 или STDIN_FILENO
, STDOUT_FILENO
и STDERR_FILENO
), когда он запускается.(То есть закройте все дескрипторы открытых файлов от 3 до sysconf(_SC_OPEN_MAX)
включительно.) Это связано с тем, что функции exec*()
не закрывают дескрипторы файлов (кроме отмеченных O_CLOEXEC
/ FD_CLOEXEC
), поэтому любые дескрипторы, которые могутбыть открытым во время exec будет оставлено открытым.Это также означает, что в случае сбоя exec ваша служба может продолжить работу в обычном режиме.