xref: /aosp_15_r20/external/executorch/extension/data_loader/mmap_data_loader.cpp (revision 523fa7a60841cd1ecfb9cc4201f1ca8b03ed023a)
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