Warning: Undefined array key 0 in /var/www/tgoop/function.php on line 65

Warning: Trying to access array offset on value of type null in /var/www/tgoop/function.php on line 65
56 - Telegram Web
Telegram Web
А как на корпоративном языке правильно сказать "it's none of your fucking business"?

Все еще не в совершенстве владею этим сложным языком, иногда не хватает лексикона.
What a week, huh? Ben, it's Monday.

Я реально большую часть дня срался в комментах в JIRA ишуях, а потом и лично. Да я же даже не умею ругаться на английском!

Сколько я там строчек кода написал сегодня, штук 10? Кошмар.
😭10🤮1
Как же мощен мой карьерный путь
🔥31😁3😢3💯21
Близкие сигналы второй степени (часть 1)

Обещал
рассказать ответ на паззлер от Андрея Паньгина, который на выходных разгадывал.

Вообще, как справедливо заметили в комментах, Андрей уже подробно описал суть паззлера и ответ на него. Но давайте расскажу и я, с пояснениями, как именно я пришел к тому же ответу, а еще чуть уточню детали по ходу дела. На русском опять таки, может это кому-то важно.



Итак, суть паззлера. Имеем код:

public class Test {
static volatile Test T = new Test();
int n;

public static void main(String[] args) {
while (true) {
Test a = T, b = T, c = T, d = T;
a.n = b.n = c.n = d.n;
}
}
}


Как заставить такой код выбросить NullPointerException без изменения кода? А еще без запуска его в нативном отладчике, хаков с памятью и использования инструментарии.

Ложные следы

Первая мысль, которая мне пришла в голову - это сломать clinit класса, но при этом как-то дотянуть исполнение до main метода, думал про верификацию и т.д., но это все мимо. Похожие ответы есть и в реплаях к тесту, где предлагается заиспользовать кастомный класслоадер, который просто сразу будет бросать NPE, но все это совершенно не связано с кодом в main, так что здесь явно что-то другое.

А вот посмотрев на тело метода main можно догадаться, что автор паззлера хочет, чтобы постоянно происходили разыменования полей. А это ведь то самое место, где и правда (с точки зрения JVM) может случиться NPE.

Неявные исключения

Теперь вспоминаем про оптимизацию налчеков. С учетом того, что в строчке a.n = b.n = c.n = d.n NPE может полететь при каждом обращению к n, самым наивным решением будет сгенерировать код, при каждом доступе проверяет на null и в случае чего бросает NPE. Что-то типа:

if (c == null || d == null) {
throw new NullPointerException();
}
c.n = d.n;
if (b == null || c == null) {
throw new NullPointerException();
}
b.n = c.n;
...
if (a == null || b == null) {
throw new NullPointerException();
}
a.n = b.n;


или может что-то чуть более оптимизированное. Но вы представляете, насколько же это будет дорого? Если КАЖДЫЙ доступ к полю будет вызывать вот такое ветвление, все бы вообще едва ползало.

Поэтому тут вход вступает моя самая любимая оптимизация: implicit null checks. Алексей Шипилев прекрасно описал ее здесь, а я уже рассказывал про нее вот тут.

После работы оптимизирующего компилятора вместо лапши из условий мы получим для строчки a.n = b.n = c.n = d.n простой и понятный asm:

mov esi, dword [rsi+10H]
mov dword [rdx+10H], esi
mov dword [rcx+10H], esi
mov dword [rax+10H], esi


Как же это работает? Максимально прямолинейно: если, скажем, в rsi у вас лежит 0 (случай, когда должно полететь NPE), мы и правда попробуем разыменовать десятку: mov esi, dword [10h]. Что, конечно, приведет к развалу. А точнее к поднятию операционной системой сигнала SIGSEGV.

Но с сигналами вот какая штука: их можно перехватывать. Вызываете сискол sigaction, указываете обработчик, который должен сработать, если процесс получил соответствующий сигнал. В этом обработчике вы также получите много интересной информации: какая инструкция вызвала поднятие сигнала, по какому адресу был доступ, контекст (значения регистров) треда, где это произошло, наконец.

Предприимчивые рантаймщики этим активно пользуются для оптимизации NPE. Случился вот у вас SIGSEGV, рантайм аккуратно посмотрит на контекст, поймет, что развал произошел в Java треде; что адрес доступа около нуля; что текущая инструкция соответствует доступу к полю. И на основе всей этой информации вместо того, чтобы крэшнуть JVM, выбросит NPE. Вот место в коде хотспота, где происходит соответствующая обработка.

