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 вообще делают в таких случаях?

Под оффтопиком: gcc возвращает 0, clang возвращает мусор.

Под онтопиком: и тот, и другой возвращают мусор.

buddhist ★★★★★
()
Последнее исправление: buddhist (всего исправлений: 1)

Как ты предлагаешь это починить, не сломав API? Учитывая, что вроде как способа проверить, сколько аргументов передано, в C нету.

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

Как ты предлагаешь это починить, не сломав API? Учитывая, что вроде как способа проверить, сколько аргументов передано, в C нету.

Я не предлагаю ничего чинить, мне интересно, как ведут себя компиляторы.

P.S. Но по факту, жирный switch-case, конечно же. Если без UB. Или разные макросы syscall3, syscall2 и т.д. В ядре вроде так.

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

А какие у них варианты, собственно?

Хороший вопрос. Меня другое интересует — получается, один из самых фундаментальных системных интерфейсов основан исключительно на том, что никто не сделает компилятор, который не выдаст панику / коррупцию стека при таком повороте событий. Как так вышло-то?

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

Забавно, что у них такая параша по всему коду. Они вроде догадались, что это не очень хорошо, и начали потихоньку исправлять.

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

жирный switch-case

Функция syscall используется в случаях, когда нужно дёрнуть что-то не предусмотренное libc. Как эта самая libc будет делать switch по тому, о чём сама не знает?

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

Функция sycall используется в случаях, когда нужно дёрнуть что-то непредусмотренное libc. Как эта самая libc будет делать switch по тому, о чём сама не знает?

Не всегда, иногда про сискол все знают, но врапер не делают. Но для кастомных это было бы проблемой, да.

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

Но по факту, жирный switch-case, конечно же.

Switch-case на что? На номер сисколла? Юзер может и так не присунуть нужное количество аргументов, и ты получишь то же UB.

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

Switch-case на что? На номер сисколла? Юзер может и так не присунуть нужное количество аргументов, и ты получишь то же UB.

Это уже проблема юзера, как с printf(). А тут у нас все прямо в бинарник зашито.

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

Результат, в текущих реалиях, будет абсолютно тем же.

Гораздо интереснее будет, если таким образом можно добраться до адреса возврата на стеке и переписать содержимое памяти по тому адресу через ядро. Но я не думаю, что такое сработает.

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

Результат, в текущих реалиях, будет абсолютно тем же.

Ну с printf() как-то справились? Конечно, там компилятор помогает, но тем не менее.

kirk_johnson ★☆
() автор топика

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

#define syscall_HELPER_sixth(x, a, b, c, d, e, f, n, ...) n                                                   
#define syscall_HELPER_nargs(...) syscall_HELPER_sixth(__VA_ARGS__, syscall_real_6, syscall_real_5, syscall_real_4, syscall_real_3, syscall_real_2, syscall_real_1, syscall_real_0)
#define syscall(...) syscall_HELPER_nargs(__VA_ARGS__)( __VA_ARGS__ )
long syscall_real_0(long n);
long syscall_real_1(long n, syscall_arg_t a);
...
rymis ★★
()
Ответ на: комментарий от rymis

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

Ну так себе утешение. Алсо я не думаю, что в gcc используют именно syscall(). Зачем оно им? А для врапперов для сисколов, о которых libc знает, musl использует кошерные __syscall1(), __syscall2() и т.д.

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

А так можно было бы сделать

В стандарте вроде написано, что это функция. И если её макросом заменить, то у кого-то может код поломаться, кмк.

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

А где же ссылка на багрепорт?

Они в курсе.

1. Почему ты так думаешь?
2. Разве не интересно получить официальный ответ/комментарий?

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

1. Почему ты так думаешь?

Потому что они такое регулярно выпиливают в других частях проекта. Ну ещё и потому, что в glibc тоже такое. И все это вряд ли сломается в обозримом будущем, но все-таки это как-то не айс.

2. Разве не интересно получить официальный ответ/комментарий?

Чуваки скопировали из glibc :))

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

