Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions backends/apple/coreai/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ endif()

enable_language(OBJCXX)

set(_coreai_runtime_sources runtime/coreai_manifest.mm
runtime/coreai_validation.mm
set(_coreai_runtime_sources
runtime/coreai_cache_root.mm runtime/coreai_fault_injection.mm
runtime/coreai_manifest.mm runtime/coreai_validation.mm
runtime/coreai_storage.mm
)

function(coreai_configure_objc_target target)
Expand All @@ -31,9 +33,11 @@ if(EXECUTORCH_BUILD_TESTS AND CMAKE_SYSTEM_NAME STREQUAL "Darwin")
add_executable(
coreai_host_test
runtime/test/coreai_host_test.mm runtime/test/coreai_manifest_test.mm
${_coreai_runtime_sources}
runtime/test/coreai_cache_root_test.mm runtime/test/coreai_storage_test.mm
runtime/test/coreai_filesystem_fixture.mm ${_coreai_runtime_sources}
)
coreai_configure_objc_target(coreai_host_test)
target_compile_definitions(coreai_host_test PRIVATE COREAI_ASSETS_TESTING=1)
find_library(COREAI_FOUNDATION_FRAMEWORK Foundation REQUIRED)
target_link_libraries(
coreai_host_test PRIVATE executorch GTest::gtest
Expand Down
37 changes: 35 additions & 2 deletions backends/apple/coreai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,45 @@ architectures=["h17p"])` requests that compiler architecture; omitting the list
lets the compiler emit its supported architectures for the target platform.
These are Core AI architecture names, not CPU names such as `arm64`.

### Asset storage

The assets root must be an absolute path. The application chooses it, should
reserve it for this backend, and is trusted not to rename, replace or modify its
contents while the backend uses it; the backend does not defend against other
processes of the same user rebinding paths. Written files and changed directories
are synced with `fsync`. Atomic publication uses `F_FULLFSYNC` on the temporary
file before rename and on the directory afterward. A failure before rename
preserves the old contents; a failure of the final flush reports an error even
though the new complete contents are already visible. Use a local APFS volume;
unsupported full-device flushes are reported as errors, not downgraded to `fsync`.

Preparing the assets root sets `NSURLIsExcludedFromBackupKey` on it, which covers
everything beneath it. Ancestors are not modified. This is backup exclusion, not
a control for iCloud Drive synchronization.

Newly created roots request
`NSFileProtectionCompleteUntilFirstUserAuthentication`. On iOS, this allows cache
access while the device is locked after its first unlock following a reboot. New
subdirectories and files inherit their parent's protection. Existing cache
contents are not migrated. Directory preparation skips creation when the root
already exists; child directories are opened first and created only when missing.
Directory creation uses default permissions filtered by the process umask, without
an explicit owner-only restriction. Existing permissions are not changed.
Backup exclusion and required durability flushes still apply on reused paths.

Cache-root preparation owns location and backup/protection policy; descriptor
storage owns no-follow operations, publication, and synchronization. Creation
and enumeration use explicit policies, with a missing child distinguishable
from an error. Fault injection is a separate, thread-local test interface and
compiles away in production.

## Host Tests

On macOS, `EXECUTORCH_BUILD_COREAI=ON` with `EXECUTORCH_BUILD_TESTS=ON`
registers the standalone `coreai_host_test` executable with CTest. It tests
manifest parsing and AOT asset selection using Foundation, without Swift or
CoreAI SDK calls. The host test target is omitted for Apple cross-builds.
manifest parsing, AOT asset selection, and filesystem storage using Foundation,
without Swift or CoreAI SDK calls. The host test target is omitted for Apple
cross-builds.

CI builds and runs these tests on macOS 27. To use the same configuration from
the repository root:
Expand Down
25 changes: 25 additions & 0 deletions backends/apple/coreai/runtime/coreai_cache_root.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#import <Foundation/Foundation.h>
#include <executorch/runtime/core/result.h>
#include "coreai_file.h"

namespace executorch::backends::coreai {

// Resolves the default location without creating or modifying storage.
runtime::Result<NSString*> default_coreai_assets_root();
// Requires an absolute directory path. With Create, makes the directory and
// excludes it from backup; otherwise a missing directory is InvalidArgument.
runtime::Result<NSString*> prepare_storage_root(
NSString* path,
StorageCreationMode mode);

} // namespace executorch::backends::coreai
103 changes: 103 additions & 0 deletions backends/apple/coreai/runtime/coreai_cache_root.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

#import "coreai_cache_root.h"
#include "coreai_fault_injection.h"
#include "coreai_file.h"

#include <sys/stat.h>
#include <cerrno>

namespace executorch::backends::coreai {
namespace {
using runtime::Error;
using runtime::Result;

Error ensure_excluded_from_backup(NSURL* directory) {
NSURL* fresh = [NSURL fileURLWithPath:directory.path isDirectory:YES];
NSNumber* excluded = nil;
NSError* error = nil;
ET_CHECK_OR_RETURN_ERROR(
[fresh getResourceValue:&excluded
forKey:NSURLIsExcludedFromBackupKey
error:&error],
AccessFailed,
"Cannot read Core AI backup exclusion: %s",
error.localizedDescription.UTF8String);
if (excluded.boolValue) {
return Error::Ok;
}
ET_CHECK_OR_RETURN_ERROR(
[fresh setResourceValue:@YES
forKey:NSURLIsExcludedFromBackupKey
error:&error],
AccessFailed,
"Cannot exclude Core AI storage from backup: %s",
error.localizedDescription.UTF8String);
return Error::Ok;
}

} // namespace

Result<NSString*> default_coreai_assets_root() {
NSURL* caches =
[NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory
inDomains:NSUserDomainMask]
.firstObject;
ET_CHECK_OR_RETURN_ERROR(
caches.isFileURL, AccessFailed, "Cannot resolve Core AI cache directory");
return [caches.path stringByAppendingPathComponent:@"executorch_coreai"];
}

Result<NSString*> prepare_storage_root(
NSString* path,
StorageCreationMode mode) {
const bool create = mode == StorageCreationMode::Create;
ET_CHECK_OR_RETURN_ERROR(
[path isKindOfClass:NSString.class] && [path hasPrefix:@"/"] &&
[path rangeOfString:@"\0"].location == NSNotFound,
InvalidArgument,
"Core AI storage requires an absolute path");
struct stat info;
const int result =
retry_eintr([&] { return stat(path.fileSystemRepresentation, &info); });
ET_CHECK_OR_RETURN_ERROR(
result == 0 || errno == ENOENT || errno == ENOTDIR,
AccessFailed,
"Cannot inspect Core AI storage directory");
ET_CHECK_OR_RETURN_ERROR(
result == 0 ? S_ISDIR(info.st_mode) : create && errno == ENOENT,
InvalidArgument,
"Core AI storage must be a directory");
if (create && result != 0) {
NSDictionary* attributes = @{
NSFileProtectionKey :
NSFileProtectionCompleteUntilFirstUserAuthentication,
};
ET_CHECK_OR_RETURN_ERROR(
fault_point(FaultPoint::BeforeCreateDirectory) == 0,
AccessFailed,
"Cannot begin Core AI directory creation");
NSError* error = nil;
ET_CHECK_OR_RETURN_ERROR(
[NSFileManager.defaultManager createDirectoryAtPath:path
withIntermediateDirectories:YES
attributes:attributes
error:&error],
AccessFailed,
"Cannot create Core AI storage directory: %s",
error.localizedDescription.UTF8String);
}
if (create) {
ET_CHECK_OK_OR_RETURN_ERROR(ensure_excluded_from_backup(
[NSURL fileURLWithPath:path isDirectory:YES]));
}
return path;
}

} // namespace executorch::backends::coreai
34 changes: 34 additions & 0 deletions backends/apple/coreai/runtime/coreai_fault_injection.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

namespace executorch::backends::coreai {

enum class FaultPoint {
Write,
SyncFile,
SyncDirectory,
FullSync,
Rename,
BeforeCreateDirectory,
};

#if defined(COREAI_ASSETS_TESTING) && COREAI_ASSETS_TESTING
int fault_point(FaultPoint point);
namespace testing {
using FaultCallback = int (*)(FaultPoint);
extern thread_local FaultCallback fault_callback;
} // namespace testing
#else
constexpr int fault_point(FaultPoint) {
return 0;
}
#endif

} // namespace executorch::backends::coreai
24 changes: 24 additions & 0 deletions backends/apple/coreai/runtime/coreai_fault_injection.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

#include "coreai_fault_injection.h"

namespace executorch::backends::coreai {

#if defined(COREAI_ASSETS_TESTING) && COREAI_ASSETS_TESTING
namespace testing {
thread_local FaultCallback fault_callback = nullptr;
}

int fault_point(FaultPoint point) {
return testing::fault_callback == nullptr ? 0
: testing::fault_callback(point);
}
#endif

} // namespace executorch::backends::coreai
75 changes: 75 additions & 0 deletions backends/apple/coreai/runtime/coreai_file.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cerrno>

namespace executorch::backends::coreai {

enum class StorageCreationMode {
OpenExisting,
Create,
};

class FileDescriptor {
public:
explicit FileDescriptor(int fd) : fd_(fd) {}
~FileDescriptor() {
reset(-1);
}
FileDescriptor(const FileDescriptor&) = delete;
FileDescriptor& operator=(const FileDescriptor&) = delete;
int get() const {
return fd_;
}
int release() {
const int fd = fd_;
fd_ = -1;
return fd;
}
void reset(int fd) {
if (fd_ >= 0) {
// Retrying close can close a descriptor reused by another thread.
::close(fd_);
}
fd_ = fd;
}

private:
int fd_;
};

template <typename F>
auto retry_eintr(F call) {
decltype(call()) result;
do {
result = call();
} while (result == -1 && errno == EINTR);
return result;
}

// Opens a child directory without following symlinks. Returns -1 with errno
// set, including ENOENT when the child is missing.
inline int open_directory_at(int parent_fd, const char* name) {
return retry_eintr([&] {
return openat(
parent_fd, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
});
}

// Reads type and permission metadata for a child without following symlinks.
inline int lstat_at(int parent_fd, const char* name, struct stat* info) {
return retry_eintr(
[&] { return fstatat(parent_fd, name, info, AT_SYMLINK_NOFOLLOW); });
}

} // namespace executorch::backends::coreai
50 changes: 50 additions & 0 deletions backends/apple/coreai/runtime/coreai_storage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#import <Foundation/Foundation.h>
#include <executorch/runtime/core/result.h>
#include <cstddef>
#include "coreai_file.h"

namespace executorch::backends::coreai {

runtime::Error sync_storage_directory(int fd);
struct StorageDirectory {
int fd;
bool created;
};
// Opens one child component without following symlinks. The caller owns fd;
// a missing child with OpenExisting returns {-1, false}. Does not flush
// directories.
runtime::Result<StorageDirectory>
open_storage_directory(int parent_fd, NSString* name, StorageCreationMode mode);
// Returns an independently owned descriptor for a stable coordination file.
int open_storage_shared_file(int parent_fd, const char* name);
// Creates a new file and missing parents, then syncs the file and its parent.
// On failure, attempts to remove the new file; created parents may remain.
runtime::Error write_storage_file(
int root_fd,
NSString* relative_path,
const void* bytes,
size_t size);
// Atomically replaces name with data and fully flushes the file and renamed
// entry. On error, readers see either the old or the new complete bytes.
runtime::Error publish_storage_data(int root_fd, NSString* name, NSData* data);

enum class StorageNamePolicy {
Strict,
SkipInvalid,
};
// Lists sorted names without changing fd's enumeration offset.
runtime::Result<NSArray<NSString*>*> storage_children(
int fd,
StorageNamePolicy policy = StorageNamePolicy::Strict);

} // namespace executorch::backends::coreai
Loading
Loading