Оцените красоту этой оптимизации! В хорошем (и самом частом) случае доступ к полям теперь абсолютно бесплатен. В плохих случаях, когда летит NPE, вы, конечно, заплатите огромную цену (поднятие сигнала операционной системой - дело дорогое). Но так ведь они и редко случаются. А на случай если вдруг начинают случаться часто, Хотспот умеет деоптимизировать код и вставлять явную проверку. Красиииво. ↓

#дух_машины
Близкие сигналы второй степени (часть 2, развязка)

И вот на фоне всей этой красоты у меня появилась идея по поводу паззлера. Сигналы ведь появляются не только из-за исполнения каких-либо проблемных инструкций, разыменования нуля и т.д. Сигналы можно... посылать. Например, можно использовать утилиту kill, передав в качестве аргумента код сигнала и PID нашего Java процесса. Что если, SIGSEGV придет снаружи? Сможет ли Hotspot его распознать или спутает с NPE?

Код SIGSEGV-а – это 11, беру PID java процесса, инстинктивно добавляю su (хотя на самом деле это было важно, объясню позже!), посылаю сигнал:

sudo kill -11 71


...и получаю прекрасный, подробный hs_err файл, где мое хулиганство очевидно обличают:

# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x00007fc12830b195 (sent by kill), pid=71, tid=71


Прям носом меня ткнул, что сигнал был sent by kill, вы представляете?

Блуждание в темноте

Тут я, конечно, приуныл (вспомните еще, что в этот момент семья смотрит на меня с очень удивленным и немного тревожным видом, ведь дело происходит в ресторане, где я достал ноут и давай это проверять).

Стал думать, что проблема в том, что не тот тред поймал мой сигнал. И действительно, sigaction работает так:

1) Вы ставите обработчик сигналов на весь процесс;

2) Если сигнал вызван исполнением кода в каком-то из тредов (случай NPE), то обаботчик срабатывает именно в этом треде (т.е. именно его контекст будет у вас под рукой);

3) Но если сигнал был внешним, отправленным всему процессу, то после его перехвата, выбирается один тред, для которого обработка данного сигнала не заблокирована специальным сисколом sigmask, и уже с его контекстом мы исполняем обработчик. Выбор этот может быть абсолютно случайным, это детали реализации операционной системы.

Поэтому треды то могли быть любыми (и hs_err это подтвержали, tid-ы там отличались). А мне нужен был именно Java тред, где исполняется код main.

Какое-то время я потратил на то, чтобы перейти с kill на tgkill, который как раз может посылать сигналы именно тредам в другом процессе. Успеха, однако, не имел: все такие же красивые hs_err. Уже потом, сильно позже, из ответа Андрея я узнал, что он предлагал посылать kill-ом сигналы именно конкретно main треду, но я не понимаю, почему это должно сработать, т.к. kill посылает сигнал всему процессу целиком, либо группе процессов, но никак не потоку.

Выход на свет

Но на самом деле здесь чуть больше спорадичности, чем просто выбор треда. Ведь мы исполняем в цикле код, в котором кроме разыменований полей объектов есть и другие действия, работа с глобалами, например:

while (true) {
Test a = T, b = T, c = T, d = T;
a.n = b.n = c.n = d.n;
}


Поэтому мало того, что мне нужно попасть в правильный тред, так еще и во время исполнения правильной инструкции.

Додумавшись, наконец, до спорадичности, я решил позапускать тест в цикле. И... на очередной итерации все получилось! Спустя несколько десятков запусков, но все-таки я и правда получил красивый NPE из строчки a.n = b.n = c.n = d.n 🎉🎉🎉

Я был немного разочарован своим бесполезным крестовым (ну ладно, это был код на чистом С) походом на tgkill, но очень рад, что моя изначальная теория таки подтвердилась. Still worthy!)

Однако, оставалась парочка вопросов ↓

#дух_машины
👍2
Близкие сигналы второй степени (часть 3, последняя)

Оставшиеся вопросы


Зачем su?

Самая забавная часть истории, которую я узнал уже позже, из ответа Андрея. Чтобы распознать NPE, хотспот в числе прочего проверяет, что адрес развала был в окрестности нуля. Т.е. в нулевой странице, от 0 до 4096. Проверяет он это, используя ту самую системную информацию, которая приходит в хендл, а именно структуру siginfo. Но siginfo при этом устроена чуть по-разному для случаев внешних сигналов и сигналов, спровоцированных исполнением кода.

