magic_enum icon indicating copy to clipboard operation
magic_enum copied to clipboard

using namespace magic_enum::bitwise_operators; is very evil

Open oxiKKK opened this issue 2 months ago • 6 comments

Just wanted to say that using

using namespace magic_enum::bitwise_operators;

in a global namespace is extremely evil and dangerous and I didn't know about this behaviour at all.

Background

I am building a c++ application that ships protobuf and is cross-compatible with most of the major platforms. Recently after rebuilding my application on macOS, my program was crashing in a way i've honestly didn't experience in a long while of writing C++. It crashed during initialization of some static descriptor pool which was part of the protobuf code.

This has never happened to me and I had no idea what was causing it. It happened at initialization time, i.e. before main is called so it was even harder to debug.

More strangly, it only happened after me compiling the binary with *specific parts of code*, which I was completely confused about.

Turns out it was this single line of code in my precompiled header file before including the protobuf headers:

using namespace magic_enum::bitwise_operators;

Proposed solution

What I wanted to point out is that after looking into the implementation, it was obvious that this may break things, but I would never expect that it would break them in such way that took me 2 days to debug.

What I suggest is to at least warn the user of this library that this code may be dangerous and may be used only with caution or in a limited namespace.


EDIT: Added section below

Reproducible example

I've made a reproducible example here: https://github.com/oxiKKK/protobuf-magic_enum-repro

The output from the following program after running it:

#include <iostream>
#include <magic_enum/magic_enum.hpp>

// >>>> THIS CAUSES CRASH <<<<
using namespace magic_enum::bitwise_operators;

// >>> NOW INCLUDE PROTOBUF <<<
#include "example.pb.h"
#include <google/protobuf/message.h>

int main() {
  std::cout << "Creating protobuf message..." << std::endl;

  example::Person person;
  person.set_name("John Doe");
  person.set_id(123);
  person.set_email("[email protected]");

  std::cout << "Person created successfully!" << std::endl;
  std::cout << "Name: " << person.name() << std::endl;
  std::cout << "ID: " << person.id() << std::endl;
  std::cout << "Email: " << person.email() << std::endl;

  // Create an AddressBook with the person
  example::AddressBook address_book;
  auto *new_person = address_book.add_people();
  *new_person = person;

  std::cout << "AddressBook created with " << address_book.people_size()
            << " person(s)" << std::endl;

  return 0;
}

Output:

> protobuf-magic_enum-repro % lldb ./build/magic_enum_protobuf_repro
(lldb) target create "./build/magic_enum_protobuf_repro"
Current executable set to '/Users/XX/dev/protobuf-magic_enum-repro/build/magic_enum_protobuf_repro' (arm64).
(lldb) r
Process 57912 launched: '/Users/XX/dev/protobuf-magic_enum-repro/build/magic_enum_protobuf_repro' (arm64)
Process 57912 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x53707937)
    frame #0: 0x0000000100002b44 magic_enum_protobuf_repro`std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>::__is_long[abi:ne190102](this="") const at string:1881:29
   1878     if (__libcpp_is_constant_evaluated() && __builtin_constant_p(__r_.first().__l.__is_long_)) {
   1879       return __r_.first().__l.__is_long_;
   1880     }
-> 1881     return __r_.first().__s.__is_long_;
   1882   }
   1883
   1884   static _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 void __begin_lifetime(pointer __begin, size_type __n) {
Target 0: (magic_enum_protobuf_repro) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x53707937)
  * frame #0: 0x0000000100002b44 magic_enum_protobuf_repro`std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>::__is_long[abi:ne190102](this="") const at string:1881:29
    frame #1: 0x0000000100002e80 magic_enum_protobuf_repro`std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>::__get_pointer[abi:ne190102](this="") const at string:2009:12
    frame #2: 0x0000000100002574 magic_enum_protobuf_repro`std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>::data[abi:ne190102](this="") const at string:1705:30
    frame #3: 0x00000001000063fc magic_enum_protobuf_repro`absl::lts_20250127::string_view::string_view<std::__1::allocator<char>>(this=0x000000016fdf8f78, str="") at string_view.h:193:25
    frame #4: 0x0000000100243e28 magic_enum_protobuf_repro`google::protobuf::internal::TcParser::FastSS1(google::protobuf::MessageLite*, char const*, google::protobuf::internal::ParseContext*, google::protobuf::internal::TcFieldData, google::protobuf::internal::TcParseTableBase const*, unsigned long long)

