2015-02-23 17 views
14

Występują problemy z zarządzaniem pamięcią związaną z bytes w Python3.2. W niektórych przypadkach bufor ob_sval wydaje się zawierać pamięć, której nie mogę uwzględnić.Jak zapewnić, że pamięć "zer" Pythona zostanie zebrana?

Dla konkretnej bezpiecznej aplikacji muszę mieć pewność, że pamięć jest "wyzerowana" i zwrócona do systemu operacyjnego tak szybko, jak to możliwe, po tym, jak nie jest już używana. Od ponownej kompilacji Python nie jest to opcja, Piszę moduł, który może być używany z LD_PRELOAD do:

  • wyłączenia buforowania pamięci zastępując PyObject_Malloc z PyMem_Malloc, PyObject_Realloc z PyMem_Realloc i PyObject_Free z PyMem_Free (na przykład: co byś otrzymał, gdybyś skompilował to bez WITH_PYMALLOC). Nie obchodzi mnie, czy pamięć jest połączona, czy nie, ale wydaje się to najłatwiejsze.
  • Okłady malloc, realloc i free tak aby śledzić, ile pamięci jest wymagane i memset wszystkiego do 0 gdy jest on zwolniony.

Przy pobieżnym spojrzeniem, takie podejście wydaje się działać świetnie:

>>> from ctypes import string_at 
>>> from sys import getsizeof 
>>> from binascii import hexlify 
>>> a = b"Hello, World!"; addr = id(a); size = getsizeof(a) 
>>> print(string_at(addr, size)) 
b'\x01\x00\x00\x00\xd4j\xb2x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00' 
>>> del a 
>>> print(string_at(addr, size)) 
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x00' 

błędny \x13 na końcu jest dziwne, ale nie pochodzi z mojej pierwotnej wartości, więc na pierwszy Sądziłem, że to w porządku . I szybko znaleźć przykłady, gdzie rzeczy nie były tak dobre, choć:

>>> a = b'Superkaliphragilisticexpialidocious'; addr = id(a); size = getsizeof(a) 
>>> print(string_at(addr, size)) 
b'\x01\x00\x00\x00\xd4j\xb2x#\x00\x00\x00\x9cb;\xc2Superkaliphragilisticexpialidocious\x00' 
>>> del s 
>>> print(string_at(addr, size)) 
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00))\n\x00\x00ous\x00' 

Oto ostatnie trzy bajty, ous, przeżył.

Więc moje pytanie:

Co się dzieje z resztek bajtów dla bytes obiektów, a dlaczego nie zostaną usunięte podczas del nazywa się na nich?

Zgaduję, że w moim podejściu brakuje czegoś podobnego do realloc, ale nie widzę, co to byłoby w bytesobject.c.

Podjęto próbę ilościowego określenia liczby "pozostawionych" bajtów, które pozostają po zbiorze śmieci i wydaje się, że jest do przewidzenia w pewnym stopniu.

from collections import defaultdict 
from ctypes import string_at 
import gc 
import os 
from sys import getsizeof 

def get_random_bytes(length=16): 
    return os.urandom(length) 

def test_different_bytes_lengths(): 
    rc = defaultdict(list) 
    for ii in range(1, 101): 
     while True: 
      value = get_random_bytes(ii) 
      if b'\x00' not in value: 
       break 
     check = [b for b in value] 
     addr = id(value) 
     size = getsizeof(value) 
     del value 
     gc.collect() 
     garbage = string_at(addr, size)[16:-1] 
     for jj in range(ii, 0, -1): 
      if garbage.endswith(bytes(bytearray(check[-jj:]))): 
       # for bytes of length ii, tail of length jj found 
       rc[jj].append(ii) 
       break 
    return {k: len(v) for k, v in rc.items()}, dict(rc) 

# The runs all look something like this (there is some variation): 
# ({1: 2, 2: 2, 3: 81}, {1: [1, 13], 2: [2, 14], 3: [3, 4, 5, 6, 7, 8, 9, 10, 11, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 83, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100]}) 
# That is: 
# - One byte left over twice (always when the original bytes object was of lengths 1 or 13, the first is likely because of the internal 'characters' list kept by Python) 
# - Two bytes left over twice (always when the original bytes object was of lengths 2 or 14) 
# - Three bytes left over in most other cases (the exact ones varies between runs but never has '12' in it) 
# For added fun, if I replace the get_random_bytes call with one that returns an encoded string or random alphanumerics then results change slightly: lengths of 13 and 14 are now fully cleared too. My original test string was 13 bytes of encoded alphanumerics, of course! 

