Commit 8ce6d9f5 authored by Ramon Nou's avatar Ramon Nou
Browse files

feat(symlinks,execve): support cross-mount symlinks and external binary resolution in execve

- In hook_symlinkat and gkfs_libc, allow storing external target paths when destination is in GekkoFS (e.g. for python venv bin/python3 -> /usr/bin/python3).
- In intercept SYS_execve and SYS_execveat, resolve symlinks targeting external host binaries before passing to kernel execve, keeping argv[0] intact so virtualenvs detect their environment.
- In gkfs_readlink, return external target strings directly without prepending mountdir.
- In get_metadata, stat external symlink targets directly on the host when following links.
- In gkfs_open_impl, delegate opening external symlink targets to host openat.
parent dbab1946
Loading
Loading
Loading
Loading
Loading
+6 −2
Changes for src/client/gkfs_libc.cpp: 6 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -2275,7 +2275,7 @@ symlink(const char* path1, const char* path2) throw() {
                return -1;
            }
            std::string resolved;
            if(path1 && path1[0] != '/') {
            if(path1[0] != '/') {
                std::string parent_dir = gkfs::path::dirname(resolved2);
                std::string abs_target = gkfs::path::prepend_path(parent_dir, path1);
                if(abs_target.empty() || abs_target.front() != '/') {
@@ -2286,6 +2286,8 @@ symlink(const char* path1, const char* path2) throw() {
                return gkfs::syscall::gkfs_mk_symlink(resolved2, resolved);
            } else if(resolve_gkfs_path(AT_FDCWD, path1, resolved) == PathStatus::Internal) {
                return gkfs::syscall::gkfs_mk_symlink(resolved2, resolved);
            } else {
                return gkfs::syscall::gkfs_mk_symlink(resolved2, path1);
            }
        }
    }
@@ -2306,7 +2308,7 @@ symlinkat(const char* path1, int newdirfd, const char* path2) throw() {
                return -1;
            }
            std::string resolved;
            if(path1 && path1[0] != '/') {
            if(path1[0] != '/') {
                std::string parent_dir = gkfs::path::dirname(resolved2);
                std::string abs_target = gkfs::path::prepend_path(parent_dir, path1);
                if(abs_target.empty() || abs_target.front() != '/') {
@@ -2317,6 +2319,8 @@ symlinkat(const char* path1, int newdirfd, const char* path2) throw() {
                return gkfs::syscall::gkfs_mk_symlink(resolved2, resolved);
            } else if(resolve_gkfs_path(AT_FDCWD, path1, resolved) == PathStatus::Internal) {
                return gkfs::syscall::gkfs_mk_symlink(resolved2, resolved);
            } else {
                return gkfs::syscall::gkfs_mk_symlink(resolved2, path1);
            }
        }
    }
+17 −5
Changes for src/client/gkfs_metadata.cpp: 17 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -323,7 +323,13 @@ gkfs_open_impl(const std::string& path, mode_t mode, int flags,
            errno = ELOOP;
            return -1;
        }
        return gkfs_open_impl(md.target_path(), mode, flags, symlink_depth + 1);
        std::string rel_target;
        if(!CTX->relativize_path(md.target_path().c_str(), rel_target)) {
            // External target (e.g. /usr/bin/python3): open on host filesystem
            return ::syscall(SYS_openat, AT_FDCWD, md.target_path().c_str(), flags,
                             mode);
        }
        return gkfs_open_impl(rel_target, mode, flags, symlink_depth + 1);
    }
    if(gkfs::config::metadata::rename_support) {
        if(md.blocks() == -1) {
@@ -1688,16 +1694,22 @@ gkfs_readlink(const std::string& path, char* buf, int bufsize) {
        errno = EINVAL;
        return -1;
    }
    int path_size = md->target_path().size() + CTX->mountdir().size();
    std::string return_path;
    std::string rel_dummy;
    if(CTX->relativize_path(md->target_path().c_str(), rel_dummy)) {
        return_path = CTX->mountdir() + rel_dummy;
    } else {
        return_path = md->target_path();
    }
    int path_size = return_path.size();
    if(path_size >= bufsize) {
        LOG(WARNING, "Destination buffer size is too short: {} < {}, {} ",
            bufsize, path_size, md->target_path());
            bufsize, path_size, return_path);
        errno = ENAMETOOLONG;
        return -1;
    }

    CTX->mountdir().copy(buf, CTX->mountdir().size());
    std::strcpy(buf + CTX->mountdir().size(), md->target_path().c_str());
    std::memcpy(buf, return_path.data(), path_size);
    return path_size;
}

+4 −2
Changes for src/client/hooks.cpp: 4 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -652,9 +652,11 @@ hook_symlinkat(const char* oldname, int newdfd, const char* newname) {
                // Source is also in GekkoFS — create symlink inside GekkoFS.
                return with_errno(gkfs::syscall::gkfs_mk_symlink(
                        newname_resolved, oldname_resolved));
            } else {
                // Cross-mount symlink pointing to an external host path (e.g. /usr/bin/python3).
                return with_errno(gkfs::syscall::gkfs_mk_symlink(
                        newname_resolved, oldname));
            }
            LOG(WARNING, "{}() cross-mount symlink not supported", __func__);
            return -ENOTSUP;
        }

        default:
