У меня возникают проблемы с управлением памятью, связанными с bytes
в Python3.2. В некоторых случаях буфер ob_sval
, похоже, содержит память, которую я не могу объяснить.
Для конкретного защищенного приложения мне необходимо убедиться, что память "обнулена" и возвращается в ОС как можно скорее после того, как она больше не используется. Поскольку повторная компиляция Python на самом деле не вариант, я пишу модуль, который можно использовать с LD_PRELOAD, чтобы:
- Отключить объединение пулов, заменив
PyObject_Malloc
наPyMem_Malloc
,PyObject_Realloc
наPyMem_Realloc
иPyObject_Free
наPyMem_Free
(например: что вы получите, если вы скомпилируете безWITH_PYMALLOC
). Мне все равно, если память объединена или нет, но это, пожалуй, самый простой способ. - Оберните
malloc
,realloc
иfree
, чтобы отслеживать, сколько памяти запрошено, иmemset
все до0
, когда оно будет выпущено.
При беглом взгляде этот подход, похоже, отлично работает:
>>> from ctypes import string_at
>>> from sys import getsizeof
>>> from binascii import hexlify
>>> a = b"Hello, World!"; addr = id(a); size = getsizeof(a)
>>> print(string_at(addr, size))
b'\x01\x00\x00\x00\xd4j\xb2x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00'
>>> del a
>>> print(string_at(addr, size))
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x00'
Заблудившийся \x13
в конце нечетный, но не исходит из моего первоначального значения, поэтому сначала я предположил, что все в порядке. Я быстро нашел примеры, где все было не так хорошо:
>>> a = b'Superkaliphragilisticexpialidocious'; addr = id(a); size = getsizeof(a)
>>> print(string_at(addr, size))
b'\x01\x00\x00\x00\xd4j\xb2x#\x00\x00\x00\x9cb;\xc2Superkaliphragilisticexpialidocious\x00'
>>> del s
>>> print(string_at(addr, size))
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00))\n\x00\x00ous\x00'
Здесь сохранились последние три байта ous
.
Итак, мой вопрос:
Что происходит с оставшимися байтами для объектов bytes
и почему они не удаляются при вызове del
?
Я предполагаю, что в моем подходе отсутствует нечто похожее на realloc
, но я не вижу, что бы это было в bytesobject.c
.
Я попытался количественно определить количество оставшихся байтов, оставшихся после сбора мусора, и, похоже, в некоторой степени предсказуемо.
from collections import defaultdict
from ctypes import string_at
import gc
import os
from sys import getsizeof
def get_random_bytes(length=16):
return os.urandom(length)
def test_different_bytes_lengths():
rc = defaultdict(list)
for ii in range(1, 101):
while True:
value = get_random_bytes(ii)
if b'\x00' not in value:
break
check = [b for b in value]
addr = id(value)
size = getsizeof(value)
del value
gc.collect()
garbage = string_at(addr, size)[16:-1]
for jj in range(ii, 0, -1):
if garbage.endswith(bytes(bytearray(check[-jj:]))):
# for bytes of length ii, tail of length jj found
rc[jj].append(ii)
break
return {k: len(v) for k, v in rc.items()}, dict(rc)
# The runs all look something like this (there is some variation):
# ({1: 2, 2: 2, 3: 81}, {1: [1, 13], 2: [2, 14], 3: [3, 4, 5, 6, 7, 8, 9, 10, 11, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 83, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100]})
# That is:
# - One byte left over twice (always when the original bytes object was of lengths 1 or 13, the first is likely because of the internal 'characters' list kept by Python)
# - Two bytes left over twice (always when the original bytes object was of lengths 2 or 14)
# - Three bytes left over in most other cases (the exact ones varies between runs but never has '12' in it)
# For added fun, if I replace the get_random_bytes call with one that returns an encoded string or random alphanumerics then results change slightly: lengths of 13 and 14 are now fully cleared too. My original test string was 13 bytes of encoded alphanumerics, of course!
Изменить 1
Я изначально выразил обеспокоенность тем, что если объект bytes
используется в функции, он вообще не очищается:
>>> def hello_forever():
... a = b"Hello, World!"; addr = id(a); size = getsizeof(a)
... print(string_at(addr, size))
... del a
... print(string_at(addr, size))
... gc.collect()
... print(string_at(addr, size))
... return addr, size
...
>>> addr, size = hello_forever()
b'\x02\x00\x00\x00\xd4J0x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00'
b'\x01\x00\x00\x00\xd4J0x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00'
b'\x01\x00\x00\x00\xd4J0x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00'
>>> print(string_at(addr, size))
b'\x01\x00\x00\x00\xd4J0x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00'
Оказывается, это искусственная проблема, которая не подпадает под мои требования. Вы можете увидеть комментарии к этому вопросу для деталей, но проблема возникает из-за того, что кортеж hello_forever.__code__.co_consts
будет содержать ссылку на Hello, World!
даже после того, как a
будет удален из locals
.
В реальном коде "безопасные" значения будут поступать из внешнего источника и никогда не будут жестко закодированы и впоследствии удалены так.
Изменить 2
Я также выразил недоумение по поводу поведения с strings
. Было указано, что они, вероятно, также испытывают ту же проблему, что и bytes
по отношению к жесткому их кодированию в функциях (например, артефакт моего тестового кода). С ними есть еще два риска, которые я не смог продемонстрировать как проблема, но буду продолжать расследование:
- Интерпретация строк выполняется Python в разных точках, чтобы ускорить доступ. Это не должно быть проблемой, поскольку интернированные строки должны быть удалены при утрате последней ссылки. Если это окажется проблемой, следует заменить
PyUnicode_InternInPlace
так, чтобы он ничего не делал. - Строки и другие "примитивные" типы объектов в Python часто содержат "свободный список", чтобы ускорить получение памяти для новых объектов. Если это окажется проблемой, методы
*_dealloc
вObjects/*.c
могут быть заменены.
Я также считал, что я вижу проблему с тем, что экземпляры классов не получают нуль правильно, но теперь я считаю, что это была ошибка с моей стороны.
Спасибо
Большое спасибо @Dunes и @Kevin за то, что они указали на проблемы, которые запутывали мой первоначальный вопрос. Эти проблемы были выше в разделе "редактировать" выше для справки.