Skip to content

Commit 01391b6

Browse files
committed
non_null: take() should poison the object
1 parent acf0e80 commit 01391b6

3 files changed

Lines changed: 126 additions & 8 deletions

File tree

CMakeLists.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,23 @@ if(NOVA_BUILD_TESTS)
6363

6464
enable_testing()
6565
add_test(NAME nova_nonnull_tests COMMAND nova_nonnull_tests)
66+
67+
add_executable(nova_nonnull_asan_take_tests
68+
tests/asan_take.cpp
69+
)
70+
target_link_libraries(nova_nonnull_asan_take_tests PRIVATE nova::nonnull )
71+
target_compile_options(nova_nonnull_asan_take_tests PRIVATE
72+
$<$<COMPILE_LANG_AND_ID:CXX,GNU,Clang,AppleClang>:-fsanitize=address;-fno-omit-frame-pointer>
73+
$<$<COMPILE_LANG_AND_ID:CXX,MSVC>:/fsanitize=address>
74+
)
75+
76+
target_link_options(nova_nonnull_asan_take_tests PRIVATE
77+
$<$<COMPILE_LANG_AND_ID:CXX,GNU,Clang,AppleClang>:-fsanitize=address>
78+
$<$<COMPILE_LANG_AND_ID:CXX,GNU>:-static-libasan>
79+
)
80+
81+
add_test(NAME nova_nonnull_asan_take_tests COMMAND nova_nonnull_asan_take_tests)
82+
set_tests_properties(nova_nonnull_asan_take_tests PROPERTIES
83+
PASS_REGULAR_EXPRESSION ".*AddressSanitizer: use-after-poison.*"
84+
)
6685
endif()

include/nova/non_null.hpp

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,25 @@
44
#pragma once
55

66
#include <cassert>
7+
#include <cstddef>
78
#include <functional>
89
#include <memory>
910
#include <optional>
1011
#include <type_traits>
1112
#include <utility>
1213

14+
#if ( defined( __has_feature ) && __has_feature( address_sanitizer ) )
15+
# define NOVA_HAVE_ASAN 1
16+
#endif
17+
#if defined( __SANITIZE_ADDRESS__ )
18+
# define NOVA_HAVE_ASAN 1
19+
#endif
20+
21+
#ifdef NOVA_HAVE_ASAN
22+
# include <sanitizer/asan_interface.h>
23+
#endif
24+
25+
1326
#if defined( __has_cpp_attribute )
1427
# if __has_cpp_attribute( assume )
1528
# define NOVA_ASSUME( expr ) [[assume( expr )]]
@@ -46,10 +59,32 @@
4659
# define NOVA_NONNULL_NONTRIVIAL
4760
#endif
4861

