/* * Copyright (C) 2019-2022 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 "JSBase.h" #import "JSValueRef.h" #import "JSScriptInternal.h" #import "APICast.h" #import "BytecodeCacheError.h" #import "CachedTypes.h" #import "CodeCache.h" #import "Completion.h" #import "Identifier.h" #import "IntegrityInlines.h" #import "JSContextInternal.h" #import "JSScriptSourceProvider.h" #import "JSSourceCode.h" #import "JSValuePrivate.h" #import "JSVirtualMachineInternal.h" #import "Symbol.h" #import #import #import #import #import #import #import #import #if JSC_OBJC_API_ENABLED @implementation JSScript { WeakObjCPtr m_virtualMachine; JSScriptType m_type; FileSystem::MappedFileData m_mappedSource; String m_source; RetainPtr m_sourceURL; RetainPtr m_cachePath; RefPtr 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.protocolIsFile()) { createError([NSString stringWithFormat:@"Cache path `%@` is not a local file", cachePathURL.createNSURL().get()], error); return false; } String systemPath = cachePathURL.fileSystemPath(); if (auto fileType = FileSystem::fileType(systemPath)) { if (*fileType != FileSystem::FileType::Regular) { createError([NSString stringWithFormat:@"Cache path `%@` already exists and is not a file", systemPath.createNSString().get()], error); return false; } } String directory = FileSystem::parentPath(systemPath); if (directory.isNull()) { createError([NSString stringWithFormat:@"Cache path `%@` does not contain in a valid directory", systemPath.createNSString().get()], error); return false; } if (FileSystem::fileType(directory) != FileSystem::FileType::Directory) { createError([NSString stringWithFormat:@"Cache directory `%@` is not a directory or does not exist", directory.createNSString().get()], 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", directory.createNSString().get()], 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; auto result = adoptNS([[JSScript alloc] init]); result->m_virtualMachine = vm; result->m_type = type; result->m_source = source; result->m_sourceURL = sourceURL; result->m_cachePath = cachePath; [result readCache]; return result.autorelease(); } + (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.protocolIsFile()) return createError([NSString stringWithFormat:@"File path %@ is not a local file", filePathURL.createNSURL().get()], error); String systemPath = filePathURL.fileSystemPath(); auto fileData = FileSystem::mapFile(systemPath, FileSystem::MappedFileMode::Shared); if (!fileData) return createError([NSString stringWithFormat:@"File at path %@ could not be mapped.", systemPath.createNSString().get()], error); if (!charactersAreAllASCII(fileData->span())) return createError([NSString stringWithFormat:@"Not all characters in file at %@ are ASCII.", systemPath.createNSString().get()], error); auto result = adoptNS([[JSScript alloc] init]); result->m_virtualMachine = vm; result->m_type = type; result->m_source = StringImpl::createWithoutCopying(byteCast(fileData->span())); result->m_mappedSource = WTF::move(*fileData); result->m_sourceURL = sourceURL; result->m_cachePath = cachePath; [result readCache]; return result.autorelease(); } - (void)readCache { if (!m_cachePath) return; String cacheFilename = [m_cachePath path]; auto handle = FileSystem::openFile(cacheFilename, FileSystem::FileOpenMode::Read, FileSystem::FileAccessPermission::All, { FileSystem::FileLockMode::Exclusive, FileSystem::FileLockMode::Nonblocking }); if (!handle) return; auto mappedFile = handle.map(FileSystem::MappedFileMode::Private); if (!mappedFile) return; auto fileData = mappedFile->span(); // Ensure we at least have a SHA1::Digest to read. if (fileData.size() < sizeof(SHA1::Digest)) { FileSystem::deleteFile(cacheFilename); return; } unsigned fileDataSize = fileData.size() - sizeof(SHA1::Digest); SHA1::Digest computedHash; SHA1 sha1; sha1.addBytes(fileData.first(fileDataSize)); sha1.computeHash(computedHash); SHA1::Digest fileHash; auto hashSpan = fileData.subspan(fileDataSize, sizeof(SHA1::Digest)); memcpySpan(std::span { fileHash }, hashSpan); if (computedHash != fileHash) { FileSystem::deleteFile(cacheFilename); return; } Ref cachedBytecode = JSC::CachedBytecode::create(WTF::move(*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 = WTF::move(cachedBytecode); else handle.truncate(0); } - (BOOL)cacheBytecodeWithError:(NSError **)error { String errorString { }; [self writeCache:errorString]; if (!errorString.isNull()) { createError(errorString.createNSString().get(), 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)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 sourceProvider = JSScriptSourceProvider::create(self, origin, WTF::move(filename), String(), JSC::SourceTaintedOrigin::Untainted, startPosition, type); JSC::SourceCode sourceCode(WTF::move(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; auto fileHandle = FileSystem::FileHandle::adopt(open(cacheFileName, O_CREAT | O_WRONLY | O_EXLOCK | O_NONBLOCK, 0600)); if (!fileHandle) { error = makeString("Could not open or lock the bytecode cache file. It's likely another VM or process is already using it. Error: "_s, safeStrerror(errno).span()); return NO; } auto tempFileHandle = FileSystem::FileHandle::adopt(open(tempFileName, O_CREAT | O_RDWR | O_EXLOCK | O_NONBLOCK, 0600)); if (!tempFileHandle) { error = makeString("Could not open or lock the bytecode cache temp file. Error: "_s, safeStrerror(errno).span()); return NO; } 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, tempFileHandle, cacheError); break; case kJSScriptTypeProgram: m_cachedBytecode = JSC::generateProgramBytecode(vm, sourceCode, tempFileHandle, cacheError); break; } if (cacheError.isValid()) { m_cachedBytecode = JSC::CachedBytecode::create(); fileHandle.truncate(0); error = makeString("Unable to generate bytecode for this JSScript because: "_s, cacheError.message()); return NO; } SHA1::Digest computedHash; SHA1 sha1; sha1.addBytes(m_cachedBytecode->span()); sha1.computeHash(computedHash); tempFileHandle.write(computedHash); tempFileHandle.flush(); rename(tempFileName, cacheFileName); return YES; } @end #endif