/* * Copyright (C) 2019-2020 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "JSScriptInternal.h" #import "APICast.h" #import "BytecodeCacheError.h" #import "CachedTypes.h" #import "CodeCache.h" #import "Identifier.h" #import "JSContextInternal.h" #import "JSScriptSourceProvider.h" #import "JSSourceCode.h" #import "JSValuePrivate.h" #import "JSVirtualMachineInternal.h" #import "Symbol.h" #import <sys/stat.h> #import <wtf/FileMetadata.h> #import <wtf/FileSystem.h> #import <wtf/SHA1.h> #import <wtf/Scope.h> #import <wtf/WeakObjCPtr.h> #import <wtf/spi/darwin/DataVaultSPI.h> #if JSC_OBJC_API_ENABLED @implementation JSScript { WeakObjCPtr<JSVirtualMachine> m_virtualMachine; JSScriptType m_type; FileSystem::MappedFileData m_mappedSource; String m_source; RetainPtr<NSURL> m_sourceURL; RetainPtr<NSURL> m_cachePath; RefPtr<JSC::CachedBytecode> m_cachedBytecode; } static JSScript *createError(NSString *message, NSError** error) { if (error) *error = [NSError errorWithDomain:@"JSScriptErrorDomain" code:1 userInfo:@{ @"message": message }]; return nil; } static bool validateBytecodeCachePath(NSURL* cachePath, NSError** error) { if (!cachePath) return true; URL cachePathURL([cachePath absoluteURL]); if (!cachePathURL.isLocalFile()) { createError([NSString stringWithFormat:@"Cache path `%@` is not a local file", static_cast<NSURL *>(cachePathURL)], error); return false; } String systemPath = cachePathURL.fileSystemPath(); if (auto metadata = FileSystem::fileMetadata(systemPath)) { if (metadata->type != FileMetadata::Type::File) { createError([NSString stringWithFormat:@"Cache path `%@` already exists and is not a file", static_cast<NSString *>(systemPath)], error); return false; } } String directory = FileSystem::directoryName(systemPath); if (directory.isNull()) { createError([NSString stringWithFormat:@"Cache path `%@` does not contain in a valid directory", static_cast<NSString *>(systemPath)], error); return false; } if (!FileSystem::fileIsDirectory(directory, FileSystem::ShouldFollowSymbolicLinks::No)) { createError([NSString stringWithFormat:@"Cache directory `%@` is not a directory or does not exist", static_cast<NSString *>(directory)], error); return false; } #if USE(APPLE_INTERNAL_SDK) if (rootless_check_datavault_flag(FileSystem::fileSystemRepresentation(directory).data(), nullptr)) { createError([NSString stringWithFormat:@"Cache directory `%@` is not a data vault", static_cast<NSString *>(directory)], error); return false; } #endif return true; } + (instancetype)scriptOfType:(JSScriptType)type withSource:(NSString *)source andSourceURL:(NSURL *)sourceURL andBytecodeCache:(NSURL *)cachePath inVirtualMachine:(JSVirtualMachine *)vm error:(out NSError **)error { if (!validateBytecodeCachePath(cachePath, error)) return nil; JSScript *result = [[[JSScript alloc] init] autorelease]; result->m_virtualMachine = vm; result->m_type = type; result->m_source = source; result->m_sourceURL = sourceURL; result->m_cachePath = cachePath; [result readCache]; return result; } + (instancetype)scriptOfType:(JSScriptType)type memoryMappedFromASCIIFile:(NSURL *)filePath withSourceURL:(NSURL *)sourceURL andBytecodeCache:(NSURL *)cachePath inVirtualMachine:(JSVirtualMachine *)vm error:(out NSError **)error { if (!validateBytecodeCachePath(cachePath, error)) return nil; URL filePathURL([filePath absoluteURL]); if (!filePathURL.isLocalFile()) return createError([NSString stringWithFormat:@"File path %@ is not a local file", static_cast<NSURL *>(filePathURL)], error); bool success = false; String systemPath = filePathURL.fileSystemPath(); FileSystem::MappedFileData fileData(systemPath, FileSystem::MappedFileMode::Shared, success); if (!success) return createError([NSString stringWithFormat:@"File at path %@ could not be mapped.", static_cast<NSString *>(systemPath)], error); if (!charactersAreAllASCII(reinterpret_cast<const LChar*>(fileData.data()), fileData.size())) return createError([NSString stringWithFormat:@"Not all characters in file at %@ are ASCII.", static_cast<NSString *>(systemPath)], error); JSScript *result = [[[JSScript alloc] init] autorelease]; result->m_virtualMachine = vm; result->m_type = type; result->m_source = String(StringImpl::createWithoutCopying(bitwise_cast<const LChar*>(fileData.data()), fileData.size())); result->m_mappedSource = WTFMove(fileData); result->m_sourceURL = sourceURL; result->m_cachePath = cachePath; [result readCache]; return result; } - (void)readCache { if (!m_cachePath) return; NSString *cachePathString = [m_cachePath path]; const char* cacheFilename = cachePathString.UTF8String; auto fd = FileSystem::openAndLockFile(cacheFilename, FileSystem::FileOpenMode::Read, {FileSystem::FileLockMode::Exclusive, FileSystem::FileLockMode::Nonblocking}); if (!FileSystem::isHandleValid(fd)) return; auto closeFD = makeScopeExit([&] { FileSystem::unlockAndCloseFile(fd); }); bool success; FileSystem::MappedFileData mappedFile(fd, FileSystem::MappedFileMode::Private, success); if (!success) return; const uint8_t* fileData = reinterpret_cast<const uint8_t*>(mappedFile.data()); unsigned fileTotalSize = mappedFile.size(); // Ensure we at least have a SHA1::Digest to read. if (fileTotalSize < sizeof(SHA1::Digest)) { FileSystem::deleteFile(cacheFilename); return; } unsigned fileDataSize = fileTotalSize - sizeof(SHA1::Digest); SHA1::Digest computedHash; SHA1 sha1; sha1.addBytes(fileData, fileDataSize); sha1.computeHash(computedHash); SHA1::Digest fileHash; memcpy(&fileHash, fileData + fileDataSize, sizeof(SHA1::Digest)); if (computedHash != fileHash) { FileSystem::deleteFile(cacheFilename); return; } Ref<JSC::CachedBytecode> cachedBytecode = JSC::CachedBytecode::create(WTFMove(mappedFile)); JSC::VM& vm = *toJS([m_virtualMachine JSContextGroupRef]); JSC::SourceCode sourceCode = [self sourceCode]; JSC::SourceCodeKey key = m_type == kJSScriptTypeProgram ? sourceCodeKeyForSerializedProgram(vm, sourceCode) : sourceCodeKeyForSerializedModule(vm, sourceCode); if (isCachedBytecodeStillValid(vm, cachedBytecode.copyRef(), key, m_type == kJSScriptTypeProgram ? JSC::SourceCodeType::ProgramType : JSC::SourceCodeType::ModuleType)) m_cachedBytecode = WTFMove(cachedBytecode); else FileSystem::truncateFile(fd, 0); } - (BOOL)cacheBytecodeWithError:(NSError **)error { String errorString { }; [self writeCache:errorString]; if (!errorString.isNull()) { createError(errorString, error); return NO; } return YES; } - (BOOL)isUsingBytecodeCache { return !!m_cachedBytecode->size(); } - (NSURL *)sourceURL { return m_sourceURL.get(); } - (JSScriptType)type { return m_type; } @end @implementation JSScript(Internal) - (instancetype)init { self = [super init]; if (!self) return nil; self->m_cachedBytecode = JSC::CachedBytecode::create(); return self; } - (unsigned)hash { return m_source.hash(); } - (const String&)source { return m_source; } - (RefPtr<JSC::CachedBytecode>)cachedBytecode { return m_cachedBytecode; } - (JSC::SourceCode)sourceCode { JSC::VM& vm = *toJS([m_virtualMachine JSContextGroupRef]); JSC::JSLockHolder locker(vm); TextPosition startPosition { }; String filename = String { [[self sourceURL] absoluteString] }; URL url = URL({ }, filename); auto type = m_type == kJSScriptTypeModule ? JSC::SourceProviderSourceType::Module : JSC::SourceProviderSourceType::Program; JSC::SourceOrigin origin(url); Ref<JSScriptSourceProvider> sourceProvider = JSScriptSourceProvider::create(self, origin, WTFMove(filename), startPosition, type); JSC::SourceCode sourceCode(WTFMove(sourceProvider), startPosition.m_line.oneBasedInt(), startPosition.m_column.oneBasedInt()); return sourceCode; } - (JSC::JSSourceCode*)jsSourceCode { JSC::VM& vm = *toJS([m_virtualMachine JSContextGroupRef]); JSC::JSLockHolder locker(vm); JSC::JSSourceCode* jsSourceCode = JSC::JSSourceCode::create(vm, [self sourceCode]); return jsSourceCode; } - (BOOL)writeCache:(String&)error { if (self.isUsingBytecodeCache) { error = "Cache for JSScript is already non-empty. Can not override it."_s; return NO; } if (!m_cachePath) { error = "No cache path was provided during construction of this JSScript."_s; return NO; } // We want to do the write as a transaction (i.e. we guarantee that it's all // or nothing). So, we'll write to a temp file first, and rename the temp // file to the cache file only after we've finished writing the whole thing. NSString *cachePathString = [m_cachePath path]; const char* cacheFileName = cachePathString.UTF8String; const char* tempFileName = [cachePathString stringByAppendingString:@".tmp"].UTF8String; int fd = open(cacheFileName, O_CREAT | O_WRONLY | O_EXLOCK | O_NONBLOCK, 0600); if (fd == -1) { 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)); return NO; } auto closeFD = makeScopeExit([&] { close(fd); }); int tempFD = open(tempFileName, O_CREAT | O_RDWR | O_EXLOCK | O_NONBLOCK, 0600); if (tempFD == -1) { error = makeString("Could not open or lock the bytecode cache temp file. Error: ", strerror(errno)); return NO; } auto closeTempFD = makeScopeExit([&] { close(tempFD); }); JSC::BytecodeCacheError cacheError; JSC::SourceCode sourceCode = [self sourceCode]; JSC::VM& vm = *toJS([m_virtualMachine JSContextGroupRef]); switch (m_type) { case kJSScriptTypeModule: m_cachedBytecode = JSC::generateModuleBytecode(vm, sourceCode, tempFD, cacheError); break; case kJSScriptTypeProgram: m_cachedBytecode = JSC::generateProgramBytecode(vm, sourceCode, tempFD, cacheError); break; } if (cacheError.isValid()) { m_cachedBytecode = JSC::CachedBytecode::create(); FileSystem::truncateFile(fd, 0); error = makeString("Unable to generate bytecode for this JSScript because: ", cacheError.message()); return NO; } SHA1::Digest computedHash; SHA1 sha1; sha1.addBytes(m_cachedBytecode->data(), m_cachedBytecode->size()); sha1.computeHash(computedHash); FileSystem::writeToFile(tempFD, reinterpret_cast<const char*>(&computedHash), sizeof(computedHash)); fsync(tempFD); rename(tempFileName, cacheFileName); return YES; } @end #endif