Давненько я не писал на Хабр, да и тема интересная появилась, так что пора поправить это постыдное упущение.
Далее собственно детектив как оно есть, "расследование" которого ещё не окончено, можно присоединиться кстати… Пост будет обновляться, по окончанию (я надеюсь что баг таки найдётся) пост изменит название получив префикс "[SOLVED]"...
Постучался тут человечек на GH, с ошибкой типа "Fail2ban ведет себя как-будто он временами в будущем". Первой мыслью было — что опять! ну снова кто-то во временных зонах потерялся.
Но нет, всё оказалось несколько хуже — иногда, редко, Fail2ban пишет в логи дату из 2023-го года.
И не только пишет, а по всей видимости действительно начинает считать что он где-то в 2023-м, со всеми вытекающими — снятием бана для блокированных адресов по истечению срока действия и т.д. и т.п.
Причем делает это для всех потоков, а чуть позже возвращается в 2021-й, чтобы позднее снова на короткое время прыгнуть в 2023-й и так снова и снова.
Т.к. Fail2ban пишет в лог системное время, я ответил товарищу что оно вероятнее всего просто постоянно меняется, т.е. либо синхронизация времени NTP каким-то образом «нарушена», либо (что весьма вероятно) какой-то сервис или скрипт постоянно изменяет системные часы на короткое время. И на этом закрыл вопрос как 3rd party issue.
Ну и человек через некоторое время вернулся, попробовав мой тест-скрипт видаwhile true; do date >> date.log && sleep 0.5 ; done
(даже с sleep 0.001
), и не обнаружил там временных аномалий (в то время как Fail2ban по его заявлению прыгал туда-сюда во времени), а также указал что дескать он, Fail2ban, единственный сервис что периодически так делает — всё остальное, вплоть до журналов syslog, остаётся в родном 2021-м.
На вопрос что же будет наблюдается для вот этого:
python3 -c $'import time\nwhile True: t=time.time(); lt=time.localtime(t); print("%.3f\t%.3f\t%s" % (time.monotonic(), t, time.strftime("%Y-%m-%dT%H:%M:%S", lt))), time.sleep(0.5)'
import time
while True:
t=time.time()
lt=time.localtime(t)
print("%.3f\t%.3f\t%s" % (
time.monotonic(), t, time.strftime("%Y-%m-%dT%H:%M:%S", lt)
))
time.sleep(0.5)
ответ был (короткий отрывок, diff синтаксис в качестве "ударения"):
18604.919 1640095233.438 2021-12-21T06:00:33
18605.435 1640095233.955 2021-12-21T06:00:33
- 19119.627 1705034426.326 2023-01-14T10:03:00
18606.468 1640095234.988 2021-12-21T06:00:34
18606.984 1640095235.504 2021-12-21T06:00:35
18607.501 1640095236.020 2021-12-21T06:00:36
- 19121.692 1705034428.642 2023-01-14T10:03:02
18608.533 1640095237.053 2021-12-21T06:00:37
18609.049 1640095237.569 2021-12-21T06:00:37
На чем вопрос в Fail2ban можно было бы действительно закрыть, но…
Захотелось помочь человеку, плюс интрига, интересно как это так и что это за магия машина времени.
Что мы тут собственно видим — "голый" python тут прыгает во времени почем зря, при этом даже на monotonic time тот "прыжок" иногда заметен, однако там он как бы больше "дрейф" чем "прыжок", ибо разница составляет лишь 514 секунд, но пока не суть важно.
Внимательный читатель наверняка заметил еще одну странность — unix time 1705M находится в 2024-м году, а не в 2023-м как strftime нам заявляет (об этом подробнее ниже). О чем товарищу было сказано собственно, с прувом и описанием всех остальных странностей в том числе и некоторым сомнением что всё это прикол шутка юмора.
$ python3 -c 'import time; print(time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(1705034426)))'
2024-01-12T04:40:26
$ python3 -c 'import time; print(time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(1705034426)))'
2024-01-12T05:40:26 (in CET)
Организованная им проверка на вшивость для syscall clock_gettime
показывает, что оно дескать всегда возвращает корректное значение, т.е. подозрение пока на python (версия 3.9.2) собственно.
Проверка на возможные изменения /etc/localtime
или чего-то в этом роде, определением TZ=UTC
перед вызовом питона показала те же результаты (только со смещением GMT+0000 в локальном времени).
Тест того же в python 2.7 на той же самой машине показал, что 2-й питон не подвержен временной аномалии от слова совсем.
Попытка оттрейсить это с помощью strace
провалилась (clock_gettime
не наблюдается вовсе), ибо ванильный python 3.9.2 (в Debian 11) или скомпилирован как-то со статически слинкованой libc (и оно там в юзерспейсе) или что-то в этом духе, т.е. имеем такой результат (в сравнении с тем же 3.9 собранным из исходниках на моём Debian 10 или ванильным 3.7 для него же):
$ strace python3 -c $'import time\nwhile True: t=time.time(); lt=time.localtime(t); print("%.3f\t%.3f\t%s" % (time.monotonic(), t, time.strftime("%Y-%m-%dT%H:%M:%S", lt))), time.sleep(0.5)'
...
- clock_gettime(CLOCK_REALTIME, {tv_sec=1640104296, tv_nsec=784712193}) = 0
- clock_gettime(CLOCK_MONOTONIC, {tv_sec=22780079, tv_nsec=358645597}) = 0
write(1, "22780079.359\t1640104296.785\t2021"..., 4822780079.359 1640104296.785 2021-12-21T17:31:36
...
(хотя согласно исходникам он однозначно вызывает clock_gettime
и в 3.9.2).
Ну на нет и суда нет, в сторону strace
, заряжаем ltrace
...
И… многочисленные попытки уточнить что это за свинство такое с помощью ltrace
(с собственным конфигом, чтобы оттрейсить локальный clock_gettime
и т.п.) показывают совершенно ненормальные результаты — что-то внутри python "арифметики" ломает верные значения clock_gettime
почём зря, при этом по видимому многократно.
В результате для:
# пишем конфиг:
printf 'typedef clockid_t = enum (CLOCK_REALTIME=0, CLOCK_MONOTONIC=1);\ntypedef timespec = struct(long, long);\ntypedef tm = struct(int, int, int, int, int, int, int, int, int);\nint clock_gettime(clockid_t, +timespec*);\ndouble floor(void*, double);\ntm* localtime_r(long*, +tm*);\n' > /tmp/ltrace.conf
# л-трейсим только *gettime, floor, localtime_r и write:
ltrace -e '*gettime+floor+localtime_r+write' -F /tmp/ltrace.conf python3 -c $'import time\nwhile True: t=time.time(); lt=time.localtime(t); print("%.3f\t%.3f\t%s" % (time.monotonic(), t, time.strftime("%Y-%m-%dT%H:%M:%S", lt))), time.sleep(0.5)'
имеем следующее (снова короткий отрывок, diff синтаксис в качестве "ударения"):
...
+ python3->clock_gettime(CLOCK_REALTIME, { 1640803034, 720044189 }) = 0
+ python3->floor(0, 1640803034.720044) = 1640803034.000000
+ python3->localtime_r(1640803034, { 14, 37, 10, 29, 11, 121, 3, 362, 0 }) = { 14, 37, 10, 29, 11, 121, 3, 362, 0 }
+ python3->clock_gettime(CLOCK_MONOTONIC, { 726625, 727901583 }) = 0
+ python3->write(1, 0x24c2cc0, 46, 0726625.728 1640803034.720 2021-12-29T10:37:14
) = 46
python3->clock_gettime(CLOCK_MONOTONIC, { 726625, 728323139 }) = 0
- python3->clock_gettime(CLOCK_REALTIME, { 1640803035, 222419463 }) = 0
- python3->floor(0, 1674361765.555368) = 1674361765.000000
- python3->localtime_r(1674361765, { 25, 29, 20, 21, 0, 123, 6, 20, 0 }) = { 25, 29, 20, 21, 0, 123, 6, 20, 0 }
- python3->clock_gettime(CLOCK_MONOTONIC, { 726626, 230566667 }) = 0
- python3->write(1, 0x24c2cc0, 46, 0755543.594 1705615768.690 2023-01-21T20:29:25
) = 46
python3->clock_gettime(CLOCK_MONOTONIC, { 726626, 231242085 }) = 0
+ python3->clock_gettime(CLOCK_REALTIME, { 1640803035, 725428043 }) = 0
+ python3->floor(0, 1640803035.725428) = 1640803035.000000
+ python3->localtime_r(1640803035, { 15, 37, 10, 29, 11, 121, 3, 362, 0 }) = { 15, 37, 10, 29, 11, 121, 3, 362, 0 }
+ python3->clock_gettime(CLOCK_MONOTONIC, { 726626, 733596674 }) = 0
+ python3->write(1, 0x24c2cc0, 46, 0726626.734 1640803035.725 2021-12-29T10:37:15
) = 46
python3->clock_gettime(CLOCK_MONOTONIC, { 726626, 734181225 }) = 0
- python3->clock_gettime(CLOCK_REALTIME, { 1640803036, 228679952 }) = 0
- python3->floor(0, 1674361766.562118) = 1674361766.000000
- python3->localtime_r(1674361766, { 26, 29, 20, 21, 0, 123, 6, 20, 0 }) = { 26, 29, 20, 21, 0, 123, 6, 20, 0 }
- python3->clock_gettime(CLOCK_MONOTONIC, { 726627, 237223939 }) = 0
- python3->write(1, 0x24c2cc0, 46, 0755544.600 1705615769.697 2023-01-21T20:29:26
) = 46
...
По поводу ненормальности результатов, как это выглядит:
t=time.time(); # 1640M retrieved, 1705M stored in t
lt=time.localtime(t); # lt is built from 1674M base
Т.е. time.time
вызывая clock_gettime(CLOCK_REALTIME)
получает {1640803035, 222419463}
, преобразуя это в double (если смотреть в исходники, это происходит через long long) оно возвращает 1705615768.690
(сохраняется в t
). А затем при вычислении time.localtime(t)
наше время (в качестве локального базиса) становится вдруг 1674361765.555368
и в результате имеем { 25 сек., 29 мин., 20 час., 21 день, 0 + 1 мес., 123 + 1900 год., ... }
что и соответствует 2023-01-21T20:29:25
в итоге при выводе в консоль через strftime
.
Как это объяснить кроме как магией, сломанной математикой double, перемешиванием инструкций byte-code питона на лету (в результате какой-то непонятной оптимизации) или вообще UB (удивительно стабильным, ну как — относительно стабильным) я не очень пока понимаю.
Возможно это какая-либо проблема синхронизации, но в простейшем тесте то всё работает однопоточно…
Ошибки в железе? Тогда почему это проявляется исключительно в питоне.
Кроме того при прыжках наблюдается еще более странное поведение — эти "непонятные" значения t
(те что 1705M
) иногда отличаются на 120 и более секунд (иррегулярно), притом что исходные значения (те что 1640M
) и значения lt
(и промежуточной арифметики, те что 1674M
) отличаются (как и положено) на 1 секунду, например:
743604.531 1705616185.204 2023-01-21T20:38:21 // *185
727162.487 1640803571.480 2021-12-29T10:46:11
727163.004 1640803571.997 2021-12-29T10:46:11
743606.036 1705616305.981 2023-01-21T20:38:22 // nix time changes to *305; the time in the date string changing to next second compared to 1st value.
743606.554 1705616306.498 2023-01-21T20:38:23
Я постучался с этим в IRC#python, там люди поудивлялись немного, но конкретных предложений не поступало.
Анализ исходников, кроме того, что оно там переворачивалось сто раз между до 3.10 (не говоря уже про master), ничего не дал (пока во всяком случае, т.к. времени очень мало свободного).
System: Host: localhost Kernel: 5.10.0-9-amd64 x86_64 bits: 64 Console: tty 0 Distro: Debian GNU/Linux 11 (bullseye)
Machine: Type: Kvm System: QEMU product: Standard PC (i440FX + PIIX, 1996) v: pc-i440fx-3.1 serial: N/A
Mobo: N/A model: N/A serial: N/A BIOS: SeaBIOS v: 1.12.0-1 date: 04/01/2014
CPU: Info: Single Core model: AMD Ryzen 9 3900X bits: 64 type: UP L2 cache: 512 KiB
Speed: 3793 MHz min/max: N/A Core speed (MHz): 1: 3793
Info: Processes: 92 Uptime: 2d 13h 08m Memory: 976.4 MiB used: 397 MiB (40.7%) Init: systemd runlevel: 5 Shell: Bash
Промежуточные результаты расследования иcследования или как issue видится мне в виде краткой выжимки:
- python 3.9.2 периодически возвращает странные значения для
time.time()
иtime.localtime()
, лежащие в будущем, при этом ни один процесс кроме python3 не показывает такого поведения… включая python 2.7, который тоже не подвержен этим временным сдвигам. - это не воспроизводится пока нигде кроме конкретной виртуалки товарища, сообщившего об ошибке.
- трейсинг показывает что
clock_gettime(CLOCK_REALTIME)
возвращает корректные значения, при этом временами внутренняя "математика" питона полностью сходит с ума (см. выше). - Значения
t = time.time()
иlt = time.localtime(t)
могут отличаться на год, при том, что дрейф не зависит от выбранной временной зоны (т.е. верно и для UTC/GMT). - Во время временного "прыжка" значения
t
могут быть иррегулярны (могут отличаться от предыдущих на минуты), при том что значенияlt
(что естьtime.localtime(t)
) стабильны, т.е. инкрементируются и выглядят регулярно. - Во время "прыжка" нередко (однако не всегда) наблюдается временной сдвиг и в
time.monotonic()
, при этом сдвиг много меньше сдвига дляtime.time()
.
Пока на этом всё. Всех с наступающим, и если есть идеи, предложения и желание поучаствовать, добро пожаловать в комменты или на github.
Комментарии (36)
Tzimie
30.12.2021 22:40Может миграция виртуалки с одного хоста на другой, с другим CPU другой частоты? Там могут быть засады с коррекцией точного времени
Gutt
30.12.2021 22:53+3Склонируйте или мигрируйте виртуалку на другой хост -- проблема сохраняется?
Если да, то проверьте пакетным менеджером, что CRC всех файлов правильные.sebres Автор
31.12.2021 01:00А разве apt при скачивании (или распаковке пакета) не будет ругаться на `hash sum mismatch`? Насколько знаю - должен.
censor2005
31.12.2021 08:35А возможно ли, что где-то битый файл на диске именно от бинарников питона?
Gutt
31.12.2021 12:09Я про проверку всех установленных пакетов. Да, мы не видим библиотечных вызовов во время работы функции, но, возможно, при старте какая-нибудь битая библиотека загрузилась по неправильному адресу и побила код вычисления времени, или файл микрокода процессора оказался побит (да, он подписан и такое повреждение крайне маловероятно), или из-за битого ядра повреждаются данные в стеке (какой-нибудь очень привилегированный поток пишет не туда, да мало ли способов повредить память другого процесса.
tbl
30.12.2021 23:53+7Возможно, что-то не так при работе с FPU в QEMU (сохранение и восстановление состояния при уходе в гипервизор и обратно). Возможно, что нужен дебаг на стороне хостовой системы.
Hisoka
31.12.2021 04:39+1Вот, очень хорошая идея. А если бы это было что-то собранное MinGW-w64, то я бы даже сказал где баг искать можно было (fesetenv, который щас исправлен, но до этого я пару лет пытался заставить их его исправить. Пришлось разобраться и самому патч писать)
amarao
31.12.2021 00:07+5Вам это удалось воспроизвести где-то ещё? Пример скрипта для воспроизведения есть? Я думаю, надо просить общественность пробовать на разных машинах, пока не найдётся закономерность.
sebres Автор
31.12.2021 01:07+3Нет, воспроизвести не удалось нигде больше (я об этом писал)... Да, скрипт есть конечно (см. статью, самый первый снипет)... можно улучшить немного, типа стоп если
t
больше 1650e6 ну или date из shell... Накидаю как доберусь до компьютера, мобильный хабр это боль.
siealex
31.12.2021 02:18-199.99% железо. Возможно, что даже проц, а не память.
Hisoka
31.12.2021 04:43У меня есть железка, которая в memtest, в одном и том же месте показывает проблему, если в систему вставлено 2 плашки(любые. пробовал и другого объёма и т.д. Но проблема всегда в первых блоках второй плашки) и тест запущен многоядерный.
dpyzhov
31.12.2021 03:52+2Клонировать виртуалку на другой хост. Искать отличия виртуалки от стандартных либ. Поставить рядом другие сборки питона этой и соседних версий.
Наверняка какой-нибудь локальный глюк типа подбитого из-за сбоя железа *.pyc (ну а вдруг?) или какой-то либы. Вангую, что шансы на баг в дистрибутивном софте исчезающе малы.
ZlobniyShurik
31.12.2021 04:37+2Хм, забавно... На некоторых моих виртуалках с fail2ban`ом периодически проскакивали строчки в логах с руганью на возможно некорректно установленную тайм-зону. Естественно, TZ были настроены нормально. Всё перерыл, ничего не нашёл, плюнул и забил. Возможно, это как раз был вышеописанный глюк.
И, да, проблема вряд ли в железе - везде ECC во все щели.
sebres Автор
31.12.2021 11:19Тут 2 варианта, либо запаздывающее логирование из сервиса (типичный случай nginx log для долгого запроса, когда запись в лог происходит по response сильно отстающий от request time, а nginx запишет именно время начала запроса). Либо баг - https://github.com/fail2ban/fail2ban/issues/2882 (поправлен для новых версий).
С проблемой из статьи конечно ничего общего (там типа "системное" время прыгает).
Portnov
31.12.2021 09:04+2У меня есть ноутбук, на котором операции с float-ами время от времени возвращают NaN без явной причины. Софт, на котором я это обнаружил — смесь питона с Си, и изучал я эту проблему довольно давно (лет 10 назад), так что уже не помню, проявлялась проблема в питоньем коде или в сишном...
neumond
31.12.2021 11:04+3Всё ждал когда же упомянут monkeypatching или какие-то модули, закрывающие собой стандартные, но не дождался.
masai
31.12.2021 15:10+3Как я понимаю, Python пробовали переустанавливать, так что стандартная библиотека вряд ли повреждена. В тестовом однострочнике ничего стороннего не импортировалось. Импорт из файла тоже маловероятен, так как fail2ban и тестовые скрипты запускались в разных директориях.
Надо, конечно, ещё PYTHONPATH посмотреть и вообще на библиотеки, но мне кажется, проблема вряд ли в этом. Мне трудно представить код, который бы патчил стандартные функции так, что получается именно такой результат.
JQuery567
31.12.2021 22:23+2комп: win7 64x py (3.8.10 64x): Z:\_py\_новогодний_детектив>py Python 3.8.10 (tags/v3.8.10:3d8993a, May 3 2021, 11:48:03) [MSC v.1928 64 bit (AMD64)] on wi n32 время: (сейчас 2021-12-31 T 21:07:41) ------------------------------- py -c "import time; print(time.strftime('%Y-%m-%dT%H:%M:%S', time.gmtime(1705034426)))" Z:\_py\_новогодний_детектив>py -c "import time; print(time.strftime('%Y-%m-%dT%H:%M:%S', time 2024-01-12T04:40:26 Z:\_py\_новогодний_детектив>py -c "import time; print(time.strftime('%Y-%m-%dT%H:%M:%S', time 2024-01-12T04:40:26 Z:\_py\_новогодний_детектив>py -c "import time; print(time.strftime('%Y-%m-%dT%H:%M:%S', time 2024-01-12T04:40:26 (сейчас 2021-12-31 T 21:07:41) ========== py -c "import time; print(time.strftime('%Y-%m-%d T %H:%M:%S', time.localtime(1705034426)))" Z:\_py\_новогодний_детектив>py -c "import time; print(time.strftime('%Y-%m-%d T %H:%M:%S', ti 2024-01-12 T 06:40:26 Z:\_py\_новогодний_детектив>py -c "import time; print(time.strftime('%Y-%m-%d T %H:%M:%S', ti 2024-01-12 T 06:40:26 Z:\_py\_новогодний_детектив>py -c "import time; print(time.strftime('%Y-%m-%d T %H:%M:%S', ti 2024-01-12 T 06:40:26
sebres Автор
01.01.2022 21:00+2Nix-time 1705M естественно находится в 2024 году (о чем и сказано в посте). Речь про то, что time.time возвращает это значение сейчас (временами, на конкретной машине)... Что этот комментарий должен сказать я не понимаю вовсе... Может поясните?
Pena
01.01.2022 05:57+3Ошибка в одном в 26 бите, значение ошибки приблизительно равно одному из числу в степени двойки
DerRotBaron
02.01.2022 01:35+1Похоже, но не совсем
>>> bin(1705615769 - 1640803036) '0b11110111001111011010111101' >>> 1640803036 | (1 << 26) 1707911900
sebres Автор
02.01.2022 02:09Хоть кошерней тут было бы не минус а xor юзать... но оно всё равно ни в long, ни в double тут не "работает" (как минимум в прямом виде).
myz0ne
01.01.2022 13:58+3Я бы предложил попытаться разделить програмная это или аппаратная ошибка. Для этого можно создать легковесную копию при помощи того же lxc/lxd/chroot/docker/whatever.
создание копии на lxc
mkdir /opt/lxc-data rsync --exclude=/dev \ --exclude=/home \ --exclude=/mnt \ --exclude=/opt/lxc-data \ --exclude=/proc \ --exclude=/root \ --exclude=/run \ --exclude=/sys \ --exclude=/tmp \ --exclude=/snap \ --exclude=/var/log \ -av / /opt/lxc-data apt install lxc lxc-create lxc --template=none cd /var/lib/lxc/lxc mv /opt/lxc-data rootfs
Добавить в
/var/lib/lxc/lxc/config
# Distribution configuration lxc.include = /usr/share/lxc/config/common.conf lxc.apparmor.profile = unconfined lxc.cgroup.devices.allow = a lxc.tty.dir = lxc lxc.pty.max = 1024 lxc.tty.max = 4 lxc.hook.clone = /usr/share/lxc/hooks/clonehostname lxc.mount.auto = cgroup:mixed proc:mixed sys:mixed lxc.mount.entry = /sys/fs/fuse/connections sys/fs/fuse/connections none bind,optional 0 0 # For Ubuntu 14.04 lxc.mount.entry = /sys/kernel/debug sys/kernel/debug none bind,optional 0 0 lxc.mount.entry = /sys/kernel/security sys/kernel/security none bind,optional 0 0 lxc.mount.entry = /sys/fs/pstore sys/fs/pstore none bind,optional 0 0 lxc.mount.entry = mqueue dev/mqueue mqueue rw,relatime,create=dir,optional 0 0 lxc.arch = linux64 # Container specific configuration lxc.rootfs.path = dir:/var/lib/lxc/lxc/rootfs lxc.uts.name = lxc
И запускать через
lxc-start -d lxc lxc-attach lxc ... lxc-stop lxc
Причем поднимать копию по идее можно на той же машине, чтобы избавиться от copy on write. И так можно сделать легковесный контейнер с минимально необходимыми данными для воспроизведения
tzlom
02.01.2022 08:22Я бы проверил память для начала, вполне возможно что это какой нибудь row hammer в дефектной памяти.
Wesha
После прочтения этой строки сразу возникает вопрос: битая память?
Elsajalee
Тогда получение kernel panic | BSOD гарантировано.
Wesha
Не всякая битая память приводит к сегфолтам.
sebres Автор
И влияет исключительно на питон, причем только третий. Хмм... Ну и ASLR должно как то проявляться тогда, нет? Я имею ввиду запустив тест много раз, процессы будут стучаться по разным адресам. А результат стабильно кривой... Да и кривой только на математике в pytime и т.д.
ALiEN175
Был пост на хабре или где-то еще, не могу найти. Вкратце — битая память приводила к появлению в логах postfix что-то вроде «postfiz». При этом сама система работала как часы.
sebres Автор
Я всё понимаю, и ECC не панацея и работать питон может худо бедно на тех же адресах (хотя ASLR)… Но как объяснить битой памятью преобразование вида:
При том что левое и правое относительно стабильны (что касается такта изменений), а то что посредине плавает, при чем 1674M в теории должно зависеть (алгоритмически) от 1705M.