代码之家  ›  专栏  ›  技术社区  ›  ead

是否可以还原损坏的interned bytes对象

  •  7
  • ead  · 技术社区  · 7 年前

    众所周知,这是一个小问题 bytes -对象由CPython自动“插入”(类似于 intern -函数(用于字符串)。 更正: explained 通过@abarnert,它更像是整数池,而不是内部字符串。

    在被“实验性”第三方库破坏后,是否有可能恢复interned bytes对象,或者是重启内核的唯一方法?

    可以使用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' 因为这个 字节 -对象被拘留后,我们可以看到不好的事情发生:

    >>> 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' 再一次


    有一点旁注:似乎不是每一个 字节 -创建进程正在使用此池。例如:

    >>> do_bad_things()
    >>> print(b'a')
    b'b'
    >>> print((97).to_bytes(1, byteorder='little')) #ord('a')=97
    b'a'
    
    2 回复  |  直到 7 年前
        1
  •  3
  •   abarnert    7 年前

    Python3不是实习生 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扩展名)无法判断是否有任何特殊情况。

    所以,没有“正确”的方法来清理这个。

    但是如果你想变得更黑

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

    除非你把事情搞得比你想象的还要糟,否则你可以只构造一个字符 字节 然后减去原来的字符 想象上的 成为。 PyBytes_FromStringAndSize("a", 1) 将返回 想象上的 成为 'a' ,即使发生在 事实上 持有 'b' .我们怎么知道的?因为这正是你想要解决的问题。

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

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

    唯一的另一个警告是,如果您尝试从Python使用 ctypes 而不是从C代码,它需要一些额外的注意, 1. 但既然你没有使用 ctypes ,我们不用担心。

    现在我们有了一个指向 characters ,我们可以走过去。我们不能仅仅删除对象来“取消匹配”它们,因为这将导致任何引用它们的人,并可能导致segfault。但我们不必这么做。桌上的任何物体,我们都知道它应该是什么 characters[i] 应该是一个字符 字节 谁的一个角色是 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调用之前,将参数转换为错误的东西。你会惊讶于所有你认为你要经过的地方 c_char_p 事实上,它被神奇地转换成 字节 .也许最好使用 POINTER(c_uint8) .

    2.如果你用 b'a' 在其中,consts数组应该引用 b'a' ,这将得到修复。但是,自从 字节 如果编译器知道的话,它们是不可变的 b'a' == b'b' ,它实际上可能存储指向 b'b' 而不是单身汉,因为同样的原因 123456 is 123456 是真的,在这种情况下 b'a' 可能无法真正解决问题。

        2
  •  2
  •   ead    7 年前

    我遵循了@abarnert的伟大解释,下面是我在Cython中对他的想法的实现。

    需要考虑的事项:

    1. 有一个字节池(就像整数的情况一样),而不是一个动态结构(就像字符串内部处理的情况一样)。因此,我们可以强制执行此池中的所有字节对象,并确保它们具有正确的值。
    2. 仅字节通过 PyBytes_FromStringAndSize 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'