они такое регулярно выпиливают в других частях проекта

Не факт, что это делается из соображений UB

Чуваки скопировали из glibc :))

Всё равно ж интересно, считают ли они это приемлемым, и как будут агрументировать/приоретизировать

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

Не факт, что это делается из соображений UB

В комменте написано «STOP DOING UB LALZ». Как минимум — догадываются.

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

Как так вышло-то?

Тут другое непонятно, у вас musl устарел и не содержит новый syscall ядра? Ибо сильно исторически вызов syscall всегда программировался в libc через ассемблер, а теперь в C добавили мусор с псевдоключевыми словами с кучей подчёркиваний, в результате получив лечение хуже болезни.

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

Тут другое непонятно, у вас musl устарел и не содержит новый syscall ядра? Ибо сильно исторически вызов syscall всегда программировался в libc через ассемблер, а теперь в C добавили мусор с псевдоключевыми словами с кучей подчёркиваний, в результате получив лечение хуже болезни.

Ничо не понял. Скорее всего, асм спрятан дальше по стеку.

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

Скорее всего, асм спрятан дальше по стеку.

Если есть поддержка в компиляторе типа gnu-того __attribute__((regparm)), то ассемблер теперь вообще не нужен до определенного количества, пока стек не юзается, но чисто эстетически это выглядит сплошным фи.

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

Если есть поддержка в компиляторе типа gnu-того __attribute__((regparm)), то ассемблер теперь вообще не нужен до определенного количества, пока стек не юзается, но чисто эстетически это выглядит сплошным фи.

#define __regparam ...

Всегда так делал с unused. Но тред не о том.

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

Но тред не о том.

Ну Ok.

что никто не сделает компилятор, который не выдаст панику / коррупцию стека при таком повороте событий.

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

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

Либо я не очень понимаю, что ты хочешь сказать, либо наоборот. printf(), пока ты не обосрался с количеством и типом аргумента, абсолютно корректен. Он делает столько va_arg(), сколько параметров в форматной строке. syscall() , пока ты не передал ему максимальное количество аргументов — нет, потому что вызов va_arg() после последнего VA элемента — UB. И по сути, нам сейчас просто очень везет.

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

Учитывая, что вроде как способа проверить, сколько аргументов передано, в C нету.

Есть говноконструкция на макросах, которой это можно сделать, недавно вбрасывал. https://wandbox.org/permlink/pYelj3j8fRYm5MrH

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

printf(), пока ты не обосрался с количеством и типом аргумента, абсолютно корректен.

Вот только va* и vprintf() появились не сразу, и все делали какой-нибудь my_error() именно вот так как у вас в стартовом коде.

UB

UB плох, когда результат непредсказуем. Тут же всё понятно. Ну можно залезть чуть глубже в стек. Но там гарантированно как минимум main(ac,av,env)+текущий вызов этого syscall, то есть в конец стека точно не упрёмся, а значения не задействованные в текущем syscall и не юзаются.

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

Ну или можно как-то так

#include <stdio.h>
#include <stdlib.h>

#define SYSCALL(n, ...) syscall_do(n, sizeof((long[]){__VA_ARGS__})/sizeof(long), (long[]){__VA_ARGS__} )

void syscall_do (long n, size_t num, long arr[])
{
  printf("%ld ", n);
  for(size_t i = 0; i < num; i++)
  {
    printf("%ld ", arr[i]);
  }
  printf("\n");
}

int main(void)
{
  SYSCALL(123, 1, 2, 3);
  SYSCALL(1234, 1, 2, 3, 4); 
  SYSCALL(12345, 1, 2, 3, 4, 5); 
  return 0;
}

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

UB плох, когда результат непредсказуем. Тут же всё понятно.

Да ни разу. va_list может содержать счетчик аргументов, в дополнение к указателю на стек. При превышении — abort(). Я бы так и сделал.

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

Я бы так и сделал

