*nix
Debugging
System administration
Software
Data storage
December 27 2018

Как я нашёл баг в GNU Tar

Translation
Source:
Chris Siebenmann
Автор статьи — Крис Зибенманн, системный администратор Unix в университете Торонто

Время от времени в моей работе происходит нечто странное, что заставляет задуматься. Даже если сразу непонятно, какие следуют выводы. Недавно я упомянул, что мы нашли ошибку в GNU Tar, и история о том, как это произошло, — один из таких случаев.

Для бэкапа файл-серверов мы используем Amanda и GNU Tar. В течение долгого времени у нас периодически возникала довольно редкая проблема, когда tar сходил с ума при резервном копировании файловой системы с каталогом /var/mail, производя огромное количество выходных данных. Обычно этот процесс уходил в бесконечность и приходилось убивать дамп; в других случаях он всё-таки завершался, выдав терабайт(ы) данных, которые вроде бы отлично сжимались. Когда мне в очередной раз попался такой гигантский файл tar, я подверг его проверке — и выяснил, что он частично состоит из нулевых байтов, которые очень не нравятся команде тестирования tar -t, после чего всё возвращается в норму.

(Из-за этого мне стало интересно, появляются ли нулевые байты естественным образом у людей в почтовых ящиках. Оказалось, что поиск нулевых байтов в текстовых файлах не такой простой и да, они там есть).

Недавно мы перенесли файловую систему с /var/mail на новые файловые серверы Linux под Ubuntu 18.04 и поэтому перешли на более позднюю и более стандартную версию GNU Tar, чем стоит на машинах OmniOS. Мы надеялись, что это решит наши проблемы, но почти сразу произошёл такой же инцидент. На этот раз GNU Tar работал на машине Ubuntu, где я хорошо знаком со всеми доступными инструментами отладки, так что я проверил запущенный процесс tar. Проверка показала, что tar выдаёт бесконечный поток read(), возвращающих 0 байт:

read(6, "", 512)   = 0
read(6, "", 512)   = 0
[...]
read(6, "", 512)   = 0
write(1, "\0\0\0\0\0"..., 10240) = 10240
read(6, "", 512)   = 0
[...]

lsof сказал, что файловый дескриптор 6 является чьим-то почтовым ящиком.

С помощью apt-get source tar я загрузил исходный код и начал искать в нём системные вызовы read(), которые не проверяют завершение файла. Разобрав несколько уровней косвенной адресации, я нашёл очевидное место, где вроде бы такая проверка опущена, а именно в функции sparse_dump_region из файла sparse.cs. И тут я кое-что вспомнил.

Несколько месяцев назад мы столкнулись с проблемой NFS в Alpine. Работая над этим багом, я сделал трассировку процесса Alpine и заметил, среди прочего, что для изменения размера почтовых ящиков он использует ftruncate(); иногда он расширяет их, временно создавая разреженный раздел файла, пока не заполнит его, и, возможно, иногда сжимает его. Казалось, это совпадало с нынешней ситуацией: разреженные области связаны, а сокращение размера файла с помощью ftruncate() создаёт ситуацию, когда tar неожиданно для себя сталкивается с завершением файла.

(Это даже объясняет, почему tar иногда восстанавливается; если позже в ящик вдруг пришла новая почта, тот возвращается к ожидаемому размеру, и tar больше не сталкивается с неожиданным завершением файла).

Я немного повозился в GDB на отладочных символах Ubuntu и исходном коде пакета tar, который я получил, и смог воспроизвести ошибку, хотя она несколько отличалась от моей первоначальной теории. Оказалось, что sparse_dump_region не сбрасывает разреженные области файла, а сбрасывает не разреженные (ну конечно), и используется для всех файлов (разреженные или нет), если вы запускаете tar с аргументом --sparse. Таким образом, фактическая ошибка заключается в том, что если вы запускаете GNU Tar с аргументом --sparse и файл сжимается в процессе его чтения, tar не может правильно обработать конец файла, полученный ранее, чем ожидалось. Если файл снова увеличивается, tar восстанавливает работу.

(За исключением случаев, когда файл разрежен только в конце и сжимается только в этом месте. В таком случае всё в порядке).

Мне подумалось, что всё то же самое я мог проверить много лет назад на наших файл-серверах OmniOS. Там есть способы трассировки системных вызовов программы и аналоги lsof, и я мог бы найти и посмотреть исходный код своей версии GNU Tar и запустить его с отладчиком OmniOS (хотя там у нас вроде не установлена GDB), и так далее. Но я этого не сделал. Вместо этого мы пожали плечами и двинулись дальше. Потребовалось переместить файловую систему под Ubuntu, чтобы я пошевелил пальцем и разобрался в проблеме.

(Дело не только в инструментах и окружении; мы ещё автоматически предположили, что на OmniOS стоит какая-то старая неподдерживаемая версия GNU Tar, которую нет смысла исследовать, ведь проблему конечно же решили в более новой версии).

P.S.: Наверное, в качестве быстрого исправления мы просто запретим Amanda использовать параметр tar --sparse при резервном копировании. Почтовые ящики не должны быть разреженными, а если такое случится, мы всё равно сжимаем бэкапы файловой системы, так что все эти нулевые байты хорошо сожмутся.

P.P.S.: Я не пытался сообщить о баге разработчикам GNU Tar, потому что обнаружил его только в пятницу, а университет сейчас на зимних каникулах. Не стесняйтесь сделать это раньше меня.
+21
7.4k 17
Comments 12
Top of the day