OLD | NEW |
| (Empty) |
1 // Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 // This file implements PEImage, a generic class to manipulate PE files. | |
6 // This file was adapted from GreenBorder's Code. | |
7 | |
8 #include "sandbox/src/pe_image.h" | |
9 | |
10 namespace sandbox { | |
11 | |
12 // Structure to perform imports enumerations. | |
13 struct EnumAllImportsStorage { | |
14 sandbox::PEImage::EnumImportsFunction callback; | |
15 PVOID cookie; | |
16 }; | |
17 | |
18 // Callback used to enumerate imports. See EnumImportChunksFunction. | |
19 bool ProcessImportChunk(const PEImage &image, LPCSTR module, | |
20 PIMAGE_THUNK_DATA name_table, | |
21 PIMAGE_THUNK_DATA iat, PVOID cookie) { | |
22 EnumAllImportsStorage &storage = *reinterpret_cast<EnumAllImportsStorage*>( | |
23 cookie); | |
24 | |
25 return image.EnumOneImportChunk(storage.callback, module, name_table, iat, | |
26 storage.cookie); | |
27 } | |
28 | |
29 // Callback used to enumerate delay imports. See EnumDelayImportChunksFunction. | |
30 bool ProcessDelayImportChunk(const PEImage &image, | |
31 PImgDelayDescr delay_descriptor, | |
32 LPCSTR module, PIMAGE_THUNK_DATA name_table, | |
33 PIMAGE_THUNK_DATA iat, PIMAGE_THUNK_DATA bound_iat, | |
34 PIMAGE_THUNK_DATA unload_iat, PVOID cookie) { | |
35 EnumAllImportsStorage &storage = *reinterpret_cast<EnumAllImportsStorage*>( | |
36 cookie); | |
37 | |
38 return image.EnumOneDelayImportChunk(storage.callback, delay_descriptor, | |
39 module, name_table, iat, bound_iat, | |
40 unload_iat, storage.cookie); | |
41 } | |
42 | |
43 } // namespace | |
44 | |
45 namespace sandbox { | |
46 | |
47 void PEImage::set_module(HMODULE module) { | |
48 module_ = module; | |
49 } | |
50 | |
51 PIMAGE_DOS_HEADER PEImage::GetDosHeader() const { | |
52 return reinterpret_cast<PIMAGE_DOS_HEADER>(module_); | |
53 } | |
54 | |
55 PIMAGE_NT_HEADERS PEImage::GetNTHeaders() const { | |
56 PIMAGE_DOS_HEADER dos_header = GetDosHeader(); | |
57 | |
58 return reinterpret_cast<PIMAGE_NT_HEADERS>( | |
59 reinterpret_cast<char*>(dos_header) + dos_header->e_lfanew); | |
60 } | |
61 | |
62 PIMAGE_SECTION_HEADER PEImage::GetSectionHeader(UINT section) const { | |
63 PIMAGE_NT_HEADERS nt_headers = GetNTHeaders(); | |
64 PIMAGE_SECTION_HEADER first_section = IMAGE_FIRST_SECTION(nt_headers); | |
65 | |
66 if (section < nt_headers->FileHeader.NumberOfSections) | |
67 return first_section + section; | |
68 else | |
69 return NULL; | |
70 } | |
71 | |
72 WORD PEImage::GetNumSections() const { | |
73 return GetNTHeaders()->FileHeader.NumberOfSections; | |
74 } | |
75 | |
76 DWORD PEImage::GetImageDirectoryEntrySize(UINT directory) const { | |
77 PIMAGE_NT_HEADERS nt_headers = GetNTHeaders(); | |
78 | |
79 return nt_headers->OptionalHeader.DataDirectory[directory].Size; | |
80 } | |
81 | |
82 PVOID PEImage::GetImageDirectoryEntryAddr(UINT directory) const { | |
83 PIMAGE_NT_HEADERS nt_headers = GetNTHeaders(); | |
84 | |
85 return RVAToAddr( | |
86 nt_headers->OptionalHeader.DataDirectory[directory].VirtualAddress); | |
87 } | |
88 | |
89 PIMAGE_SECTION_HEADER PEImage::GetImageSectionFromAddr(PVOID address) const { | |
90 PBYTE target = reinterpret_cast<PBYTE>(address); | |
91 PIMAGE_SECTION_HEADER section; | |
92 | |
93 for (UINT i = 0; NULL != (section = GetSectionHeader(i)); i++) { | |
94 // Don't use the virtual RVAToAddr. | |
95 PBYTE start = reinterpret_cast<PBYTE>( | |
96 PEImage::RVAToAddr(section->VirtualAddress)); | |
97 | |
98 DWORD size = section->Misc.VirtualSize; | |
99 | |
100 if ((start <= target) && (start + size > target)) | |
101 return section; | |
102 } | |
103 | |
104 return NULL; | |
105 } | |
106 | |
107 PIMAGE_SECTION_HEADER PEImage::GetImageSectionHeaderByName( | |
108 LPCSTR section_name) const { | |
109 if (NULL == section_name) | |
110 return NULL; | |
111 | |
112 PIMAGE_SECTION_HEADER ret = NULL; | |
113 int num_sections = GetNumSections(); | |
114 | |
115 for (int i = 0; i < num_sections; i++) { | |
116 PIMAGE_SECTION_HEADER section = GetSectionHeader(i); | |
117 if (0 == _strnicmp(reinterpret_cast<LPCSTR>(section->Name), section_name, | |
118 sizeof(section->Name))) { | |
119 ret = section; | |
120 break; | |
121 } | |
122 } | |
123 | |
124 return ret; | |
125 } | |
126 | |
127 PDWORD PEImage::GetExportEntry(LPCSTR name) const { | |
128 PIMAGE_EXPORT_DIRECTORY exports = GetExportDirectory(); | |
129 | |
130 if (NULL == exports) | |
131 return NULL; | |
132 | |
133 WORD ordinal = 0; | |
134 if (!GetProcOrdinal(name, &ordinal)) | |
135 return NULL; | |
136 | |
137 PDWORD functions = reinterpret_cast<PDWORD>( | |
138 RVAToAddr(exports->AddressOfFunctions)); | |
139 | |
140 return functions + ordinal - exports->Base; | |
141 } | |
142 | |
143 FARPROC PEImage::GetProcAddress(LPCSTR function_name) const { | |
144 PDWORD export_entry = GetExportEntry(function_name); | |
145 if (NULL == export_entry) | |
146 return NULL; | |
147 | |
148 PBYTE function = reinterpret_cast<PBYTE>(RVAToAddr(*export_entry)); | |
149 | |
150 PBYTE exports = reinterpret_cast<PBYTE>( | |
151 GetImageDirectoryEntryAddr(IMAGE_DIRECTORY_ENTRY_EXPORT)); | |
152 DWORD size = GetImageDirectoryEntrySize(IMAGE_DIRECTORY_ENTRY_EXPORT); | |
153 | |
154 // Check for forwarded exports as a special case. | |
155 if (exports <= function && exports + size > function) | |
156 #pragma warning(push) | |
157 #pragma warning(disable: 4312) | |
158 // This cast generates a warning because it is 32 bit specific. | |
159 return reinterpret_cast<FARPROC>(0xFFFFFFFF); | |
160 #pragma warning(pop) | |
161 | |
162 return reinterpret_cast<FARPROC>(function); | |
163 } | |
164 | |
165 bool PEImage::GetProcOrdinal(LPCSTR function_name, WORD *ordinal) const { | |
166 if (NULL == ordinal) | |
167 return false; | |
168 | |
169 PIMAGE_EXPORT_DIRECTORY exports = GetExportDirectory(); | |
170 | |
171 if (NULL == exports) | |
172 return false; | |
173 | |
174 if (IsOrdinal(function_name)) { | |
175 *ordinal = ToOrdinal(function_name); | |
176 } else { | |
177 PDWORD names = reinterpret_cast<PDWORD>(RVAToAddr(exports->AddressOfNames)); | |
178 PDWORD lower = names; | |
179 PDWORD upper = names + exports->NumberOfNames; | |
180 int cmp = -1; | |
181 | |
182 // Binary Search for the name. | |
183 while (lower != upper) { | |
184 PDWORD middle = lower + (upper - lower) / 2; | |
185 LPCSTR name = reinterpret_cast<LPCSTR>(RVAToAddr(*middle)); | |
186 | |
187 cmp = strcmp(function_name, name); | |
188 | |
189 if (cmp == 0) { | |
190 lower = middle; | |
191 break; | |
192 } | |
193 | |
194 if (cmp > 0) | |
195 lower = middle + 1; | |
196 else | |
197 upper = middle; | |
198 } | |
199 | |
200 if (cmp != 0) | |
201 return false; | |
202 | |
203 | |
204 PWORD ordinals = reinterpret_cast<PWORD>( | |
205 RVAToAddr(exports->AddressOfNameOrdinals)); | |
206 | |
207 *ordinal = ordinals[lower - names] + static_cast<WORD>(exports->Base); | |
208 } | |
209 | |
210 return true; | |
211 } | |
212 | |
213 bool PEImage::EnumSections(EnumSectionsFunction callback, PVOID cookie) const { | |
214 PIMAGE_NT_HEADERS nt_headers = GetNTHeaders(); | |
215 UINT num_sections = nt_headers->FileHeader.NumberOfSections; | |
216 PIMAGE_SECTION_HEADER section = GetSectionHeader(0); | |
217 | |
218 for (UINT i = 0; i < num_sections; i++, section++) { | |
219 PVOID section_start = RVAToAddr(section->VirtualAddress); | |
220 DWORD size = section->Misc.VirtualSize; | |
221 | |
222 if (!callback(*this, section, section_start, size, cookie)) | |
223 return false; | |
224 } | |
225 | |
226 return true; | |
227 } | |
228 | |
229 bool PEImage::EnumExports(EnumExportsFunction callback, PVOID cookie) const { | |
230 PVOID directory = GetImageDirectoryEntryAddr(IMAGE_DIRECTORY_ENTRY_EXPORT); | |
231 DWORD size = GetImageDirectoryEntrySize(IMAGE_DIRECTORY_ENTRY_EXPORT); | |
232 | |
233 // Check if there are any exports at all. | |
234 if (NULL == directory || 0 == size) | |
235 return true; | |
236 | |
237 PIMAGE_EXPORT_DIRECTORY exports = reinterpret_cast<PIMAGE_EXPORT_DIRECTORY>( | |
238 directory); | |
239 UINT ordinal_base = exports->Base; | |
240 UINT num_funcs = exports->NumberOfFunctions; | |
241 UINT num_names = exports->NumberOfNames; | |
242 PDWORD functions = reinterpret_cast<PDWORD>(RVAToAddr( | |
243 exports->AddressOfFunctions)); | |
244 PDWORD names = reinterpret_cast<PDWORD>(RVAToAddr(exports->AddressOfNames)); | |
245 PWORD ordinals = reinterpret_cast<PWORD>(RVAToAddr( | |
246 exports->AddressOfNameOrdinals)); | |
247 | |
248 for (UINT count = 0; count < num_funcs; count++) { | |
249 PVOID func = RVAToAddr(functions[count]); | |
250 if (NULL == func) | |
251 continue; | |
252 | |
253 // Check for a name. | |
254 LPCSTR name = NULL; | |
255 UINT hint; | |
256 for (hint = 0; hint < num_names; hint++) { | |
257 if (ordinals[hint] == count) { | |
258 name = reinterpret_cast<LPCSTR>(RVAToAddr(names[hint])); | |
259 break; | |
260 } | |
261 } | |
262 | |
263 if (name == NULL) | |
264 hint = 0; | |
265 | |
266 // Check for forwarded exports. | |
267 LPCSTR forward = NULL; | |
268 if (reinterpret_cast<char*>(func) >= reinterpret_cast<char*>(directory) && | |
269 reinterpret_cast<char*>(func) <= reinterpret_cast<char*>(directory) + | |
270 size) { | |
271 forward = reinterpret_cast<LPCSTR>(func); | |
272 func = 0; | |
273 } | |
274 | |
275 if (!callback(*this, ordinal_base + count, hint, name, func, forward, | |
276 cookie)) | |
277 return false; | |
278 } | |
279 | |
280 return true; | |
281 } | |
282 | |
283 bool PEImage::EnumRelocs(EnumRelocsFunction callback, PVOID cookie) const { | |
284 PVOID directory = GetImageDirectoryEntryAddr(IMAGE_DIRECTORY_ENTRY_BASERELOC); | |
285 DWORD size = GetImageDirectoryEntrySize(IMAGE_DIRECTORY_ENTRY_BASERELOC); | |
286 PIMAGE_BASE_RELOCATION base = reinterpret_cast<PIMAGE_BASE_RELOCATION>( | |
287 directory); | |
288 | |
289 if (directory == NULL || size < sizeof(IMAGE_BASE_RELOCATION)) | |
290 return true; | |
291 | |
292 while (base->SizeOfBlock) { | |
293 PWORD reloc = reinterpret_cast<PWORD>(base + 1); | |
294 UINT num_relocs = (base->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / | |
295 sizeof(WORD); | |
296 | |
297 for (UINT i = 0; i < num_relocs; i++, reloc++) { | |
298 WORD type = *reloc >> 12; | |
299 PVOID address = RVAToAddr(base->VirtualAddress + (*reloc & 0x0FFF)); | |
300 | |
301 if (!callback(*this, type, address, cookie)) | |
302 return false; | |
303 } | |
304 | |
305 base = reinterpret_cast<PIMAGE_BASE_RELOCATION>( | |
306 reinterpret_cast<char*>(base) + base->SizeOfBlock); | |
307 } | |
308 | |
309 return true; | |
310 } | |
311 | |
312 bool PEImage::EnumImportChunks(EnumImportChunksFunction callback, | |
313 PVOID cookie) const { | |
314 DWORD size = GetImageDirectoryEntrySize(IMAGE_DIRECTORY_ENTRY_IMPORT); | |
315 PIMAGE_IMPORT_DESCRIPTOR import = GetFirstImportChunk(); | |
316 | |
317 if (import == NULL || size < sizeof(IMAGE_IMPORT_DESCRIPTOR)) | |
318 return true; | |
319 | |
320 for (; import->FirstThunk; import++) { | |
321 LPCSTR module_name = reinterpret_cast<LPCSTR>(RVAToAddr(import->Name)); | |
322 PIMAGE_THUNK_DATA name_table = reinterpret_cast<PIMAGE_THUNK_DATA>( | |
323 RVAToAddr(import->OriginalFirstThunk)); | |
324 PIMAGE_THUNK_DATA iat = reinterpret_cast<PIMAGE_THUNK_DATA>( | |
325 RVAToAddr(import->FirstThunk)); | |
326 | |
327 if (!callback(*this, module_name, name_table, iat, cookie)) | |
328 return false; | |
329 } | |
330 | |
331 return true; | |
332 } | |
333 | |
334 bool PEImage::EnumOneImportChunk(EnumImportsFunction callback, | |
335 LPCSTR module_name, | |
336 PIMAGE_THUNK_DATA name_table, | |
337 PIMAGE_THUNK_DATA iat, PVOID cookie) const { | |
338 if (NULL == name_table) | |
339 return false; | |
340 | |
341 for (; name_table && name_table->u1.Ordinal; name_table++, iat++) { | |
342 LPCSTR name = NULL; | |
343 WORD ordinal = 0; | |
344 WORD hint = 0; | |
345 | |
346 if (IMAGE_SNAP_BY_ORDINAL(name_table->u1.Ordinal)) { | |
347 ordinal = static_cast<WORD>(IMAGE_ORDINAL32(name_table->u1.Ordinal)); | |
348 } else { | |
349 PIMAGE_IMPORT_BY_NAME import = reinterpret_cast<PIMAGE_IMPORT_BY_NAME>( | |
350 RVAToAddr(name_table->u1.ForwarderString)); | |
351 | |
352 hint = import->Hint; | |
353 name = reinterpret_cast<LPCSTR>(&import->Name); | |
354 } | |
355 | |
356 if (!callback(*this, module_name, ordinal, name, hint, iat, cookie)) | |
357 return false; | |
358 } | |
359 | |
360 return true; | |
361 } | |
362 | |
363 bool PEImage::EnumAllImports(EnumImportsFunction callback, PVOID cookie) const { | |
364 EnumAllImportsStorage temp = { callback, cookie }; | |
365 return EnumImportChunks(ProcessImportChunk, &temp); | |
366 } | |
367 | |
368 bool PEImage::EnumDelayImportChunks(EnumDelayImportChunksFunction callback, | |
369 PVOID cookie) const { | |
370 PVOID directory = GetImageDirectoryEntryAddr( | |
371 IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT); | |
372 DWORD size = GetImageDirectoryEntrySize(IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT); | |
373 PImgDelayDescr delay_descriptor = reinterpret_cast<PImgDelayDescr>(directory); | |
374 | |
375 if (directory == NULL || size == 0) | |
376 return true; | |
377 | |
378 for (; delay_descriptor->rvaHmod; delay_descriptor++) { | |
379 PIMAGE_THUNK_DATA name_table; | |
380 PIMAGE_THUNK_DATA iat; | |
381 PIMAGE_THUNK_DATA bound_iat; // address of the optional bound IAT | |
382 PIMAGE_THUNK_DATA unload_iat; // address of optional copy of original IAT | |
383 LPCSTR module_name; | |
384 | |
385 // check if VC7-style imports, using RVAs instead of | |
386 // VC6-style addresses. | |
387 bool rvas = (delay_descriptor->grAttrs & dlattrRva) != 0; | |
388 | |
389 if (rvas) { | |
390 module_name = reinterpret_cast<LPCSTR>( | |
391 RVAToAddr(delay_descriptor->rvaDLLName)); | |
392 name_table = reinterpret_cast<PIMAGE_THUNK_DATA>( | |
393 RVAToAddr(delay_descriptor->rvaINT)); | |
394 iat = reinterpret_cast<PIMAGE_THUNK_DATA>( | |
395 RVAToAddr(delay_descriptor->rvaIAT)); | |
396 bound_iat = reinterpret_cast<PIMAGE_THUNK_DATA>( | |
397 RVAToAddr(delay_descriptor->rvaBoundIAT)); | |
398 unload_iat = reinterpret_cast<PIMAGE_THUNK_DATA>( | |
399 RVAToAddr(delay_descriptor->rvaUnloadIAT)); | |
400 } else { | |
401 #pragma warning(push) | |
402 #pragma warning(disable: 4312) | |
403 // These casts generate warnings because they are 32 bit specific. | |
404 module_name = reinterpret_cast<LPCSTR>(delay_descriptor->rvaDLLName); | |
405 name_table = reinterpret_cast<PIMAGE_THUNK_DATA>( | |
406 delay_descriptor->rvaINT); | |
407 iat = reinterpret_cast<PIMAGE_THUNK_DATA>(delay_descriptor->rvaIAT); | |
408 bound_iat = reinterpret_cast<PIMAGE_THUNK_DATA>( | |
409 delay_descriptor->rvaBoundIAT); | |
410 unload_iat = reinterpret_cast<PIMAGE_THUNK_DATA>( | |
411 delay_descriptor->rvaUnloadIAT); | |
412 #pragma warning(pop) | |
413 } | |
414 | |
415 if (!callback(*this, delay_descriptor, module_name, name_table, iat, | |
416 bound_iat, unload_iat, cookie)) | |
417 return false; | |
418 } | |
419 | |
420 return true; | |
421 } | |
422 | |
423 bool PEImage::EnumOneDelayImportChunk(EnumImportsFunction callback, | |
424 PImgDelayDescr delay_descriptor, | |
425 LPCSTR module_name, | |
426 PIMAGE_THUNK_DATA name_table, | |
427 PIMAGE_THUNK_DATA iat, | |
428 PIMAGE_THUNK_DATA bound_iat, | |
429 PIMAGE_THUNK_DATA unload_iat, | |
430 PVOID cookie) const { | |
431 UNREFERENCED_PARAMETER(bound_iat); | |
432 UNREFERENCED_PARAMETER(unload_iat); | |
433 | |
434 for (; name_table->u1.Ordinal; name_table++, iat++) { | |
435 LPCSTR name = NULL; | |
436 WORD ordinal = 0; | |
437 WORD hint = 0; | |
438 | |
439 if (IMAGE_SNAP_BY_ORDINAL(name_table->u1.Ordinal)) { | |
440 ordinal = static_cast<WORD>(IMAGE_ORDINAL32(name_table->u1.Ordinal)); | |
441 } else { | |
442 PIMAGE_IMPORT_BY_NAME import; | |
443 bool rvas = (delay_descriptor->grAttrs & dlattrRva) != 0; | |
444 | |
445 if (rvas) { | |
446 import = reinterpret_cast<PIMAGE_IMPORT_BY_NAME>( | |
447 RVAToAddr(name_table->u1.ForwarderString)); | |
448 } else { | |
449 #pragma warning(push) | |
450 #pragma warning(disable: 4312) | |
451 // This cast generates a warning because it is 32 bit specific. | |
452 import = reinterpret_cast<PIMAGE_IMPORT_BY_NAME>( | |
453 name_table->u1.ForwarderString); | |
454 #pragma warning(pop) | |
455 } | |
456 | |
457 hint = import->Hint; | |
458 name = reinterpret_cast<LPCSTR>(&import->Name); | |
459 } | |
460 | |
461 if (!callback(*this, module_name, ordinal, name, hint, iat, cookie)) | |
462 return false; | |
463 } | |
464 | |
465 return true; | |
466 } | |
467 | |
468 bool PEImage::EnumAllDelayImports(EnumImportsFunction callback, | |
469 PVOID cookie) const { | |
470 EnumAllImportsStorage temp = { callback, cookie }; | |
471 return EnumDelayImportChunks(ProcessDelayImportChunk, &temp); | |
472 } | |
473 | |
474 bool PEImage::VerifyMagic() const { | |
475 PIMAGE_DOS_HEADER dos_header = GetDosHeader(); | |
476 | |
477 if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) | |
478 return false; | |
479 | |
480 PIMAGE_NT_HEADERS nt_headers = GetNTHeaders(); | |
481 | |
482 if (nt_headers->Signature != IMAGE_NT_SIGNATURE) | |
483 return false; | |
484 | |
485 if (nt_headers->FileHeader.SizeOfOptionalHeader != | |
486 sizeof(IMAGE_OPTIONAL_HEADER)) | |
487 return false; | |
488 | |
489 if (nt_headers->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC) | |
490 return false; | |
491 | |
492 return true; | |
493 } | |
494 | |
495 bool PEImage::ImageRVAToOnDiskOffset(DWORD rva, DWORD *on_disk_offset) const { | |
496 LPVOID address = RVAToAddr(rva); | |
497 return ImageAddrToOnDiskOffset(address, on_disk_offset); | |
498 } | |
499 | |
500 bool PEImage::ImageAddrToOnDiskOffset(LPVOID address, | |
501 DWORD *on_disk_offset) const { | |
502 if (NULL == address) | |
503 return false; | |
504 | |
505 // Get the section that this address belongs to. | |
506 PIMAGE_SECTION_HEADER section_header = GetImageSectionFromAddr(address); | |
507 if (NULL == section_header) | |
508 return false; | |
509 | |
510 #pragma warning(push) | |
511 #pragma warning(disable: 4311) | |
512 // These casts generate warnings because they are 32 bit specific. | |
513 // Don't follow the virtual RVAToAddr, use the one on the base. | |
514 DWORD offset_within_section = reinterpret_cast<DWORD>(address) - | |
515 reinterpret_cast<DWORD>(PEImage::RVAToAddr( | |
516 section_header->VirtualAddress)); | |
517 #pragma warning(pop) | |
518 | |
519 *on_disk_offset = section_header->PointerToRawData + offset_within_section; | |
520 return true; | |
521 } | |
522 | |
523 PVOID PEImage::RVAToAddr(DWORD rva) const { | |
524 if (rva == 0) | |
525 return NULL; | |
526 | |
527 return reinterpret_cast<char*>(module_) + rva; | |
528 } | |
529 | |
530 PVOID PEImageAsData::RVAToAddr(DWORD rva) const { | |
531 if (rva == 0) | |
532 return NULL; | |
533 | |
534 PVOID in_memory = PEImage::RVAToAddr(rva); | |
535 DWORD dummy; | |
536 | |
537 if (!ImageAddrToOnDiskOffset(in_memory, &dummy)) | |
538 return NULL; | |
539 | |
540 return in_memory; | |
541 } | |
542 | |
543 } // namespace sandbox | |
OLD | NEW |