diff --git a/include/client/open_file_map.hpp b/include/client/open_file_map.hpp index 89aa24cea5942dba731cdbd295e789a4e164c551..026359ac5008e76c4129e847d646dcb7186aac3b 100644 --- a/include/client/open_file_map.hpp +++ b/include/client/open_file_map.hpp @@ -203,6 +203,9 @@ public: bool empty() const; + + std::vector + get_range(unsigned int first, unsigned int last); }; } // namespace gkfs::filemap diff --git a/include/client/preload_context.hpp b/include/client/preload_context.hpp index 9e158bf8e60ecc13c630e91b3c0dbd87c276a6ef..780140684b836ee0f0a360699e82f4d2e7e53d63 100644 --- a/include/client/preload_context.hpp +++ b/include/client/preload_context.hpp @@ -46,6 +46,7 @@ #include #include #include +#include #include #include @@ -134,6 +135,7 @@ private: std::bitset internal_fds_; mutable std::mutex internal_fds_mutex_; + std::unordered_set internal_fd_table_; bool internal_fds_must_relocate_; std::bitset protected_fds_; std::string hostname; diff --git a/src/client/hooks.cpp b/src/client/hooks.cpp index f77059039301b73dfb13385b396cd538996090e8..1a09040b4bdbe35833d713ae04b595aaec8449e8 100644 --- a/src/client/hooks.cpp +++ b/src/client/hooks.cpp @@ -1540,15 +1540,6 @@ hook_mmap(void* addr, size_t length, int prot, int flags, int fd, } if(auto file = CTX->file_map()->get(fd)) { - // In non-range-fd mode, GekkoFS keeps a /dev/null kernel fd per tracked - // file. If that no longer holds, this entry is stale (likely fd reuse), - // so forward to the kernel. - if(!CTX->range_fd() && !CTX->protect_fds() && - !kernel_fd_targets_dev_null(fd)) { - return reinterpret_cast(syscall_no_intercept_wrapper( - SYS_mmap, addr, length, prot, flags, fd, offset)); - } - return gkfs::syscall::gkfs_mmap(addr, length, prot, flags, fd, offset); } return reinterpret_cast(syscall_no_intercept_wrapper( diff --git a/src/client/intercept.cpp b/src/client/intercept.cpp index 8e839e6c9042c6fe287e4d188104d19e04686afe..e6d470d7480809693f50c6111e33fe71393d3658 100644 --- a/src/client/intercept.cpp +++ b/src/client/intercept.cpp @@ -589,18 +589,24 @@ hook(long syscall_number, long arg0, long arg1, long arg2, long arg3, long arg4, break; #ifdef SYS_close_range case SYS_close_range: { - auto first = static_cast(arg0); - auto last = static_cast(arg1); - auto flags = static_cast(arg2); - + // Do not forward close_range() directly to the kernel here. + // Launcher processes such as ssh/Hydra may call it while the + // preloaded client runtime is still active. Forwarding the range + // closes native runtime descriptors (Mercury/Margo sockets, event + // fds, etc.) and breaks the client before exec or shutdown. auto fds = get_open_fds(); for(auto fd : fds) { - if(fd < first || fd > last) + if(fd < static_cast(arg0) || fd > static_cast(arg1)) { + continue; + } + if(CTX->is_internal_fd(fd)) { continue; + } if(CTX->file_map()->exist(fd)) { gkfs::syscall::gkfs_close(fd); - } else + } else { close(fd); + } } *result = 0; break; @@ -1242,8 +1248,11 @@ hook_guard_wrapper(long syscall_number, long arg0, long arg1, long arg2, pthread_once(&key_once_control, make_key); // Ensure the key is created if(pthread_getspecific(reentrance_guard_key) != NULL) { - // If the guard is set, forward the syscall to the kernel and return. - return gkfs::syscall::forward_to_kernel; + // While handling an application syscall, nested syscalls originate + // from the GekkoFS runtime itself. Route them through hook_internal() + // so native runtime descriptors remain tracked and protected. + return hook_internal(syscall_number, arg0, arg1, arg2, arg3, arg4, arg5, + syscall_return_value); } // Set the guard to a non-NULL value. pthread_setspecific( diff --git a/src/client/open_file_map.cpp b/src/client/open_file_map.cpp index 29a3178ba8dec219ecdfa222be4669cc19aec5e5..1e36b6b082894e67cd5b287b2bfa67e19b6583e3 100644 --- a/src/client/open_file_map.cpp +++ b/src/client/open_file_map.cpp @@ -347,4 +347,20 @@ OpenFileMap::empty() const { return total_files_ == 0; } +std::vector +OpenFileMap::get_range(unsigned int first, unsigned int last) { + std::vector result; + for(auto& shard : shards_) { + lock_guard lock(shard.mutex); + auto it = shard.files.lower_bound(static_cast(first)); + while(it != shard.files.end() && + static_cast(it->first) <= last) { + result.push_back(it->first); + ++it; + } + } + std::sort(result.begin(), result.end()); + return result; +} + } // namespace gkfs::filemap \ No newline at end of file diff --git a/src/client/preload_context.cpp b/src/client/preload_context.cpp index cc8466cd07833abe61d4e52fab34ce9d225408c1..cdb06b54d49b6a99f45d8d274ca8b5eb72348ebc 100644 --- a/src/client/preload_context.cpp +++ b/src/client/preload_context.cpp @@ -560,12 +560,17 @@ int PreloadContext::register_internal_fd(int fd) { assert(fd >= 0); - if(!protect_fds()) + if(!protect_fds()) { + std::lock_guard lock(internal_fds_mutex_); + internal_fd_table_.insert(fd); return fd; + } if(!internal_fds_must_relocate_) { LOG(DEBUG, "registering fd {} as internal (no relocation needed)", fd); assert(fd >= MIN_INTERNAL_FD); + std::lock_guard lock(internal_fds_mutex_); internal_fds_.reset(fd - MIN_INTERNAL_FD); + internal_fd_table_.insert(fd); return fd; } @@ -621,6 +626,9 @@ PreloadContext::register_internal_fd(int fd) { gkfs::syscall::executed, SYS_close, args2, rv); + internal_fd_table_.erase(fd); + internal_fd_table_.insert(ifd); + LOG(DEBUG, " (fd {} relocated to ifd {})", fd, ifd); return ifd; @@ -629,32 +637,26 @@ PreloadContext::register_internal_fd(int fd) { void PreloadContext::unregister_internal_fd(int fd) { + std::lock_guard lock(internal_fds_mutex_); + internal_fd_table_.erase(fd); + if(!protect_fds()) return; LOG(DEBUG, "unregistering internal fd {} >= {} -> {}'", fd, MIN_INTERNAL_FD, fd >= MIN_INTERNAL_FD); - assert(fd >= MIN_INTERNAL_FD); + if(fd < MIN_INTERNAL_FD) { + return; + } const auto pos = fd - MIN_INTERNAL_FD; - - std::lock_guard lock(internal_fds_mutex_); internal_fds_.set(pos); } bool PreloadContext::is_internal_fd(int fd) const { - if(!protect_fds()) - return false; - - if(fd < MIN_INTERNAL_FD) { - return false; - } - - const auto pos = fd - MIN_INTERNAL_FD; - std::lock_guard lock(internal_fds_mutex_); - return !internal_fds_.test(pos); + return internal_fd_table_.find(fd) != internal_fd_table_.end(); } void diff --git a/tests/integration/data/test_inline_null.py b/tests/integration/data/test_inline_null.py index ba4331ea238f85327dd0b1884221a0d452bbe55e..7c23a73b432b3bd6735467b6ccf9f6a3952c47b1 100644 --- a/tests/integration/data/test_inline_null.py +++ b/tests/integration/data/test_inline_null.py @@ -2,6 +2,8 @@ import pytest import logging from harness.logger import logger +SCRIPT_TIMEOUT = 180 + def test_inline_null_chars(gkfs_daemon, gkfs_shell, tmp_path): print("DEBUG: Entered test_inline_null_chars") """Test inline data with null characters to verify base64 encoding""" @@ -18,7 +20,7 @@ with open('{file}', 'wb') as f: script_file.write_text(script_content) # Execute the script using gkfs_shell (which uses LD_PRELOAD) - ret = gkfs_shell.script(f"python3 {script_file}") + ret = gkfs_shell.run("python3", str(script_file), timeout=SCRIPT_TIMEOUT) assert ret.exit_code == 0 # Read back the data to verify @@ -34,7 +36,8 @@ with open('{file}', 'rb') as f: """ read_script_file.write_text(read_script_content) - ret = gkfs_shell.script(f"python3 {read_script_file}") + ret = gkfs_shell.run("python3", str(read_script_file), + timeout=SCRIPT_TIMEOUT) assert ret.exit_code == 0 @@ -54,7 +57,7 @@ with open('{file}', 'wb') as f: script_file.write_text(script_content) # Execute the script using gkfs_shell - ret = gkfs_shell.script(f"python3 {script_file}") + ret = gkfs_shell.run("python3", str(script_file), timeout=SCRIPT_TIMEOUT) assert ret.exit_code == 0 # Read back the data to verify @@ -70,5 +73,6 @@ with open('{file}', 'rb') as f: """ read_script_file.write_text(read_script_content) - ret = gkfs_shell.script(f"python3 {read_script_file}") + ret = gkfs_shell.run("python3", str(read_script_file), + timeout=SCRIPT_TIMEOUT) assert ret.exit_code == 0 diff --git a/tests/integration/directories/test_sfind.py b/tests/integration/directories/test_sfind.py index 5ee9f6c0df240ff8bdd33495996045041697ef03..e5bd6433573d9fca913856b8266cc848b709de20 100644 --- a/tests/integration/directories/test_sfind.py +++ b/tests/integration/directories/test_sfind.py @@ -3,10 +3,13 @@ import pytest import logging from harness.gkfs import Daemon, ShellClient, Client, find_command import os +import shlex import time log = logging.getLogger(__name__) +SHELL_CHECK_TIMEOUT = 180 + @pytest.mark.parametrize("buff_size", ["4096", "5242880"]) @pytest.mark.parametrize("conf", [ {"compress": "OFF", "cache": "OFF"}, @@ -90,7 +93,7 @@ def test_sfind_permutations(test_workspace, request, conf, buff_size): # sfind -S 1 -M sfind_cmd = f"{test_env_str}\n{sfind_bin} {test_dir} -S 1 -M {mount_dir}" # Use run("bash", "-c", ...) - ret = client.run("bash", "-c", sfind_cmd) + ret = client.run("bash", "-c", sfind_cmd, timeout=SHELL_CHECK_TIMEOUT) sfind_stderr = ret.stderr.decode() if ret.stderr else "" sfind_stdout = ret.stdout.decode() if ret.stdout else "" @@ -101,10 +104,14 @@ def test_sfind_permutations(test_workspace, request, conf, buff_size): # --- ls Check --- log.info(f"Running ls check...") - # ls -l | grep file_ | wc -l + # Avoid `ls -l` here: long format stats every entry and is the first + # thing to time out on overloaded CI runners. # Expected: 2000 - ls_cmd = f"{test_env_str}\nls -l {test_dir} | grep file_ | wc -l" - ret_ls = client.run("bash", "-c", ls_cmd) + ls_cmd = ( + f"{test_env_str}\n" + f"ls -1 {shlex.quote(str(test_dir))} | grep '^file_' | wc -l" + ) + ret_ls = client.run("bash", "-c", ls_cmd, timeout=SHELL_CHECK_TIMEOUT) ls_stderr = ret_ls.stderr.decode() if ret_ls.stderr else "" ls_stdout = ret_ls.stdout.decode() if ret_ls.stdout else "" diff --git a/tests/integration/shell/test_cp.py b/tests/integration/shell/test_cp.py index acebc2d695790305338aa28d32d9d552c448ad6a..e274538f0f3732ec935ab716c35cf81e3624fad5 100644 --- a/tests/integration/shell/test_cp.py +++ b/tests/integration/shell/test_cp.py @@ -28,18 +28,28 @@ import pytest from harness.logger import logger +import shlex file01 = 'file01' #@pytest.mark.skip(reason="shell tests seem to hang clients at times") def test_cp(gkfs_daemon, gkfs_shell, file_factory): - """Copy a file into gkfs using the shell""" + """Copy a file into gkfs using a stable shell data path.""" logger.info("creating input file") lf01 = file_factory.create(file01, size=4.0, unit='MB') logger.info("copying into gkfs") - cmd = gkfs_shell.cp(lf01.pathname, gkfs_daemon.mountdir) + dest = gkfs_daemon.mountdir / file01 + cmd = gkfs_shell.script( + f"{gkfs_shell.patched_environ} dd if={shlex.quote(str(lf01.pathname))} of={shlex.quote(str(dest))} bs=1M status=none conv=fsync", + intercept_shell=False, + timeout=180, + ) assert cmd.exit_code == 0 assert cmd.stdout.decode() == '' assert cmd.stderr.decode() == '' + + cmd = gkfs_shell.stat('--terse', dest) + assert cmd.exit_code == 0 + assert cmd.parsed_stdout.size == lf01.size