+64 −3
Changes for src/client/intercept.cpp: 64 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@
#include <pthread.h>
#include <client/intercept.hpp>
#include <client/preload.hpp>
#include <client/preload_util.hpp>
#include <client/hooks.hpp>
#include <client/logging.hpp>
#include <client/gkfs_functions.hpp>
@@ -680,26 +681,86 @@ hook(long syscall_number, long arg0, long arg1, long arg2, long arg3, long arg4,

    switch(syscall_number) {

        case SYS_execve:
        case SYS_execve: {
            // If we do not set this to false, we are in trouble with vforks
            pthread_once(&key_once_control, make_key);
            pthread_setspecific(reentrance_guard_key, NULL);
            const char* pathname = reinterpret_cast<const char*>(arg0);
            std::string resolved_exec;
            if(pathname != nullptr) {
                std::string rel_path;
                if(CTX->relativize_path(pathname, rel_path)) {
                    std::string current_rel = rel_path;
                    for(unsigned int depth = 0; depth < 40; ++depth) {
                        auto md = gkfs::utils::get_metadata(current_rel, false);
                        if(!md) {
                            break;
                        }
                        if(md->is_link()) {
                            std::string target = md->target_path();
                            std::string next_rel;
                            if(!CTX->relativize_path(target.c_str(), next_rel)) {
                                resolved_exec = target;
                                break;
                            }
                            current_rel = next_rel;
                        } else {
                            break;
                        }
                    }
                }
            }
            const char* exec_path =
                    resolved_exec.empty() ? pathname : resolved_exec.c_str();
            *result = syscall_no_intercept_wrapper(
                    syscall_number, reinterpret_cast<const char*>(arg0),
                    syscall_number, exec_path,
                    reinterpret_cast<const char* const*>(arg1),
                    reinterpret_cast<const char* const*>(arg2));
            break;
        }

#ifdef SYS_execveat
        case SYS_execveat:
        case SYS_execveat: {
            // If we do not set this to false, we are in trouble with vforks
            pthread_once(&key_once_control, make_key);
            pthread_setspecific(reentrance_guard_key, NULL);
            int dirfd = static_cast<int>(arg0);
            const char* pathname = reinterpret_cast<const char*>(arg1);
            std::string resolved_exec;
            if(pathname != nullptr) {
                std::string rel_path;
                if(CTX->relativize_fd_path(dirfd, pathname, rel_path, false) ==
                   gkfs::preload::RelativizeStatus::internal) {
                    std::string current_rel = rel_path;
                    for(unsigned int depth = 0; depth < 40; ++depth) {
                        auto md = gkfs::utils::get_metadata(current_rel, false);
                        if(!md) {
                            break;
                        }
                        if(md->is_link()) {
                            std::string target = md->target_path();
                            std::string next_rel;
                            if(!CTX->relativize_path(target.c_str(), next_rel)) {
                                resolved_exec = target;
                                break;
                            }
                            current_rel = next_rel;
                        } else {
                            break;
                        }
                    }
                }
            }
            if(!resolved_exec.empty()) {
                arg0 = AT_FDCWD;
                arg1 = reinterpret_cast<long>(resolved_exec.c_str());
            }
            *result = syscall_no_intercept_wrapper(
                    syscall_number, arg0, reinterpret_cast<const char*>(arg1),
                    reinterpret_cast<const char* const*>(arg2),
                    reinterpret_cast<const char* const*>(arg3), arg4);
            break;
        }
#endif
#ifdef SYS_open
        case SYS_open:
+19 −2
Changes for src/client/preload_util.cpp: 19 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -59,6 +59,7 @@

#include <fstream>
#include <ostream>
#include <sys/stat.h>
#include <thread>
#include <algorithm>
#include <sstream>
@@ -351,10 +352,26 @@ get_metadata(const string& path, bool follow_links, bool include_inline) {
        gkfs::metadata::Metadata md{attr};
        auto original = md;
        while(md.is_link()) {
            std::string rel_target;
            if(!CTX->relativize_path(md.target_path().c_str(), rel_target)) {
                // External target (e.g. /usr/bin/python3): stat directly on host
                struct stat host_st{};
                if(::stat(md.target_path().c_str(), &host_st) != 0) {
                    return {};
                }
                gkfs::metadata::Metadata host_md{};
                host_md.mode(host_st.st_mode);
                host_md.size(host_st.st_size);
                host_md.ctime(host_st.st_ctime);
                host_md.atime(host_st.st_atime);
                host_md.mtime(host_st.st_mtime);
                host_md.blocks(host_st.st_blocks);
                return host_md;
            }
            if(gkfs::config::proxy::fwd_stat && CTX->use_proxy()) {
                err = gkfs::rpc::forward_stat_proxy(md.target_path(), attr);
                err = gkfs::rpc::forward_stat_proxy(rel_target, attr);
            } else {
                err = gkfs::rpc::forward_stat(md.target_path(), attr,
                err = gkfs::rpc::forward_stat(rel_target, attr,
                                              inline_data, 0, include_inline);
            }
            if(err) {