CodiLime Tech Talk - Jarek Łukow: You need a cloud to test a cloud: using Ope...
CodiLime Tech Talk - Michał Cłapiński, Mateusz Jabłoński: Debugging faultily inherited file handles on Microsoft Windows.
1. Tonący Python 2 pliku się chwyta,
czyli jak debugowaliśmy wyciekające uchwyty pod Windowsem.
Michał Cłapiński, Mateusz Jabłoński
23.05.2018
2. Czym się zajmujemy?
● Michał Cłapiński
○ michal.clapinski@codilime.com
● Mateusz Jabłoński
○ mateusz.jablonski@codilime.com
● Pracujemy w CodiLime
● Portujemy Tungsten Fabric (dawniej OpenContrail) na Windowsa
● Przygotowujemy system continuous integration w celu testowania naszego kodu
3. Krótko o Tungsten Fabric
● Multicloud multistack SDN
● SDN = Software-defined networking
● Projekt Linux Foundation
● Budowany za pomocą SConsów
4. Napotkany problem
● Włączyliśmy budowanie wielowątkowe w naszym CI
○ Znaczne przyśpieszenie budowania (20 minut / 2 godziny)
○ Zysk w CI i na maszynach programistów
5. Napotkany problem
● Włączyliśmy budowanie wielowątkowe w naszym CI
○ Znaczne przyśpieszenie budowania (20 minut / 2 godziny)
○ Zysk w CI i na maszynach programistów
● Zaczęły pojawiać się nieprzewidywalne błędy
○ ~5% buildów zakończonych niepowodzeniem
○ Błędy na różnym etapie budowania
○ Jeżeli jeden build kompiluje 1000 plików to błąd występował raz na 20000 plików
○ Za każdym razem błąd dotyczył pliku tworzonego przez Pythona
6. Napotkany problem - przykład
css_DT_bootstrap_css.cpp(4): error C2065: 'DT_bootstrap_css': undeclared identifier
css_DT_bootstrap_css.cpp(4): error C2065: 'DT_bootstrap_css_len': undeclared identifier
scons: *** [sandesh_http.obj] Error 2
scons: building terminated because of errors.
● Plik css_DT_bootstrap_css.cpp jest jest generowany przez SConscript i później
kompilowany
● Plik został źle wygenerowany?
7. Napotkany problem - przykład
Kod generujący plik css_DT_bootstrap_css.cpp:
1. with open(cname, 'w') as cfile:
2. cfile.write('namespace {n')
3.
4. subprocess.call('xxd -i ' + hname + ' >> ' + os.path.basename(cname), shell=True, cwd=opath)
5.
6. with open(cname, 'a') as cfile:
7. cfile.write('}n')
8. cfile.write(tail_content)
● Brakujący kod jest generowany w linii 4
● Dodanie sprawdzania rezultatu subprocess.call pozwala wychwycić błąd wcześniej
● Nadal nie wiemy jaka jest przyczyna
8. Napotkany problem - przykład
c1xx: fatal error C1083: Cannot open source file: 'multicast_html.cpp': No such file or directory
scons: *** [multicast_html.obj] Error 2
● multicast_html.cpp jest generowany podobnie do css_DT_bootstrap_css.cpp
● Dlaczego dostajemy błąd ‘No such file or directory’?
● Plik istnieje. Czy został stworzony po próbie jego kompilacji?
10. Podejście pierwsze: ProcMon
● Process Monitor: narzędzie do monitorowania aktywności systemu plików, rejestru i wątków
● Umożliwia filtrowanie na podstawie różnych kryteriów
○ Nazwa procesu, PID, ścieżka do używanego pliku, wynik i inne
● Chcemy zobaczyć jakie procesy używały jakich plików i kiedy miało to miejsce
● Wielkość logów uniemożliwia budowanie do czasu napotkania błędu z raz włączonym ProcMonem
● Rzadko spotykany błąd, więc ręczny restart ProcMona i budowania trwałby wiele godzin
11. Podejście pierwsze: ProcMon
Automatyzacja za pomocą skryptu PowerShell:
$ErrorActionPreference = "SilentlyContinue"
Do {
[remove build artifacts]
.internalsprocmon /Quiet /BackingFile proclogslog.pml
.internalsprocmon /WaitForIdle
scons -j 8 contrail-vrouter-agent
$code = $LastExitCode
.internalsprocmon /Terminate
While (Get-Process procmon) {
Start-Sleep 1
}
} While ($code -eq 0)
13. Podejście pierwsze: ProcMon
● cmd.exe otwiera plik, do którego pisze xxd z powodu użycia przekierowania powłoki (‘>>’)
● Python otworzył plik 3 razy, zamknął 2 razy
● cl.exe próbuje otworzyć plik, do którego uchwyt jest nadal używany przez Pythona
● ‘No such file or directory’, które widzieliśmy w logach to w rzeczywistości SHARING VIOLATION
● Brakujące CloseFile jest wywoływane przez cmd.exe?
14. Krótko o trybach współdzielenia
HANDLE WINAPI CreateFile(..., _In_ DWORD dwDesiredAccess, _In_ DWORD dwShareMode, ...);
● dwDesiredAccess:
○ GENERIC_READ, GENERIC_WRITE, GENERIC_READ | GENERIC_WRITE (i inne)
● dwShareMode:
○ 0, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE i kombinacje
● ERROR_SHARING_VIOLATION w przypadku niekompatybilnych trybów
● dwShareMode musi brać pod uwagę wcześniejsze wywołanie CreateFile
16. Krótko o trybach współdzielenia
● Python otworzył plik z GENERIC_WRITE | READ_CONTROL
● cl.exe próbuje otworzyć go z FILE_SHARE_READ
● Wywołanie kończy się niepowodzeniem (ERROR_SHARING_VIOLATION)
● Wniosek: Uchwyt na plik nie został zamknięty
17. Podejście drugie: Frida
● Potrzebowaliśmy potężniejszego, skryptowalnego sposobu monitorowania
● Frida: narzędzie do dynamicznej instrumentacji
● Pozwala wstrzykiwać własny kod do dowolnych procesów
○ Wstrzykuje silnik JavaScriptu V8, pozwala uruchamiać własny kod JS
● Umożliwia przechwytywanie wywołań dowolnych funkcji
○ Logowanie wywołań
○ Modyfikacja argumentów
○ Modyfikacja wartości zwracanej
19. Podejście drugie: Frida
var getPathAAddress = Module.findExportByName(null, "GetFinalPathNameByHandleA")
var getPathA = new NativeFunction(getPathAAddress, 'uint32', ['pointer', 'pointer', 'uint32', 'uint32'])
[...]
var handlePath = Memory.alloc(2001)
getPathA(handle, handlePath, 2000, 0)
send("CreateFile " + String(handle.toInt32()) + " " + String(Memory.readUtf8String(handlePath)))
20. Podejście drugie: Frida
● Wstrzyknęliśmy kod JS do procesu Pythona. Monitorowaliśmy CreateFile i CloseHandle
CreateFile 14468 16:2:46.429 1704 [...]multicast_html.cpp
[...]
CloseHandle 14468 16:2:46.432 1704 [...]multicast_html.cpp
CloseHandle: 1
● Python w każdym przypadku wywoływał poprawnie CloseHandle
● Dlaczego cmd.exe zamyka plik, którego nie otwiera?
● Dlaczego ProcMon nie pokazuje wywołania CloseHandle?
22. Podejście drugie: Frida
auto f = _open("costam.txt", 0);
char *argv[] = { "cmd.exe", "/c", "pwd && ping 127.0.0.1 -n 5", NULL };
_spawnve(_P_NOWAIT, "C:WindowsSystem32cmd.exe", argv, NULL);
_close(f);
● ProcMon pokazuje jedynie CloseHandle na ostatnim egzemplarzu danego uchwytu (zniszczenie
obiektu w kernelu)
● Wniosek: procesy potomne Pythona niekiedy dziedziczą uchwyty do plików otwieranych przez Pythona
23. Reprodukcja problemu
Kolejny krok: analiza wywołań CreateFile i CreateProcess:
● Otwarcie pliku i stworzenie procesu cmd.exe nastąpiło praktycznie w tym samym momencie
● Działo się tak za każdym razem, gdy wyciekał uchwyt
● Wyścig w implementacji CPythona? Wyścig w API Windowsa?
24. Reprodukcja problemu
● SCons do uruchamiania procesu kompilatora używa os.spawnve
● os.spawnve jest zaimplementowane przy użyciu _spawnve z Windows API
● _spawnve jest funkcją Microsoft Windows, inspirowaną funkcjami fork i exec
25. Reprodukcja problemu
● SCons do uruchamiania procesu kompilatora używa os.spawnve
● os.spawnve jest zaimplementowane przy użyciu _spawnve z Windows API
● _spawnve jest funkcją Microsoft Windows, inspirowaną funkcjami fork i exec
● Problem z implementacją _spawnve?
● Napisaliśmy fragment kodu odtwarzającego problem
○ Nieskończone otwieranie i zamykanie pliku w jednym wątku
○ Nieskończone uruchamianie cmd.exe w drugim wątku
26. Reprodukcja problemu
import os
from time import sleep
from threading import Thread
def threaded_function1():
while True:
with open("sample.txt", "w"):
pass
def threaded_function2():
while True:
os.spawnve(os.P_WAIT, 'C:Windowssystem32cmd.exe', ['cmd.exe', '/c'], os.environ)
thread1 = Thread(target = threaded_function1)
thread2 = Thread(target = threaded_function2)
thread1.start()
thread2.start()
27. Reprodukcja problemu
● ProcMon pokazał dziedziczenie wielu uchwytów do ‘sample.txt’ przez cmd.exe
● Nadal nie wiemy, czy jest to problem z implementacją CPythona czy ze _spawnve
28. Reprodukcja problemu
● ProcMon pokazał dziedziczenie wielu uchwytów do ‘sample.txt’ przez cmd.exe
● Nadal nie wiemy, czy jest to problem z implementacją CPythona czy ze _spawnve
● Szybka weryfikacja: Przepisaliśmy wywołanie os.spawnve na subprocess.call
○ subprocess.call używa funkcji CreateProcess
29. Reprodukcja problemu
● ProcMon pokazał dziedziczenie wielu uchwytów do ‘sample.txt’ przez cmd.exe
● Nadal nie wiemy, czy jest to problem z implementacją CPythona czy ze _spawnve
● Szybka weryfikacja: Przepisaliśmy wywołanie os.spawnve na subprocess.call
○ subprocess.call używa funkcji CreateProcess
● Błąd nadal występował
● Wniosek: Prawdopodobnie problem z implementacją CPythona
30. Rozwiązanie
● Uruchomiliśmy przykład weryfikacyjny przy użyciu aktualnej wersji Pythona 3
● Problem zniknął (zarówno dla subprocess.call jak i _spawnve)
● Zdecydowaliśmy się przepisać wszystkie skrypty budujące na Pythona 3
● Od tej pory problem nie występuje - budujemy kod w CI wielowątkowo
● Rozwiązaliśmy problem wyłącznie w naszym CI. Nadal nie znamy jego przyczyny
31. Rozwiązanie
● Użyliśmy wyszukiwania binarnego
● Instalowaliśmy oficjalne wydania
● Błąd nie występuje w wersji >=3.4
● Kompilowaliśmy pojedyncze commity
● Wymagane były starsze wersje Visual Studio…
32. Rozwiązanie
● Użyliśmy wyszukiwania binarnego
● Instalowaliśmy oficjalne wydania
● Błąd nie występuje w wersji >=3.4
● Kompilowaliśmy pojedyncze commity
● Wymagane były starsze wersje Visual Studio…
● ...które wymagają starszych wersji Windowsa
33. Rozwiązanie
Fragment kodu rozwiązujący problem (źródło):
#ifdef MS_WINDOWS
flags |= O_NOINHERIT;
#elif defined(O_CLOEXEC)
flags |= O_CLOEXEC;
#endif
...
self->fd = open(name, flags, 0666);
34. Rozwiązanie
Issue #18571: Implementation of the PEP 446: file descriptors and file handles
“This PEP proposes to make all file descriptors created by Python non-inheritable by default to reduce
the risk of these issues. This PEP fixes also a race condition in multi-threaded applications on operating
systems supporting atomic flags to create non-inheritable file descriptors.”
Źródło: PEP 446
36. Wnioski
● Omijajcie Pythona 2 (w szczególności na Windowsie)
● Uważajcie na przypadkowe dziedziczenie (szczególnie w API POSIXowym)
● ProcMon loguje zamknięcie tylko ostatniego egzemplarza danego uchwytu