Skip to content

Implement destructor tombstones #5315

@StephanTLavavej

Description

@StephanTLavavej

Related to STL Hardening (#5090 implemented by #5274).

Overview

Destructor tombstones are complementary to precondition hardening, which prevents access to non-existent elements. When containers and smart pointers are destroyed, their owned elements/objects no longer exist. To resist use-after-free mistakes, the destructors of containers and smart pointers should set their pointers to some invalid value, instead of continuing to point to deallocated memory.

Choosing a tombstone value

Null is an obvious possibility, and it's certainly safer than nothing, but it could tempt users into making non-Standard assumptions. (We previously encountered cases where users assumed that basic_string could be repeatedly destroyed without harm.)

Windows guarantees that the first 64K of address space is permanently bogus. I believe that decimal 19937 is a good sentinel value, for the following reasons:

  • It's associated with the STL (mt19937), potentially making it easier to figure out "where did this value come from?" during debugging
  • It's in the middle-ish of 64K, ensuring that moderate-size additions and subtractions remain bogus
  • It's somewhat less than the 32K midpoint, leaving more room for addition offsets (which are much more likely when indexing)
  • It's odd (prime!), ensuring that it isn't usefully aligned for anything

Some users have expressed interest in customizing this, potentially to a value that's chosen at runtime, to further reduce the temptation for programmer-users to form improper expectations. (I don't see how this would be valuable, given how reliably 19937 stops program execution, but I'm not a user.)

Classes

This is the initial list of classes where I'm implementing and testing tombstones. The goal is to target widely used classes, not literally every class in the STL. Iostreams and concurrency (future etc.) are potential candidates for the future.

  • Non-array containers:
    • vector<T> and vector<bool>
    • deque
    • list
    • forward_list
    • set etc. (ordered associative)
    • unordered_set etc. (unordered associative)
  • Strings:
    • basic_string
  • Smart pointers:
    • unique_ptr<T> and unique_ptr<T[]>
    • shared_ptr and weak_ptr
    • exception_ptr (a special kind of smart pointer)
  • Can own dynamically allocated function objects:
    • function
    • move_only_function
  • Vaguely like a container:
    • optional
      • Setting it to empty will work with precondition hardening to prevent access to the object.
      • Even though a T& or T* could have been previously obtained, we should not attempt to scribble over T's bytes. That would be much more expensive, we don't know what bytes are valid for T (indeed, they may all be valid), and we should focus on cheap and significant interventions.
    • variant
      • Similarly, this can be set to valueless_by_exception(), recording that it doesn't hold a value.
      • Similarly, we shouldn't scribble over the rest of the storage.
    • any
      • Can own a dynamically allocated object.
      • Can be properly tombstoned, not just set to empty.
  • Others:
    • valarray
      • Not popular, but container-ish, and easy to tombstone.
    • regex
      • Also owns lots of dynamically allocated nodes, and easy to tombstone.
    • pmr::polymorphic_allocator
      • Stores a memory_resource* which is easy to tombstone, and allocation machinery is a common target for attackers.

Macros

Control Macro

_MSVC_STL_DESTRUCTOR_TOMBSTONES

Defining this to 1 enables the feature; defining it to 0 disables the feature.

We plan to initially ship this as disabled-by-default, but plan to change it to enabled-by-default in a future release.

Value Macro

_MSVC_STL_UINTPTR_TOMBSTONE_VALUE

This defaults to uintptr_t{19937}, but can be entirely replaced. If replaced with a function call, it should be ::fully::qualified().

Special considerations

  • This is applicable to raw pointers only. We'll use if constexpr to avoid messing with (extremely unusual) fancy pointers.
  • This is applicable to runtime only. We'll use _Is_constant_evaluated() to avoid interfering with constexpr evaluation (where we can't use reinterpret_cast).
  • We'll need to update the VSO_0000000_wcfb01_idempotent_container_destructors test.
  • I verified that MSVC's optimizer doesn't skip such assignments.
  • Classes with optimizations to avoid dynamic memory allocation (e.g. basic_string, function, move_only_function, any) need careful analysis to determine whether we should use "small mode" or "large mode" for the tombstone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementSomething can be improvedfixedSomething works now, yay!

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions