blob: 93506db6c6152f3311c07c2d2cb120dcf5683f5d [file] [log] [blame]
/*
* 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 "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 <sys/stat.h>
#import <wtf/FileHandle.h>
#import <wtf/FileSystem.h>
#import <wtf/SHA1.h>
#import <wtf/SafeStrerror.h>
#import <wtf/Scope.h>
#import <wtf/StdLibExtras.h>
#import <wtf/WeakObjCPtr.h>
#import <wtf/spi/darwin/DataVaultSPI.h>
#import <wtf/text/MakeString.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.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<Latin1Character>(fileData->span()));
result->m_mappedSource = WTFMove(*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(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
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<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), String(), JSC::SourceTaintedOrigin::Untainted, 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;
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