Edycja 1

miałem pierwotnie wyraziła zaniepokojenie faktem, że jeśli obiekt bytes jest używana w funkcji nie był sprzątany w ogóle:

>>> def hello_forever(): 
...  a = b"Hello, World!"; addr = id(a); size = getsizeof(a) 
...  print(string_at(addr, size)) 
...  del a 
...  print(string_at(addr, size)) 
...  gc.collect() 
...  print(string_at(addr, size)) 
...  return addr, size 
... 
>>> addr, size = hello_forever() 
b'\x02\x00\x00\x00\xd4J0x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00' 
b'\x01\x00\x00\x00\xd4J0x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00' 
b'\x01\x00\x00\x00\xd4J0x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00' 
>>> print(string_at(addr, size)) 
b'\x01\x00\x00\x00\xd4J0x\r\x00\x00\x00<J\xf6\x0eHello, World!\x00' 

Okazuje się, że jest to sztuczny problem, który nie jest objęty moimi wymaganiami. Możesz zobaczyć komentarze do tego pytania dla szczegółów, ale problem wynika z tego, że krotka hello_forever.__code__.co_consts będzie zawierać odniesienie do Hello, World! nawet po usunięciu z locals.

W prawdziwym kodzie "bezpieczne" wartości pochodzą z zewnętrznego źródła i nigdy nie zostaną zakodowane na stałe, a następnie usunięte w ten sposób.

Edycja 2

ja również wyrażona niejasności co do zachowania z strings. Zwrócono uwagę, że prawdopodobnie cierpią one również na ten sam problem co bytes w odniesieniu do ich kodowania w funkcjach (np .: artefakt mojego kodu testowego). Występują z nimi jeszcze dwa inne zagrożenia, których nie udało mi się wykazać jako problem, ale będę dalej badał:

  • Intrygowanie napisami odbywa się przez Pythona w różnych punktach w celu przyspieszenia dostępu. Nie powinno to stanowić problemu, ponieważ internowane ciągi powinny zostać usunięte, gdy utracono ostatnie odniesienie. Jeśli okaże się, że jest to problemem, powinna istnieć możliwość zamiany PyUnicode_InternInPlace, tak aby nic nie działo.
  • Ciągi i inne "pierwotne" typy obiektów w Pythonie często przechowują "wolną listę", aby przyspieszyć otrzymywanie pamięci dla nowych obiektów. Jeśli okaże się, że jest to problem, metody *_dealloc można zastąpić metodami.

Wierzyłem również, że widzę problem z nieprawidłowym wyzerowaniem klas, ale teraz uważam, że był to błąd z mojej strony.

Dzięki

Much thanks do @Dunes i @Kevin dla wskazując na problemy, które ukrywane moje pierwotne pytanie. Te kwestie zostały powyżej w punktach "edytuj" powyżej.

+4

Python prawdopodobnie interweniuje w łańcuchach. – Kevin

+5

Python definiuje tutaj ciągi znaków, są one przechowywane na liście stałych funkcji - 'hello_forever .__ kod __. Co_consts'. – Dunes

+0

Czy rozważałeś zmianę makr '_Py_Dealloc' lub' Py_DECREF', aby wyzerować pamięć po dezalokacji? W przeciwieństwie do zakłócania przydziału pamięci. – Dunes

Odpowiedz

2

Okazuje się, że problem był absolutnie głupim błędem w moim własnym kodzie, który zrobił memset. Zamierzam dotrzeć do @Calyth, który hojnie dodał nagrodę do tego pytania, zanim "zaakceptuje" tę odpowiedź.

W skrócie i uproszczone, malloc/free funkcje wrapper działa tak: nazywa

  • Kod malloc prośbą o N bajtów pamięci.
    • Opakowanie wywołuje rzeczywistą funkcję, ale prosi o podanie N+sizeof(size_t) bajtów.
    • Zapisuje N na początku zakresu i zwraca wskaźnik przesunięcia.
  • Kod używa wskaźnika przesunięcia, niepomny na to, że jest on dołączony do nieco większej porcji pamięci niż była wymagana.
  • Wywołania kodu free z prośbą o zwrócenie pamięci i przekazanie tego wskaźnika przesunięcia.
    • Opakowanie wygląda przed wskaźnikiem przesunięcia, aby uzyskać pierwotnie żądany rozmiar pamięci.
    • Wywołuje memset, aby upewnić się, że wszystko jest ustawione na zero (biblioteka jest kompilowana bez optymalizacji, aby zapobiec ignorowaniu przez kompilator memset).
    • Tylko wtedy wywołuje prawdziwą funkcję.

