Tre*_*ley 14 python memory memory-pool
我遇到了与bytesPython3.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'
Run Code Online (Sandbox Code Playgroud)
最后的错误\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'
Run Code Online (Sandbox Code Playgroud)
这里最后三个字节ous幸免于难.
所以,我的问题:
bytes对象的剩余字节发生了什么,为什么在del调用它们时它们不会被删除?
我猜我的方法缺少类似于a的东西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!
Run Code Online (Sandbox Code Playgroud)
编辑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'
Run Code Online (Sandbox Code Playgroud)
事实证明,这是一个人为的问题,我的要求没有涵盖.您可以查看此问题的注释以获取详细信息,但问题来自hello_forever.__code__.co_consts元组将包含引用的方式,Hello, World!即使a从中删除了locals.
在实际代码中,"安全"值将来自外部源,并且永远不会被硬编码,之后会像这样被删除.
编辑2
我也对这种行为表示了困惑strings.已经指出它们可能也遇到bytes与在函数中对它们进行硬编码相同的问题(例如:我的测试代码的工件).他们还有另外两个风险,我无法证明这是一个问题但会继续调查:
PyUnicode_InternInPlace以便它不做任何事情.*_dealloc方法Objects/*.c.我也相信我看到类实例没有正确归零的问题,但我现在认为这是我的错误.
谢谢
非常感谢@Dunes和@Kevin指出了混淆我原来问题的问题.这些问题已在上面的"编辑"部分中留下,以供参考.
一般来说,您无法保证内存将被清零,甚至无法及时收集垃圾。有一些启发式方法,但如果您担心安全性到这种程度,那可能还不够。
相反,您可以直接处理可变类型,例如bytearray将每个元素显式归零:
# Allocate (hopefully without copies)
bytestring = bytearray()
unbuffered_file.readinto(bytestring)
# Do stuff
function(bytestring)
# Zero memory
for i in range(len(bytestring)):
bytestring[i] = 0
Run Code Online (Sandbox Code Playgroud)
安全地使用此功能将要求您仅使用您知道不会制作临时副本的方法,这可能意味着您自己进行复制。但这并不能阻止某些缓存搞乱事情。
zdan 在另一个问题中给出了一个很好的建议:使用子进程来完成工作并在完成后用火杀死它。
事实证明,这个问题是我自己的代码中一个绝对愚蠢的错误memset。在“接受”这个答案之前,我将联系@Calyth,他慷慨地为这个问题添加了赏金。
简而言之,简单地说,malloc/free包装函数的工作原理如下:
malloc调用请求N内存字节。
N+sizeof(size_t)字节。N范围的开头并返回偏移指针。free要求返回内存并传入该偏移指针。
memset以确保所有内容都设置为零(该库是在没有优化的情况下编译的,以防止编译器忽略 )memset。我的错误是调用等效项memset(actual_pointer, 0, requested_size)而不是memset(actual_pointer, 0, actual_size).
我现在面临着一个令人难以置信的问题:为什么不总是有“3”个剩余字节(我的单元测试验证我随机生成的字节对象都不包含任何空值)以及为什么字符串不会也有这个问题(是吗? Python 可能过度分配了字符串缓冲区的大小)。然而,这些都是以后的问题了。
所有这一切的结果是,确保字节和字符串在被垃圾收集后设置为零是相对容易的!(关于硬编码字符串、空闲列表等有很多警告,因此尝试执行此操作的其他人应该仔细阅读原始问题、问题的评论以及这个“答案”。)
| 归档时间: |
|
| 查看次数: |
1013 次 |
| 最近记录: |