Можно ли восстановить поврежденные «интернированные» байты-объекты - PullRequest
0 голосов
/ 05 июня 2018

Хорошо известно, что небольшие bytes -объекты автоматически "интернируются" CPython (аналогично intern -функции для строк). Исправление: Как объяснил @abarnert , это больше похоже на целочисленный пул, чем на внутренние строки.

Возможно ли восстановить внутренние байтовые объекты после того, как онибыли повреждены, скажем, «экспериментальной» сторонней библиотекой или это единственный способ перезапустить ядро?

Подтверждение концепции можно сделать с помощью функциональности 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 голосов
/ 06 июня 2018

Я последовал замечательному объяснению @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'
0 голосов
/ 06 июня 2018

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) не могутскажи, что происходит что-то особенное.

Итак, нет "правильного" способа убрать это.

Но если вы хотите стать хакером ...

Если у вас естьссылка на любой из байтов с одним символом, и вы знаете, каким символом он должен был быть, вы можете добраться до начала массива и затем очистить все это.

Если вы не испортилидаже больше, чем вы думаете, вы можете простовозьмите один символ bytes и вычтите символ, которым он должен был быть .PyBytes_FromStringAndSize("a", 1) вернет объект, который должен быть 'a', даже если это произойдет с на самом деле hold 'b'.Откуда мы это знаем?Потому что это именно та проблема, которую вы пытаетесь решить.

На самом деле, возможно, есть способы, которыми вы могли бы сломать вещи еще хуже ... которые кажутся маловероятными, но для безопасности давайте использовать персонажа, которого выменее вероятно, что он сломался, чем a, как \x80:

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

Единственное другое предостережение: если вы попытаетесь сделать это из Python с ctypes вместо кода C, это будеттребуется дополнительная осторожность, 1 , но так как вы не используете ctypes, давайте не будем об этом беспокоиться.

Итак, теперь у нас есть указатель на characters, мы можем идтиЭто.Мы не можем просто удалить объекты, чтобы «удалить их», потому что это будет мешать любому, кто имеет ссылку на какой-либо из них, и, вероятно, приведет к segfault.Но мы не должны.Любой объект, который находится в таблице, мы знаем, каким он должен быть - 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.И вы будете удивлены во всех местах, где, по вашему мнению, вы проходите вокруг c_char_p, и он действительно волшебным образом превращается в bytes и обратно.Вероятно, лучше использовать POINTER(c_uint8).

2.Если вы скомпилировали некоторый код с b'a', массив consts должен иметь ссылку на b'a', которая будет исправлена.Но, поскольку bytes известны как неизменяемые для компилятора, если он знает, что b'a' == b'b', он может вместо этого хранить указатель на синглтон b'b', по той же причине, что 123456 is 123456 верно, и в этом случае исправлениеb'a' может на самом деле не решить проблему.

...