Moim błędem było powołanie równowartość memset(actual_pointer, 0, requested_size) zamiast memset(actual_pointer, 0, actual_size).

jestem teraz stoi zadziwiające pytanie, dlaczego nie było zawsze „3” resztki bajtów (moje testy jednostkowe zweryfikować, że żaden z moich losowo generowanych przedmiotów bajty zawierają żadnych wartości null) i dlaczego nie będzie smyczki również mają ten problem (może Python over-alokować rozmiar bufora ciąg, być może). Są to jednak problemy na inny dzień.

Skutkiem tego wszystkiego jest to, że stosunkowo łatwo jest zapewnić, że bajty i łańcuchy zostaną ustawione na zero, gdy zostaną zebrane śmieci! (Istnieje szereg zastrzeżeń dotyczących zakodowanych ciągów, wolnych list i tak dalej, więc każdy, kto próbuje to zrobić, powinien uważnie przeczytać oryginalne pytanie, komentarze na pytanie i tę "odpowiedź".)

+0

Z ciekawości, dlaczego na świecie ** potrzebujesz **, aby wyzerować swoją pamięć, zanim przejdzie do zbierania śmieci? Czy to jest kwestia bezpieczeństwa? – KronoS

+0

Tak. (+10 więcej, aby przejść ...) – Calyth

+0

@KronosS, tak, jest to zabezpieczenie dla dwóch rodzajów ataków. 1) Upewniając się, że pamięć jest ustawiona na zero, zanim zostanie zwrócona do systemu operacyjnego, chronimy przed aplikacją, która przydziela sporo pamięci i przegląda ją dla takich rzeczy jak klucze SSL. 2) Powszechnym sposobem hakowania jest próba wykorzystania błędów w aplikacji i uzyskanie zrzutu pamięci, którą przetrzymał; poprzez unikanie pul pamięci Pythona nie ma pamięci utrzymywanej dłużej niż jest to absolutnie konieczne, więc zmniejsza się podatność na tego rodzaju atak. (Należy pamiętać, że gc.collect musi zostać wywołane ręcznie w kluczowych punktach.) – TrevorWiley

3

Ogólnie rzecz biorąc nie masz takich gwarancji, że pamięć zostanie wyzerowana, a nawet śmieci zebrane w odpowiednim czasie. Istnieją heurystyki, ale jeśli martwisz się bezpieczeństwem w tym zakresie, prawdopodobnie to za mało.

Co można zrobić zamiast tego jest praca bezpośrednio na modyfikowalnych typów, takich jak bytearray i wyraźnie każdy element zerowy:

# Allocate (hopefully without copies) 
bytestring = bytearray() 
unbuffered_file.readinto(bytestring) 

# Do stuff 
function(bytestring) 

# Zero memory 
for i in range(len(bytestring)): 
    bytestring[i] = 0 

Bezpiecznie pomocą tego będzie wymagać jedynie metody wykorzystują znasz nie będzie tymczasowe kopie, co prawdopodobnie oznacza toczenie własne. Nie przeszkadza to jednak niektórym buforom.

zdan gives a good suggestion w innym pytaniu: użyj podprocesu do wykonania pracy i zabij go ogniem, gdy już to zrobi.

+0

W naszym przypadku z przyjemnością wywołają gc.collect(), gdy tylko zostaną wykonane z obiektami (które będą bajtami i ciągami, ale nie znakami bytowymi). Poprosiłem o dostarczenie podklasy tych typów, która używałaby na przykład ctypów i memset do wyczyszczenia pamięci po ich usunięciu, ale nie będą działać, ponieważ zostaną przekazane do kodu Pythona innej firmy, który może być tymczasowy kopie. – TrevorWiley

+0

@Trevor: Jeśli obiekty są niezmienne, kopiowanie powinno po prostu zwrócić odniesienie do oryginalnego obiektu. – Kevin

+0

@Kevin Jest to prawdopodobnie częściowo paranoja, ale wydaje mi się, że obawiają się również, że podłoe "przecieka" w ten sposób. – TrevorWiley