LINUX.ORG.RU
ФорумTalks

Undefined behavior в MUSL

 , ,


1

3

Привет, мои любители пердолинга с C и C++. Я вчера наткнулся на забавный пассаж в MUSL:

$ cat src/misc/syscall.c 
#define _BSD_SOURCE
#include <unistd.h>
#include "syscall.h"
#include <stdarg.h>

#undef syscall

long syscall(long n, ...)
{
	va_list ap;
	syscall_arg_t a,b,c,d,e,f;
	va_start(ap, n);
	a=va_arg(ap, syscall_arg_t);
	b=va_arg(ap, syscall_arg_t);
	c=va_arg(ap, syscall_arg_t);
	d=va_arg(ap, syscall_arg_t);
	e=va_arg(ap, syscall_arg_t);
	f=va_arg(ap, syscall_arg_t);
	va_end(ap);
	return __syscall_ret(__syscall(n,a,b,c,d,e,f));
}

Как вы наверное понимаете, вызовы syscall() сильно различаются по количеству аргументов. Все стандарты C, что я видел, говорят, что вызывать va_arg, если аргумента нет — злостное UB. Поскольку мне лень ковыряться в кишках компиляторов, я решил аппелировать к массовому сознанию LOR — а что GCC и clang вообще делают в таких случаях?

Ответ на: комментарий от kirk_johnson

Ты про stack protector слышал?

В va_*? Нет, не слышал. То, что можно проверять на каждом шаге — да наздоровье, но только это, пожалуйста, в debug, если так неймётся.

Потому что тут это сделать логичнее всего.

Пока у вас получается увы — бездоказательно.

Ну код-то почитай. Функция open() — враппер с va_args, где проверяется flags.

Похоже пора заканчивать... Вы нить разовора держать умеете? Причём тут код open(), если разговор о косвенном вызове любого сискола? Я же вам открытым тесктом спросил, что уже тут ранее тоже спрашивали про парсинг номера сискола огромным switch,

vodz ★★★★★
()
Последнее исправление: vodz (всего исправлений: 1)
Ответ на: комментарий от kirk_johnson

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

но если говорить о syscall в общем виде, то других реализаций просто не существует в природе. иначе можно предложить юзеру дёргать вызовы типа syscall0, syscall2, syscall5 и т.д.

Iron_Bug ★★★★★
()
Последнее исправление: Iron_Bug (всего исправлений: 3)
Ответ на: комментарий от kirk_johnson

я не проверяла все реализации компиляторов. количество платформ у musl ограничено. на x86_64 и gcc это так. остальное оставлю на совесть разработчиков.

я тебе ещё раз повторяю: для syscall в общем виде нет других реализаций. физически нет. но и syscall в таком виде практически не используется юзерспейсом. а в ядре нет libc.

Iron_Bug ★★★★★
()
Ответ на: комментарий от Iron_Bug

я тебе ещё раз повторяю: для syscall в общем виде нет других реализаций. физически нет. но и syscall в таком виде практически не используется юзерспейсом. а в ядре нет libc.

Да йопт, ядро-то здесь причем? Это юзерспейсная часть. Сисколы из юзерспейса зовут. В ядре сисколы звать не нужно.

kirk_johnson ★☆
() автор топика
Последнее исправление: kirk_johnson (всего исправлений: 2)
Ответ на: комментарий от kirk_johnson

читай мой пост выше, про реализацию read, для примера. и для юзера libc это и есть основное использование библиотеки. он не дёргает syscall с номером системного вызова. только редкие отладочные и системные утлиты это делают.

Iron_Bug ★★★★★
()
Ответ на: комментарий от Iron_Bug

он не дёргает syscall с номером системного вызова. только редкие отладочные и системные утлиты это делают.

Linux AIO используется базах данных, в связке с O_DIRECT. Для этой группы сисколов нет врапперов в libc. Только руками, только хардкор. Ну или через libaio, где либо сами асм делают, либо зовут syscall(). А в нем UB.

kirk_johnson ★☆
() автор топика
Последнее исправление: kirk_johnson (всего исправлений: 3)
Ответ на: комментарий от kirk_johnson

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

