Repository navigation
Expand file tree
/
Copy pathCorpseMemoryTest.cpp
More file actions
440 lines (392 loc) · 20.8 KB
/
Copy pathCorpseMemoryTest.cpp
File metadata and controls
440 lines (392 loc) · 20.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
/*
* Copyright (C) 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "CorpseMemoryTest.h"
#if (OS(MACOS) || USE(APPLE_INTERNAL_SDK)) && !PLATFORM(MACCATALYST) && !PLATFORM(IOS_FAMILY_SIMULATOR)
#include "LibJSCToolsTestUtilities.h"
#include <JavaScriptCore/CorpseAddress.h>
#include <JavaScriptCore/CorpseMemory.h>
#include <JavaScriptCore/CorpseProcess.h>
#include <JavaScriptCore/CorpseSnapshot.h>
#include <array>
#include <limits>
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <span>
#include <unistd.h>
#include <wtf/StdLibExtras.h>
#include <wtf/WeakRandom.h>
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
namespace JSCToolsTest {
using JSC::Corpse::Address;
using JSC::Corpse::Memory;
using JSC::Corpse::Process;
using JSC::Corpse::Snapshot;
namespace {
// The arena's page layout, all of it set up before the corpse is taken. The pages the
// kernel will not hand back are what the error cases below are about.
enum ArenaPage : unsigned {
Readable0 = 0, // Three contiguous readable pages, so a range can span them.
Readable1 = 1,
Readable2 = 2,
Hole = 3, // Deallocated, so the arena has a gap in the middle.
ReadOnly = 4, // A region of its own, whose neighbour...
NeighbourOfReadOnly = 5, // ...is a separate region with different protections.
NoAccess = 6, // Mapped but inaccessible, like a reservation never committed.
Readable3 = 7,
};
constexpr unsigned arenaPageCount = 8;
constexpr uint64_t patternBase = 0xC03B5E0000000000ull;
uint64_t patternFor(unsigned page) { return patternBase | page; }
constexpr uint64_t tailPatternBase = 0x7A11E00000000000ull;
uint64_t tailPatternFor(unsigned page) { return tailPatternBase | page; }
// Each page holds one pattern at its first word and another at its last, so a read can
// be attributed to its page and contiguity across a boundary can be checked.
class Arena {
public:
bool build()
{
m_pageSize = Memory::pageSize();
kern_return_t kr = mach_vm_allocate(mach_task_self(), &m_base, arenaPageCount * m_pageSize, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS)
return false;
for (unsigned page = 0; page < arenaPageCount; ++page) {
*headOf(page) = patternFor(page);
*tailOf(page) = tailPatternFor(page);
}
if (mach_vm_deallocate(mach_task_self(), addressOf(Hole), m_pageSize) != KERN_SUCCESS)
return false;
if (mach_vm_protect(mach_task_self(), addressOf(ReadOnly), m_pageSize, FALSE, VM_PROT_READ) != KERN_SUCCESS)
return false;
if (mach_vm_protect(mach_task_self(), addressOf(NoAccess), m_pageSize, FALSE, VM_PROT_NONE) != KERN_SUCCESS)
return false;
return true;
}
size_t pageSize() const { return m_pageSize; }
mach_vm_address_t addressOf(unsigned page) const { return m_base + page * m_pageSize; }
Address corpseAddressOf(unsigned page) const { return Address(addressOf(page)); }
// The last word of a page, which is where a range that crosses into the next page
// starts in the tests below.
Address corpseAddressOfTailOf(unsigned page) const
{
return Address(addressOf(page) + m_pageSize - sizeof(uint64_t));
}
private:
uint64_t* headOf(unsigned page) const { return reinterpret_cast<uint64_t*>(addressOf(page)); }
uint64_t* tailOf(unsigned page) const
{
return reinterpret_cast<uint64_t*>(addressOf(page) + m_pageSize - sizeof(uint64_t));
}
mach_vm_address_t m_base { 0 };
size_t m_pageSize { 0 };
};
// Takes a std::span, so that handing a Memory::Span straight to it is what checks a
// handle stands in for one.
uint64_t firstWordOf(std::span<const uint8_t> bytes)
{
RELEASE_ASSERT(bytes.size() >= sizeof(uint64_t));
uint64_t word = 0;
memcpySpan(asMutableByteSpan(word), bytes.first(sizeof(uint64_t)));
return word;
}
// Checks whether `address` is mapped in this process.
bool isMappedLocally(const void* address)
{
mach_vm_address_t regionAddress = reinterpret_cast<mach_vm_address_t>(address);
mach_vm_size_t regionSize = 0;
vm_region_basic_info_data_64_t info;
mach_msg_type_number_t infoCount = VM_REGION_BASIC_INFO_COUNT_64;
mach_port_t objectName = MACH_PORT_NULL;
kern_return_t kr = mach_vm_region(mach_task_self(), ®ionAddress, ®ionSize,
VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&info), &infoCount, &objectName);
if (kr != KERN_SUCCESS)
return false;
auto queried = reinterpret_cast<mach_vm_address_t>(address);
return queried >= regionAddress && queried < regionAddress + regionSize;
}
} // anonymous namespace
void testMemory()
{
SuiteTracer tracer("Memory");
if (!tracer.shouldRun())
return;
Arena arena;
if (!arena.build()) {
TEST_ASSERT(false, "the test arena could be built");
return;
}
size_t pageSize = arena.pageSize();
// The corpse is taken after the arena is laid out, so it holds the same layout at
// the same addresses.
RefPtr<Process> process = Process::create(getpid());
if (!process->attach()) {
TEST_ASSERT(false, "attaching to this process succeeds");
return;
}
Snapshot snapshot(process);
if (!snapshot.isValid()) {
TEST_ASSERT(false, "a snapshot of this process is valid");
return;
}
Memory& memory = snapshot.memory();
TEST_ASSERT_EQ(memory.regionCount(), 0uz, "a new Memory has nothing mapped");
{
auto word = memory.ptr<uint64_t>(arena.corpseAddressOf(Readable0));
TEST_ASSERT(word.isValid(), "a readable page maps");
TEST_ASSERT(static_cast<bool>(word), "a valid Ptr is true");
TEST_ASSERT_EQ(word.error(), Memory::Error::None, "a mapped range reports no error");
TEST_ASSERT_HEX_EQ(*word, patternFor(Readable0), "it reads back the page's pattern");
TEST_ASSERT_EQ(word.sizeInBytes(), sizeof(uint64_t), "it is as large as the type it was read as");
TEST_ASSERT_EQ(memory.regionCount(), 1uz, "one region is live");
TEST_ASSERT_EQ(memory.mappedPageCount(), 1uz, "covering one page");
}
TEST_ASSERT_EQ(memory.regionCount(), 0uz,
"the region is released as soon as the last handle on it dies");
{
// A second request for the same page rides on the first region, whichever kind of
// handle asks for it.
auto first = memory.ptr<uint64_t>(arena.corpseAddressOf(Readable0));
auto second = memory.span<uint64_t>(arena.corpseAddressOf(Readable0), 1);
TEST_ASSERT(first.isValid() && second.isValid(), "both requests for one page are valid");
TEST_ASSERT(first.get() == second.data(), "they land at the same local address");
TEST_ASSERT_EQ(memory.regionCount(), 1uz, "and share one region");
}
{
// An offset into a page maps the page, and the handle points at the offset.
auto atOffset = memory.span<uint8_t>(arena.corpseAddressOf(Readable1) + 64, sizeof(uint64_t));
TEST_ASSERT(atOffset.isValid(), "an address partway into a page maps");
auto wholePage = memory.span<uint8_t>(arena.corpseAddressOf(Readable1), sizeof(uint64_t));
TEST_ASSERT(wholePage.isValid(), "so does the page it is in");
TEST_ASSERT_EQ(memory.regionCount(), 1uz, "both come from the same region, keyed on the page they share");
TEST_ASSERT(atOffset.data() == wholePage.data() + 64, "the offset within the page is preserved");
}
{
// The case a single-page region cannot serve: a range that crosses into the next
// page has to be one contiguous mapping of both.
auto words = memory.span<uint64_t>(arena.corpseAddressOfTailOf(Readable1), 2);
TEST_ASSERT(words.isValid(), "a range crossing a page boundary maps");
TEST_ASSERT_EQ(memory.mappedPageCount(), 2uz, "as two pages");
TEST_ASSERT_EQ(words.size(), 2uz, "it is sized in elements");
TEST_ASSERT_EQ(words.sizeInBytes(), 2 * sizeof(uint64_t), "and in bytes");
TEST_ASSERT_HEX_EQ(words[0], tailPatternFor(Readable1), "its first word is the last word of the first page");
TEST_ASSERT_HEX_EQ(words[1], patternFor(Readable2),
"and the word after it is the next page's first, so the two pages are contiguous");
}
{
// Nothing shorter is mapped, so a one-page request rides on the longer region
// rather than making one of its own.
auto threePages = memory.span<uint8_t>(arena.corpseAddressOf(Readable0), 3 * pageSize);
TEST_ASSERT(threePages.isValid(), "a three-page range maps");
auto onePage = memory.span<uint8_t>(arena.corpseAddressOf(Readable0), sizeof(uint64_t));
TEST_ASSERT(onePage.isValid(), "a one-page range at the same base maps");
TEST_ASSERT_EQ(memory.regionCount(), 1uz, "out of the region already there");
TEST_ASSERT(onePage.data() == threePages.data(), "at the same local address");
}
{
// Smallest fit: with regions of two lengths at one base, a request takes the
// shortest that can hold it, so a short read does not pin a longer region.
auto onePage = memory.span<uint8_t>(arena.corpseAddressOf(Readable0), sizeof(uint64_t));
TEST_ASSERT_EQ(memory.regionCount(), 1uz, "a one-page region to start with");
// Three pages cannot come out of a one-page region.
auto threePages = memory.span<uint8_t>(arena.corpseAddressOf(Readable0), 3 * pageSize);
TEST_ASSERT(threePages.isValid(), "a three-page range at the same base maps");
TEST_ASSERT_EQ(memory.regionCount(), 2uz, "as a second region");
TEST_ASSERT(threePages.data() != onePage.data(), "at a local address of its own");
auto onePageAgain = memory.span<uint8_t>(arena.corpseAddressOf(Readable0), sizeof(uint64_t));
TEST_ASSERT_EQ(memory.regionCount(), 2uz, "another one-page request adds no region");
TEST_ASSERT(onePageAgain.data() == onePage.data(), "and takes the one-page region, not the longer one");
auto twoPages = memory.span<uint8_t>(arena.corpseAddressOf(Readable0), pageSize + sizeof(uint64_t));
TEST_ASSERT_EQ(memory.regionCount(), 2uz, "nor does a two-page request");
TEST_ASSERT(twoPages.data() == threePages.data(),
"which the one-page region cannot hold, so it takes the three-page one");
}
TEST_ASSERT_EQ(memory.regionCount(), 0uz, "all of those are released");
{
// Moving a handle moves its reference with it rather than duplicating it.
Memory::Span<uint64_t> moved;
TEST_ASSERT(!moved.isValid(), "a default Span is invalid");
TEST_ASSERT(!moved.data(), "and reads as a null pointer");
TEST_ASSERT(moved.empty(), "and holds nothing");
{
auto span = memory.span<uint64_t>(arena.corpseAddressOf(Readable3), 1);
moved = WTF::move(span);
TEST_ASSERT(!span.isValid(), "a moved-from Span is invalid");
TEST_ASSERT(span.empty(), "and holds nothing");
}
TEST_ASSERT(moved.isValid(), "the moved-to Span still maps");
TEST_ASSERT_HEX_EQ(moved[0], patternFor(Readable3), "and still reads");
TEST_ASSERT_EQ(memory.regionCount(), 1uz, "holding the one region");
}
TEST_ASSERT_EQ(memory.regionCount(), 0uz, "which the move did not release twice");
{
// A Ptr carries its reference the same way.
Memory::Ptr<uint64_t> moved;
TEST_ASSERT(!moved, "a default Ptr is invalid");
TEST_ASSERT(!moved.get(), "and reads as a null pointer");
{
auto word = memory.ptr<uint64_t>(arena.corpseAddressOf(Readable3));
moved = WTF::move(word);
TEST_ASSERT(!word, "a moved-from Ptr is invalid");
}
TEST_ASSERT_HEX_EQ(*moved, patternFor(Readable3), "the moved-to one still reads");
TEST_ASSERT_EQ(memory.regionCount(), 1uz, "holding the one region");
}
TEST_ASSERT_EQ(memory.regionCount(), 0uz, "which that move did not release twice");
{
// One call covers two regions with different protections, and a Span stands in
// for a std::span, which is what lets anything that parses bytes read it without
// knowing about corpses.
auto bytes = memory.span<uint8_t>(arena.corpseAddressOf(ReadOnly), pageSize + sizeof(uint64_t));
TEST_ASSERT(bytes.isValid(), "a range spanning two regions maps");
TEST_ASSERT_EQ(bytes.sizeInBytes(), pageSize + sizeof(uint64_t), "as large as was asked for");
TEST_ASSERT_HEX_EQ(firstWordOf(bytes), patternFor(ReadOnly),
"a Span passes for a std::span, and the read-only page reads back");
TEST_ASSERT_HEX_EQ(firstWordOf(bytes.subspan(pageSize)), patternFor(NeighbourOfReadOnly),
"so does its neighbour, through a subspan");
TEST_ASSERT_HEX_EQ(bytes.front(), patternFor(ReadOnly) & 0xff, "front() is the first byte of the range");
TEST_ASSERT_EQ(bytes.first(4).size(), 4uz, "first(n) is n long");
TEST_ASSERT_EQ(bytes.last(4).size(), 4uz, "and so is last(n)");
}
{
// Iteration, so that a handle works with anything that walks a range.
auto words = memory.span<uint64_t>(arena.corpseAddressOf(Readable0), 2);
TEST_ASSERT(words.isValid(), "a two-element range maps");
size_t visited = 0;
uint64_t firstSeen = 0;
for (uint64_t word : words) {
if (!visited)
firstSeen = word;
++visited;
}
TEST_ASSERT_EQ(visited, 2uz, "a range-for visits every element");
TEST_ASSERT_HEX_EQ(firstSeen, patternFor(Readable0), "starting at the first");
TEST_ASSERT_HEX_EQ(words.back(), *(words.data() + 1), "back() is the last element");
}
{
auto span = memory.span<uint8_t>(arena.corpseAddressOf(Hole), sizeof(uint64_t));
TEST_ASSERT(!span.isValid(), "an unmapped page does not map");
TEST_ASSERT_EQ(span.error(), Memory::Error::UnmappedHole, "and says the range is not mapped");
TEST_ASSERT(!span.data(), "and reads as a null pointer");
TEST_ASSERT(span.empty(), "and holds nothing");
TEST_ASSERT(std::span<const uint8_t>(span).empty(), "so the std::span it stands in for is empty too");
}
{
auto span = memory.span<uint8_t>(arena.corpseAddressOf(Readable2), 2 * pageSize);
TEST_ASSERT(!span.isValid(), "a range running into a hole does not map");
TEST_ASSERT_EQ(span.error(), Memory::Error::UnmappedHole, "for the same reason");
}
{
auto span = memory.span<uint8_t>(arena.corpseAddressOf(NoAccess), sizeof(uint64_t));
TEST_ASSERT(!span.isValid(), "an inaccessible page does not map");
TEST_ASSERT_EQ(span.error(), Memory::Error::NotReadable, "and says it denies read access");
}
{
auto span = memory.span<uint8_t>(arena.corpseAddressOf(Readable0), 0);
TEST_ASSERT(!span.isValid(), "a zero-length range does not map");
TEST_ASSERT_EQ(span.error(), Memory::Error::InvalidRequest, "and is rejected as a bad request");
}
{
auto span = memory.span<uint8_t>(Address(static_cast<mach_vm_address_t>(8)), sizeof(uint64_t));
TEST_ASSERT(!span.isValid(), "a near-null address does not map");
TEST_ASSERT_EQ(span.error(), Memory::Error::InvalidRequest, "and is rejected as a bad request");
}
{
auto span = memory.span<uint8_t>(arena.corpseAddressOf(Readable0),
std::numeric_limits<size_t>::max());
TEST_ASSERT(!span.isValid(), "a range that runs off the address space does not map");
TEST_ASSERT_EQ(span.error(), Memory::Error::InvalidRequest, "and is rejected as a bad request");
}
{
// An element count whose byte length overflows is caught before it can wrap into
// a request that looks reasonable.
auto span = memory.span<uint64_t>(arena.corpseAddressOf(Readable0),
std::numeric_limits<size_t>::max() / 4);
TEST_ASSERT(!span.isValid(), "an element count too large to size does not map");
TEST_ASSERT_EQ(span.error(), Memory::Error::InvalidRequest, "and is rejected as a bad request");
}
{
auto word = memory.ptr<uint64_t>(arena.corpseAddressOf(Hole));
TEST_ASSERT(!word, "ptr<T> on an unmapped page does not map");
TEST_ASSERT(!word.get(), "and reads as a null pointer");
TEST_ASSERT_EQ(word.error(), Memory::Error::UnmappedHole, "and reports why");
TEST_ASSERT_EQ(word.sizeInBytes(), 0uz, "and holds nothing");
}
TEST_ASSERT_EQ(memory.regionCount(), 0uz, "nothing is left mapped");
{
// Test that the mapped memory is released after the Span goes out of scope.
const void* localBase = nullptr;
{
auto span = memory.span<uint8_t>(arena.corpseAddressOf(Readable0), pageSize);
TEST_ASSERT(span.isValid(), "the page maps");
localBase = span.data();
TEST_ASSERT(isMappedLocally(localBase), "and its pages are present while the handle lives");
}
TEST_ASSERT_EQ(memory.regionCount(), 0uz, "the region leaves the bookkeeping");
TEST_ASSERT(!isMappedLocally(localBase), "and its pages are actually unmapped");
}
{
// Mapping the same page many times must yield the same local mapping, and not leak address space.
static constexpr unsigned rounds = 50;
const void* lastLocalBase = nullptr;
{
auto word = memory.ptr<uint64_t>(arena.corpseAddressOf(Readable0));
TEST_ASSERT(word.isValid(), "mapping should succeed");
lastLocalBase = word.get();
TEST_ASSERT_EQ(memory.regionCount(), 1uz, "first mapping");
for (unsigned round = 0; round < rounds; ++round) {
auto word = memory.ptr<uint64_t>(arena.corpseAddressOf(Readable0));
TEST_ASSERT(word.isValid(), "every round maps");
TEST_ASSERT_EQ(lastLocalBase, word.get(), "should map to the same page");
TEST_ASSERT(isMappedLocally(lastLocalBase), "and its pages are present while the handle lives");
TEST_ASSERT_EQ(memory.regionCount(), 1uz, "repeated mapping of same address should not add additional mapping");
}
TEST_ASSERT_EQ(memory.regionCount(), 1uz, "repeated mapping and release leaves no additional mapping behind");
}
TEST_ASSERT_EQ(memory.regionCount(), 0uz, "last mapping should have been released");
TEST_ASSERT(!isMappedLocally(lastLocalBase),
"and the last round's pages are unmapped, not leaked");
}
{
// Repeatedly mapping and releasing random pages many times should not leak.
static constexpr unsigned rounds = 100;
static constexpr std::array arenaPages { Readable0, Readable1, Readable2, Readable3 };
std::array<const void*, rounds> localBases { };
WTF::WeakRandom random;
dataLogLn(" leak test seed ", RawHex(random.seed()));
for (unsigned round = 0; round < rounds; ++round) {
ArenaPage page = arenaPages[random.getUint32(arenaPages.size())];
auto word = memory.ptr<uint64_t>(arena.corpseAddressOf(page));
TEST_ASSERT(word.isValid(), "every round maps");
localBases[round] = word.get();
TEST_ASSERT(isMappedLocally(localBases[round]), "the page should be mapped");
}
TEST_ASSERT_EQ(memory.regionCount(), 0uz, "all mappings should have been released");
for (const void* localBase : localBases)
TEST_ASSERT(!isMappedLocally(localBase), "all mapped pages should be unmapped, not leaked");
}
}
} // namespace JSCToolsTest
WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
#endif // (OS(MACOS) || USE(APPLE_INTERNAL_SDK)) && !PLATFORM(MACCATALYST) && !PLATFORM(IOS_FAMILY_SIMULATOR)