Это сработает, только если действительно вышли за границу кадра стека. Это даже не помогает printf, так как там бОльшая проблема ошибка в порядке следования, а не в количестве. Или, например, есть ещё валидные переменные в стеке, которые потом будут прочитаны после va_next(). А со сисколами вообще бяда, вот, скажем, open(): может быть 2 или 3 аргумента.

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

Это сработает, только если действительно вышли за границу кадра стека.

Зачем? va_arg() инкрементирует счетчик, если перебрал — abort(). Стек тут ни при чем.

А со сисколами вообще бяда, вот, скажем, open(): может быть 2 или 3 аргумента.

И? В open проверяется flags, и если там O_CREAT или O_TMPFILE, ты должен передать аргумент. Иначе бобо. И в этом случае abort() тоже уместен, чтобы вместо mode какую-то хрень со стека не передал.

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

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

если есть какая-то разноформатная фигня неопределённой длины, которую надо передавать в функцию, иногда её просто сериализуют. ну или какие-то указатели на void и типы, в массиве, например, или в списке.

а что касается сисколла, в большинстве ABI там 6 параметров. емнип, musl поддерживает не все архитектуры и, видимо, они ограничились стандартными шестью.

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

а что касается сисколла, в большинстве ABI там 6 параметров. емнип, musl поддерживает не все архитектуры и, видимо, они ограничились стандартными шестью.

Тут UB в реализации. Не важно, шесть их или десять.

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

в чём ты видишь UB? там всегда шесть аргументов. musl - стандартная библиотека и она привязана к ABI.

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

но вообще, musl пожёстче, чем glibc. у него нет цели сделать безопасно. у него есть цель работать быстро и эффективно. поэтому многие косяки, которые могут прокатить в glibc, в musl не прокатывают. и я часто патчу вполне рабочий код для glibc, который имеет такие мелкие косяки. в musl'е они вылезают на поверхность.

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

в чём ты видишь UB? там всегда шесть аргументов. musl - стандартная библиотека и она привязана к ABI.

В том, что syscall() принимает произвольное количество аргументов. Например, для io_submit их три, и для него нет враппера в libc. Вызов последующих va_arg() — UB.

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

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

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

musl используется в эмбеддеде, там, где нужна скорость, лёгкость, отсутствие ненужных проверок на каждый чих. он простой и очень эффективный. он ближе к первым реализациям libc: вполне рабочий велосипед без дополнительных свистоперделок.

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

Зачем? va_arg() инкрементирует счетчик,

Какой счётчик? Это же C, тут как написано printf(fmt, a...) так и транслируем. Хотите странную защиту, то и вводите счётчик, а va_* то причём?

И? В open проверяется flags,

Я так не играю. То вам автосчётчик, то flags. Вы уж определитесь... Или вы внутри syscall() будете switch таки делать?

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

вообще, разработчики musl придерживаются идеологии

Можно подумать, syscall() в других библиотеках как-то по другому сделан.

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

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

но в ассемблерных реализациях есть ещё вариации по количеству аргументов. и к конкретным вызовам типа там read привязаны конкретные реализации, чтобы было быстрее.

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

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

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

Какой счётчик? Это же C, тут как написано printf(fmt, a...) так и транслируем.

Ты про stack protector слышал? В C давно не просто трансляция.

Хотите странную защиту, то и вводите счётчик, а va_* то причём?

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

Я так не играю. То вам автосчётчик, то flags. Вы уж определитесь... Или вы внутри syscall() будете switch таки делать?

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

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

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

Причем тут ядро? Я тебе говорю о том, что дергать va_arg() без наличия аргумента в va_list — UB, и компилятор, в обще случае, может хоть abort() в этот вызов засунуть.

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

ты говоришь о стандарте. а стандартная библиотека привязана к конкретным компиляторам. и там UB превращается во вполне себе конкретные значения.

это не юзерский уровень. это уже системные вещи и они не имеют общих реализаций.

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

ты говоришь о стандарте. а стандартная библиотека привязана к конкретным компиляторам.

Это кто тебе такое рассказал? Вот выпустил МЦСТ свой цомпилер для эльбруса и что, в нем работать не должно? Какой тогда смысл в стандартах вообще?

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