Iron_Bug ★★★★★
()
Ответ на: комментарий от Iron_Bug

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

Что понимают? Вызов syscall() с отличным от шести количеством аргументов — UB. Не то чтобы многие регулярно лазают код libc читать, стиль кодирования по большей части отвратный.

kirk_johnson ★☆
() автор топика
Ответ на: комментарий от Harald

undefined же, не prohibited :) они ж не предлагают тебе этот код MSVC конпелировать

Напомнить, к каким чудесам ведет integer overflow?

kirk_johnson ★☆
() автор топика
Последнее исправление: kirk_johnson (всего исправлений: 1)

По большей части, я все это к чему — когда у Rust стабилизируют ABI, получится нормальный язык, в котором такой срани не будет. Офигенно же!

kirk_johnson ★☆
() автор топика
Ответ на: комментарий от kirk_johnson

а тебе кто-нибудь где-нибудь гарантировал, что в реализации стандартной библиотеки не должно быть UB?

Harald ★★★★★
()
Ответ на: комментарий от Harald

а тебе кто-нибудь где-нибудь гарантировал, что в реализации стандартной библиотеки не должно быть UB?

Линукс на автомобили ставят. Ты мне и расскажи.

kirk_johnson ★☆
() автор топика
Ответ на: комментарий от kirk_johnson

ну поведение конкретной версии библиотеки с конкретной версией конпелятора на заданной архитектуре вполне себе defined, можно хоть формальную верификацию делать

Harald ★★★★★
()
Последнее исправление: Harald (всего исправлений: 1)
Ответ на: комментарий от Harald

ну поведение конкретной версии библиотеки с конкретной версией конпелятора на заданной архитектуре вполне себе defined, можно хоть формальную верификацию делать

Как с memset() было в свое время, да?

kirk_johnson ★☆
() автор топика
Ответ на: комментарий от kirk_johnson

Да, в glibc то же самое.

Вроде не для всех архитектур. Для некоторых на асме написана функция эта.

Deleted
()

Автор, а не хочешь ли ты написать багрепорт в glibc и musl?

ИМХО надо либо получить некий официальный ответ и от тех и от тех на эту тему, либо пусть чинят.

Deleted
()
Ответ на: комментарий от Deleted

Автор, а не хочешь ли ты написать багрепорт в glibc и musl?

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

vodz ★★★★★
()
Ответ на: комментарий от vodz

если будет продемонстрировано: библиотеку без этой «некрасивости»

В glibc для некоторых архитектур функция syscall() написана на асме, при этом никакого UB очевидно быть не может.

Deleted
()
Последнее исправление: Deleted (всего исправлений: 1)
Ответ на: комментарий от Deleted

В glibc для некоторых архитектур функция syscall() написана на асме, при этом никакого UB очевидно быть не может.

Этот код полностью эквивалентен такой же реализации на C: помещаем архитектурно-зависимое количество аргументов из регистров и (с тем же самым «UB») берем оставшиеся из стека. Ассемблер тут просто делает это быстрее и качественнее, не устраняя то «UB» никак. Печально, что вы это не видите, ибо проблема тут не в libc, а собственно в новых веяниях C - желательность деклараций функций, и для таких как open/fcntl/ioctl/syscall это вступает в конфликт, исторически привнесенный.

vodz ★★★★★
()
Ответ на: комментарий от vodz

Этот код полностью эквивалентен такой же реализации на C

Нет. При вызове с меньшим количеством опциональных аргументов, чем вызовов va_arg() код на C не эквивалентен коду на асме.

Скажем, в будущей версии gcc могут прикрутить проверку внутрь va_arg(), которая делает abort(), если такого аргумента точно нет. Это будет совершенно валидное поведение, при этом сабжевый код начнёт валиться. И я удивлён, что в ubsan этого ещё нет =).

Ассемблер тут просто делает это быстрее и качественнее, не устраняя то «UB» никак.

Суть UB в том, что результат выполнения может быть любым - на усмотрение компилятора/процессора/etc. Конкретно в этом случае ассемблерный код никакого UB не содержит, а сишный - содержит.

