
Пользователь Хабра под ником virtual-anvil описал случай: после перезапуска Windows перестали открываться привычные программы, потому что их exe-файлы пропали. Папки на диске оставались, а содержимое внутри исчезало. В каталоге C:\custom, где лежали программы, проекты и другие данные, осталось почти пусто — около 800 ГБ данных удалилось. Виновником оказался не диск и не вирус, а Telegram Desktop, точнее его подсистема проверки орфографии в версиях 7.1.0 и 7.1.1.
Главный вывод истории лежит не в плоскости «Telegram плохой». Каждое отдельное решение в этой цепочке выглядело разумным: сохранить пользовательские слова во внутренний файл, заменить повреждённый служебный файл каталогом, взять готовую функцию рекурсивного удаления. Опасной комбинация стала потому, что перед удалением никто не проверил итоговый путь. Ниже — как именно сложилась цепочка и где стоял предохранитель, которого не хватило.
Как локализовали источник
Первая гипотеза автора была про ошибку обновления: Telegram стоял в C:\custom\program\Telegram Desktop, мог чистить свои старые файлы и задевать соседние. Проверка эту версию отмела. Мессенджер переустановили в C:\telegram\Telegram Desktop, папку загрузок тоже вынесли за пределы C:\custom, а в сам каталог положили тестовый Текстовый документ.txt. После запуска Telegram файл исчезал, после повторного создания и перезапуска — исчезал снова. Программа из C:\telegram продолжала вычищать никак не связанную с ней папку.
Сравнение версий дало границу: на 6.9.4 всё работало, после обновления до 7.1.1 файлы удалялись. Дальше в дело пошёл Process Monitor. Тестовому файлу через права Windows запретили удаление — Telegram дошёл до операции Delete, получил отказ, и в логе остался весь путь процесса: открытие C:\custom, чтение списка файлов и папок, обход вложенных каталогов, запрос прав Read Attributes и Delete. Между открытием папки и попыткой удаления проходило около 7 секунд. Стеки вызовов вели внутрь Telegram.exe — это была штатная логика приложения, а не сторонний процесс. С этими данными автор завёл issue #31170, и разработчик ошибку подтвердил.
Почему целью стала именно папка custom
Telegram хранит добавленные пользователем слова в файле с именем custom; за эту часть отвечает библиотека lib_spellcheck. В версии 7.1.0 изменили работу пользовательских слов со встроенной проверкой орфографии Windows: после коммита 4929c892 код для Windows тоже начал обращаться к внутреннему файлу со словами. Путь к этому файлу задавался уже после return, который завершал функцию раньше нужного. Если упростить старую логику:
if (IsSystemSpellchecker()) { return; }
SetWorkingDirPath(DictionariesPath());
На Windows со встроенной проверкой орфографии условие срабатывало, функция выходила, и рабочий путь оставался пустым. Дальше библиотека строила путь к пользовательскому словарю:
customWordsFile = WorkingDirPath() + "/custom";
Из пустой строки получалось /custom. Qt интерпретировал это как папку custom в корне текущего диска — то есть C:\custom. Код рассчитывал, что по этому пути лежит файл. Обнаружив вместо файла каталог, он считал это признаком повреждения и удалял его:
if (QFileInfo(path).isDir()) { QDir(path).removeRecursively(); }
В штатном сценарии здесь удалялась бы небольшая служебная папка внутри каталога Telegram. Из-за пустого пути та же строчка добралась до пользовательских данных. Отдельная деталь: QDir::removeRecursively() продолжает работу, даже если часть файлов удалить не удалось. Поэтому занятые системой DLL остались на месте, а всё остальное пропало — отсюда и картина с «пустыми папками».
Цепочка и две недостающие проверки
Ошибка раскладывается в короткую последовательность: Windows-код обратился к внутреннему словарю → путь к словарю не был задан → пустой путь превратился в /custom → Qt превратил его в C:\custom → запустилось рекурсивное удаление. В этой же истории есть ирония: коммит 40441b29, участвовавший в цепочке, сам чинил другую ошибку с удалением каталогов словарей.
Исправление оказалось маленьким. В коммите 9c316281 присваивание пути перенесли до return:
SetWorkingDirPath(DictionariesPath());
if (IsSystemSpellchecker()) { return; }
Плюс коммитом ac399e5c в саму библиотеку добавили проверку: если путь пустой, работать со словарём нельзя. То есть закрыли обе точки — и порядок операций, и валидацию входных данных перед разрушающим действием. Формулировка из разбора: перед таким удалением программа должна проверить хотя бы две вещи — что путь не пустой и что он действительно ведёт внутрь папки приложения.
Что из этого следует для тех, кто пишет похожий код
Класс ошибки повторяется в разных проектах: разрушающая операция получает путь, собранный из нескольких частей, и одна из частей оказывается пустой или неожиданной. Конкатенация строк для путей это маскирует — "" + "/custom" не падает, а даёт валидную с виду строку. Проверка «этот путь внутри моего рабочего каталога» дешевле любого разбора логов Process Monitor постфактум.
Полезно относиться к removeRecursively() и её аналогам как к операции с обязательным предусловием, а не как к обычному вызову. Минимум: путь непустой, он абсолютный и является поддиректорией известного корня; выход за пределы корня — это исключение, а не «считаем каталог повреждённым и удаляем». Поведение «продолжать, несмотря на ошибки удаления» тоже стоит осознавать: оно удобно для служебной уборки и опасно, когда цель выбрана неверно, потому что частичный успех выглядит как «файлы просто пропали».
Баг попал в релизы 7.1.0 и 7.1.1, исправление вошло в 7.1.2, версии с ошибкой были доступны около 66 часов. На Linux и macOS такую же цепочку не воспроизвели — там путь к системному словарю задавался иначе. Для пользователя вывод сводится к банальному, но подтверждённому этим случаем: резервная копия важных рабочих каталогов защищает от чужих ошибок в чужом коде лучше, чем репутация популярного продукта.
Обсудить проект
Остались вопросы? Напишите нам
Оставьте имя и телефон — перезвоним в течение часа, разберём задачу и предложим решение.