... More in the repository's README
(lldb) 

oxiKKK avatar Oct 20 '25 16:10 oxiKKK

I think myself magic enum should provide a macro thst generates the operators or give an opt in way

template<>
bool constexpr enable_operators<UrEnum> = true;

instead of a namespace, also if this applies to bitwise_operators then it will need to be applied to iostream_operators as well.

(Also I would say it is more the fault of having a using declaration in a header than the namespace itself being probamtic)

ZXShady avatar Oct 20 '25 21:10 ZXShady

For those uninitiated, could you elaborate a bit more on what exactly is changed in the behaviour of the program due to this imported namespace that causes the program to crash? What interaction between magic_enum's operators and the protobuf headers is responsible for causing this issue? TIA.

BenBE avatar Oct 21 '25 09:10 BenBE

The exact crash of the applicaiton is protobuf-specific in a very weird and unpredicatble way but could apply for other applications as well (haven't tested). I've also fired an issue in the protobuf repository because I'm not entirely sure what caused it. And it was only macOS-specific.

I've just wanted to point this issue out because it caused me a lot of pain while debugging this out, and it may produce a very bad behaviour in applications where users put the specific piece of code in a header file / precompiled header.

I'm totally aware of the fact it is quite bad to put this in a header file, but I've never read this anywhere nor did I realize until I've looked at the inner implementation and what it actually does and that it might cause harm in global scope.

Maybe provide some kind of mechanism to not be so generic, i.e. to not apply to the global namespace? Dunno, just a thought. Or at least warn the user that this might be an issue if used in a global namespace.

oxiKKK avatar Oct 21 '25 10:10 oxiKKK

I think, a rough (executive) summary could be something along the lines:

magic_enum's using namespace magic_enum::bitwise_operators injects free operator templates into the global namespace, changing overload resolution and causing different template/inline instantiations to be emitted in that translation unit. Those altered instantiations clash with protobuf/absl's inline/template code used during static descriptor parsing, producing ODR-like undefined behavior that corrupts std::string state and crashes at static initialization.

I think, looking into why magic_enum's operator templates win in the protobuf header code, might be worthwhile for avoiding similar situations in the future, but an immediate work-around is likely to reduce scope of symbol visibility, i.e. using namespace magic_enum::bitwise_operators and alikes only in limited scopes (inside functions) or after all other headers where included (if really necessary).

BenBE avatar Oct 21 '25 11:10 BenBE

I see two potential solutions to address this issue:

First approach: Implement an opt-in mechanism by requiring explicit specialization, such as enable_bitwise_operators<Enum> = true, as suggested above. This would prevent the operators from being inadvertently applied to all enum types globally and would give developers fine-grained control over which enums should support bitwise operations.​

Second approach: Enhance the documentation to explicitly warn users that using namespace bitwise_operators can cause severe issues, particularly ODR violations and conflicts with other libraries (such as protobuf/absl). The documentation should strongly recommend limiting the scope of this directive—ideally using it only within function bodies or specific limited scopes rather than at namespace or global level.

Neargye avatar Nov 21 '25 14:11 Neargye

Given that bit-shift operators on enum types are used for more than just flags you will likely need to have control over this behavior. Thus IMO going the first route and make this opt-in is a sensible way to go.

BenBE avatar Nov 21 '25 16:11 BenBE