Skip to content

Commit 6efefb6

Browse files
committed
api: improve hardening macros and the start of contract assertions
Review: we have long had two assertion macros: OIIO_ASSERT which aborts upon failure in Debug builds and prints but continues in Release builds, and OIIO_DASSERT which aborts in Debug builds and is completely inactive for Relase builds. Inspired by C++26 contracts, and increasingly available "hardening modes" in major compilers (especially with the LLVM/clang project's libc++), I'm introducing some new verification helpers. New macro `OIIO_CONTRACT_ASSERT` more closely mimics C++26 contract_assert in many ways, and perhaps will simply wrap C++ contract_assert when C++26 is on our menu. Important ways that OIIO_CONTRACT_ASSERT differs from OIIO_ASSERT and OIIO_DASSERT in a few ways, described below. * Keeping in line with C++ contracts, there are 4 possible responses to a failed contract assertion: Ignore, Observe (print only), Enforce (print and abort) and Quick-Enforce (just abort). * By default, the contract failure response is Ignore for release builds and Enforce for debug builds. But it's overrideable (independent of Release/Debug, and optionally on a per-translation-unit basis) by setting OIIO_ASSERTION_RESPONSE_DEFAULT before any OIIO headers are included. * Also define hardening levels: None, Fast, Extensive, and Debug, mimicking the levels of libc++. The idea is that maybe there will be some CONTRACT_ASSERT checks you only want to do for certain hardening levels. * Macros for explicit hardening levels: OIIO_HARDENING_ASSERT_FAST(), EXTENSIVE(), and DEBUG(), which call CONTRACT_ASSERT only when the hardening level is what's required or stricter. I also changed the bounds checking in operator[] of string_view, span, and image_span to use the contract assertions. Note that this adds a little bit of overhead, since the default is "enforce" for release builds. I added some benchmarking that proves that the bounds check adds only about 20% overhead to an element access for a trivial `span<float>`. For more complex things, or code that does more than just repeatedly access elements with bounds checks, I expect this overhead to be negligible. Since libc++ and upcoming C++ standards do the same for most container types, I expect the compilers to get better and better at eliding these checks when they can determine that it's an in-bounds access. Also please note that one way to avoid these extra bounds checks entirely is to change an index-oriented loop like span s; for (size_t i = 0; i < s.size(); ++i) foo(s[i]); // maybe bounds check on each iteration? to a range based loop: span s; for (auto& v : s) foo(v); which is inherently safe and requires no in-loop checks at all. Signed-off-by: Larry Gritz <[email protected]>
1 parent b86a46a commit 6efefb6

File tree

8 files changed

+268
-24
lines changed

8 files changed

+268
-24
lines changed

src/include/OpenImageIO/dassert.h

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,158 @@
99
#include <cstdio>
1010
#include <cstdlib>
1111

12+
#include <OpenImageIO/oiioversion.h>
1213
#include <OpenImageIO/platform.h>
1314

1415

16+
17+
// General resources about security and hardening for C++:
18+
//
19+
// https://best.openssf.org/Compiler-Hardening-Guides/Compiler-Options-Hardening-Guide-for-C-and-C++.html
20+
// https://www.gnu.org/software/libc/manual/html_node/Source-Fortification.html
21+
// https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_macros.html
22+
// https://libcxx.llvm.org/Hardening.html
23+
// https://cheatsheetseries.owasp.org/cheatsheets/C-Based_Toolchain_Hardening_Cheat_Sheet.html
24+
// https://stackoverflow.com/questions/13544512/what-is-the-most-hardened-set-of-options-for-gcc-compiling-c-c
25+
// https://medium.com/@simontoth/daily-bit-e-of-c-hardened-mode-of-standard-library-implementations-18be2422c372
26+
// https://en.cppreference.com/w/cpp/contract
27+
// https://en.cppreference.com/w/cpp/language/contracts
28+
29+
30+
31+
// Define hardening levels for OIIO: which checks should we do?
32+
// NONE - YOLO mode, no extra checks (not recommended)
33+
// FAST - Minimal checks that have low performance impact
34+
// EXTENSIVE - More thorough checks, may impact performance
35+
// DEBUG - Maximum checks, for debugging purposes
36+
#define OIIO_HARDENING_NONE 0
37+
#define OIIO_HARDENING_FAST 1
38+
#define OIIO_HARDENING_EXTENSIVE 2
39+
#define OIIO_HARDENING_DEBUG 3
40+
41+
// OIIO_HARDENING_DEFAULT defines the default hardening level we actually use.
42+
// By default, we use NONE for release builds and DEBUG for debug builds. But
43+
// any translation unit (including clients of OIIO) may override this by
44+
// defining OIIO_HARDENING_DEFAULT before including any OIIO headers. But note
45+
// that this only affects calls to inline functions or templates defined in
46+
// the headers. Non-inline functions compiled into the OIIO library, including
47+
// OIIO internal code itself, will have been compiled with whatever hardening
48+
// level was selected when the library was built.
49+
#ifndef OIIO_HARDENING_DEFAULT
50+
# ifdef NDEBUG
51+
# define OIIO_HARDENING_DEFAULT OIIO_HARDENING_NONE
52+
# else
53+
# define OIIO_HARDENING_DEFAULT OIIO_HARDENING_DEBUG
54+
# endif
55+
#endif
56+
57+
58+
/// Choices for what to do when a contract assertion fails.
59+
/// This mimics the C++26 standard's std::contract behavior.
60+
#define OIIO_ASSERTION_RESPONSE_IGNORE 0
61+
#define OIIO_ASSERTION_RESPONSE_OBSERVE 1
62+
#define OIIO_ASSERTION_RESPONSE_ENFORCE 2
63+
#define OIIO_ASSERTION_RESPONSE_QUICK_ENFORCE 3
64+
65+
// OIIO_ASSERTION_RESPONSE_DEFAULT defines the default response to failed
66+
// contract assertions. By default, in NONE hardening mode and in release
67+
// builds, we do nothing. In all other cases, we abort. But any translation
68+
// unit (including clients of OIIO) may override this by defining
69+
// OIIO_ASSERTION_RESPONSE_DEFAULT before including any OIIO headers. But note
70+
// that this only affects calls to inline functions or templates defined in
71+
// the headers. Non-inline functions compiled into the OIIO library, including
72+
// OIIO internal code itself, will have been compiled with whatever response
73+
// was selected when the library was built.
74+
#ifndef OIIO_ASSERTION_RESPONSE_DEFAULT
75+
# if OIIO_HARDENING_DEFAULT == OIIO_HARDENING_NONE && defined(NDEBUG)
76+
# define OIIO_ASSERTION_RESPONSE_DEFAULT OIIO_ASSERTION_RESPONSE_ENFORCE
77+
# else
78+
# define OIIO_ASSERTION_RESPONSE_DEFAULT OIIO_ASSERTION_RESPONSE_ENFORCE
79+
# endif
80+
#endif
81+
82+
83+
84+
// `OIIO_CONTRACT_ASSERT(condition)` checks if the condition is met, and if
85+
// not, calls the contract violation handler with the appropriate response.
86+
// `OIIO_CONTRACT_ASSERT_MSG(condition, msg)` is the same, but allows a
87+
// different message to be passed to the handler.
88+
#if OIIO_ASSERTION_RESPONSE_DEFAULT == OIIO_ASSERTION_RESPONSE_IGNORE
89+
# define OIIO_CONTRACT_ASSERT_MSG(condition, message) (void)0
90+
#elif OIIO_ASSERTION_RESPONSE_DEFAULT == OIIO_ASSERTION_RESPONSE_QUICK_ENFORCE
91+
# define OIIO_CONTRACT_ASSERT_MSG(condition, message) \
92+
(OIIO_LIKELY(condition) ? ((void)0) : (std::abort(), (void)0))
93+
#elif OIIO_ASSERTION_RESPONSE_DEFAULT == OIIO_ASSERTION_RESPONSE_OBSERVE
94+
# define OIIO_CONTRACT_ASSERT_MSG(condition, message) \
95+
(OIIO_LIKELY(condition) ? ((void)0) \
96+
: (OIIO::contract_violation_handler( \
97+
__FILE__ ":" OIIO_STRINGIZE(__LINE__), \
98+
OIIO_PRETTY_FUNCTION, message), \
99+
(void)0))
100+
#elif OIIO_ASSERTION_RESPONSE_DEFAULT == OIIO_ASSERTION_RESPONSE_ENFORCE
101+
# define OIIO_CONTRACT_ASSERT_MSG(condition, message) \
102+
(OIIO_LIKELY(condition) ? ((void)0) \
103+
: (OIIO::contract_violation_handler( \
104+
__FILE__ ":" OIIO_STRINGIZE(__LINE__), \
105+
OIIO_PRETTY_FUNCTION, message), \
106+
std::abort(), (void)0))
107+
#else
108+
# error "Unknown OIIO_ASSERTION_RESPONSE_DEFAULT"
109+
#endif
110+
111+
#define OIIO_CONTRACT_ASSERT(condition) \
112+
OIIO_CONTRACT_ASSERT_MSG(condition, #condition)
113+
114+
// Macros to use to select whether or not to do a contract check, based on the
115+
// hardening level:
116+
// - OIIO_HARDENING_ASSERT_FAST : only checks contract for >= FAST hardening.
117+
// - OIIO_HARDENING_ASSERT_EXTENSIVE : only checks contract for >= EXTENSIVE.
118+
// - OIIO_HARDENING_ASSERT_DEBUG : only checks contract for DEBUG hardening.
119+
#if OIIO_HARDENING_DEFAULT >= OIIO_HARDENING_FAST
120+
# define OIIO_HARDENING_ASSERT_FAST_MSG(condition, message) \
121+
OIIO_CONTRACT_ASSERT_MSG(condition, message)
122+
#else
123+
# define OIIO_HARDENING_ASSERT_FAST_MSG(...) (void)0
124+
#endif
125+
126+
#if OIIO_HARDENING_DEFAULT >= OIIO_HARDENING_EXTENSIVE
127+
# define OIIO_HARDENING_ASSERT_EXTENSIVE_MSG(condition, message) \
128+
OIIO_CONTRACT_ASSERT_MSG(condition, message)
129+
#else
130+
# define OIIO_HARDENING_ASSERT_EXTENSIVE_MSG(...) (void)0
131+
#endif
132+
133+
#if OIIO_HARDENING_DEFAULT >= OIIO_HARDENING_DEBUG
134+
# define OIIO_HARDENING_ASSERT_DEBUG_MSG(condition, message) \
135+
OIIO_CONTRACT_ASSERT_MSG(condition, message)
136+
#else
137+
# define OIIO_HARDENING_ASSERT_DEBUG_MSG(...) (void)0
138+
#endif
139+
140+
#define OIIO_HARDENING_ASSERT_NONE(condition) \
141+
OIIO_HARDENING_ASSERT_NONE_MSG(condition, #condition)
142+
#define OIIO_HARDENING_ASSERT_FAST(condition) \
143+
OIIO_HARDENING_ASSERT_FAST_MSG(condition, #condition)
144+
#define OIIO_HARDENING_ASSERT_EXTENSIVE(condition) \
145+
OIIO_HARDENING_ASSERT_EXTENSIVE_MSG(condition, #condition)
146+
#define OIIO_HARDENING_ASSERT_DEBUG(condition) \
147+
OIIO_HARDENING_ASSERT_DEBUG_MSG(condition, #condition)
148+
149+
150+
OIIO_NAMESPACE_3_1_BEGIN
151+
// Internal contract assertion handler
152+
OIIO_UTIL_API void
153+
contract_violation_handler(const char* location, const char* function,
154+
const char* msg = "");
155+
OIIO_NAMESPACE_3_1_END
156+
157+
OIIO_NAMESPACE_BEGIN
158+
#ifndef OIIO_DOXYGEN
159+
using v3_1::contract_violation_handler;
160+
#endif
161+
OIIO_NAMESPACE_END
162+
163+
15164
/// OIIO_ABORT_IF_DEBUG is a call to abort() for debug builds, but does
16165
/// nothing for release builds.
17166
#ifndef NDEBUG

src/include/OpenImageIO/hash.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,11 @@ strhash (string_view s)
247247
size_t len = s.length();
248248
if (! len) return 0;
249249
unsigned int h = 0;
250-
for (size_t i = 0; i < len; ++i) {
251-
h += (unsigned char)(s[i]);
250+
for (auto c : s) {
251+
// Note: by using range for here, instead of looping over indices and
252+
// calling operator[] to get each char, we avoid the bounds checking
253+
// that operator[] does.
254+
h += (unsigned char)(c);
252255
h += h << 10;
253256
h ^= h >> 6;
254257
}

src/include/OpenImageIO/image_span.h

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -271,18 +271,30 @@ template<typename T, size_t Rank = 4> class image_span {
271271
/// Return a pointer to the value at channel c, pixel (x,y,z).
272272
inline T* getptr(int c, int x, int y, int z = 0) const
273273
{
274-
// Bounds check in debug mode
275-
OIIO_DASSERT(unsigned(c) < unsigned(nchannels())
276-
&& unsigned(x) < unsigned(width())
277-
&& unsigned(y) < unsigned(height())
278-
&& unsigned(z) < unsigned(depth()));
279274
if constexpr (Rank == 2) {
280275
OIIO_DASSERT(y == 0 && z == 0);
276+
OIIO_CONTRACT_ASSERT(unsigned(c) < unsigned(nchannels())
277+
&& unsigned(x) < unsigned(width()));
278+
return (T*)((char*)data() + c * chanstride());
281279
} else if constexpr (Rank == 3) {
282280
OIIO_DASSERT(z == 0);
281+
OIIO_CONTRACT_ASSERT(unsigned(c) < unsigned(nchannels())
282+
&& unsigned(x) < unsigned(width())
283+
&& unsigned(y) < unsigned(height()));
284+
return (T*)((char*)data() + c * chanstride() + x * xstride()
285+
+ y * ystride());
286+
} else {
287+
// Rank == 4
288+
OIIO_CONTRACT_ASSERT(unsigned(c) < unsigned(nchannels())
289+
&& unsigned(x) < unsigned(width())
290+
&& unsigned(y) < unsigned(height())
291+
&& unsigned(z) < unsigned(depth()));
292+
return (T*)((char*)data() + c * chanstride() + x * xstride()
293+
+ y * ystride() + z * zstride());
283294
}
284-
return (T*)((char*)data() + c * chanstride() + x * xstride()
285-
+ y * ystride() + z * zstride());
295+
#ifdef __INTEL_COMPILER
296+
return nullptr; // should never get here, but icc is confused
297+
#endif
286298
}
287299

288300
/// Return a pointer to the value at channel 0, pixel (x,y,z).

src/include/OpenImageIO/span.h

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,28 +207,28 @@ class span {
207207
/// optimized builds, there is no bounds check. Note: this is different
208208
/// from C++ std::span, which never bounds checks `operator[]`.
209209
constexpr reference operator[] (size_type idx) const {
210-
OIIO_DASSERT(idx < m_size && "OIIO::span::operator[] range check");
210+
OIIO_CONTRACT_ASSERT(idx < m_size);
211211
return m_data[idx];
212212
}
213213
constexpr reference operator() (size_type idx) const {
214-
OIIO_DASSERT(idx < m_size && "OIIO::span::operator() range check");
214+
OIIO_CONTRACT_ASSERT(idx < m_size);
215215
return m_data[idx];
216216
}
217217
/// Bounds-checked access, throws an assertion if out of range.
218218
reference at (size_type idx) const {
219219
if (idx >= size())
220-
throw (std::out_of_range ("OpenImageIO::span::at"));
220+
throw (std::out_of_range ("OIIO::span::at"));
221221
return m_data[idx];
222222
}
223223

224224
/// The first element of the span.
225225
constexpr reference front() const noexcept {
226-
OIIO_DASSERT(m_size >= 1);
226+
OIIO_CONTRACT_ASSERT(m_size >= 1);
227227
return m_data[0];
228228
}
229229
/// The last element of the span.
230230
constexpr reference back() const noexcept {
231-
OIIO_DASSERT(m_size >= 1);
231+
OIIO_CONTRACT_ASSERT(m_size >= 1);
232232
return m_data[size() - 1];
233233
}
234234

@@ -374,14 +374,16 @@ class span_strided {
374374
constexpr stride_type stride() const noexcept { return m_stride; }
375375

376376
constexpr reference operator[] (size_type idx) const {
377+
OIIO_CONTRACT_ASSERT(idx < m_size);
377378
return m_data[m_stride*idx];
378379
}
379380
constexpr reference operator() (size_type idx) const {
381+
OIIO_CONTRACT_ASSERT(idx < m_size);
380382
return m_data[m_stride*idx];
381383
}
382384
reference at (size_type idx) const {
383385
if (idx >= size())
384-
throw (std::out_of_range ("OpenImageIO::span_strided::at"));
386+
throw (std::out_of_range ("OIIO::span_strided::at"));
385387
return m_data[m_stride*idx];
386388
}
387389
constexpr reference front() const noexcept { return m_data[0]; }

src/include/OpenImageIO/string_view.h

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <stdexcept>
1515
#include <string>
1616

17+
#include <OpenImageIO/dassert.h>
1718
#include <OpenImageIO/export.h>
1819
#include <OpenImageIO/oiioversion.h>
1920
#include <OpenImageIO/platform.h>
@@ -205,11 +206,12 @@ class basic_string_view {
205206
/// Is the view empty, containing no characters?
206207
constexpr bool empty() const noexcept { return m_len == 0; }
207208

208-
/// Element access of an individual character. For debug build, does
209-
/// bounds check with assertion. For optimized builds, there is no bounds
210-
/// check. Note: this is different from C++ std::span, which never bounds
211-
/// checks `operator[]`.
212-
constexpr const_reference operator[](size_type pos) const { return m_chars[pos]; }
209+
/// Element access of an individual character. For debug or hardened
210+
/// builds, does bounds check with assertion.
211+
constexpr const_reference operator[](size_type pos) const {
212+
OIIO_CONTRACT_ASSERT(pos < m_len);
213+
return m_chars[pos];
214+
}
213215
/// Element access with bounds checking and exception if out of bounds.
214216
constexpr const_reference at(size_t pos) const
215217
{
@@ -218,9 +220,15 @@ class basic_string_view {
218220
return m_chars[pos];
219221
}
220222
/// The first character of the view.
221-
constexpr const_reference front() const { return m_chars[0]; }
223+
constexpr const_reference front() const {
224+
OIIO_CONTRACT_ASSERT(m_len >= 1);
225+
return m_chars[0];
226+
}
222227
/// The last character of the view.
223-
constexpr const_reference back() const { return m_chars[m_len - 1]; }
228+
constexpr const_reference back() const {
229+
OIIO_CONTRACT_ASSERT(m_len >= 1);
230+
return m_chars[m_len - 1];
231+
}
224232
/// Return the underlying data pointer to the first character.
225233
constexpr const_pointer data() const noexcept { return m_chars; }
226234

src/libutil/errorhandler.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,15 @@ ErrorHandler::operator()(int errcode, const std::string& msg)
5858
fflush(stderr);
5959
}
6060

61+
62+
63+
void
64+
contract_violation_handler(const char* location, const char* function,
65+
const char* msg)
66+
{
67+
Strutil::print(stderr, "{} ({}): Contract assertion failed: {}\n", location,
68+
function, msg ? msg : "");
69+
fflush(stderr);
70+
}
71+
6172
OIIO_NAMESPACE_3_1_END

0 commit comments

Comments
 (0)