62+
4963
namespace nova {
5064

5165
namespace detail {
5266

67+
#if defined( NOVA_HAVE_ASAN )
68+
inline void nova_asan_poison( void const* NOVA_NONNULL p, std::size_t s ) noexcept
69+
{
70+
__asan_poison_memory_region( p, s );
71+
}
72+
inline void nova_asan_unpoison( void const* NOVA_NONNULL p, std::size_t s ) noexcept
73+
{
74+
__asan_unpoison_memory_region( p, s );
75+
}
76+
/* In ASAN builds we cannot keep take() constexpr because it calls runtime
77+
instrumentation functions. Control the constexpr-ness via this macro. */
78+
# define NOVA_ASAN_CONSTEXPR /* empty */
79+
#else
80+
inline void nova_asan_poison( void const* NOVA_NONNULL, std::size_t ) noexcept
81+
{}
82+
inline void nova_asan_unpoison( void const* NOVA_NONNULL, std::size_t ) noexcept
83+
{}
84+
# define NOVA_ASAN_CONSTEXPR constexpr
85+
#endif
86+
87+
5388
template < typename T, typename = void >
5489
struct element_type_trait
5590
{
@@ -144,8 +179,15 @@ class non_null
144179
// - For move-only pointers (unique_ptr): move deleted; use take() instead
145180
// This prevents accidental moves of move-only types while enabling efficient
146181
// moves of copyable types.
147-
non_null( const non_null& ) = default;
148-
non_null& operator=( const non_null& ) = default;
182+
non_null( const non_null& ) = default;
183+
non_null& operator=( const non_null& other ) noexcept
184+
{
185+
// If this object was previously poisoned by take(), ensure we can write
186+
// into ptr_ without ASAN reporting a write to poisoned memory.
187+
detail::nova_asan_unpoison( &ptr_, sizeof( ptr_ ) );
188+
ptr_ = other.ptr_;
189+
return *this;
190+
}
149191

150192
/**
151193
* @brief Move constructor, enabled only for copyable pointer types.
@@ -157,13 +199,19 @@ class non_null
157199
ptr_( std::move( other.ptr_ ) )
158200
{}
159201

202+
~non_null()
203+
{
204+
detail::nova_asan_unpoison( &ptr_, sizeof( ptr_ ) );
205+
}
206+
160207
/**
161208
* @brief Move assignment, enabled only for copyable pointer types.
162209
* Raw pointers and std::shared_ptr support moves; std::unique_ptr does not.
163210
*/
164211
constexpr non_null& operator=( non_null&& other ) noexcept
165212
requires detail::copyable_pointer< T >
166213
{
214+
detail::nova_asan_unpoison( &ptr_, sizeof( ptr_ ) );
167215
ptr_ = std::move( other.ptr_ );
168216
return *this;
169217
}
@@ -186,12 +234,16 @@ class non_null
186234
* For copyable pointer types the result can be re-wrapped immediately:
187235
* auto nn2 = non_null( take( std::move(nn1) ) );
188236
*/
189-
friend constexpr T NOVA_NONNULL_NONTRIVIAL take( non_null&& nn ) noexcept
237+
friend NOVA_ASAN_CONSTEXPR T NOVA_NONNULL_NONTRIVIAL take( non_null&& nn ) noexcept
190238
#if defined( __clang__ ) && ( __clang_major__ >= 20 )
191239
NOVA_RETURNS_NONNULL
192240
#endif
193241
{
194-
return std::move( nn.ptr_ );
242+
T tmp = std::move( nn.ptr_ );
243+
// Poison the source wrapper storage so accidental use-after-take
244+
// triggers ASAN in instrumented builds.
245+
detail::nova_asan_poison( &nn.ptr_, sizeof( nn.ptr_ ) );
246+
return tmp;
195247
}
196248

197249
/**
@@ -200,6 +252,10 @@ class non_null
200252
*/
201253
constexpr void swap( non_null& other ) noexcept
202254
{
255+
// Unpoison both sides before swapping so the swap operation can write
256+
// into the underlying storage safely under ASAN.
257+
detail::nova_asan_unpoison( &ptr_, sizeof( ptr_ ) );
258+
detail::nova_asan_unpoison( &other.ptr_, sizeof( other.ptr_ ) );
203259
using std::swap;
204260
swap( ptr_, other.ptr_ );
205261
}
@@ -515,9 +571,20 @@ class non_null_function< R( Args... ) >
515571
detail::assume_not_empty( fn_ );
516572
}
517573

574+
~non_null_function()
575+
{
576+
detail::nova_asan_unpoison( &fn_, sizeof( fn_ ) );
577+
}
578+
518579
// Copy ctor and copy assignment are defaulted (std::function is copyable)
519-
non_null_function( const non_null_function& ) = default;
520-
non_null_function& operator=( const non_null_function& ) = default;
580+
non_null_function( const non_null_function& ) = default;
581+
non_null_function& operator=( const non_null_function& other )
582+
{
583+
// Unpoison target storage in case it was poisoned by a previous take().
584+
detail::nova_asan_unpoison( &fn_, sizeof( fn_ ) );
585+
fn_ = other.fn_;
586+
return *this;
587+
}
521588

522589
// Implicit move: deleted to prevent accidental moves that leave the
523590
// wrapper in an unusable (empty) state. Use take() to transfer ownership explicitly.
@@ -563,6 +630,8 @@ class non_null_function< R( Args... ) >
563630
*/
564631
constexpr void swap( non_null_function& other ) noexcept
565632
{
633+
detail::nova_asan_unpoison( &fn_, sizeof( fn_ ) );
634+
detail::nova_asan_unpoison( &other.fn_, sizeof( other.fn_ ) );
566635
fn_.swap( other.fn_ );
567636
}
568637

@@ -575,7 +644,9 @@ class non_null_function< R( Args... ) >
575644
*/
576645
friend function_type take( non_null_function&& nn ) noexcept
577646
{
578-
return std::move( nn.fn_ );
647+
function_type tmp = std::move( nn.fn_ );
648+
detail::nova_asan_poison( &nn.fn_, sizeof( nn.fn_ ) );
649+
return tmp;
579650
}
580651

581652
private:
@@ -688,6 +759,8 @@ class non_null_move_only_function< R( Args... ) >
688759
*/
689760
constexpr void swap( non_null_move_only_function& other ) noexcept
690761
{
762+
NOVA_ASAN_UNPOISON( &fn_, sizeof( fn_ ) );
763+
NOVA_ASAN_UNPOISON( &other.fn_, sizeof( other.fn_ ) );
691764
fn_.swap( other.fn_ );
692765
}
693766

@@ -701,7 +774,9 @@ class non_null_move_only_function< R( Args... ) >
701774
*/
702775
friend function_type take( non_null_move_only_function&& nn ) noexcept
703776
{
704-
return std::move( nn.fn_ );
777+
function_type tmp = std::move( nn.fn_ );
778+
NOVA_ASAN_POISON( &nn.fn_, sizeof( nn.fn_ ) );
779+
return tmp;
705780
}
706781

707782
private:

tests/asan_take.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Simple ASAN smoke test: use-after-take should be reported by AddressSanitizer.
2+
#include <iostream>
3+
4+
#include <nova/non_null.hpp>
5+
6+
int main()
7+
{
8+
using namespace nova;
9+
10+
auto nn = make_non_null_unique< int >( 42 );
11+
12+
// Extract the unique_ptr out of the non_null wrapper.
13+
auto up = take( std::move( nn ) );
14+
15+
// nn is now moved-from; accessing it should trigger ASAN when built with -fsanitize=address
16+
// Intentional use-after-take:
17+
std::cerr << "About to do use-after-take (expect ASAN)\n";
18+
// Force a call that will read the wrapper state
19+
volatile auto p = nn.get();
20+
(void)p;
21+
22+
(void)up;
23+
return 0;
24+
}

0 commit comments

Comments
 (0)