Meltdown

Meltdown i Spectre – luka w procesorach Intel. Krótkie porównanie wydajności po instalacji łatek

2018 rok rozpoczął się w branży IT prawdziwym trzęsieniem ziemi. Wybuchła ogromna afera medialna w roli głównej z udziałem Intela. Specjaliści z Google odkryli bowiem dwie luki w konstrukcji większości procesorów wyprodukowanych po 2010 roku i sprzedawanych przez wspomnianą firmę. EuroLinux bardzo szybko zaimplementował jednak łatki zabezpieczające przed groźnymi lukami. Czy w jakikolwiek sposób wpływają one na wydajność systemu EuroLinux?

2018 rok rozpoczął się w branży IT prawdziwym trzęsieniem ziemi. Wybuchła ogromna afera medialna w roli głównej z udziałem Intela. Specjaliści z Google odkryli bowiem dwie luki w konstrukcji większości procesorów wyprodukowanych po 2010 roku i sprzedawanych przez wspomnianą firmę. EuroLinux bardzo szybko zaimplementował jednak łatki zabezpieczające przed groźnymi lukami. Czy w jakikolwiek sposób wpływają one na wydajność systemu EuroLinux?

O Meltdown i Spectre w ostatnim czasie napisało już pół Internetu. Temat jest bardzo nośny, niestety często także pełen nieścisłości. Mówiąc najprościej – meltdown pozwala na to, by niezidentyfikowany proces mógł bezproblemowo przedostać się do pamięci podręcznej naszego procesora. Z kolei Spectre pozwala na odczytanie tego, co znajduje się w pamięci. Obydwie luki mogą być dosyć groźne. Dlatego też firma EuroLinux w szybkim czasie zaimplementowała do swojego systemu zabezpieczające łatki.

Najpełniejszą stroną z informacjami dotyczącymi błędu jest oficjalna strona: https://meltdownattack.com.

Istnieje także repozytorium z programami będącymi Proof of Concept ataku – https://github.com/IAIK/meltdown.

Błędy posiadają nawet własne logo i można znaleźć oferty sprzedaży T-shirtów z nimi. Jednak to nie dzięki świetnej identyfikacji graficznej zostaną nam na dłużej w pamięci. Meltdown olbrzymie nagłośnienie zawdzięcza spadkowi wydajności oraz faktowi, że CEO Intela w ostatnim czasie pozbył się znacznych aktywów, a także fatalnemu zachowaniu inżynierów Intela, co bardzo celnie w swoim złośliwym stylu skomentował Linus Torvalds na listach mailingowych Kernela.

https://lkml.org/lkml/2018/1/3/797

On Wed, Jan 3, 2018 at 3:09 PM, Andi Kleen <[email protected]> wrote:
> This is a fix for Variant 2 in
> https://googleprojectzero.blogspot.com/2018/01/reading-privileged-memory-with-side.html
>
> Any speculative indirect calls in the kernel can be tricked
> to execute any kernel code, which may allow side channel
> attacks that can leak arbitrary kernel data.

Why is this all done without any configuration options?

A *competent* CPU engineer would fix this by making sure speculation
doesn’t happen across protection domains. Maybe even a L1 I$ that is
keyed by CPL.

I think somebody inside of Intel needs to really take a long hard look
at their CPU’s, and actually admit that they have issues instead of
writing PR blurbs that say that everything works as designed.

.. and that really means that all these mitigation patches should be
written with “not all CPU’s are crap” in mind.

Or is Intel basically saying “we are committed to selling you shit
forever and ever, and never fixing anything”?

Because if that’s the case, maybe we should start looking towards the
ARM64 people more.

Please talk to management. Because I really see exactly two possibibilities:

– Intel never intends to fix anything

OR

– these workarounds should have a way to disable them.

Which of the two is it?

Linus

Pisanie o kompetentnych inżynierach i zobowiązaniu do sprzedawania produktów niskiej jakości na zawsze oraz nienaprawianiu nigdy niczego sprawiło, że musiałem czyścić monitor z porannej kawy.

Meltdown i Spectre wymusił na dostawcach największych rozwiązań chmurowych aplikowanie patchy, które spowalniają dostępne dla użytkowników maszyny wirtualne. Nie muszę chyba tłumaczyć, że jest to bardzo niekomfortowa sytuacja. Posiadając mocno obciążoną instancję w AWS przed łatką, po łatce jesteśmy zmuszeni na przejście na, z reguły, dwa razy droższą, mocniejszą maszynę.

W tym artykule postaram się przeprowadzić stosunkowo proste testy wydajnościowe w celu zbadania wpływu nowych poprawek na EuroLinuxa w wersji 7.

