Post-Mortem: Segment Fault from Unchecked Vector Iterator Erasure
class FdMonitor::Internal {
public:
void unregister(int fd);
void registerFd(int fd);
std::vector<int> awaitEvents();
private:
std::vector<int> watchList_;
std::mutex listMutex_;
std::pair<int, int> signalPipe_;
};
The fault manifested in the unregister() routine:
void FdMonitor::Internal::unregister(int fd)
{
std::lock_guard<std::mutex> lock(listMutex_);
watchList_.erase(std::find(watchList_.begin(), watchList_.end(), fd));
}
When the file descriptor is absent from the container, std::find returns watchList_.end(). Passing this past-the-end iterator to erase() triggers undefined behavior, typically manifesting as a segmentation fault during the subsequent memory relocation.
The robust implementation validates the iterator before mutation:
void FdMonitor::Internal::unregister(int fd)
{
std::lock_guard<std::mutex> lock(listMutex_);
auto target = std::find(watchList_.begin(), watchList_.end(), fd);
if (target == watchList_.end()) {
return;
}
watchList_.erase(target);
}
Analyzing the core dump reveals the execution path leading to the crash:
#0 __memcpy_avx_unaligned_erms ()
#1 std::__copy_move<true, true, std::random_access_iterator_tag>::__copy_m<int>
(__first=0x4, __last=0x0, __result=0x0)
#2 std::__copy_move_a2<true, int*, int*> (__first=0x4, __last=0x0, __result=0x0)
#3 std::__copy_move_a1<true, int*, int*> (__first=0x4, __last=0x0, __result=0x0)
#4 std::__copy_move_a<...> (__first=<error: Cannot access memory at address 0x4>,
__last=non-dereferenceable iterator, __result=non-dereferenceable iterator)
#5 std::move<...> (__first=<error: Cannot access memory at address 0x4>,
__last=non-dereferenceable iterator, __result=non-dereferenceable iterator)
#6 std::vector<int>::_M_erase (this=0x55a8f87e22d0,
__position=non-dereferenceable iterator)
#7 std::vector<int>::erase (this=0x55a8f87e22d0,
__position=non-dereferenceable iterator)
#8 FdMonitor::Internal::unregister (this=0x55a8f87e22d0, fd=0)
The stack trace exposes the mechanism of failure. At frame #6, std::vector::_M_erase invokes the move operation to shift elements leftward, filling the gap at the erased position. The implemantation conceptually executes:
_GLIBCXX_MOVE3(__position + 1, end(), __position);
When __position equals end() (address 0x0 in the optimized implementation), incrementing it yields 0x4 (frame #5 and below). This invalid source pointer propagates down to __copy_move, where memmove attempts to read from unmapped memory, generating the SIGSEGV.
The critical observation lies in frame #1: both __last and __result hold null pointers, while __first contains the corrupted address 0x4. This pattern—where the destination and end-boundary pointers are null but the source pointer is a small non-zero value—strongly indicates that the erasure algorithm received the container's end iterator as the deletion point.
In std::vector's internal architecture, _M_erase expects a dereferenceable iterator within the valid element range [begin(), end()). When this precondition is violated, the pointer arithmetic inside the move routine produces addresses out side the allocaetd buffer, causing the memory subsystem to abort the process during the block-copy operation.
Always validate search results against the container's end iterator before passing them to modifier algorithms.