Skip to content

Commit f9b0851

Browse files
Improve BSD support and test in CI (#308)
1 parent 4bf063d commit f9b0851

File tree

6 files changed

+239
-3
lines changed

6 files changed

+239
-3
lines changed

.github/workflows/ci.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,78 @@ jobs:
950950
-DCPPTRACE_STD_FORMAT=Off
951951
ninja
952952
953+
unittest-freebsd:
954+
runs-on: ubuntu-24.04
955+
needs: unittest-linux
956+
steps:
957+
- uses: actions/checkout@v4
958+
- name: build and test
959+
id: test
960+
uses: vmactions/freebsd-vm@v1
961+
with:
962+
usesh: true
963+
run: |
964+
pkg install -y cmake lang/gcc ninja git
965+
mkdir build
966+
cd build
967+
cmake .. \
968+
-GNinja \
969+
-DCMAKE_BUILD_TYPE=Debug \
970+
-DCPPTRACE_WERROR_BUILD=On \
971+
-DCPPTRACE_STD_FORMAT=Off \
972+
-DCPPTRACE_BUILD_TESTING=On
973+
ninja
974+
./unittest
975+
976+
unittest-netbsd:
977+
runs-on: ubuntu-24.04
978+
needs: unittest-linux
979+
steps:
980+
- uses: actions/checkout@v4
981+
- name: build and test
982+
id: test
983+
uses: vmactions/netbsd-vm@v1
984+
with:
985+
usesh: true
986+
run: |
987+
pkg_add -u pcre2
988+
pkg_add cmake ninja-build git
989+
mkdir build
990+
cd build
991+
cmake .. \
992+
-GNinja \
993+
-DCMAKE_BUILD_TYPE=Debug \
994+
-DCPPTRACE_WERROR_BUILD=On \
995+
-DCPPTRACE_STD_FORMAT=Off \
996+
-DCPPTRACE_BUILD_TESTING=On
997+
ninja
998+
paxctl +m ./unittest
999+
./unittest
1000+
1001+
unittest-openbsd:
1002+
runs-on: ubuntu-24.04
1003+
needs: unittest-linux
1004+
steps:
1005+
- uses: actions/checkout@v4
1006+
- name: build and test
1007+
id: test
1008+
uses: vmactions/openbsd-vm@v1
1009+
with:
1010+
usesh: true
1011+
run: |
1012+
pkg_add cmake ninja git
1013+
mkdir build
1014+
cd build
1015+
cmake .. \
1016+
-GNinja \
1017+
-DCMAKE_BUILD_TYPE=Debug \
1018+
-DCPPTRACE_WERROR_BUILD=On \
1019+
-DCPPTRACE_STD_FORMAT=Off \
1020+
-DCPPTRACE_BUILD_TESTING=On \
1021+
-DCMAKE_EXE_LINKER_FLAGS="-Wl,-z,norelro"
1022+
ninja
1023+
./unittest
1024+
9531025
unittest-windows-32-bit:
9541026
runs-on: windows-2022
9551027
needs: unittest-windows

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<br/>
88
[![Try on Compiler Explorer](https://img.shields.io/badge/-Compiler%20Explorer-brightgreen?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAYAAAAmlE46AAAACXBIWXMAAACwAAAAsAEUaqtpAAABSElEQVQokYVTsU7DMBB9QMTCEJbOMLB5oF0tRfUPIPIJZctYJkZYu3WMxNL+ARUfQKpImcPgDYnsXWBgYQl61TkYyxI3Wef37j3fnQ/6vkcsikY9AbiWq0mpbevDBmLRqDEAA4CEHMADgFRwrwDmch6X2i73RCFVHvC/WCeCMAFpC2AFoPPu5x4md4rnAN4luS61nYWSgauNU8ydkr0bLTMYAoIYtWqxM4LtEumeERDtfUjlMDrp7L67iddyyJtOvUIu2rquVn4iiVSOKXYhiMSJWLwUJZLuQ2CWmVldV4MT11UmXgB8fr0dX3WP6VHMiVrscim6Da2mJxffzwSU2v6xWzSKmzQ4cUTOaCBTvWgU14xkzjhckKm/q3wnrRAcAhksxMZNAdxEf0fRKI6E8zqT1C0X28ccRpqAUltW5pu4sxv5Mb8B4AciE3bHMxz/+gAAAABJRU5ErkJggg==&labelColor=2C3239&style=flat&label=Try+it+on&color=30C452)](https://godbolt.org/z/aP8PsxxeY)
99

10-
Cpptrace is a simple and portable C++ stacktrace library supporting C++11 and greater on Linux, macOS, and Windows
10+
Cpptrace is a simple and portable C++ stacktrace library supporting C++11 and greater on Linux, BSD, macOS, and Windows
1111
including MinGW and Cygwin environments. The goal: Make stack traces simple for once.
1212

1313
In addition to providing access to stack traces, cpptrace also provides a mechanism for getting stacktraces from thrown

src/from_current.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,13 +294,28 @@ namespace detail {
294294
if(type_info_addr - page_addr + sizeof(void*) > static_cast<unsigned>(page_size)) {
295295
throw internal_error("pointer crosses page boundaries");
296296
}
297+
#if IS_WINDOWS
297298
auto old_protections = mprotect_page_and_return_old_protections(
298299
reinterpret_cast<void*>(page_addr),
299300
page_size,
300301
memory_readwrite
301302
);
302303
*static_cast<void**>(type_info_pointer) = static_cast<void*>(new_vtable + 2);
303304
mprotect_page(reinterpret_cast<void*>(page_addr), page_size, old_protections);
305+
#else
306+
auto old_protections = get_page_protections(reinterpret_cast<void*>(page_addr));
307+
// If the page is already writable, skip mprotect entirely.
308+
// This isn't just an optimization, it's needed on openbsd where mprotect would fail with EPERM but we can use
309+
// -Wl,-z,norelro to make the pages we care about writable
310+
bool need_mprotect = !(old_protections & PROT_WRITE);
311+
if(need_mprotect) {
312+
mprotect_page(reinterpret_cast<void*>(page_addr), page_size, memory_readwrite);
313+
}
314+
*static_cast<void**>(type_info_pointer) = static_cast<void*>(new_vtable + 2);
315+
if(need_mprotect) {
316+
mprotect_page(reinterpret_cast<void*>(page_addr), page_size, old_protections);
317+
}
318+
#endif
304319
}
305320

306321
bool can_catch(

src/platform/memory_mapping.cpp

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
#ifdef CPPTRACE_HAS_MACH_VM
1919
#include <mach/mach_vm.h>
2020
#endif
21+
#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
22+
#include <sys/sysctl.h>
23+
#if defined(__FreeBSD__)
24+
#include <sys/user.h>
25+
#endif
2126
#else
2227
#include <fstream>
2328
#include <ios>
@@ -107,6 +112,131 @@ namespace detail {
107112
}
108113
return perms;
109114
}
115+
#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
116+
#if !defined(__OpenBSD__)
117+
// Fetch VM mappings via sysctl with retry on ENOMEM.
118+
// FreeBSD uses a len * 4/3 heuristic in kinfo_getvmmap, we try to be more robust by retrying.
119+
// On all BSD I have investigated, sysctl returns ENOMEM when the buffer is too small (mappings grew between the
120+
// size query and the data fetch). When this happens, oldlenp gives the amount copied, not the amount needed, so we
121+
// must re-query the size from scratch.
122+
std::vector<char> sysctl_vmmap(const int* mib, unsigned int miblen) {
123+
constexpr int max_retries = 3;
124+
for(int attempt = 0; attempt < max_retries; attempt++) {
125+
size_t len = 0;
126+
if(sysctl(mib, miblen, nullptr, &len, nullptr, 0) != 0) {
127+
throw internal_error("sysctl vmmap size query failed: {}", strerror(errno));
128+
}
129+
auto original_len = len;
130+
// https://github.com/lattera/freebsd/blob/401a161083850a9a4ce916f37520c084cff1543b/lib/libutil/kinfo_getvmmap.c#L32C2-L32C20
131+
len = len * 4 / 3;
132+
len -= len % sizeof(struct kinfo_vmentry);
133+
len = std::max(len, original_len);
134+
std::vector<char> buf(len);
135+
if(sysctl(mib, miblen, buf.data(), &len, nullptr, 0) == 0) {
136+
buf.resize(len);
137+
return buf;
138+
}
139+
if(errno != ENOMEM) {
140+
throw internal_error("sysctl vmmap failed: {}", strerror(errno));
141+
}
142+
}
143+
throw internal_error("sysctl vmmap failed after {} retries due to growing memory mappings", max_retries);
144+
}
145+
#endif
146+
#if defined(__FreeBSD__)
147+
int get_page_protections(void* page) {
148+
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_VMMAP, getpid()};
149+
auto buf = sysctl_vmmap(mib, 4);
150+
auto addr = reinterpret_cast<uintptr_t>(page);
151+
char* pos = buf.data();
152+
char* end = pos + buf.size();
153+
while(pos < end) {
154+
auto* entry = reinterpret_cast<struct kinfo_vmentry*>(pos);
155+
if(entry->kve_structsize == 0) break;
156+
if(addr >= entry->kve_start && addr < entry->kve_end) {
157+
int perms = 0;
158+
if(entry->kve_protection & KVME_PROT_READ) perms |= PROT_READ;
159+
if(entry->kve_protection & KVME_PROT_WRITE) perms |= PROT_WRITE;
160+
if(entry->kve_protection & KVME_PROT_EXEC) perms |= PROT_EXEC;
161+
return perms;
162+
}
163+
pos += entry->kve_structsize;
164+
}
165+
throw internal_error(
166+
"Failed to find mapping for {>16:0h} via sysctl KERN_PROC_VMMAP",
167+
reinterpret_cast<uintptr_t>(page)
168+
);
169+
}
170+
#elif defined(__NetBSD__)
171+
int get_page_protections(void* page) {
172+
int mib[5] = {
173+
CTL_VM, VM_PROC, VM_PROC_MAP, getpid(),
174+
static_cast<int>(sizeof(struct kinfo_vmentry))
175+
};
176+
auto buf = sysctl_vmmap(mib, 5);
177+
auto addr = reinterpret_cast<uintptr_t>(page);
178+
auto count = buf.size() / sizeof(struct kinfo_vmentry);
179+
auto* entries = reinterpret_cast<struct kinfo_vmentry*>(buf.data());
180+
for(size_t i = 0; i < count; i++) {
181+
if(addr >= entries[i].kve_start && addr < entries[i].kve_end) {
182+
int perms = 0;
183+
if(entries[i].kve_protection & KVME_PROT_READ) perms |= PROT_READ;
184+
if(entries[i].kve_protection & KVME_PROT_WRITE) perms |= PROT_WRITE;
185+
if(entries[i].kve_protection & KVME_PROT_EXEC) perms |= PROT_EXEC;
186+
return perms;
187+
}
188+
}
189+
throw internal_error(
190+
"Failed to find mapping for {>16:0h} via sysctl VM_PROC_MAP",
191+
reinterpret_cast<uintptr_t>(page)
192+
);
193+
}
194+
#elif defined(__OpenBSD__)
195+
// OpenBSD's KERN_PROC_VMMAP returns at most VMMAP_MAXLEN (64KB) per call and rejects larger buffers with EINVAL.
196+
// The API is paginated, setting kve_start in the first buffer entry tells the kernel where to resume.
197+
int get_page_protections(void* page) {
198+
int mib[3] = {CTL_KERN, KERN_PROC_VMMAP, getpid()};
199+
size_t buf_size = 0;
200+
if(sysctl(mib, 3, nullptr, &buf_size, nullptr, 0) != 0) {
201+
throw internal_error("sysctl vmmap size query failed: {}", strerror(errno));
202+
}
203+
buf_size -= buf_size % sizeof(struct kinfo_vmentry);
204+
auto addr = reinterpret_cast<uintptr_t>(page);
205+
std::vector<char> buf(buf_size);
206+
unsigned long next_start = 0;
207+
while(true) {
208+
reinterpret_cast<struct kinfo_vmentry*>(buf.data())->kve_start = next_start;
209+
size_t len = buf_size;
210+
if(sysctl(mib, 3, buf.data(), &len, nullptr, 0) != 0) {
211+
throw internal_error("sysctl vmmap failed: {}", strerror(errno));
212+
}
213+
if(len == 0) {
214+
break;
215+
}
216+
auto count = len / sizeof(struct kinfo_vmentry);
217+
auto* entries = reinterpret_cast<struct kinfo_vmentry*>(buf.data());
218+
for(size_t i = 0; i < count; i++) {
219+
if(addr >= entries[i].kve_start && addr < entries[i].kve_end) {
220+
int perms = 0;
221+
if(entries[i].kve_protection & KVE_PROT_READ) perms |= PROT_READ;
222+
if(entries[i].kve_protection & KVE_PROT_WRITE) perms |= PROT_WRITE;
223+
if(entries[i].kve_protection & KVE_PROT_EXEC) perms |= PROT_EXEC;
224+
return perms;
225+
}
226+
}
227+
if(len < buf_size) {
228+
break;
229+
}
230+
// basic sanity check for forward progress
231+
VERIFY(next_start == 0 || entries[count - 1].kve_end > next_start);
232+
next_start = entries[count - 1].kve_end;
233+
}
234+
throw internal_error(
235+
"Failed to find mapping for {>16:0h} via sysctl KERN_PROC_VMMAP",
236+
reinterpret_cast<uintptr_t>(page)
237+
);
238+
}
239+
#endif
110240
#else
111241
// Code for reading /proc/self/maps
112242
// Unfortunately this is the canonical and only way to get memory permissions on linux

src/platform/memory_mapping.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ namespace detail {
2121
constexpr auto memory_readwrite = PROT_READ | PROT_WRITE;
2222
#endif
2323
int get_page_size();
24+
#if !IS_WINDOWS
25+
int get_page_protections(void* page);
26+
#endif
2427
int mprotect_page_and_return_old_protections(void* page, int page_size, int protections);
2528
void mprotect_page(void* page, int page_size, int protections);
2629
void* allocate_page(int page_size);

test/unit/main.cpp

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,25 @@ import cpptrace;
66
#include <cpptrace/cpptrace.hpp>
77
#endif
88

9-
int main(int argc, char** argv) {
10-
testing::InitGoogleTest(&argc, argv);
9+
void init() {
1110
cpptrace::absorb_trace_exceptions(false);
1211
cpptrace::use_default_stderr_logger();
12+
}
13+
14+
#if !defined(_MSC_VER) && !defined(__APPLE__)
15+
#define CONSTRUCTOR_INIT 1
16+
__attribute__((constructor(101)))
17+
void do_init() {
18+
init();
19+
}
20+
#else
21+
#define CONSTRUCTOR_INIT 0
22+
#endif
23+
24+
int main(int argc, char** argv) {
25+
testing::InitGoogleTest(&argc, argv);
26+
#if !CONSTRUCTOR_INIT
27+
init();
28+
#endif
1329
return RUN_ALL_TESTS();
1430
}

0 commit comments

Comments
 (0)