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

时间:2018-06-05 20:45:42

标签: python python-3.x cpython python-internals

众所周知,小bytes - 对象由CPython自动“实现”(类似于intern - 字符串函数)。 更正: @abarnert的explained更像是整数池而不是实习字符串。

是否可以通过假设“实验性”第三方库来恢复被破坏的字节对象,或者是重启内核的唯一方法?

可以使用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 - 创建过程都在使用此池。例如:

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

2 个答案:

答案 0 :(得分:3)

Python 3没有以bytes的方式对象str对象。相反,它使用int保持静态数组。

这在封面上是非常不同的。在不利方面,这意味着没有表格(带有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扩展名)不能告诉你有什么特别的事情发生。

所以,没有&#34;正确&#34;清理它的方法。

但如果你想得到hacky ......

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

除非你搞得比你想象的还要多,否则你可以构造一个字符bytes并减去所谓的字符。 PyBytes_FromStringAndSize("a", 1)会将假设的的对象返回'a',即使实际暂挂'b'也是如此}。我们怎么知道的?因为这正是您尝试解决的问题。

实际上,你可能有办法让事情变得更糟......这一切似乎都不太可能,但为了安全起见,让我们使用一个你不太可能破坏的角色而不是{{1} },如a

\x80

唯一的另一个警告是,如果你试图用Python PyBytesObject *byte80 = (PyBytesObject *)PyBytes_FromStringAndSize("\x80", 1); PyBytesObject *characters = byte80 - 0x80; 而不是C代码来做这件事,那么需要额外注意, 1 但是因为你&#39;不要使用ctypes,请不要担心。

所以,现在我们有一个指向ctypes的指针,我们可以走了它。我们不能将对象删除到&#34; unintern&#34;他们,因为这会影响任何提及其中任何一个的人,并可能导致段错误。但我们不必这样做。表格中的任何对象,我们知道它应该是什么 - characters应该是一个字符characters[i],其中一个字符是bytes 。所以只需将它设置回来,使用类似这样的循环:

i

这就是它的全部内容。

好吧,除了编译。 2

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

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

<子> 1。在进入C调用之前,编译器可能会将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 } } 参数转换为错误的参数。你会认为自己在b'\x80'周围经过的所有地方都会感到惊讶,而且它实际上会被c_char_p神奇地转换为。{1}}。可能更好地使用bytes

<子> 2。如果您在其中编译了一些代码POINTER(c_uint8),则consts数组应该引用b'a',这将得到修复。但是,由于b'a'已知为编译器不可变,如果它知道bytes,它实际上可能存储指向b'a' == b'b'单例的指针,原因与{{1}相同。是的,在这种情况下,修复b'b'实际上可能无法解决问题。

答案 1 :(得分:2)

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

需要考虑的事项:

  1. 有一个字节池(就像整数一样)而不是动态结构(就像字符串实例一样)。因此,我们可以强制强制此池中的所有字节对象,并确保它们具有正确的值。
  2. 只有通过PyBytes_FromStringAndSizePyBytes_FromString构建的字节对象才使用内部池,因此请务必使用它们。
  3. 这导致以下实施:

    %%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. 没有使用可能损坏的字节对象。
    3. 现在:

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