Во втором случае в поле si_addr структуры лежит адрес, доступ по которому вызвал развал. А вот в случае внешнего сигнала по той же самой памяти в структуре лежит уже совсем другое: там последовательно расположены si_uid и si_pid. Это информация о процессе, который посылает сигнал: его real user ID, и собственно его PID (позволю себе здесь своровать иллюстрацию из ответа Андрея):

63 31 0
+-----------------+
| si_addr |
+-----------------+
| si_uid | si_pid |
+-----------------+


Так вот, чтобы эта парочка стала похожа на адрес доступа около нуля, нужно, чтобы si_uid был равен нулю (а это как раз верно для супер-юзера, рута. Вот поэтому нужен su), да еще и pid процесса, посылающего сигнал должен быть меньше 4096 (еще один источник спорадичности!).

Поэтому su оказался очень важен, хотя я его поставил скорее интуитивно. Что-то плохое ведь всегда лучше делать под рутом, верно?

Почему в случае неудачи в hs_err было написано, что сигнал посылается именно с kill?

Да, строчка (sent by kill) меня сильно путала, мне казалось это доказательством, что вся моя теория гнилая. И действительно, есть способ по той же siginfo понять, кто послал сигнал. Если это был kill, то будет верно вот такое условие: si->si_code == SI_USER.

И Хотспот и правда проверяет это условие! Но слишком поздно, когда уже печатает hs_err файл, вот здесь. Именно такой проверки не хватает в обработке implicit null checks, если бы она была там, подобная атака не была бы возможной.

А что у нас?

Что мы все о Хотспоте, да о Хотспоте. В нашей VM обработка сигналов устроена чуть иначе, что дало мне надежду, что мы то точно так просто не облажаемся!

...

Облажались точно так же, прям один в один. Чтож, может и не стоит быть святее Папы Римского. Но ишуй на починку этого дела я все-таки создал.

--

Когда-то раньше у нас была дополнительная проверка: мы сверяли офсеты при доступе во время развала. Т.е. недостаточно было иметь адрес доступа от 0 до 4096, нужен был прям точный офсет до поля. Соответствующую инфу генерировал компилятор. Это позволяло лучше отличать всякие шальные развалы (или вот внешние сигналы) и благородные NPE. Но увы: слишком уж много памяти эти таблички с дополнительной инфой занимали занимали, поэтому их решили дропнуть.



Вот такой забавный паззлер с довольно глубоким контекстом для понимания. Спасибо Андрею за него, мне было очень весело!

Кстати, мне кажется, это место – отличный кандидат для улучшения Хотспота. Пофиксайте - и станете разработчиком самой популярной JVM в мире ;)

#дух_машины
🔥10💯2
Да, форматирование в JIRA - это какой-то прикол.
💯13
Сегодня @liontiger23 изобрел в разговоре с китайским коллегой фразу "you are measuring how many elephants fit inside of a unicorn" в контексте "ты меришь погоду в унитазе". Очень круто!

А какие еще есть варианты такой фразы на английском? Например, в контексте некорректны бенчей, неправильно организованных экспериментов и т.д.
😁14
выбирай сердцем ❤️
11🤔3👾1
веду пары в начальной школе

(на самом деле нет: угадайте, про какой алгоритм пойдет речь через минуту)
9
разбить рабочий ноут утром перед тем, как лететь в командировку в Китай
😢40😨4👨‍💻2
Красивая письменность все-таки.

Чуваки на часах снизу, видимо, бегут навстречу дедлайну.
😁20
Segfault: объясняем на карточках
20
This media is not supported in your browser
VIEW IN TELEGRAM
Еще final поставить на чужих переменных и импорты пооптимизировать
💯14😁8
В китайских офисах Хуавей есть забавная традиция: в 8-30 начинают раздавать вот такие небольшие пакеты с едой. Внутри разные комбинации продуктов: всякие фрукты, булочки, вода, молоко.

Каждый сотрудник может забрать один такой пакет, поднеся пропуск, а «продавцы» за это получают по 25 юаней (300 рублей). Получается такой маленький шумный базарчик прям на территории кампуса, где каждый продавец хочет, чтобы ты купил именно его набор еды)