Печально, что вы это не видите, ибо проблема тут не в libc, а собственно в новых веяниях C - желательность деклараций функций, и для таких как open/fcntl/ioctl/syscall это вступает в конфликт, исторически привнесенный.

Неприятие говнокода и подхода «работает жы, значит всё норм!» - это не новое веяние.

Deleted
()
Ответ на: комментарий от Iron_Bug

=)

поэтому ядро привязано к не просто к gcc, а даже к конкретной версии.

Это от того, что у вас пиратская версия ядра. В лицензионной версии никакой привязки к конкретной версии gcc нет, есть только ограничение на минимальную версию. И от привязки конкретно к gcc тоже уже почти ничего не осталось. Гугль вон клангом линуксовое ядро собирает.

Deleted
()
Ответ на: комментарий от Deleted

Нет. При вызове с меньшим количеством опциональных аргументов, чем вызовов va_arg() код на C не эквивалентен коду на асме.

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

vodz ★★★★★
()
Ответ на: комментарий от Deleted

Иди уже почитай про UB и перестань нести ахинею.

Если вы не заметили, я употреблял терминологию этого топика, с которой я не согласен, либо как «UB», либо как «UB TCа». Так что на лицо типичный ЛОР: конкретно и аргументированно сказать нечего, но послать тут обязательно...

vodz ★★★★★
()
Последнее исправление: vodz (всего исправлений: 1)
Ответ на: комментарий от vodz

Я уже выше аргументированно сказал про замену невалидного вызова va_arg() на abort(). Стандарт это разрешает, а код в libc от этого очевидным образом сломается. Всё. Не очень понимаю какие ещё аргументы тебе нужны.

Deleted
()
Ответ на: комментарий от Deleted

Я уже выше аргументированно сказал про замену невалидного вызова va_arg() на abort().

Ну так сделайте еще один шаг: перепишите вот тот ассемблерный код, который и не исправляет батхерт ТСа с этим super_va*. И что получим? А получим abort() в 99%, так как сисколов с 6 параметрами шиш да маленько.

vodz ★★★★★
()
Последнее исправление: vodz (всего исправлений: 2)
Ответ на: комментарий от vodz

А я ещё раз отправлю тебя читать про UB и его смысл. Мне не лень.

Смотри стандарт C, википедию, статьи в интернете, которые это разжёвывают. Можешь ещё поискать сообщения о проблемах, к которым приводит UB в коде.

Possible undefined behavior ranges from ignoring the situation completely with unpredictable results,to behaving during translation or program execution in a documented manner characteristic of the environment (with orwithout the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnosticmessage).

Deleted
()
Ответ на: комментарий от Deleted

А я ещё раз отправлю тебя читать про UB и его смысл. Мне не лень.

Ну то есть с аргументами обосрались и решили соскочить на тему тупого профессора, по принципу главное сделать умный вид, а осадочек всё равно останется?

vodz ★★★★★
()
Ответ на: комментарий от vodz

Я уже выше аргументированно сказал про замену невалидного вызова va_arg() на abort(). Стандарт это разрешает, а код в libc от этого очевидным образом сломается. Всё. Не очень понимаю какие ещё аргументы тебе нужны.

Deleted
()
Ответ на: комментарий от Deleted

Ты точно понимаешь чем отличается C от ассемблера?

Всего вам наилучшего. Чатик с тупняком мне не доставляет.

vodz ★★★★★
()
Ответ на: комментарий от Deleted

У меня такое ощущение, что чувак не понимает, что не заполнить регистры, которые ядро все равно читать не будет (аргументов-то меньше) и вызвать код на C, который упасть может — это вообще ортогональные вещи.

kirk_johnson ★☆
() автор топика
Ответ на: комментарий от Deleted

Автор, а не хочешь ли ты написать багрепорт в glibc и musl?

Ну, собственно, а чо бы и нет.

kirk_johnson ★☆
() автор топика
Ответ на: комментарий от vodz

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

Нет, конечно, лол, про стек ты вещать начал. Я говорю о том, что вызов va_arg() без аргумента — UB. UB означает потенциальный abort() в реализации va_arg(). Стек тут ни при чем.

