GC Hole — баг, ситуация, когда в программе образуется ссылка на объект, уже собранный или перемещённый сборщиком мусора. Часто встречается в коде, который решили "микрооптимизировать".
Я никогда не встречал подобного бага вне кода, найденного в интернетах. Предположу, что его допускают в первую очередь программисты, переходящие на C# с языков с ручным управлением памятью — там аналогичная проблема называется use-after-free. По сути, GC Hole — частный случай use-after-free, только освобождение памяти выполняется не вручную, а сборщиком мусора.
Причина бага — попытка работать со ссылками на managed объекты как с адресами в памяти (числами).
Пример:
long address = (long)Unsafe.AsPointer(ref value);Пока на объект ссылаются обычными ссылками, или через `ref` — GC отслеживает эти ссылки и не будет собирать объект как мусор. При дефрагментации (compaction) кучи GC переместит объекты на новые места в памяти и обновит отслеживаемые им ссылки.
void*, int*, IntPtr, long и подобные типы — просто числа, GC про них ничего не знает. В итоге, если между получением адреса и его использованием случится GC — программа может обратиться уже по некорректному адресу и прочитать/перезаписать данные из случайного места. Потенциально, GC Hole — уязвимость, security bug.Проблема может долго оставаться незамеченной, например, просто везёт и GC не успевает сработать, или ссылка ведёт в Large Object Heap, который не дефрагментируется в нормальном режиме.
Как избежать GC Hole:
- не пытаться превращать managed ссылки в адреса-числа
- не делать оптимизаций, которые дают выигрыш по производительности в пределах погрешности: обычно нет смысла использовать указатели, чтобы убрать проверки на выход за границы массива и выиграть 0.01%
- если очень надо, например для интеропа, использовать пиннинг (запрет GC перемещать объект при дефрагментации), с помощью
fixed или GCHandle.Alloc(obj, GCHandleType.Pinned). Долгоживущие массивы можно аллоцировать в Pinned Object Heap- осторожно работать с
ref. ref может указывать на элемент массива или поле объекта- использовать методы, работающие с
ref, которые позволяют обойтись без перехода к адресам-числам:-
Unsafe.IsAddressLessThan(ref a, ref b) вместо Unsafe.AsPointer(ref a) < Unsafe.AsPointer(ref b)-
Unsafe.Add/Subtract, Unsafe.ByteOffset и подобные вместо арифметики указателей.@epeshkblog | Поддержать канал