Я обнаружил эту известную проблему в cgo несколько лет назад, но она актуальна и сегодня. Несмотря на то, что проблема задокументирована, ее все равно сложно отладить, если вы не знаете о ней заранее. В настоящее время в погоне за производительностью программисты часто используют ручное управление памятью, вызывая malloc/free через механизм cgo. В первую очередь меня интересовало использование jemalloc для одного из моих проектов. Позвонить из Go довольно просто.

Тем временем, в процессе тестирования проекта, я время от времени сталкивался с крашем "плохой указатель в барьере записи".

Несмотря на то, что я столкнулся с проблемой еще в Go 1.7.6, она по-прежнему актуальна в Go 1.18.4. Проблема возникала крайне редко, раз в несколько недель или даже месяцев, что указывало на ее вероятностный характер, и в то же время серьезно осложняло отладку. Первой моей мыслью было изучить, что такое барьеры записи, но в процессе выяснилось, что проблема не непосредственно в них, а в том, как этот механизм работает в связке с С-памятью. В поисках решения этой проблемы я изучил документацию cgo и наткнулся на следующее замечание.

Примечание: текущая реализация содержит ошибку. Хотя коду Go разрешено записывать nil или указатель C (но не указатель Go) в память C, текущая реализация может иногда вызывать ошибку времени выполнения, если содержимое памяти C выглядит как указатель Go. Поэтому избегайте передачи неинициализированной памяти C в код Go, если код Go будет хранить в ней значения указателя. Обнулите память в C, прежде чем передавать ее в Go.

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

Похоже, условие src ‹ minPhysPageSize не выполнено. Давайте посмотрим, как это может быть, и возьмем следующий вызов вверх по стеку.

Это просто итерация по массиву указателей с вызовом writebarrier_nostore для каждого из них. Эта функция вызывается из typedmemmove.

Ну, это обычный memmove (который написан на ассемблере), за которым следует heapBitsBulkBarrier. Итак, при копировании объекта в Go вызывается функция typedmemmove, которая вызывает функцию heapBitsBulkBarrier, что приводит к проверке всех возможных указателей по writebarrier_nostore. Однако представьте, что мы присваиваем объекту в C-памяти неинициализированные данные (например, je_malloc возвращает).

Если часть этих данных вдруг станет неинициализированным указателем, который не удовлетворяет условию src ‹ minPhysPageSize, произойдет сбой. Однако, поскольку у нас есть условие src != 0, сбой никогда не произойдет, если у нас будет нулевой указатель.

Решение оказалось невероятно коротким. Все, что нам нужно, это обнулить память на стороне C, как описано в примечании cgo . Самый простой способ сделать это — использовать je_calloc вместо je_malloc.

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