JSScript.mm   [plain text]

/*
* 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