Swift Static Binary Fails with "Unable to obtain Swift runtime path" on Custom Filesystem Layout

I'm building a Swift application for a custom ARM64 Linux distribution and encountering a runtime error after changing from a POSIX-standard filesystem layout to a custom directory structure.

Environment

  • Platform: ARM64 Linux (custom distribution)
  • Swift SDK: aarch64-swift-linux-musl
  • Build Configuration: Static linking with musl
  • Target: Custom embedded system

Build Command

swift build \ -c release \ --scratch-path $SRC_DIR/workbench/build \ --swift-sdk aarch64-swift-linux-musl \ --static-swift-stdlib 

Binary Verification

The binary appears correctly built as static:

$ file workbench workbench: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, BuildID[sha1]=fb50f07fe80fed49d38daceb2840c2acc7268012, with debug_info, not stripped $ ldd workbench not a dynamic executable 

Filesystem Layout Change

Working layout (POSIX standard):

/bin, /sbin, /var, /proc, /sys, /etc, /usr, /lib64 

New layout (custom - causing issues):

/Applications /System/Binary /System/Processes /System/Boot /System/Settings /Settings /Boot /Station 

Error Message

Unable to obtain Swift runtime path 

What Changed

The application worked perfectly on the standard POSIX filesystem layout. After migrating to our custom directory structure (moving from /bin, /sbin, etc. to /Applications, /System/Binary, etc.), the same statically-linked binary now fails with the runtime path error.

Questions

  1. Why would a statically-linked Swift binary need to resolve runtime paths? I expected static linking to embed everything needed.

  2. Does Swift runtime have hardcoded assumptions about standard POSIX paths like /usr/lib, /lib, etc.?

  3. Are there build flags or environment variables to override Swift's runtime path detection for custom filesystem layouts?

  4. Could this be related to the Swift runtime trying to access /proc or other standard directories that don't exist in our custom layout? -> moved to /System/Processes?

What I've Tried

  • Verified the binary is truly statically linked
  • Confirmed all Swift stdlib is statically linked with --static-swift-stdlib
  • The only change between working and non-working versions was the filesystem directory structure (I think, try to analyse the git history of my team, but only kernel options are added)

Additional Context

This is for an embedded system project where we need a custom filesystem layout for organizational reasons. The Swift application serves as a terminal interface (TUI) for system management.

Any insights into Swift's runtime path resolution mechanism or workarounds for custom filesystem layouts would be greatly appreciated!

Thank you,
Kris

it is the missing /proc filesystem that is now mounted at /System/Processes. If I symlink it, everything works fine.

Is there is any way to build the application and tell it to use the new Processes folder?

Looks like you'd have to rebuild the swift runtime to do that: swift/stdlib/public/runtime/Paths.cpp at 3bacfa7fb974e89ad7278f727ccd54a6643b87b6 · swiftlang/swift · GitHub

Since this is a static executable, a more practical solution may be possible: if one declares their own Paths.o with the three symbols it exports (swift_getRuntimeLibraryPath etc), it should be possible to replace the function calls in the stdlib without needing to rebuild it. IIRC there’s some finicky aspects like link order, there was another Forums post about something similar a few months ago which might be worth digging up.

EDIT: found it, see:

The idea would be similar (and if interposing the stdlib turns out to be hard, another solution would be to replace musl readlink)

2 Likes

Thank you very much. I definitely take a look into this.

Another solution came into my mind last night I want to share with you. To enable POSIX-Compatibility Layer we could make a transparent file proxy as a kernel module. Here is a draft (only proxy /proc to /System/Processes:

#include <linux/module.h> #include <linux/kernel.h> #include <linux/kprobes.h> #include <linux/slab.h> #include <linux/uaccess.h> #include <linux/fs.h> #include <linux/namei.h> #include <linux/string.h> #define PHOBOS_PREFIX "/System/Processes" #define PROC_PREFIX "/proc" #define PREFIX_PROC_LEN 5 MODULE_LICENSE("GPL"); MODULE_AUTHOR("PhobOS Team"); MODULE_DESCRIPTION("PhobOS Transparent Proc Redirection System"); MODULE_VERSION("1.0"); // kprobe instances static struct kprobe kp_getname_flags; // debug mode parameter static bool debug_mode = false; module_param(debug_mode, bool, 0644); MODULE_PARM_DESC(debug_mode, "Enable debug logging"); // util: check if path starts with /proc static inline bool is_proc_path(const char *p) { return (p && strncmp(p, PROC_PREFIX, PREFIX_PROC_LEN) == 0); } // util: translate /proc to /System/Processes static char *translate_proc_path(const char *orig) { size_t newlen; char *buf; newlen = strlen(orig) - PREFIX_PROC_LEN + strlen(PHOBOS_PREFIX); buf = kmalloc(newlen + 1, GFP_ATOMIC); if (!buf) return NULL; strcpy(buf, PHOBOS_PREFIX); strcat(buf, orig + PREFIX_PROC_LEN); // skip "/proc" return buf; } // kprobe pre_handler for getname_flags static int pre_getname_flags(struct kprobe *p, struct pt_regs *regs) { const char __user *filename_user; char *kernel_buf = NULL; char *translated; #ifdef CONFIG_ARM64 filename_user = (const char __user *)regs->regs[0]; #elif defined(CONFIG_X86_64) filename_user = (const char __user *)regs->di; #else #warning "Unsupported architecture for demo" return 0; #endif // copy user filename into kernel space kernel_buf = strndup_user(filename_user, PATH_MAX); if (IS_ERR(kernel_buf)) return 0; if (is_proc_path(kernel_buf)) { translated = translate_proc_path(kernel_buf); if (translated) { if (debug_mode) pr_info("phobos: redirect %s -> %s\n", kernel_buf, translated); // copy back into userspace memory if (copy_to_user((void __user *)filename_user, translated, strlen(translated)+1) != 0) { if (debug_mode) pr_err("phobos: copy_to_user failed\n"); } kfree(translated); } } kfree(kernel_buf); return 0; // continue execution } static int __init phobos_init(void) { int ret; kp_getname_flags.symbol_name = "getname_flags"; kp_getname_flags.pre_handler = pre_getname_flags; kp_getname_flags.post_handler = NULL; ret = register_kprobe(&kp_getname_flags); if (ret < 0) { pr_err("phobos: register_kprobe failed, returned %d\n", ret); return ret; } pr_info("phobos: Transparent proc redirection loaded\n"); return 0; } static void __exit phobos_exit(void) { unregister_kprobe(&kp_getname_flags); pr_info("phobos: Transparent proc redirection unloaded\n"); } module_init(phobos_init); module_exit(phobos_exit); 

(author: me and claude.ai)

This makes me wonder: could we write kernel modules in Swift, too? ;)

You may want to look at Luke's embedded work with Yocto Swift too.

1 Like

Mounting /proc elsewhere is... somewhat non-standard, I imagine Swift is going to be the least of your troubles :)

Re: the kernel module:

  • use sizeof(PHOBOS_PREFIX) - 1 instead of strlen(PHOBOS_PREFIX)
  • you can use a stack allocated buffer of PATH_MAX length instead of allocating memory (just be mindful of checking length)
  • you can use memcpy() instead of strcpy()and strcat(), you know the length of PHOBOS_PREFIX at compile time

But I would just leave it mounted at /proc if were you. :slight_smile:

PS. Likely you could write kernel modules using Embedded Swift.

2 Likes

Excelent. Thank you very much!