Status błędu w EuroLinuxie

W chwili obecnej zarówno EuroLinux 6, jak i 7 zawierają wersję jądra odporne na podane ataki. Jądro zostało dostarczone z dodatkową flagą `CONFIG_KAISER=y`. Łatki zostały zaimplementowane do pakietów EuroLinux w następującym czasie:

  • Kernel – 04.01.2018 r. (Red Hat – 03.01.2018 r., Oracle – 04.01.2018 r.)
  • Libvirt – 05.01.2018 r. (Red Hat – 04.01.2018 r., Oracle – 05.01.2018 r.)
  • qemu-kvm – 05.01.2018 r. (Red Hat – 04.01.2018 r., Oracle – 04.01.2018 r.)
  • dracut – 07.01.2018 r. (Red Hat – 04.01.2018 r., Oracle – 04.01.2018 r.)

Microsoft pachta całościowego do swojego systemu wydał 04.01.2018 r.

By zobaczyć konfigurację jądra, należy zajrzeć do pliku /boot/cofnig-$(wersja jądra). Załatane zostały także pakiety odpowiedzialne za wirtualizację (qemu-kvm, libvirt) oraz dracut.

Co istotne, błędy i dostępne do nich erraty nie są określane jako krytyczne (critical), lecz „tylko” ważne – important. Wynika to z faktu, iż atakujący potrzebuje dostępu do naszej maszyny, by przeprowadzić atak. Niestety dostępem w tym wypadku jest także posiadanie maszyny wirtualnej.

Jako że błąd jest wciąż świeży, a sprawa się rozwija możliwe, że w przyszłości ten akapit trzeba będzie uaktualnić o nowe informacje, co oczywiście odpowiednio zaznaczę :).

Maszyna testowa

Za maszynę testową posłużył mi mój, naprawdę kochany Thinkpad 430. Krótki rzut oka na podzespoły.

– CPU: i5-3320M
– Dysk (na testowanej ścieżce /): GoodRam Iridium PRO 480GB
– Pamięć: 12GB DDR3 pracujące z prędkością 1600 MHz

Nowa łatka ma w znaczący sposób uderzać w programy używające dużo wywołań systemowych. Jednym z typów wywołań są operacje na plikach. Programy wykonujące ich dużo (np. baza danych), powinny więc znacząco to odczuć. W związku z tym faktem osoby posiadające szybsze nośniki (np. na złączu NVMe) bardziej odczują „zalety” nowej łatki, gdyż stosunek pracy procesora do czekania na dane z dysku wypada niekorzystnie.

Metodyka pomiaru

System w żaden sposób nie był strojony (tuned) – posiada standardowe ustawienia jądra. Na systemie jest włączony SELinux.

System został uruchomiony na dwóch jądrach – jedno posiada najnowsze poprawki, drugie nie. Bonusem jest wyłączenie części opcji PTI (Page Table Isolation) (Meltdown) z pozostawieniem (Indirect Branch Restricted Speculation) i (Indirect Branch Prediction Barriers) (Spectre), ustawienia takie są domyślnymi ustawieniami na procesory AMD.

Do testowania wydajności systemu posłużył sysbench, który jest dostępny w EPEL. W celu zainstalowania go używamy:

sudo yum install -y sysbench:

Drugi test przeprowadzę na najnowszej wersji PostgreSQL – 10. Mógłbym co prawda użyć EuroDB, ale zależało mi na tym, by każdy zainteresowany mógł samodzielnie odtworzyć wyniki testu.

Udajemy się więc na stronę https://yum.postgresql.org/repopackages.php#pg10 i instalujemy odpowiednią paczkę (zawierającą informację dotyczącą repozytoriów).

Następnie instalujemy PostgreSQLa.

sudo yum install postgresql10
sudo /usr/pgsql-10/bin/postgresql-10-setup initdb
sudo systemctl enable postgresql-10.service
sudo systemctl start postgresql-10

Następnie tworzymy bazę danych dla pgbencha.

su - postgres
psql
create database pgbench; # Ta komenda w konsoli psql
\q # ta też

Teraz nadszedł czas na inicjalizację bazy danych pgbencha. Tę operację także wykonujemy jako user postgres.

su - postgres
/usr/pgsql-10/bin/pgbench -i -s 10 pgbench

Przełącznik „-i” mówi o inicjalizacji a „-s” o skali, jaką chcemy uzyskać.

Używane komendy

CPU x 3

sysbench cpu --cpu-max-prime=10000 run
sysbench cpu --cpu-max-prime=20000 --threads=4 run
sysbench cpu --cpu-max-prime=20000 --threads=16 run

RAM x 3