kirk_johnson ★☆
() автор топика
Ответ на: комментарий от kirk_johnson

Я говорю о том, что вызов va_arg() без аргумента — UB.

функции va* - получение доступа к значениям из переданных списком аргументов. Для предотвращения ошибок подсчёта текущего количества уже считанного, в C принято неопределённое заранее количество аргументов передавать только через стек. Макросы va* разворачиваются иногда даже не в ассемблерные, а C-шные простейшие манипуляции с указателями. Откуда тут могут быть неопределенное поведение?

Да, мне припоминается, что в БЭСМ-6 была защита стека. Были такие архитектуры, где стеков было несколько: один для данных, другой для адресов возврата из функций. Там нельзя было ни поменять адрес возврата (хотя номер кадра стека можно было перепрыгнуть, но это вроде не является проблемой для написания злонамеренного кода), ни даже влезть в кадр несвоей функции. Но на таких архитектурах, как ни странно, переписывать va* тоже не надо. Вы получите аппаратный trap при вылезании, что просто замечательно.

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

vodz ★★★★★
()
Ответ на: комментарий от vodz

функции va* - получение доступа к значениям из переданных списком аргументов. Для предотвращения ошибок подсчёта текущего количества уже считанного, в C принято неопределённое заранее количество аргументов передавать только через стек.

Увы, нет. На x86_64 допустимо передавать через регистры.

Вот тут чувак об этом пишет: https://blog.nelhage.com/2010/10/amd64-and-va_arg/

Макросы va* разворачиваются иногда даже не в ассемблерные, а C-шные простейшие манипуляции с указателями. Откуда тут могут быть неопределенное поведение?

Потому что стандарт так говорит. Как именно закодили va_arg() — не определено стандартом. Он может хоть через UDP у соседнего датацентра запрашивать аргументы.

Проблему вы выявили интересную: именно всё выше сказанное не относится именно к сисколам:

Да что вы к сисколам-то привязались? Это просто функция, в которой баг. Там есть другие такие же функции. Дело не в том, что тут сисколы, а в том, что va_arg() неправильно используют. Почему только mironov_ivan это с первого раза понял?

там принято максимально юзать регистры, и только когда уж совсем их не остаётся, то передавать значение в сискол через стек.

Господи Иисусе, мне кажется, что вы с Iron_Bug какой-то другой тред читаете. Что там ядро с сисколами делает — тут абсолютно нерелевантно, UB в неверном использовании va_arg().

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

Очень просто — убрать к черту функцию из стандарта, как это сделали с gets(). И сделать функцию, которая принимает заодно и кол-во аргументов. Ну или переписать её на asm, с ним проблемы не будет, потому что он просто копирует регистры, а не зовет va_arg().

kirk_johnson ★☆
() автор топика
Последнее исправление: kirk_johnson (всего исправлений: 3)
Ответ на: комментарий от kirk_johnson

Увы, нет. На x86_64 допустимо передавать через регистры.
Вот тут чувак об этом пишет: https://blog.nelhage.com/2010/10/amd64-and-va_arg/

Хорошая статья, только вы её не поняли. Там говорится, что если бы переписать va* и стандарт языка и решить проблему подсчёта уже считаного через введение специальной структуры, то можно заюзать тот же принцип передачи вначале через регистры и для функций с переменным количеством аргументов.

Да что вы к сисколам-то привязались? Это просто функция, в которой баг. Там есть другие такие же функции.

А! Вы, наверное, не в курсе, что это не баг вовсе, а хорошее решение для уменьшения накладных расходов при вызове сисколов. Доступ к пользовательскому стеку из ядра — весьма накладная вещь. Страница может уже быть даже в свопе и со всем от сюда вытекающим. Толи дело — регистры.

vodz ★★★★★
()
Ответ на: комментарий от vodz

Хорошая статья, только вы её не поняли. Там говорится, что если бы переписать va* и стандарт языка и решить проблему подсчёта уже считаного через введение специальной структуры, то можно заюзать тот же принцип передачи вначале через регистры и для функций с переменным количеством аргументов.

