From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: by sourceware.org (Postfix, from userid 2049) id 5B667385742B; Wed, 26 Oct 2022 11:06:50 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 5B667385742B DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gcc.gnu.org; s=default; t=1666782419; bh=66wjXqBP6mjG5ZIXiO8spM173UPdLzYzqB5t9hWDX6g=; h=From:To:Subject:Date:From; b=IIwDxdKvX8rPeHO0pTND+GrW1789tCt5Or6PhR2UWU8Lp/mlBUiuL7dGu+ySoOq4z QuUPBCMNx0dZd5pHcSenmToE1Z3NDEZyvszb40u3/M+80cDiBVUN2Gk7PMHhZ7LjUm hzTV2i89YiPaqKt0YcFO4f0gPGCN5DIP5Tu+3/0k= Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: Matthew Malcomson To: gcc-cvs@gcc.gnu.org, libstdc++-cvs@gcc.gnu.org Subject: [gcc(refs/vendors/ARM/heads/morello)] Update PMR allocators for CHERI X-Act-Checkin: gcc X-Git-Author: Matthew Malcomson X-Git-Refname: refs/vendors/ARM/heads/morello X-Git-Oldrev: 977fc61dbcf2124725f9fee2a67cac6df2ff4ebb X-Git-Newrev: 70924d59c8f02388cbd22a3b6389126ca89e2fac Message-Id: <20221026110659.5B667385742B@sourceware.org> Date: Wed, 26 Oct 2022 11:06:50 +0000 (GMT) List-Id: https://gcc.gnu.org/g:70924d59c8f02388cbd22a3b6389126ca89e2fac commit 70924d59c8f02388cbd22a3b6389126ca89e2fac Author: Matthew Malcomson Date: Wed Oct 26 12:04:38 2022 +0100 Update PMR allocators for CHERI The allocation methods of the C++17 allocators based of the memory_resource interface are different to each other. However it happens that the two main allocation methods naturally provide the ability to return precisely bounded pointers from allocation requests. The two allocator approaches built on top of memory_resource in libstdc++ are that for the monotonic_resource and that for the {un,}synchronized_pool_resource. --- The monotonic_resource is a very simple allocator. It never reclaims memory that has been freed by the user. This allocator maintains a large buffer (which it gets from `new`). When an allocation is requested this allocator moves the "current" pointer along in this buffer an amount according to the allocation required. The space left behind is the space given to the user. It is clear that returning a tightly bounded capability from such an allocation would cause no problems. --- The {un,}synchronized_pool_resource classes are more complicated. These both maintain a set of pools from which to allocate, and each pool is implemented by a vector of `chunk` structures. These `chunk` structures allocate blocks by determining which bits in a bitset are free. An allocation consists of finding a free bit, returning a pointer generated from the base pointer to a chunk plus an offset according to which bit was chosen. A deallocation consists of finding which bit in which chunk a pointer corresponded to and clearing that bit. Since the allocation is always generated from an internal base pointer and the deallocation only needs the address of a capability, providing a bounded pointer naturally fits with this mechanism of tracking allocations. There is nothing to do beyond ensuring that the returned pointer is bounded according to the size of allocation. --- In contrast to the ext/pool_allocator and ext/mt_allocator, these allocators do allow allocations larger than the threshold at which we need to pay attention to padding and alignment concerns for CHERI precise bounds. To handle that we can use the two builtins introduced for this reason: - __builtin_cheri_round_representable_length - __builtin_cheri_representable_alignment_mask Across the allocator code there are a few places which look like they might need accounting for alignment but do not. Here we justify the lack of extra code in each place. In __pool_resource::_Pool::replenish, we replenish the slab backing allocations. This slab is obtained from some underlying allocator (by default the `new_delete_resource`, but could be anything provided by the user). The alignment we request for this slab needs to be enough for the chunks that we give out (it is relied upon in the allocation routines of {un,}synchronized_pool_resource). We can not rely on the underlying allocator providing us with an aligned pointer, since even if it is adjusted for CHERI precise bounds it is valid for the returned pointer to be unaligned with padding and bounds extending to smaller addresses than the pointer that is returned. That means that the alignment we request must be as large as we require. It happens that the existing algorithm already ensures that the alignment requested is std::bit_ceil(__block_size) where __block_size is the size that this chunk will be handling. Given the properties of CHERI bounds compression, this alignment is always enough to provide precise bounds on the given block size. {un,}synchronized_pool_resource chooses the block_size from which to satisfy allocations based on the maximum of the length and alignment requested. This only relates to the block from which we satisfy requests, and does not affect the bytes we actually want to provide to the user. Hence as long as we have adjusted our request for CHERI precise bounds before this adjustment there is no problem. --- There is a static assert in memory_resource.cc on the size of a `struct chunk`. This is there to ensure that the padding at the end of a `struct bitset` is used in the chunk structure that inherits from it. The increased size and alignment requirements of a pointer on a CHERI architecture means that there is more padding in a `struct chunk`, and hence the static_assert fails with the existing expression. Here we adjust the assertion expression and include a comment as to where the padding has come from. --- We had to update the testcase unsynchronized_pool_resource/allocate.cc to account for the fact that the size and alignment requested by the user may be modified by the time the allocation request is passed down to an underlying allocator. We hence have to adjust the check according to CHERI bounds compression adjustments. Diff: --- libstdc++-v3/include/std/memory_resource | 34 +++++++++++++- libstdc++-v3/src/c++17/memory_resource.cc | 52 +++++++++++++++++++--- .../check_awkward_pool_size.cc | 44 ++++++++++++++++++ .../check_read_out_of_bounds.cc | 40 +++++++++++++++++ .../check_read_out_of_bounds_2.cc | 45 +++++++++++++++++++ .../unsynchronized_pool_resource/allocate.cc | 23 +++++++++- .../check_awkward_pool_size.cc | 41 +++++++++++++++++ .../check_read_out_of_bounds.cc | 38 ++++++++++++++++ .../check_read_out_of_bounds_2.cc | 43 ++++++++++++++++++ 9 files changed, 351 insertions(+), 9 deletions(-) diff --git a/libstdc++-v3/include/std/memory_resource b/libstdc++-v3/include/std/memory_resource index 2b8735b8c39..56700bc3e23 100644 --- a/libstdc++-v3/include/std/memory_resource +++ b/libstdc++-v3/include/std/memory_resource @@ -129,6 +129,37 @@ namespace pmr { return !(__a == __b); } #endif + inline void + __maybe_cheri_round_allocation(size_t* __bytesp, size_t* __alignmentp) + noexcept + { +#ifdef __CHERI_PURE_CAPABILITY__ + size_t __bytes = *__bytesp; + size_t __alignment = *__alignmentp; + __bytes = __builtin_cheri_round_representable_length(__bytes); + // Check for overflow. If overflow occured there's nothing we can do + // so we return without adjustment. + if (__bytes < *__bytesp) + return; + const size_t __alignment_mask + = __builtin_cheri_representable_alignment_mask(__bytes); + const size_t tmp = ((size_t)1) << __builtin_ctzl(__alignment_mask); + __alignment = std::max(tmp, __alignment); + + *__bytesp = __bytes; + *__alignmentp = __alignment; +#endif + } + + inline void* + __maybe_cheri_bounded(void* __ret, size_t __length) + { +#ifdef __CHERI_PURE_CAPABILITY__ + __ret = __builtin_cheri_bounds_set_exact(__ret, __length); +#endif + return __ret; + } + // C++17 23.12.3 Class template polymorphic_allocator template class polymorphic_allocator @@ -639,6 +670,7 @@ namespace pmr if (__bytes == 0) __bytes = 1; // Ensures we don't return the same pointer twice. + __maybe_cheri_round_allocation(&__bytes, &__alignment); void* __p = std::align(__alignment, __bytes, _M_current_buf, _M_avail); if (!__p) { @@ -647,7 +679,7 @@ namespace pmr } _M_current_buf = (char*)_M_current_buf + __bytes; _M_avail -= __bytes; - return __p; + return __maybe_cheri_bounded(__p, __bytes); } void diff --git a/libstdc++-v3/src/c++17/memory_resource.cc b/libstdc++-v3/src/c++17/memory_resource.cc index facc3244538..ac397f21997 100644 --- a/libstdc++-v3/src/c++17/memory_resource.cc +++ b/libstdc++-v3/src/c++17/memory_resource.cc @@ -544,10 +544,22 @@ namespace pmr // For 32-bit and 20-bit pointers it's four pointers (16 bytes). // For 16-bit pointers it's five pointers (10 bytes). // TODO pad 64-bit to 4*sizeof(void*) to avoid splitting across cache lines? -#ifndef __CHERI_PURE_CAPABILITY__ - // MORELLO TODO: FIXME. This code will need adapting for capabilities. +#if __SIZEOF_POINTER__ <= 8 static_assert(sizeof(chunk) == sizeof(bitset::size_type) + sizeof(uint32_t) + 2 * sizeof(void*)); +#elif __SIZEOF_POINTER__ == 16 + // CHERI pointers on 64 bit targets are 128 bits (i.e. 16 bytes) in size. + // Hence there is extra padding required to align _M_p, even though we do + // manage to use some padding from bitset in the chunk definition. + // Similar question about padding for cache lines as is made above could + // apply here. + static_assert(sizeof(chunk) + == sizeof(bitset::size_type) + sizeof(uint32_t) + 2 * sizeof(void*) + 8); +#else + // Do not make assertions for other sizes, any architectures with new pointer + // sizes may want to revisit this and decide for themselves if the current + // situation is acceptable to them. +#error "New pointer size -- suggest investigating padding in PMR chunk struct." #endif // An oversized allocation that doesn't fit in a pool. @@ -689,6 +701,7 @@ namespace pmr const size_t __words = (__blocks + __bits - 1) / __bits; const size_t __block_size = block_size(); size_t __bytes = __blocks * __block_size + __words * sizeof(word); + // N.b. this alignment is also enough for CHERI precise bounds. size_t __alignment = std::__bit_ceil(__block_size); void* __p = __r->allocate(__bytes, __alignment); __try @@ -912,6 +925,12 @@ namespace pmr // Setting _M_opts to the largest pool allows users to query it: opts.largest_required_pool_block = std::end(pool_sizes)[-1]; } + +#ifdef __CHERI_PURE_CAPABILITY__ + opts.largest_required_pool_block + = __builtin_cheri_round_representable_length + (opts.largest_required_pool_block); +#endif return opts; } @@ -1204,6 +1223,13 @@ namespace pmr synchronized_pool_resource:: do_allocate(size_t bytes, size_t alignment) { + // N.b. this rounding up for a representable size is not needed when we + // request the size from `_M_impl`. That __pool_resource would + // automatically handle adding associated bounds and rounding up since it + // would be passed to some "upstream" memory resource. + // That said, there is also no harm to adjusting the size we request here, + // and it makes the code slightly easier to read. + __maybe_cheri_round_allocation(&bytes, &alignment); const auto block_size = std::max(bytes, alignment); const pool_options opts = _M_impl._M_opts; if (block_size <= opts.largest_required_pool_block) @@ -1217,7 +1243,7 @@ namespace pmr { // Need exclusive lock to replenish so use try_allocate: if (void* p = pools[index].try_allocate()) - return p; + return __maybe_cheri_bounded(p, bytes); // Need to take exclusive lock and replenish pool. } // Need to allocate or replenish thread-specific pools using @@ -1230,7 +1256,9 @@ namespace pmr exclusive_lock dummy(_M_mx); _M_tpools = _M_alloc_shared_tpools(dummy); } - return _M_tpools->pools[index].allocate(upstream_resource(), opts); + void* ret + = _M_tpools->pools[index].allocate(upstream_resource(), opts); + return __maybe_cheri_bounded(ret, bytes); } // N.B. Another thread could call release() now lock is not held. @@ -1240,7 +1268,8 @@ namespace pmr auto pools = _M_thread_specific_pools(); if (!pools) pools = _M_alloc_tpools(excl)->pools; - return pools[index].allocate(upstream_resource(), opts); + void* ret = pools[index].allocate(upstream_resource(), opts); + return __maybe_cheri_bounded(ret, bytes); } exclusive_lock l(_M_mx); return _M_impl.allocate(bytes, alignment); // unpooled allocation @@ -1410,14 +1439,25 @@ namespace pmr void* unsynchronized_pool_resource::do_allocate(size_t bytes, size_t alignment) { + // N.b. this rounding up for a representable size is not needed when we + // request the size from `_M_impl`. That __pool_resource would + // automatically handle adding associated bounds and rounding up since it + // would be passed to some "upstream" memory resource. + // That said, there is also no harm to adjusting the size we request here, + // and it makes the code slightly easier to read. + __maybe_cheri_round_allocation(&bytes, &alignment); const auto block_size = std::max(bytes, alignment); + if (block_size <= _M_impl._M_opts.largest_required_pool_block) { // Recreate pools if release() has been called: if (__builtin_expect(_M_pools == nullptr, false)) _M_pools = _M_impl._M_alloc_pools(); if (auto pool = _M_find_pool(block_size)) - return pool->allocate(upstream_resource(), _M_impl._M_opts); + { + void* ret = pool->allocate(upstream_resource(), _M_impl._M_opts); + return __maybe_cheri_bounded(ret, bytes); + } } return _M_impl.allocate(bytes, alignment); } diff --git a/libstdc++-v3/testsuite/20_util/synchronized_pool_resource/check_awkward_pool_size.cc b/libstdc++-v3/testsuite/20_util/synchronized_pool_resource/check_awkward_pool_size.cc new file mode 100644 index 00000000000..cacd9379ccc --- /dev/null +++ b/libstdc++-v3/testsuite/20_util/synchronized_pool_resource/check_awkward_pool_size.cc @@ -0,0 +1,44 @@ +// Copyright (C) 2022 Free Software Foundation, Inc. +// +// This file is part of the GNU ISO C++ Library. This library is free +// software; you can redistribute it and/or modify it under the +// terms of the GNU General Public License as published by the +// Free Software Foundation; either version 3, or (at your option) +// any later version. + +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License along +// with this library; see the file COPYING3. If not see +// . + +// { dg-do run } +// { dg-options "-std=gnu++17 -pthread" } +// { dg-require-effective-target c++17 } +// { dg-require-effective-target pthread } +// { dg-require-gthreads "" } + +#include +#include + +void +test01() +{ + std::pmr::synchronized_pool_resource r({0, 0x40130}); + void * p1 = r.allocate(0x40008); + void * p2 = r.allocate(0x40008); +#ifdef __CHERI_PURE_CAPABILITY__ + VERIFY ( (size_t)p1 == __builtin_cheri_base_get (p1)); + VERIFY ( (size_t)p2 == __builtin_cheri_base_get (p2)); +#endif +} + +int +main() +{ + test01(); +} + diff --git a/libstdc++-v3/testsuite/20_util/synchronized_pool_resource/check_read_out_of_bounds.cc b/libstdc++-v3/testsuite/20_util/synchronized_pool_resource/check_read_out_of_bounds.cc new file mode 100644 index 00000000000..df99c38c084 --- /dev/null +++ b/libstdc++-v3/testsuite/20_util/synchronized_pool_resource/check_read_out_of_bounds.cc @@ -0,0 +1,40 @@ +// Copyright (C) 2022 Free Software Foundation, Inc. +// +// This file is part of the GNU ISO C++ Library. This library is free +// software; you can redistribute it and/or modify it under the +// terms of the GNU General Public License as published by the +// Free Software Foundation; either version 3, or (at your option) +// any later version. + +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License along +// with this library; see the file COPYING3. If not see +// . + +// { dg-do run } +// { dg-options "-std=gnu++17 -pthread" } +// { dg-require-effective-target c++17 } +// { dg-require-effective-target pthread } +// { dg-require-gthreads "" } +// { dg-shouldfail-purecap "out of bounds" } + +#include +#include + +void +test01() +{ + std::pmr::synchronized_pool_resource r; + char *ret = (char *)r.allocate (8, 1); + ret[8] = 'x'; // BOOM for purecap. +} + +int +main() +{ + test01(); +} diff --git a/libstdc++-v3/testsuite/20_util/synchronized_pool_resource/check_read_out_of_bounds_2.cc b/libstdc++-v3/testsuite/20_util/synchronized_pool_resource/check_read_out_of_bounds_2.cc new file mode 100644 index 00000000000..c1b7de46d2a --- /dev/null +++ b/libstdc++-v3/testsuite/20_util/synchronized_pool_resource/check_read_out_of_bounds_2.cc @@ -0,0 +1,45 @@ +// Copyright (C) 2022 Free Software Foundation, Inc. +// +// This file is part of the GNU ISO C++ Library. This library is free +// software; you can redistribute it and/or modify it under the +// terms of the GNU General Public License as published by the +// Free Software Foundation; either version 3, or (at your option) +// any later version. + +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License along +// with this library; see the file COPYING3. If not see +// . + +// { dg-do run } +// { dg-options "-std=gnu++17 -pthread" } +// { dg-require-effective-target c++17 } +// { dg-require-effective-target pthread } +// { dg-require-gthreads "" } +// { dg-shouldfail-purecap "out of bounds" } + +#include +#include + +void +test01() +{ + std::pmr::synchronized_pool_resource r; + // Allocate twice so that the second allocation is from the same allocation + // as the first (and hence that with non-CHERI-bounded allocations we can + // access one byte before the second allocation). + char *ret = (char *)r.allocate (8, 1); + char *ret2 = (char *)r.allocate (8, 1); + ret[1] = 'x'; + ret2[-1] = 'x'; // BOOM for purecap. +} + +int +main() +{ + test01(); +} diff --git a/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/allocate.cc b/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/allocate.cc index 5bf20cf262c..0e8466bf6c3 100644 --- a/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/allocate.cc +++ b/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/allocate.cc @@ -186,9 +186,28 @@ test06() return std::pmr::new_delete_resource()->allocate(bytes, align); // This is a large, unpooled allocation. Check the arguments: - if (bytes < expected_size) + size_t size_expect = expected_size; + size_t alignment_expect = expected_alignment; +#ifdef __CHERI_PURE_CAPABILITY__ + // Since allocations need to be padded and bounded by the allocator for + // precise bounds on CHERI architectures, we account for that here. + { + size_t s = expected_size; + size_t size_tmp = __builtin_cheri_round_representable_length (s); + size_t mask = __builtin_cheri_representable_alignment_mask (s); + size_t alignment_tmp = 1UL << __builtin_ctzl(mask); + // Check for overflow. Nothing is done in the allocators if there is + // overflow in the rounding of representable length. + if (size_tmp >= expected_size) + { + alignment_expect = std::max (alignment_tmp, expected_alignment); + size_expect = std::max (size_tmp, expected_size); + } + } +#endif + if (bytes < size_expect) throw bad_size(); - else if (align != expected_alignment) + else if (align != alignment_expect) throw bad_alignment(); // Else just throw, don't really try to allocate: throw std::bad_alloc(); diff --git a/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/check_awkward_pool_size.cc b/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/check_awkward_pool_size.cc new file mode 100644 index 00000000000..7e784d8f93e --- /dev/null +++ b/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/check_awkward_pool_size.cc @@ -0,0 +1,41 @@ +// Copyright (C) 2022 Free Software Foundation, Inc. +// +// This file is part of the GNU ISO C++ Library. This library is free +// software; you can redistribute it and/or modify it under the +// terms of the GNU General Public License as published by the +// Free Software Foundation; either version 3, or (at your option) +// any later version. + +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License along +// with this library; see the file COPYING3. If not see +// . + +// { dg-options "-std=gnu++17" } +// { dg-do run { target c++17 } } + +#include +#include + +void +test01() +{ + std::pmr::unsynchronized_pool_resource r({0, 0x40130}); + void * p1 = r.allocate(0x40008); + void * p2 = r.allocate(0x40008); +#ifdef __CHERI_PURE_CAPABILITY__ + VERIFY ( (size_t)p1 == __builtin_cheri_base_get (p1)); + VERIFY ( (size_t)p2 == __builtin_cheri_base_get (p2)); +#endif +} + +int +main() +{ + test01(); +} + diff --git a/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/check_read_out_of_bounds.cc b/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/check_read_out_of_bounds.cc new file mode 100644 index 00000000000..6a9e5e4f6df --- /dev/null +++ b/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/check_read_out_of_bounds.cc @@ -0,0 +1,38 @@ +// Copyright (C) 2022 Free Software Foundation, Inc. +// +// This file is part of the GNU ISO C++ Library. This library is free +// software; you can redistribute it and/or modify it under the +// terms of the GNU General Public License as published by the +// Free Software Foundation; either version 3, or (at your option) +// any later version. + +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License along +// with this library; see the file COPYING3. If not see +// . + +// { dg-options "-std=gnu++17" } +// { dg-do run { target c++17 } } +// { dg-shouldfail-purecap "out of bounds" } + +#include +#include + +void +test01() +{ + std::pmr::unsynchronized_pool_resource r; + char *ret = (char *)r.allocate (8, 1); + ret[8] = 'x'; // BOOM for purecap. +} + +int +main() +{ + test01(); +} + diff --git a/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/check_read_out_of_bounds_2.cc b/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/check_read_out_of_bounds_2.cc new file mode 100644 index 00000000000..83b09c287fb --- /dev/null +++ b/libstdc++-v3/testsuite/20_util/unsynchronized_pool_resource/check_read_out_of_bounds_2.cc @@ -0,0 +1,43 @@ +// Copyright (C) 2022 Free Software Foundation, Inc. +// +// This file is part of the GNU ISO C++ Library. This library is free +// software; you can redistribute it and/or modify it under the +// terms of the GNU General Public License as published by the +// Free Software Foundation; either version 3, or (at your option) +// any later version. + +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License along +// with this library; see the file COPYING3. If not see +// . + +// { dg-options "-std=gnu++17" } +// { dg-do run { target c++17 } } +// { dg-shouldfail-purecap "out of bounds" } + +#include +#include + +void +test01() +{ + std::pmr::unsynchronized_pool_resource r; + // Allocate twice so that the second allocation is from the same allocation + // as the first (and hence that with non-CHERI-bounded allocations we can + // access one byte before the second allocation). + char *ret = (char *)r.allocate (8, 1); + char *ret2 = (char *)r.allocate (8, 1); + ret[1] = 'x'; + ret2[-1] = 'x'; // BOOM for purecap. +} + +int +main() +{ + test01(); +} +