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