sysbench memory --memory-total-size=10G run
sysbench memory --memory-total-size=40G run
sysbench memory --memory-block-size=4K --memory-scope=global --memory-total-size=50G --memory-oper=read run
sysbench memory --memory-block-size=4K --memory-scope=global --memory-total-size=50G --memory-oper=write run

FileIO x 1

sysbench --test=fileio --file-total-size=1G prepare
sysbench fileio --file-total-size=1G --threads=4 --file-test-mode=rndrw --max-time=1200 --max-requests=0 run
sysbench --test=fileio --file-total-size=1G cleanup

PGBENCH

# select only
/usr/pgsql-10/bin/pgbench -S -t 100000 pgbench # 100000 selectów 1 klient
/usr/pgsql-10/bin/pgbench -S -c 4 -t 100000 pgbench # 400000 selectów 4 klienci 4 watki
/usr/pgsql-10/bin/pgbench -S -j 4 -c 4 -t 100000 pgbench # 400000 selectów 4 klienci 4 watki
# tpcb-like
/usr/pgsql-10/bin/pgbench -b tpcb-like -t 10000 pgbench # 100000 selectów 1 klient
/usr/pgsql-10/bin/pgbench -b tpcb-like -c 4 -t 10000 pgbench # 40000 selectów 4 klienci 4 watki
/usr/pgsql-10/bin/pgbench -b tpcb-like -j 4 -c 4 -t 10000 pgbench # 40000 selectów 4 klienci 4 watki

Został ustawiony target mutli-user. systemctl set-default multi-user.target.

Wykonanie

skrypt run.sh

#!/bin/bash
my_name='bench_1' # 1, 2, 3
mkdir $my_name
date
echo "Start CPU"
for i in {1..0}; do
sysbench cpu --cpu-max-prime=5000 run >> ${my_name}/cpu.1
sysbench cpu --cpu-max-prime=10000 --threads=4 run >> ${my_name}/cpu.2
sysbench cpu --cpu-max-prime=10000 --threads=16 run >> ${my_name}/cpu.3
done

echo "Start Memory"
for i in {1..0}; do
sysbench memory --memory-total-size=10G run >> ${my_name}/mem.1
sysbench memory --memory-total-size=40G run >> ${my_name}/mem.2
sysbench memory --memory-block-size=4K --memory-scope=global --memory-total-size=50G --memory-oper=read run >> ${my_name}/mem.3
sysbench memory --memory-block-size=4K --memory-scope=global --memory-total-size=50G --memory-oper=write run >> ${my_name}/mem.4
done
echo "START FILEIO"

sysbench --test=fileio --file-total-size=128M prepare > ${my_name}/fileio.1
sysbench fileio --file-total-size=128M --threads=4 --file-test-mode=rndrw --max-time=180 --max-requests=0 run > ${my_name}/fileio.2
sysbench --test=fileio --file-total-size=128M cleanup > ${my_name}/fileio.3

echo "START SELECT"
su postgres -c '/usr/pgsql-10/bin/pgbench -S -t 100000 pgbench' > ${my_name}/pg.1
su postgres -c '/usr/pgsql-10/bin/pgbench -S -c 4 -t 100000 pgbench' > ${my_name}/pg.2
su postgres -c '/usr/pgsql-10/bin/pgbench -S -j 4 -c 4 -t 100000 pgbench' > ${my_name}/pg.3
echo "START tpcb"
su postgres -c '/usr/pgsql-10/bin/pgbench -b tpcb-like -t 10000 pgbench' > ${my_name}/pg.4
su postgres -c '/usr/pgsql-10/bin/pgbench -b tpcb-like -c 4 -t 10000 pgbench' > ${my_name}/pg.5
su postgres -c '/usr/pgsql-10/bin/pgbench -b tpcb-like -j 4 -c 4 -t 10000 pgbench' > ${my_name}/pg.6
date

Wyniki

Ku mojemu zdumieniu tylko w przypadku testów postgresowych można zauważyć spadek wydajności. Nie jest on jednak tak spektakularny, jak się spodziewałem. Poniżej zamieszczam skrypt ze sczytanymi danymi i wygenerowane wykresy.

# testname_benchnumber
#CPU
cpu1_1 <­- c(2754.89,2771.17,2776.88)
cpu1_2 <­- c(2753.45,2764.65,2763.78)
cpu1_3 <­- c(2759.46,2774.00,2773.67)
cpu1 <­- c(mean(cpu1_1),mean(cpu1_2),mean(cpu1_3))

cpu2_1 <­- c(2952.30,2951.21,2952.92)
cpu2_2 <­- c(2950.72,2953.55,2953.84)
cpu2_3 <­- c(2952.96,2951.82,2952.28)
cpu2 <­- c(mean(cpu2_1),mean(cpu2_2),mean(cpu2_3) )