Нет, там говорится, что в LLVM этого не было в 2010 году, а в clang было:

A while back, I was poking around LLVM bugs, and discovered, to my surprise, that LLVM doesn’t support the va_arg intrinsic, used by functions to accept multiple arguments, at all on amd64. It turns out that clang and llvm-gcc, the compilers that backend to LLVM, have their own implementations in the frontend, so this isn’t as big a deal as it might sound, but it was still a surprise to me.

Чувак решил написать патч для LLVM:

Figuring that this might just be something no one got around to, and couldn’t actually be that hard, I pulled out my copy of the amd64 ABI specification, figuring that maybe I could throw together a patch and fix this issue.

Maybe half an hour of reading later, I stopped in terror and gave up, repenting of my foolish ways to go work on something else. va_arg on amd64 is a hairy, hairy beast, and probably not something I was going to hack together in an evening. And so instead I decided to blog about it.

Ты либо очень жырно меня троллируешь, либо не умеешь в английский.

А! Вы, наверное, не в курсе, что это не баг вовсе, а хорошее решение для уменьшения накладных расходов при вызове сисколов. Доступ к пользовательскому стеку из ядра — весьма накладная вещь. Страница может уже быть даже в свопе и со всем от сюда вытекающим. Толи дело — регистры.

UB — не баг, а оптимизация. Чертяка, жги ещё!

kirk_johnson ★☆
() автор топика
Последнее исправление: kirk_johnson (всего исправлений: 2)
Ответ на: комментарий от kirk_johnson

Нет, там говорится, что в LLVM этого не было в 2010 году, а в clang было:

Я именно об этом и сказал, что можно изменив встроенный в компилятор метод работы с va* с простейших манипуляций до функций со вспомогательными структурами поменять принцип работы передачи неопределенного количества аргументов. Возможно в результате будет работать всё и быстрее. Ну и что?

vodz ★★★★★
()
Ответ на: комментарий от vodz

Я именно об этом и сказал, что можно изменив встроенный в компилятор метод работы с va* с простейших манипуляций до функций со вспомогательными структурами поменять принцип работы передачи неопределенного количества аргументов. Возможно в результате будет работать всё и быстрее. Ну и что?

Во-первых, ты сказал не это:

Там говорится, что если бы переписать va* и стандарт языка

Во-вторых, ничего переписывать не нужно, потому что эта структура ТАМ И ТАК ИСПОЛЬЗУЕТСЯ (в clang конечно же, в llvm этого просто не было).

А вот и алгоритм:

+  // Decide which area this value should be read from.
+  // TODO: Implement the AMD64 ABI in its entirety. This simple
+  // selection mechanism works only for the basic types.
+  if (ArgVT == MVT::f80) {
+    llvm_unreachable("va_arg for f80 not yet implemented");
+  } else if (ArgVT.isFloatingPoint() && ArgSize <= 16 /*bytes*/) {
+    ArgMode = 2;  // Argument passed in XMM register. Use fp_offset.
+  } else if (ArgVT.isInteger() && ArgSize <= 32 /*bytes*/) {
+    ArgMode = 1;  // Argument passed in GPR64 register(s). Use gp_offset.
+  } else {
+    llvm_unreachable("Unhandled argument type in LowerVAARG");
+  }

kirk_johnson ★☆
() автор топика
Последнее исправление: kirk_johnson (всего исправлений: 3)
Ответ на: комментарий от kirk_johnson

Во-первых, ты сказал не это:

Про стандарт языка я намекнул именно по причине удивления автора статьи, что втихаря поменян общий и простейший принцип. Ибо в стандарте этот момент, хоть и явно архитектурно-зависимый, но прописан, что передаём через стек.

vodz ★★★★★
()
Ответ на: комментарий от vodz

Про стандарт языка я намекнул именно по причине удивления автора статьи, что втихаря поменян общий и простейший принцип. Ибо в стандарте этот момент, хоть и явно архитектурно-зависимый, но прописан, что передаём через стек.

Ооо детка, дай-ка мне ссылочку на это высказывание в C11.

kirk_johnson ★☆
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.