1 /*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9 #include <executorch/extension/data_loader/mmap_data_loader.h>
10
11 #include <cerrno>
12 #include <cstring>
13 #include <limits>
14
15 #include <fcntl.h>
16 #include <sys/mman.h>
17 #include <sys/stat.h>
18 #include <sys/types.h>
19 #include <unistd.h>
20
21 #include <executorch/runtime/core/error.h>
22 #include <executorch/runtime/core/result.h>
23 #include <executorch/runtime/platform/log.h>
24
25 using executorch::runtime::Error;
26 using executorch::runtime::FreeableBuffer;
27 using executorch::runtime::Result;
28
29 namespace executorch {
30 namespace extension {
31
32 namespace {
33
34 struct Range {
35 // Address or offset.
36 uintptr_t start;
37 // Size in bytes.
38 size_t size;
39 };
40
41 /**
42 * Given an address region, returns the start offset and byte size of the set of
43 * pages that completely covers the region.
44 */
get_overlapping_pages(uintptr_t offset,size_t size,size_t page_size)45 Range get_overlapping_pages(uintptr_t offset, size_t size, size_t page_size) {
46 size_t page_mask = ~(page_size - 1);
47 // The address of the page that starts at or before the beginning of the
48 // region.
49 uintptr_t start = offset & page_mask;
50 // The address of the page that starts after the end of the region.
51 uintptr_t end = (offset + size + ~page_mask) & page_mask;
52 return {
53 /*start=*/start,
54 /*size=*/static_cast<size_t>(end - start),
55 };
56 }
57
58 } // namespace
59
~MmapDataLoader()60 MmapDataLoader::~MmapDataLoader() {
61 // file_name_ can be nullptr if this instance was moved from, but freeing a
62 // null pointer is safe.
63 std::free(const_cast<char*>(file_name_));
64 // fd_ can be -1 if this instance was moved from, but closing a negative fd is
65 // safe (though it will return an error).
66 ::close(fd_);
67 }
68
from(const char * file_name,MmapDataLoader::MlockConfig mlock_config)69 Result<MmapDataLoader> MmapDataLoader::from(
70 const char* file_name,
71 MmapDataLoader::MlockConfig mlock_config) {
72 // Cache the page size.
73 long page_size = sysconf(_SC_PAGESIZE);
74 if (page_size < 0) {
75 ET_LOG(Error, "Could not get page size: %s (%d)", ::strerror(errno), errno);
76 return Error::AccessFailed;
77 }
78 if ((page_size & ~(page_size - 1)) != page_size) {
79 ET_LOG(Error, "Page size 0x%ld is not a power of 2", page_size);
80 return Error::InvalidState;
81 }
82
83 // Use open() instead of fopen() because mmap() needs a file descriptor.
84 int fd = ::open(file_name, O_RDONLY);
85 if (fd < 0) {
86 ET_LOG(
87 Error,
88 "Failed to open %s: %s (%d)",
89 file_name,
90 ::strerror(errno),
91 errno);
92 return Error::AccessFailed;
93 }
94
95 // Cache the file size.
96 struct stat st;
97 int err = ::fstat(fd, &st);
98 if (err < 0) {
99 ET_LOG(
100 Error,
101 "Could not get length of %s: %s (%d)",
102 file_name,
103 ::strerror(errno),
104 errno);
105 ::close(fd);
106 return Error::AccessFailed;
107 }
108 size_t file_size = st.st_size;
109
110 // Copy the filename so we can print better debug messages if reads fail.
111 const char* file_name_copy = ::strdup(file_name);
112 if (file_name_copy == nullptr) {
113 ET_LOG(Error, "strdup(%s) failed", file_name);
114 ::close(fd);
115 return Error::MemoryAllocationFailed;
116 }
117
118 return MmapDataLoader(
119 fd,
120 file_size,
121 file_name_copy,
122 static_cast<size_t>(page_size),
123 mlock_config);
124 }
125
126 namespace {
127 /**
128 * FreeableBuffer::FreeFn-compatible callback.
129 *
130 * `context` is actually the OS page size as a uintptr_t.
131 */
MunmapSegment(void * context,void * data,size_t size)132 void MunmapSegment(void* context, void* data, size_t size) {
133 const uintptr_t page_size = reinterpret_cast<uintptr_t>(context);
134
135 Range range =
136 get_overlapping_pages(reinterpret_cast<uintptr_t>(data), size, page_size);
137 int ret = ::munmap(reinterpret_cast<void*>(range.start), range.size);
138 if (ret < 0) {
139 // Let the user know that something went wrong, but there's nothing we can
140 // do about it.
141 ET_LOG(
142 Error,
143 "munmap(0x%zx, %zu) failed: %s (ignored)",
144 (size_t)range.start,
145 range.size,
146 ::strerror(errno),
147 errno);
148 }
149 }
150 } // namespace
151
load(size_t offset,size_t size,ET_UNUSED const DataLoader::SegmentInfo & segment_info) const152 Result<FreeableBuffer> MmapDataLoader::load(
153 size_t offset,
154 size_t size,
155 ET_UNUSED const DataLoader::SegmentInfo& segment_info) const {
156 ET_CHECK_OR_RETURN_ERROR(
157 // Probably had its value moved to another instance.
158 fd_ >= 0,
159 InvalidState,
160 "Uninitialized");
161 ET_CHECK_OR_RETURN_ERROR(
162 offset + size <= file_size_,
163 InvalidArgument,
164 "File %s: offset %zu + size %zu > file_size_ %zu",
165 file_name_,
166 offset,
167 size,
168 file_size_);
169 ET_CHECK_OR_RETURN_ERROR(
170 // Recommended by a lint warning.
171 offset <= std::numeric_limits<off_t>::max(),
172 InvalidArgument,
173 "Offset %zu too large for off_t",
174 offset);
175
176 // mmap() will fail if the size is zero.
177 if (size == 0) {
178 return FreeableBuffer(nullptr, 0, /*free_fn=*/nullptr);
179 }
180
181 // Find the range of pages that covers the requested region.
182 Range range =
183 get_overlapping_pages(static_cast<uintptr_t>(offset), size, page_size_);
184
185 // Map the pages read-only. MAP_PRIVATE vs. MAP_SHARED doesn't matter since
186 // the data is read-only, but use PRIVATE just to further avoid accidentally
187 // modifying the file.
188 void* pages = ::mmap(
189 nullptr,
190 range.size,
191 PROT_READ,
192 MAP_PRIVATE,
193 fd_,
194 static_cast<off_t>(range.start));
195 ET_CHECK_OR_RETURN_ERROR(
196 pages != MAP_FAILED,
197 AccessFailed,
198 "Failed to map %s: mmap(..., size=%zd, ..., fd=%d, offset=0x%zx)",
199 file_name_,
200 range.size,
201 fd_,
202 range.start);
203
204 if (mlock_config_ == MlockConfig::UseMlock ||
205 mlock_config_ == MlockConfig::UseMlockIgnoreErrors) {
206 int err = ::mlock(pages, size);
207 if (err < 0) {
208 if (mlock_config_ == MlockConfig::UseMlockIgnoreErrors) {
209 ET_LOG(
210 Debug,
211 "Ignoring mlock error for file %s (off=0x%zd): "
212 "mlock(%p, %zu) failed: %s (%d)",
213 file_name_,
214 offset,
215 pages,
216 size,
217 ::strerror(errno),
218 errno);
219 } else {
220 ET_LOG(
221 Error,
222 "File %s (off=0x%zd): mlock(%p, %zu) failed: %s (%d)",
223 file_name_,
224 offset,
225 pages,
226 size,
227 ::strerror(errno),
228 errno);
229 ::munmap(pages, size);
230 return Error::NotSupported;
231 }
232 }
233 // No need to keep track of this. munmap() will unlock as a side effect.
234 }
235
236 // The requested data is at an offset into the mapped pages.
237 const void* data = static_cast<const uint8_t*>(pages) + offset - range.start;
238
239 return FreeableBuffer(
240 // The callback knows to unmap the whole pages that encompass this region.
241 data,
242 size,
243 MunmapSegment,
244 /*free_fn_context=*/
245 reinterpret_cast<void*>(
246 // Pass the cached OS page size to the callback so it doesn't need to
247 // query it again.
248 static_cast<uintptr_t>(page_size_)));
249 }
250
size() const251 Result<size_t> MmapDataLoader::size() const {
252 ET_CHECK_OR_RETURN_ERROR(
253 // Probably had its value moved to another instance.
254 fd_ >= 0,
255 InvalidState,
256 "Uninitialized");
257 return file_size_;
258 }
259
260 } // namespace extension
261 } // namespace executorch
262