Если вы понимаете данный баг, то вы знаете питон лучше 95% людей
А если нет, то вы многое узнаете про то, как работает память и почему мутабельностью стоит пользоваться с осторожностью. Недавно я увидел один из лучших багов в CPython за долгое время. А я видел много багов 🌚️️
Вот код, который делает две критичные безумные вещи (попробуйте их найти прежде, чем читать дальше):
class Evil: def __eq__(self, other): return other
leaked = vars(list) == Evil() name = "example" leaked[name] = lambda self: "probe" print(getattr(list, name)([])) del leaked[name] print(hasattr(list, name))
Разбор бага
Во-первых, что произойдет? 1. Мы мутируем встроенный и иммутабельный тип list, хотя такое должно быть невозможно 2. Интерпретатор закрашится; не упадет с исключением, а словит core dump на уровне C кода
Но почему? Пройдемся по каждой строке. Со ссылками на исходники: кликайте и читайте!
1. Сначала мы создадим класс Evil, который просто возвращает из __eq__ второй объект, который ему передали. Так можно делать, тут нет ничего сломанного. 2. Далее, мы сравниваем vars(list) с Evil, и вот тут как раз в leaked попадет второй объект из Evil.__eq__, в нашем случае vars(list) 3. vars возвращает вам list.__dict__, который является не обычным dict, а types.MappingProxyType, то есть иммутабельным маппингом поверх оригинального значения. Добавлять в него ключи нельзя. Потому что мы не хотим, чтобы в список или другие типы нам подкидывали какие-то новые методы во время работы программы 4. Как работает сравнение для mappingproxy? mappingproxy хранит в себе оригинальный мутабельный словарь, который он "проксирует" или "защищает от изменений". И сравнивает на самом деле не себя, а оригинальный объект 5. В случае с list.__dict__ мы получаем PyDictProxy_New(self->tp_dict), где хранится тот самый настоящий и защищенный __dict__ из типа list, который обычно не доступен вне C кода 6. При сравнении mappingproxy разворачивается и достает из себя ->mapping, тот самый чистый и мутабельный ->tp_dict 7. Теперь у нас есть ->tp_dict, мы можем в него добавлять методы: leaked[name] = lambda self: "probe". Они будут работать. Мы только что достигли пункта 1. и мутировали встроенный Python тип без единого импорта 8. Далее происходит еще более дикое. Мы удаляем метод, который добавили через del leaked[name] 9. Питон не ожидает такого: методы у встроенных типов не могут появляться и исчезать. И при следующем обращении к hasattr(list, name) крашится вот тут на обращении к уже освобожденной памяти. EXC_BAD_ACCESS, пункт 2. пал
пу-пу-пу
Фикс
Баг: https://github.com/python/cpython/issues/152405
Как такое чинить? 1. Нужно сохранить обратную совместимость для всех видов сравнений. Менять типы или значения нельзя 2. Необходимо убрать креш и мутацию типа 3. Сильно раздувать потребление памяти / время работы тоже нельзя
Мой PR: https://github.com/python/cpython/pull/152483
Что он делает? Если мы сравниваем прокси поверх обычного словаря, но не с известными нам безопасными типами, то мы делаем копию словаря и сравниваем ее:
if ( PyDict_CheckExact(v->mapping) && !(PyAnyDict_CheckExact(w) || PyODict_CheckExact(w)) ) { // So, instead we send a copy: PyObject *copy = PyDict_Copy(v->mapping); if (copy == NULL) { return NULL; } PyObject *res = PyObject_RichCompare(copy, w, op); Py_DECREF(copy); return res; }
Таким образом - все ошибки выше уходят. Любая мутация останется в копии. Доп память не тратится в большом количестве популярных случаев. Данный баг все еще есть на всех версиях питона. Я вам его не показывал, вы ничего не видели.
Обсуждение: какие у вас были самые кринжовые / прикольные баги?
| Поддержать | YouTube | GitHub | Чат |