/ API / JSScript.mm
JSScript.mm
  1  /*
  2   * Copyright (C) 2019-2020 Apple Inc. All rights reserved.
  3   *
  4   * Redistribution and use in source and binary forms, with or without
  5   * modification, are permitted provided that the following conditions
  6   * are met:
  7   * 1. Redistributions of source code must retain the above copyright
  8   *    notice, this list of conditions and the following disclaimer.
  9   * 2. Redistributions in binary form must reproduce the above copyright
 10   *    notice, this list of conditions and the following disclaimer in the
 11   *    documentation and/or other materials provided with the distribution.
 12   *
 13   * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 14   * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 15   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 16   * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 17   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 18   * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 19   * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 20   * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 21   * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 22   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 23   * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 24   */
 25  
 26  #import "config.h"
 27  #import "JSScriptInternal.h"
 28  
 29  #import "APICast.h"
 30  #import "BytecodeCacheError.h"
 31  #import "CachedTypes.h"
 32  #import "CodeCache.h"
 33  #import "Identifier.h"
 34  #import "JSContextInternal.h"
 35  #import "JSScriptSourceProvider.h"
 36  #import "JSSourceCode.h"
 37  #import "JSValuePrivate.h"
 38  #import "JSVirtualMachineInternal.h"
 39  #import "Symbol.h"
 40  #import <sys/stat.h>
 41  #import <wtf/FileMetadata.h>
 42  #import <wtf/FileSystem.h>
 43  #import <wtf/SHA1.h>
 44  #import <wtf/Scope.h>
 45  #import <wtf/WeakObjCPtr.h>
 46  #import <wtf/spi/darwin/DataVaultSPI.h>
 47  
 48  #ifdef DARLING_NONUNIFIED_BUILD
 49  #include "runtime/Completion.h"
 50  #endif
 51  
 52  #if JSC_OBJC_API_ENABLED
 53  
 54  #if defined(DARLING) && __i386__
 55  @implementation JSScript
 56  #else
 57  @implementation JSScript {
 58      WeakObjCPtr<JSVirtualMachine> m_virtualMachine;
 59      JSScriptType m_type;
 60      FileSystem::MappedFileData m_mappedSource;
 61      String m_source;
 62      RetainPtr<NSURL> m_sourceURL;
 63      RetainPtr<NSURL> m_cachePath;
 64      RefPtr<JSC::CachedBytecode> m_cachedBytecode;
 65  }
 66  #endif
 67  
 68  static JSScript *createError(NSString *message, NSError** error)
 69  {
 70      if (error)
 71          *error = [NSError errorWithDomain:@"JSScriptErrorDomain" code:1 userInfo:@{ @"message": message }];
 72      return nil;
 73  }
 74  
 75  static bool validateBytecodeCachePath(NSURL* cachePath, NSError** error)
 76  {
 77      if (!cachePath)
 78          return true;
 79  
 80      URL cachePathURL([cachePath absoluteURL]);
 81      if (!cachePathURL.isLocalFile()) {
 82          createError([NSString stringWithFormat:@"Cache path `%@` is not a local file", static_cast<NSURL *>(cachePathURL)], error);
 83          return false;
 84      }
 85  
 86      String systemPath = cachePathURL.fileSystemPath();
 87  
 88      if (auto metadata = FileSystem::fileMetadata(systemPath)) {
 89          if (metadata->type != FileMetadata::Type::File) {
 90              createError([NSString stringWithFormat:@"Cache path `%@` already exists and is not a file", static_cast<NSString *>(systemPath)], error);
 91              return false;
 92          }
 93      }
 94  
 95      String directory = FileSystem::directoryName(systemPath);
 96      if (directory.isNull()) {
 97          createError([NSString stringWithFormat:@"Cache path `%@` does not contain in a valid directory", static_cast<NSString *>(systemPath)], error);
 98          return false;
 99      }
100  
101      if (!FileSystem::fileIsDirectory(directory, FileSystem::ShouldFollowSymbolicLinks::No)) {
102          createError([NSString stringWithFormat:@"Cache directory `%@` is not a directory or does not exist", static_cast<NSString *>(directory)], error);
103          return false;
104      }
105  
106  #if USE(APPLE_INTERNAL_SDK)
107      if (rootless_check_datavault_flag(FileSystem::fileSystemRepresentation(directory).data(), nullptr)) {
108          createError([NSString stringWithFormat:@"Cache directory `%@` is not a data vault", static_cast<NSString *>(directory)], error);
109          return false;
110      }
111  #endif
112  
113      return true;
114  }
115  
116  + (instancetype)scriptOfType:(JSScriptType)type withSource:(NSString *)source andSourceURL:(NSURL *)sourceURL andBytecodeCache:(NSURL *)cachePath inVirtualMachine:(JSVirtualMachine *)vm error:(out NSError **)error
117  {
118      if (!validateBytecodeCachePath(cachePath, error))
119          return nil;
120  
121      JSScript *result = [[[JSScript alloc] init] autorelease];
122      result->m_virtualMachine = vm;
123      result->m_type = type;
124      result->m_source = source;
125      result->m_sourceURL = sourceURL;
126      result->m_cachePath = cachePath;
127      [result readCache];
128      return result;
129  }
130  
131  + (instancetype)scriptOfType:(JSScriptType)type memoryMappedFromASCIIFile:(NSURL *)filePath withSourceURL:(NSURL *)sourceURL andBytecodeCache:(NSURL *)cachePath inVirtualMachine:(JSVirtualMachine *)vm error:(out NSError **)error
132  {
133      if (!validateBytecodeCachePath(cachePath, error))
134          return nil;
135  
136      URL filePathURL([filePath absoluteURL]);
137      if (!filePathURL.isLocalFile())
138          return createError([NSString stringWithFormat:@"File path %@ is not a local file", static_cast<NSURL *>(filePathURL)], error);
139  
140      bool success = false;
141      String systemPath = filePathURL.fileSystemPath();
142      FileSystem::MappedFileData fileData(systemPath, FileSystem::MappedFileMode::Shared, success);
143      if (!success)
144          return createError([NSString stringWithFormat:@"File at path %@ could not be mapped.", static_cast<NSString *>(systemPath)], error);
145  
146      if (!charactersAreAllASCII(reinterpret_cast<const LChar*>(fileData.data()), fileData.size()))
147          return createError([NSString stringWithFormat:@"Not all characters in file at %@ are ASCII.", static_cast<NSString *>(systemPath)], error);
148  
149      JSScript *result = [[[JSScript alloc] init] autorelease];
150      result->m_virtualMachine = vm;
151      result->m_type = type;
152      result->m_source = String(StringImpl::createWithoutCopying(bitwise_cast<const LChar*>(fileData.data()), fileData.size()));
153      result->m_mappedSource = WTFMove(fileData);
154      result->m_sourceURL = sourceURL;
155      result->m_cachePath = cachePath;
156      [result readCache];
157      return result;
158  }
159  
160  - (void)readCache
161  {
162      if (!m_cachePath)
163          return;
164  
165      NSString *cachePathString = [m_cachePath path];
166      const char* cacheFilename = cachePathString.UTF8String;
167  
168      auto fd = FileSystem::openAndLockFile(cacheFilename, FileSystem::FileOpenMode::Read, {FileSystem::FileLockMode::Exclusive, FileSystem::FileLockMode::Nonblocking});
169      if (!FileSystem::isHandleValid(fd))
170          return;
171      auto closeFD = makeScopeExit([&] {
172          FileSystem::unlockAndCloseFile(fd);
173      });
174  
175      bool success;
176      FileSystem::MappedFileData mappedFile(fd, FileSystem::MappedFileMode::Private, success);
177      if (!success)
178          return;
179  
180      const uint8_t* fileData = reinterpret_cast<const uint8_t*>(mappedFile.data());
181      unsigned fileTotalSize = mappedFile.size();
182  
183      // Ensure we at least have a SHA1::Digest to read.
184      if (fileTotalSize < sizeof(SHA1::Digest)) {
185          FileSystem::deleteFile(cacheFilename);
186          return;
187      }
188  
189      unsigned fileDataSize = fileTotalSize - sizeof(SHA1::Digest);
190  
191      SHA1::Digest computedHash;
192      SHA1 sha1;
193      sha1.addBytes(fileData, fileDataSize);
194      sha1.computeHash(computedHash);
195  
196      SHA1::Digest fileHash;
197      memcpy(&fileHash, fileData + fileDataSize, sizeof(SHA1::Digest));
198  
199      if (computedHash != fileHash) {
200          FileSystem::deleteFile(cacheFilename);
201          return;
202      }
203  
204      Ref<JSC::CachedBytecode> cachedBytecode = JSC::CachedBytecode::create(WTFMove(mappedFile));
205  
206      JSC::VM& vm = *toJS([m_virtualMachine JSContextGroupRef]);
207      JSC::SourceCode sourceCode = [self sourceCode];
208      JSC::SourceCodeKey key = m_type == kJSScriptTypeProgram ? sourceCodeKeyForSerializedProgram(vm, sourceCode) : sourceCodeKeyForSerializedModule(vm, sourceCode);
209      if (isCachedBytecodeStillValid(vm, cachedBytecode.copyRef(), key, m_type == kJSScriptTypeProgram ? JSC::SourceCodeType::ProgramType : JSC::SourceCodeType::ModuleType))
210          m_cachedBytecode = WTFMove(cachedBytecode);
211      else
212          FileSystem::truncateFile(fd, 0);
213  }
214  
215  - (BOOL)cacheBytecodeWithError:(NSError **)error
216  {
217      String errorString { };
218      [self writeCache:errorString];
219      if (!errorString.isNull()) {
220          createError(errorString, error);
221          return NO;
222      }
223  
224      return YES;
225  }
226  
227  - (BOOL)isUsingBytecodeCache
228  {
229      return !!m_cachedBytecode->size();
230  }
231  
232  - (NSURL *)sourceURL
233  {
234      return m_sourceURL.get();
235  }
236  
237  - (JSScriptType)type
238  {
239      return m_type;
240  }
241  
242  @end
243  
244  @implementation JSScript(Internal)
245  
246  - (instancetype)init
247  {
248      self = [super init];
249      if (!self)
250          return nil;
251  
252      self->m_cachedBytecode = JSC::CachedBytecode::create();
253  
254      return self;
255  }
256  
257  - (unsigned)hash
258  {
259      return m_source.hash();
260  }
261  
262  - (const String&)source
263  {
264      return m_source;
265  }
266  
267  - (RefPtr<JSC::CachedBytecode>)cachedBytecode
268  {
269      return m_cachedBytecode;
270  }
271  
272  - (JSC::SourceCode)sourceCode
273  {
274      JSC::VM& vm = *toJS([m_virtualMachine JSContextGroupRef]);
275      JSC::JSLockHolder locker(vm);
276  
277      TextPosition startPosition { };
278      String filename = String { [[self sourceURL] absoluteString] };
279      URL url = URL({ }, filename);
280      auto type = m_type == kJSScriptTypeModule ? JSC::SourceProviderSourceType::Module : JSC::SourceProviderSourceType::Program;
281      JSC::SourceOrigin origin(url);
282      Ref<JSScriptSourceProvider> sourceProvider = JSScriptSourceProvider::create(self, origin, WTFMove(filename), startPosition, type);
283      JSC::SourceCode sourceCode(WTFMove(sourceProvider), startPosition.m_line.oneBasedInt(), startPosition.m_column.oneBasedInt());
284      return sourceCode;
285  }
286  
287  - (JSC::JSSourceCode*)jsSourceCode
288  {
289      JSC::VM& vm = *toJS([m_virtualMachine JSContextGroupRef]);
290      JSC::JSLockHolder locker(vm);
291      JSC::JSSourceCode* jsSourceCode = JSC::JSSourceCode::create(vm, [self sourceCode]);
292      return jsSourceCode;
293  }
294  
295  - (BOOL)writeCache:(String&)error
296  {
297      if (self.isUsingBytecodeCache) {
298          error = "Cache for JSScript is already non-empty. Can not override it."_s;
299          return NO;
300      }
301  
302      if (!m_cachePath) {
303          error = "No cache path was provided during construction of this JSScript."_s;
304          return NO;
305      }
306  
307      // We want to do the write as a transaction (i.e. we guarantee that it's all
308      // or nothing). So, we'll write to a temp file first, and rename the temp
309      // file to the cache file only after we've finished writing the whole thing.
310  
311      NSString *cachePathString = [m_cachePath path];
312      const char* cacheFileName = cachePathString.UTF8String;
313      const char* tempFileName = [cachePathString stringByAppendingString:@".tmp"].UTF8String;
314      int fd = open(cacheFileName, O_CREAT | O_WRONLY | O_EXLOCK | O_NONBLOCK, 0600);
315      if (fd == -1) {
316          error = makeString("Could not open or lock the bytecode cache file. It's likely another VM or process is already using it. Error: ", strerror(errno));
317          return NO;
318      }
319  
320      auto closeFD = makeScopeExit([&] {
321          close(fd);
322      });
323  
324      int tempFD = open(tempFileName, O_CREAT | O_RDWR | O_EXLOCK | O_NONBLOCK, 0600);
325      if (tempFD == -1) {
326          error = makeString("Could not open or lock the bytecode cache temp file. Error: ", strerror(errno));
327          return NO;
328      }
329  
330      auto closeTempFD = makeScopeExit([&] {
331          close(tempFD);
332      });
333  
334      JSC::BytecodeCacheError cacheError;
335      JSC::SourceCode sourceCode = [self sourceCode];
336      JSC::VM& vm = *toJS([m_virtualMachine JSContextGroupRef]);
337      switch (m_type) {
338      case kJSScriptTypeModule:
339          m_cachedBytecode = JSC::generateModuleBytecode(vm, sourceCode, tempFD, cacheError);
340          break;
341      case kJSScriptTypeProgram:
342          m_cachedBytecode = JSC::generateProgramBytecode(vm, sourceCode, tempFD, cacheError);
343          break;
344      }
345  
346      if (cacheError.isValid()) {
347          m_cachedBytecode = JSC::CachedBytecode::create();
348          FileSystem::truncateFile(fd, 0);
349          error = makeString("Unable to generate bytecode for this JSScript because: ", cacheError.message());
350          return NO;
351      }
352  
353      SHA1::Digest computedHash;
354      SHA1 sha1;
355      sha1.addBytes(m_cachedBytecode->data(), m_cachedBytecode->size());
356      sha1.computeHash(computedHash);
357      FileSystem::writeToFile(tempFD, reinterpret_cast<const char*>(&computedHash), sizeof(computedHash));
358  
359      fsync(tempFD);
360      rename(tempFileName, cacheFileName);
361      return YES;
362  }
363  
364  @end
365  
366  #endif