Вроде и понятно, что это такая завлекаловка, чтобы сотрудники засиживались на работе почаще, а все равно приятно: в продуктовый по пути домой заходить уже не надо 😴
16👍1😱1
Только я мог простыть в такую погоду, конечно 😢
😢19🫡3🤯1
An Exceptional Case

Недавние разговоры про сигналы и NPE из-за sudo kill -11 разблокировали у меня воспоминание про давний пост от Игоря на схожую тему в нашем старом блоге. С его разрешения раскапываю еще одну стюардессу и публикую здесь соответствующую адаптацию.

--

Итак, как вы помните, исполнение некоторых инструкций может спровоцировать поднятие сигнала операционной системой, чем рантайм с удовольствием пользуется, перехватывая их для своих коварных целей. Самый простой и обсужденный ранее пример - это SIGSEGV, который перехватывается рантаймом и трактуется, как NullPointerException.

Но может ли одна инструкция спровоцировать поднятие двух разных сигналов в зависимости от пути исполнения? На самом деле да, например, вот такую красоту как-то сгенерил наш компилятор:

idiv dword [ecx+10H]


С одной стороны, здесь есть налчек: он спрятан в операнде деления (там как раз происходит разыменование по офсету первого поля объекта), а потом собественно деление. Само деление тоже может пройти неудачно - если получившийся аргумент был равен 0, полетит другой сигнал: SIGFPE. Этот сигнал в свою очередь тоже может быть перехвачен рантаймом, который после этого выбросит джавовский ArithmeticException.

--

На самом деле для нас в рантайме такая ситуация (маленький шедевр компилятора, без иронии) оказалась полной неожиданностью.

Обработать два сигнала из одной инструкции мы, допустим, смогли бы (просто аккуратным диспатчем по типу сигнала), но тут ситуация была сильно хуже.

Дело в том, что на самом деле оригинальный код был какой-то такой:

int foo(Bar k) {
return k.x;
}

void baz() {
Bar k(42);
int r = x / foo(k);
}


Т.е. по факту деление и обращение к полю происходило в разных методах: обращение в foo, а деление в baz. Компилятор просто проинлайнил foo в baz, что позволило ему пойти с оптимизациями дальше и объединить налчек и деление.

И это все прекрасно, но зачем мы все это делаем? Чтобы потом в рантайме выбросить в одному случае NullPointerException, а в другом ArithmeticException. И вот в чем беда: стектрейсы у них должны отличаться, один должен заканчиваться на foo -> baz, а второй просто на baz.

Ну, как должны, вообще JVM не обязана хоть сколько-нибудь содержательные стектрейсы предоставлять, в спеке про это нет ни слова. Можно просто всегда возвращать пустой (почти пустой) стектрейс и спокойно пройти JCK, но клиентам это почему-то очень не нравится.

Получается, что чтобы сделать все красиво, для одной инструкции нужны два набора метаифнормации (включающей в себя информацию об инлайне), с которой рантайм будет консультироваться при построении стектрейса. И вот к этому мы оказались максимально не готовы: вся система хранения метаинформации ожидала, что на одну инструкцию может быть только один такой набор, что звучало вполне логично до описанного случая.

--

Что с этим делать? Можно было прокачивать систему сбора метаинформации, это бы резко усложнило ее архитекутуру и увеличило стоимость поддержки в рантайме. И ради чего? Ради одной хитрой интсрукции? С другой стороны, можно было пренебречь точностью стектрейса одного из исключений (ведь JVM не обязана строить их точно, да вообще хоть как-то!). Это позволило бы оставить только один набор метаинформации, хотя и чуть уменьшило бы удобство пользователя. В целом, приемлемый компромисс, так тогда и поступили.

--

Но на будущее задумались: так ли вообще нужна такая оптимизация? Нужен ли этот маленький шедевр компилятора? По факту кроме эстетического удовольствия смотрящего в асмы, он ничего и не давал, место в коде было не очень горячее. Поэтому при последующей поддержке новых платформ решили, что такие оптимизации делать просто не стоит, а компиялтору стоит быть попроще.

Иногда более простое (но не такое красивое) решение приносит больше пользы в глобальной перспективе, так случилось и здесь. Больше мы инструкций, которые могут поднимать по два сигнала в сгенерированном коде, не видели, так что это был действительно исключительный случай.

#откопали_стюардессу
#дух_машины
👍7💯4🔥2
2025/07/14 00:46:44
Back to Top
HTML Embed Code: