首页 > 解决方案 > 是否可以恢复损坏的“interned”字节对象

问题描述

众所周知,bytesCPython 会自动“实习”小对象(类似于字符串的intern函数)。更正:正如@a​​barnert 所解释的,它更像是整数池而不是内部字符串。

是否有可能在它们被“实验性”第三方库损坏后恢复内部字节对象,或者是重新启动内核的唯一方法?

可以使用 Cython 功能(Cython>=0.28)完成概念证明:

%%cython
def do_bad_things():
   cdef bytes b=b'a'
   cdef const unsigned char[:] safe=b  
   cdef char *unsafe=<char *> &safe[0]   #who needs const and type-safety anyway?
   unsafe[0]=98                          #replace through `b`

或如@jfs 所建议的那样ctypes

import ctypes
import sys
def do_bad_things():
    b = b'a'; 
    (ctypes.c_ubyte * sys.getsizeof(b)).from_address(id(b))[-2] = 98

显然,通过滥用 C 功能,do_bad_things将不可变(或 CPython 认为的)对象b'a'更改为b'b',并且因为这个对象bytes是实习的,我们可以看到之后发生了不好的事情:

>>> do_bad_things() #b'a' means now b'b'
>>> b'a'==b'b'  #wait for a surprise  
True
>>> print(b'a') #another one
b'b'

可以恢复/清除字节对象池,这b'a'意味着b'a'再次?


一点旁注:似乎不是每个bytes-creation 进程都在使用这个池。例如:

>>> do_bad_things()
>>> print(b'a')
b'b'
>>> print((97).to_bytes(1, byteorder='little')) #ord('a')=97
b'a'

标签: pythonpython-3.xcpythonpython-internals

解决方案


Python 3 不像它那样实习bytes对象str。相反,它像使用int.

这在幕后是非常不同的。不利的一面是,这意味着没有要操作的表(带有 API)。从好的方面来说,这意味着如果你能找到静态数组,你就可以修复它,就像处理 int 一样,因为数组索引和字符串的字符值应该是相同的。

如果您查看bytesobject.c,则该数组在顶部声明:

static PyBytesObject *characters[UCHAR_MAX + 1];

…然后,例如,在PyBytes_FromStringAndSize

if (size == 1 && str != NULL &&
    (op = characters[*str & UCHAR_MAX]) != NULL)
{
#ifdef COUNT_ALLOCS
    one_strings++;
#endif
    Py_INCREF(op);
    return (PyObject *)op;
}

请注意,数组是static,因此无法从该文件外部访问它,并且它仍在对对象进行引用计数,因此调用者(甚至是解释器中的内部内容,更不用说您的 C API 扩展)无法判断有什么特别的事情发生.

因此,没有“正确”的方法来清理它。

但是,如果您想变得 hacky……</p>

如果您有对任何单字符字节的引用,并且您知道它应该是哪个字符,则可以到达数组的开头,然后清理整个内容。

除非你搞砸的比你想象的还要多,否则你可以构造一个单字符bytes并减去它应该是的字符。PyBytes_FromStringAndSize("a", 1)将返回应该是的对象'a',即使它碰巧实际持有'b'。我们怎么知道?因为这正是您要解决的问题。

实际上,您可能有一些方法可以使事情变得更糟……这一切似乎都不太可能,但为了安全起见,让我们使用一个您不太可能破坏的角色a,例如\x80

PyBytesObject *byte80 = (PyBytesObject *)PyBytes_FromStringAndSize("\x80", 1);
PyBytesObject *characters = byte80 - 0x80;

唯一需要注意的是,如果您尝试使用 Pythonctypes而不是 C 代码执行此操作,则需要格外小心,1但由于您没有使用ctypes,所以不用担心。

所以,现在我们有一个指向 的指针characters,我们可以遍历它。我们不能只删除对象以“取消实习”它们,因为这会影响任何引用它们的人,并可能导致段错误。但我们不必这样做。表中的任何对象,我们都知道它应该是什么——<code>characters[i] 应该是一个单字符bytes,其一个字符是i. 因此,只需将其设置回那个,使用类似这样的循环:

for (size_t char i=0; i!=UCHAR_MAX; i++) {
    if (characters[i]) {
        // do the same hacky stuff you did to break the string in the first place
    }
}

这里的所有都是它的。


好吧,除了编译。2

幸运的是,在交互式解释器中,每个完整的顶级语句都是它自己的编译单元,所以……你应该可以在运行修复程序后输入任何新行。

但是你导入的一个模块,必须编译,而你有损坏的字符串?你可能搞砸了它的常数。除了强制重新编译和重新导入每个模块之外,我想不出一个好方法来清理它。


1. 编译器可能会在你的b'\x80'参数到达 C 调用之前把它变成错误的东西。你会惊讶于所有你认为你正在经过的地方 ac_char_p并且它实际上正在神奇地转换为和从bytes. 可能更好地使用POINTER(c_uint8).

2. 如果你在其中编译了一些代码b'a',consts 数组应该有一个对 的引用b'a',这将得到修复。但是,由于bytes编译器知道它们是不可变的,如果它知道b'a' == b'b',它实际上可能会存储指向b'b'单例的指针,原因与此相同123456 is 123456,在这种情况下修复b'a'可能无法真正解决问题。


推荐阅读