cpu3_1 <­- c(2967.02,2967.88,2967.42)
cpu3_2 <­- c(2966.89,2966.77,2967.64)
cpu3_3 <­- c(2965.68,2966.15,2966.60)
cpu3 <­- c(mean(cpu3_1),mean(cpu3_2),mean(cpu3_3) )

# MEM
mem1_1 <­- c(4633546.67,4612254.62,4625537.77)
mem1_2 <­- c(4503442.39,4550892.25,4550062.05)
mem1_3 <­- c(4556019.26,4551395.38,4555740.97)
mem1 <­ c(mean(mem1_1),mean(mem1_2),mean(mem1_3) )

mem2_1 <­- c(4443425.06,4634798.61,4637708.93)
mem2_2 <­- c(4536877.33,4561183.67,4534956.31)
mem2_3 <­- c(4556349.79,4559066.11,4550771.30)
mem2 <­- c(mean(mem2_1),mean(mem2_2),mean(mem2_3) )

mem3_1 <­- c(4170833.04,4001983.11,4187500.21)
mem3_2 <­- c(3983233.58,4135204.33,4154492.55)
mem3_3 <­- c(4146563.63,3986775.88,4152701.40)
mem3 <­- c(mean(mem3_1),mean(mem3_2),mean(mem3_3) )

mem_4_1 <­- c(2511290.19,2513708.55,2512041.11)
mem_4_2 <­- c(2479849.53,2484226.63,2481916.47)
mem_4_3 <­- c(2481839.50,2478594.53,2481912.26)
mem_4 <­- c(mean(mem4_1),mean(mem4_2),mean(mem4_3) )

# FILEIO first one is bench_1 second is bench_2 etc.
reads_per_s <-­c (1267.29, 1242.95, 1284.28)
writes_per_s <­-c (844.86, 828.65, 856.19)
fsyncs_per_s <­-c (2703.10, 2651.14, 2739.22)

pg_1 <­- c(9807.743930, 9879.473226, 9466.316596)
pg_2 <­- c(30847.651379, 30724.505662, 30058.210408)
pg_3 <­- c(36914.843658, 36773.945451, 35758.956943)
pg_4 <­- c(354.046591, 349.590370, 356.289145)
pg_5 <­- c(736.258622, 738.021144, 735.160172)
pg_6 <­- c(735.197540, 734.390789, 736.229064)

# Plots
barplot(cpu1, main="CPU Test 1", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Events per second")
barplot(cpu2, main="CPU Test 2", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Events per second")
barplot(cpu3, main="CPU Test 3", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Events per second")
barplot(mem1, main="Memory Test 1", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Operations per second")
barplot(mem2, main="Memory Test 2", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Operations per second")
barplot(mem3, main="Memory Test 3", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Operations per second")
barplot(mem4, main="Memory Test 4", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Operations per second")

# NOTE: Only one plot is needed ­ results are symetrical.
barplot(reads_per_s, main="FileIO Test", names.arg = c("Unpatched", "Patched-­=TPI", "Patched"),
        col=rainbow(3), ylab="Reads per second")
barplot(writes_per_s, main="FileIO Test", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Writes per second")
barplot(fsyncs_per_s, main="FileIO Test", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="fsync() per second")

barplot(pg_1, main="PGBench Test 1", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Transaction per second")
barplot(pg_2, main="PGBench Test 2", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Transaction per second")
barplot(pg_3, main="PGBench Test 3", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Transaction per second")
barplot(pg_4, main="PGBench Test 4", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Transaction per second")
barplot(pg_5, main="PGBench Test 5", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Transaction per second")
barplot(pg_6, main="PGBench Test 6", names.arg = c("Unpatched", "Patched­-=TPI", "Patched"),
        col=rainbow(3), ylab="Transaction per second")

 

Meltdown

Meltdown

Środkowy słupek reprezentuje nowy kernel z wyłączonym pti, jednak z ibrs i ibpb.

Jak widać, wyniki nie są tak szokujące, jak można było się spodziewać. W najbliższym czasie postaram się wykonać dodatkowe testy na innych maszynach z nowszymi podzespołami. Niemniej dziękuję Państwu za uwagę.

Autorzy

Artykuły na blogu są pisane przez osoby z zespołu EuroLinux. 80% treści zawdzięczamy naszym developerom, pozostałą część przygotowuje dział sprzedaży lub marketingu. Dokładamy starań, żeby treści były jak najlepsze merytorycznie i językowo, ale nie jesteśmy nieomylni. Jeśli zauważysz coś wartego poprawienia lub wyjaśnienia, będziemy wdzięczni za wiadomość.