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

Я пытаюсь написать небольшую и простую библиотеку сопрограмм, чтобы лучше понять сопрограммы С++ 20. Кажется, это работает нормально, но когда я компилирую с помощью clang's address sanitizer, меня выдает.

Я сузил проблему до следующего примера кода (доступен с выходными данными компилятора и дезинфицирующего средства по адресу https://godbolt.org/z/WqY6Gd), но я до сих пор не могу в этом разобраться.

// namespace coro = std::/std::experimental;

// inlining this suppresses the error
__attribute__((noinline)) void foo(int& i) { i = 0; }

struct task {
  struct promise_type {
    promise_type() = default;
    coro::suspend_always initial_suspend() noexcept { return {}; }
    coro::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() noexcept { std::terminate(); }
    void return_value(int v) noexcept { value = v; }
    task get_return_object() {
      return task{coro::coroutine_handle<promise_type>::from_promise(*this)};
    }
    int value{};
  };

  void Start() { return handle_.resume(); }
  int Get() {
    auto& promise = handle_.promise();
    return promise.value;
  }

  coro::coroutine_handle<promise_type> handle_;
};

task func() { co_return 3; }

int main() {
  auto t = func();
  t.Start();
  const auto result = t.Get();
  foo(t.handle_.promise().value);
  // moving this one line down or separating this into a noinline 
  // function suppresses the error
  // removing this removes the stack-use-after-scope, but (rightfully) reports a leak
  t.handle_.destroy();
  if (result != 3) return 1;
}

Дезинфицирующее средство адресов сообщает об использовании после области действия (полный вывод доступен на сайте godbolt, ссылка выше). С некоторой помощью от lldb я обнаружил, что ошибка возникает в main, точнее: переход на строку 112 в листинге сборки, jne .LBB2_15, переходит к отчету asan и никогда не возвращается. Кажется, это внутри пролога main.

Как видно из комментариев, перемещение destroy() на строку вниз или вызов его в отдельной функции noinline1 меняет поведение средства очистки адресов. Единственными двумя объяснениями этого, по-видимому, являются неопределенное поведение и ложное срабатывание asan (или сам -fsanitize=address создает проблемы с продолжительностью жизни, что в некотором смысле одно и то же).

На данный момент я почти уверен, что в приведенном выше коде нет UB: и task, и result живут во фрейме основного стека, объект обещания живет во фрейме сопрограммы. Сам фрейм размещается (в стеке main из-за отсутствия точек приостановки) в строке 1 main и уничтожается прямо перед возвратом после последнего обращения к нему в foo(). Фрейм сопрограммы не уничтожается автоматически, поскольку управление никогда не передается co_await final_suspend(), согласно стандарт. Однако я некоторое время смотрел на этот код, поэтому, пожалуйста, простите меня, если я пропустил что-то очевидное.

Сборка, сгенерированная без санации, мне кажется, имеет смысл, и весь доступ к памяти происходит в пределах [rsp, rsp+24], как выделено. Кроме того, компиляция с помощью -fsanitize=address,undefined, или просто -fsanitize=undefined, или просто компиляция с помощью gcc с -fsanitize=address не сообщает об ошибках, что наводит меня на мысль, что проблема скрыта где-то в коде, сгенерированном asan.

К сожалению, я не могу понять, что именно происходит в коде, созданном asan, и поэтому я публикую это. У меня есть общее представление об алгоритме дезинфекции адресов, но я не могу сопоставить сборку доступ/распределение памяти к тому, что происходит в коде C++.

Я надеюсь, что ответ поможет мне

  1. Понять, где спрятаны жизненные проблемы, если они есть
  2. Поймите, что именно происходит в main при компиляции с asan, чтобы человек, читающий это, мог иметь более ясный способ определить, какой доступ к памяти в коде C++ вызвал ошибку, и где (если где-нибудь) была выделена и освобождена эта память.
  3. Последовательно подавляйте это конкретное ложное срабатывание и немного уточните, что его вызывает, если проблема действительно в асане.

Заранее спасибо.


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


person ARentalTV    schedule 16.03.2021    source источник
comment
меньший/более чистый пример: godbolt.org/z/d76xTa   -  person ARentalTV    schedule 18.03.2021


Ответы (1)


Я думаю, что понял это, но в основном потому, что это уже исправлено в clang12.0.

Выполнение более мелкого/чистого примера с clang-12 не показывает ошибок asan. Отличие заключается в следующих строках:

    movabs  rcx, -866669180174077455    
    mov     qword ptr [r13 + 2147450880], rcx   
    mov     dword ptr [r13 + 2147450888], -202116109    
    lea     rdi, [r12 + 40] 
    mov     rcx, rdi    
    shr     rcx, 3  
    cmp     byte ptr [rcx + 2147450880], 0  
    jne     .LBB2_14    
    lea     r14, [r12 + 32] 
    mov     qword ptr [r14 + 8], offset f() [clone .cleanup]    
    lea     rdi, [r14 + 16] 
    mov     byte ptr [r13 + 2147450884], 0  
    mov     rcx, rdi    
    shr     rcx, 3  
    mov     dl, byte ptr [rcx + 2147450880] 
    test    dl, dl  
    jne     .LBB2_7 
.LBB2_8:    
    mov     qword ptr [rbx + 16], rax       # 8-byte Spill  
    mov     dword ptr [r14 + 16], 0

Что есть у clang-11, а у clang-12 нет. Судя по всему, дезинфицирующее средство адресов пытается проверить, что r12+40 (который должен быть методом очистки промиса) инициализирован перед его инициализацией. Clang-12 просто не проверяет промис, оставляя весь приведенный выше код вне поля зрения.

TL;DR: (вероятно) ошибка в санации сопрограммы clang-11, исправленная в 12.0, возможно, и в более поздних версиях clang-11.

person ARentalTV    schedule 10.05.2021