ConflictSet is implicitly copyable in C++98/C++03, causing double-free #48

Closed
opened 2026-06-21 13:05:41 +00:00 by weaselbot · 0 comments
Member

include/ConflictSet.h only deletes the copy constructor and copy assignment for C++11 and later:

#if __cplusplus > 199711L
  ConflictSet(ConflictSet &&) noexcept;
  ConflictSet &operator=(ConflictSet &&) noexcept;
  ConflictSet(const ConflictSet &) = delete;
  ConflictSet &operator=(const ConflictSet &) = delete;
#endif

(include/ConflictSet.h:123-128)

For C++98/C++03 (__cplusplus == 199711L), that block is skipped, so the compiler implicitly generates a public copy constructor and copy assignment operator. Because ConflictSet owns the opaque Impl *impl pointer, a copy shares the same Impl. When both objects are destroyed, internal_destroy is called twice, resulting in a double-free.

Impact: Any C++98/C++03 consumer who copies a ConflictSet (for example ConflictSet b = a;) will crash with a double-free. The C++ API is intended to be move-only, but that contract is not enforced for pre-C++11 compilation units.

Reproduction:

#include "ConflictSet.h"
int main() {
  weaselab::ConflictSet a(0);
  weaselab::ConflictSet b = a;  // compiles in C++98, double-free at exit
  return 0;
}

Compile with -std=c++98 and link against libconflict-set.so.0. In C++11 mode the same program correctly fails to compile because the copy constructor is deleted.

Expected behavior: ConflictSet(const ConflictSet &) and operator=(const ConflictSet &) should be inaccessible in C++98/C++03 (the usual pre-C++11 idiom is to declare them private and leave them undefined).
Actual behavior: Copying is allowed and causes a double-free.

`include/ConflictSet.h` only deletes the copy constructor and copy assignment for C++11 and later: ```cpp #if __cplusplus > 199711L ConflictSet(ConflictSet &&) noexcept; ConflictSet &operator=(ConflictSet &&) noexcept; ConflictSet(const ConflictSet &) = delete; ConflictSet &operator=(const ConflictSet &) = delete; #endif ``` (`include/ConflictSet.h:123-128`) For C++98/C++03 (`__cplusplus == 199711L`), that block is skipped, so the compiler implicitly generates a public copy constructor and copy assignment operator. Because `ConflictSet` owns the opaque `Impl *impl` pointer, a copy shares the same `Impl`. When both objects are destroyed, `internal_destroy` is called twice, resulting in a double-free. **Impact:** Any C++98/C++03 consumer who copies a `ConflictSet` (for example `ConflictSet b = a;`) will crash with a double-free. The C++ API is intended to be move-only, but that contract is not enforced for pre-C++11 compilation units. **Reproduction:** ```cpp #include "ConflictSet.h" int main() { weaselab::ConflictSet a(0); weaselab::ConflictSet b = a; // compiles in C++98, double-free at exit return 0; } ``` Compile with `-std=c++98` and link against `libconflict-set.so.0`. In C++11 mode the same program correctly fails to compile because the copy constructor is deleted. **Expected behavior:** `ConflictSet(const ConflictSet &)` and `operator=(const ConflictSet &)` should be inaccessible in C++98/C++03 (the usual pre-C++11 idiom is to declare them private and leave them undefined). **Actual behavior:** Copying is allowed and causes a double-free.
weaselbot was assigned by andrew 2026-06-21 13:10:38 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: weaselab/conflict-set#48