是否有可能恢复损坏的“interned”字节对象

Is it possible to restore corrupted “interned” bytes-objects

众所周知,小型 bytes-objects 由 CPython 自动 "interned"(类似于 @abarnert 的 intern-function for strings). Correction: As 它更像是整数池而不是实习字符串。

是否可以在被"experimental"第三方库损坏后恢复驻留的字节对象,或者是重启内核的唯一方法?

可以使用 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-对象是interned,我们可以看到之后发生了不好的事情:

>>> 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'

可以restore/clear字节对象池,所以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'

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

一样保留它们的静态数组

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

如果你查看 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 扩展)不能说有什么特别的事情发生。

所以,没有 "correct" 清理它的方法。

但是如果你想变黑……

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

除非你比你想象的更糟,否则你可以构造一个单字符 bytes 并减去 假设 的字符。 PyBytes_FromStringAndSize("a", 1) 将 return 应该 的对象 'a',即使 实际上 ] 坚持'b'。我们怎么知道的?因为这正是您要解决的问题。

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

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

唯一的其他警告是,如果您尝试从 Python 使用 ctypes 而不是从 C 代码执行此操作,则需要格外小心,1 但是因为你没有使用 ctypes,所以我们不用担心。

所以,现在我们有了指向 characters 的指针,我们可以遍历它。我们不能只删除对象以 "unintern" 它们,因为这将限制任何引用它们的人,并可能导致段错误。但我们不必这样做。 table 中的任何对象,我们知道它应该是什么——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.编译器甚至可能在到达 C 调用之前将您的 b'\x80' 参数变成错误的东西。你会惊讶于你认为你正在传递 c_char_p 的所有地方,它实际上神奇地转换为 bytes 和从 bytes 转换。可能最好使用 POINTER(c_uint8).

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

我遵循了@abarnert 的精彩解释,这是我在 Cython 中实现他的想法的方法。

需要考虑的事项:

  1. 有一个字节池(就像整数的情况)而不是动态结构(就像字符串驻留的情况)。所以我们可以暴力破解这个池中的所有字节对象并确保它们具有正确的值。
  2. 只有通过 PyBytes_FromStringAndSize and PyBytes_FromString 构造的字节对象正在使用内部池,因此请务必使用它们。

这导致以下实现:

%%cython
from libc.limits cimport UCHAR_MAX
from cpython.bytes cimport PyBytes_FromStringAndSize

cdef replace_first_byte(bytes obj, unsigned char new_value):
   cdef const unsigned char[:] safe=obj  
   cdef unsigned char *unsafe=<unsigned char *> &safe[0]   
   unsafe[0]=new_value


def restore_bytes_pool():
    cdef char[1] ch
    #create all possible bytes-objects b`\x00` to b`x255`:
    for i in range(UCHAR_MAX+1):               
        ch[0]=<unsigned char>(i)
        obj=PyBytes_FromStringAndSize(ch, 1) #use it so the pool is used
        replace_first_byte(obj,i)

略有不同(在我看来对原始提案的优势):

  1. 这个版本不需要知识,字节对象池是如何构建的,它是一个连续的数组。
  2. 没有使用可能损坏的字节对象。

现在:

>>> do_bad_things()
>>> print(b'a')
b'b'

>>> restore_bytes_pool()
>>> print(b'a')
b'a'

出于测试目的,有一个函数破坏了(几乎)池中的所有对象:

def corrupt_bytes_pool():
    cdef char[1] ch
    for i in range(UCHAR_MAX+1):
        ch[0]=<unsigned char>(i)
        obj=PyBytes_FromStringAndSize(ch, 1)
        replace_first_byte(obj,98)           #sets all to b'b'