From e24a44414749cf35e4240fb14c672ea09886bb4e Mon Sep 17 00:00:00 2001 From: Roman Tarasov Date: Tue, 14 Jul 2026 22:37:49 +0300 Subject: [PATCH] Harden managed mount teardown --- include/sm_filesystem.h | 4 +- include/sm_mount_diag.h | 7 + src/sm_fakelib.c | 11 +- src/sm_filesystem.c | 125 ++++++++-------- src/sm_mount_device.c | 13 +- src/sm_mount_diag.c | 323 ++++++++++++++++++++++++++++++++++++++++ src/sm_scanner.c | 6 + 7 files changed, 419 insertions(+), 70 deletions(-) create mode 100644 include/sm_mount_diag.h create mode 100644 src/sm_mount_diag.c diff --git a/include/sm_filesystem.h b/include/sm_filesystem.h index 2a2138b..f065ee5 100644 --- a/include/sm_filesystem.h +++ b/include/sm_filesystem.h @@ -43,7 +43,7 @@ bool rollback_title_nullfs_mount(const char *title_id, const char *src_path); // Drop runtime layers for a title while preserving its metadata/link files. bool unmount_title_runtime_layers(const char *title_id); // Release every managed title layer while preserving link metadata. -void unmount_all_title_runtime_layers(void); +bool unmount_all_title_runtime_layers(void); // Reconcile the title mount stack against the expected source/backport state. bool reconcile_title_backport_mount(const char *title_id, const char *src_path, const char *expected_backport_path, @@ -54,7 +54,7 @@ bool mount_backport_overlay(const char *mount_point, const char *backport_path, const char *title_id); // Unmount all managed /system_ex/app/ mount stacks on shutdown. -void shutdown_title_mounts(void); +bool shutdown_title_mounts(void); // Remove stale mount links and optionally restore image-backed mounts. void cleanup_mount_links(const char *removed_source_root, bool unmount_system_ex_bind); diff --git a/include/sm_mount_diag.h b/include/sm_mount_diag.h new file mode 100644 index 0000000..37cd17f --- /dev/null +++ b/include/sm_mount_diag.h @@ -0,0 +1,7 @@ +#ifndef SM_MOUNT_DIAG_H +#define SM_MOUNT_DIAG_H + +// Log read-only evidence explaining why unmount(path) returned EBUSY. +void sm_mount_diag_log_busy(const char *mount_point); + +#endif diff --git a/src/sm_fakelib.c b/src/sm_fakelib.c index edb4b63..2a13d88 100644 --- a/src/sm_fakelib.c +++ b/src/sm_fakelib.c @@ -3,6 +3,7 @@ #include "sm_fakelib.h" #include "sm_config_mount.h" #include "sm_log.h" +#include "sm_mount_diag.h" #include "sm_types.h" #include @@ -52,15 +53,19 @@ static bool mount_fakelib_overlay(const char *title_id, static bool unmount_fakelib_overlay(const fakelib_layer_t *layer) { const char *mount_path = layer->mount_path; - if (unmount(mount_path, MNT_FORCE) == 0 || errno == ENOENT || + if (unmount(mount_path, 0) == 0 || errno == ENOENT || errno == EINVAL) { log_debug(" [FAKELIB] %s libraries unmounted: %s -> %s", layer->label, layer->source_path, mount_path); return true; } - log_debug(" [FAKELIB] %s unmount failed for %s: %s", layer->label, - mount_path, strerror(errno)); + int unmount_errno = errno; + if (unmount_errno == EBUSY) + sm_mount_diag_log_busy(mount_path); + log_debug(" [FAKELIB] %s unmount deferred for %s: %s", layer->label, + mount_path, strerror(unmount_errno)); + errno = unmount_errno; return false; } diff --git a/src/sm_filesystem.c b/src/sm_filesystem.c index d65439c..938a335 100644 --- a/src/sm_filesystem.c +++ b/src/sm_filesystem.c @@ -4,6 +4,7 @@ #include "sm_config_mount.h" #include "sm_limits.h" #include "sm_log.h" +#include "sm_mount_diag.h" #include "sm_image_cache.h" #include "sm_image.h" #include "sm_path_utils.h" @@ -89,7 +90,7 @@ typedef bool (*title_app_dir_iter_fn)(const char *title_id, const title_link_paths_t *paths, void *ctx); -static void for_each_title_app_dir(const char *open_error_context, +static bool for_each_title_app_dir(const char *open_error_context, bool stop_on_signal, title_app_dir_iter_fn fn, void *ctx) { DIR *d = opendir(APP_BASE); @@ -97,23 +98,30 @@ static void for_each_title_app_dir(const char *open_error_context, if (errno != ENOENT) log_debug(" [LINK] open %s failed%s: %s", APP_BASE, open_error_context ? open_error_context : "", strerror(errno)); - return; + return errno == ENOENT; } + bool completed = true; struct dirent *entry; while ((entry = readdir(d)) != NULL) { - if (stop_on_signal && should_stop_requested()) + if (stop_on_signal && should_stop_requested()) { + completed = false; break; + } if (!resolve_title_app_dir(entry, NULL, 0)) continue; title_link_paths_t paths; build_title_link_paths(entry->d_name, &paths); - if (!fn(entry->d_name, &paths, ctx)) + if (!fn(entry->d_name, &paths, ctx)) { + completed = false; break; + } } - closedir(d); + if (closedir(d) != 0) + completed = false; + return completed; } bool read_mount_link(const char *title_id, char *out, size_t out_size) { @@ -286,8 +294,7 @@ bool resolve_title_app_dir(const struct dirent *entry, char *app_dir, return true; } -static bool source_path_needs_cleanup(const char *source_path, - bool *tried_image_recovery) { +static bool source_path_needs_cleanup(const char *source_path) { if (is_under_image_mount_base(source_path)) { char image_source_path[MAX_PATH]; bool has_image_source = resolve_image_source_from_mount_cache( @@ -314,11 +321,6 @@ static bool source_path_needs_cleanup(const char *source_path, if (path_exists(eboot_path)) return false; - if (!*tried_image_recovery && is_under_image_mount_base(source_path)) { - cleanup_stale_image_mounts(); - *tried_image_recovery = true; - } - return !path_exists(eboot_path); } @@ -608,10 +610,13 @@ static bool unmount_top_controlled_layer(const char *path) { if (unmount(path, 0) == 0 || errno == ENOENT || errno == EINVAL) return true; - if (unmount(path, MNT_FORCE) == 0 || errno == ENOENT || errno == EINVAL) - return true; + int unmount_errno = errno; + if (unmount_errno == EBUSY) + sm_mount_diag_log_busy(path); - log_debug(" [LINK] unmount failed for %s: %s", path, strerror(errno)); + log_debug(" [LINK] unmount deferred for %s: %s", path, + strerror(unmount_errno)); + errno = unmount_errno; return false; } @@ -650,24 +655,32 @@ bool unmount_title_runtime_layers(const char *title_id) { static bool unmount_managed_title_entry( const char *title_id, const title_link_paths_t *paths, void *ctx) { - (void)ctx; + bool *all_released = (bool *)ctx; char source_path[MAX_PATH]; if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) { return true; } title_mount_state_t state; - if (!inspect_title_stack(title_id, source_path, NULL, &state) || - !state.has_our_nullfs) { + if (!inspect_title_stack(title_id, source_path, NULL, &state)) { + *all_released = false; return true; } - (void)unmount_title_runtime_layers(title_id); + if (!state.has_our_nullfs) { + return true; + } + if (!unmount_title_runtime_layers(title_id)) + *all_released = false; return true; } -void unmount_all_title_runtime_layers(void) { - for_each_title_app_dir(" during runtime mount cleanup", false, - unmount_managed_title_entry, NULL); +bool unmount_all_title_runtime_layers(void) { + bool all_released = true; + if (!for_each_title_app_dir(" during runtime mount cleanup", false, + unmount_managed_title_entry, &all_released)) { + all_released = false; + } + return all_released; } bool reconcile_title_backport_mount(const char *title_id, const char *src_path, @@ -767,23 +780,6 @@ static void unmount_mount_point_for_recovery(const char *path) { if (!unmount_controlled_mount_stack(path)) { log_debug(" [LINK] failed to reset mount stack for recovery: %s", path); } - - if (unmount(path, 0) == 0) { - log_debug(" [LINK] extra unmount for recovery: %s", path); - return; - } - if (errno == ENOENT || errno == EINVAL) - return; - - if (unmount(path, MNT_FORCE) == 0) { - log_debug(" [LINK] extra forced unmount for recovery: %s", path); - return; - } - if (errno == ENOENT || errno == EINVAL) - return; - - log_debug(" [LINK] extra unmount for recovery did not complete for %s: %s", - path, strerror(errno)); } static bool recover_mount_directory_after_stat_failure( @@ -1049,8 +1045,7 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) { return false; } if (!path_exists(src_eboot)) { - bool tried_image_recovery = false; - if (source_path_needs_cleanup(src_path, &tried_image_recovery)) { + if (source_path_needs_cleanup(src_path)) { log_debug(" [LINK] source eboot.bin missing for %s: %s", title_id, src_eboot); return false; @@ -1114,20 +1109,16 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) { } if (runtime_sleep_mode_active()) { - if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL) - (void)unmount(dst, MNT_FORCE); + if (!unmount_top_controlled_layer(dst)) + log_debug(" [LINK] nullfs rollback deferred during sleep: %s", dst); return false; } if (!path_exists(dst_eboot)) { log_debug(" [LINK] mounted nullfs but eboot.bin is missing at target: %s", dst_eboot); - if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL) { - if (unmount(dst, MNT_FORCE) != 0 && errno != ENOENT && errno != EINVAL) { - log_debug(" [LINK] failed to rollback empty nullfs mount %s: %s", dst, - strerror(errno)); - } - } + if (!unmount_top_controlled_layer(dst)) + log_debug(" [LINK] empty nullfs rollback deferred: %s", dst); return false; } log_debug(" [LINK] nullfs mounted: %s -> %s", src_path, dst); @@ -1137,7 +1128,6 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) { typedef struct { const char *removed_source_root; bool unmount_system_ex_bind; - bool tried_image_recovery; bool force_remove_matching_source; bool match_usb_sources; bool preserve_mount_links; @@ -1184,12 +1174,10 @@ static bool cleanup_mount_links_entry(const char *title_id, return true; should_remove = ctx->force_remove_matching_source || (has_image_source && !path_exists(image_source_path)) || - source_path_needs_cleanup(source_path, - &ctx->tried_image_recovery); + source_path_needs_cleanup(source_path); } else { should_remove = (has_image_source && !path_exists(image_source_path)) || - source_path_needs_cleanup(source_path, - &ctx->tried_image_recovery); + source_path_needs_cleanup(source_path); } } @@ -1272,7 +1260,6 @@ void cleanup_mount_links(const char *removed_source_root, cleanup_mount_links_ctx_t ctx = { .removed_source_root = removed_source_root, .unmount_system_ex_bind = unmount_system_ex_bind, - .tried_image_recovery = false, .force_remove_matching_source = false, .match_usb_sources = false, .preserve_mount_links = false, @@ -1288,7 +1275,6 @@ void cleanup_mount_links_for_source_unmount(const char *source_root) { cleanup_mount_links_ctx_t ctx = { .removed_source_root = source_root, .unmount_system_ex_bind = true, - .tried_image_recovery = false, .force_remove_matching_source = true, .match_usb_sources = false, .preserve_mount_links = false, @@ -1301,7 +1287,6 @@ void cleanup_usb_mount_links_for_suspend(void) { cleanup_mount_links_ctx_t ctx = { .removed_source_root = NULL, .unmount_system_ex_bind = true, - .tried_image_recovery = false, .force_remove_matching_source = false, .match_usb_sources = true, .preserve_mount_links = true, @@ -1313,7 +1298,7 @@ void cleanup_usb_mount_links_for_suspend(void) { static bool shutdown_title_mounts_entry(const char *title_id, const title_link_paths_t *paths, void *ctx) { - (void)ctx; + bool *all_released = (bool *)ctx; char source_path[MAX_PATH]; if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) { @@ -1321,20 +1306,32 @@ static bool shutdown_title_mounts_entry(const char *title_id, } title_mount_state_t state; - if (!inspect_title_stack(title_id, source_path, NULL, &state) || - !state.has_our_nullfs || - !title_stack_top_is_managed(&state)) { + if (!inspect_title_stack(title_id, source_path, NULL, &state)) { + *all_released = false; + return true; + } + if (!state.has_our_nullfs) + return true; + if (!title_stack_top_is_managed(&state)) { + log_debug(" [LINK] shutdown stack blocked by unmanaged top for %s", + state.system_ex_path); + *all_released = false; return true; } if (!unmount_controlled_mount_stack(state.system_ex_path)) { log_debug(" [LINK] failed to unmount shutdown stack for %s", state.system_ex_path); + *all_released = false; } return true; } -void shutdown_title_mounts(void) { - for_each_title_app_dir(" during shutdown", false, - shutdown_title_mounts_entry, NULL); +bool shutdown_title_mounts(void) { + bool all_released = true; + if (!for_each_title_app_dir(" during shutdown", false, + shutdown_title_mounts_entry, &all_released)) { + all_released = false; + } + return all_released; } diff --git a/src/sm_mount_device.c b/src/sm_mount_device.c index eb51d92..6ee04b9 100644 --- a/src/sm_mount_device.c +++ b/src/sm_mount_device.c @@ -105,7 +105,18 @@ bool resolve_device_from_mount(const char *mount_point, static bool is_path_mountpoint(const char *path) { struct statfs sfs; - return (statfs(path, &sfs) == 0 && strcmp(sfs.f_mntonname, path) == 0); + if (statfs(path, &sfs) == 0 && strcmp(sfs.f_mntonname, path) == 0) + return true; + + // An unreadable filesystem can make statfs(path) fail while it is still in + // the mount table. Treat it as active until unmount removes that entry. + struct statfs *mntbuf = NULL; + int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT); + for (int i = 0; i < mntcount && mntbuf; ++i) { + if (strcmp(mntbuf[i].f_mntonname, path) == 0) + return true; + } + return false; } bool is_active_image_mount_point(const char *path) { diff --git a/src/sm_mount_diag.c b/src/sm_mount_diag.c new file mode 100644 index 0000000..9f4b8f2 --- /dev/null +++ b/src/sm_mount_diag.c @@ -0,0 +1,323 @@ +#include "sm_platform.h" + +#include +#include +#include + +#include "sm_log.h" +#include "sm_mount_diag.h" +#include "sm_path_utils.h" + +#define MOUNT_DIAG_MAX_ROOTS 16u +#define MOUNT_DIAG_MAX_VM_HITS 32u +#define MOUNT_DIAG_MAX_VNODE_HITS 16u + +typedef struct { + char paths[MOUNT_DIAG_MAX_ROOTS][MAX_PATH]; + size_t count; +} mount_diag_roots_t; + +static void *read_sysctl_snapshot(const int *mib, u_int mib_count, + size_t *size_out) { + for (int attempt = 0; attempt < 3; ++attempt) { + size_t needed = 0; + if (sysctl(mib, mib_count, NULL, &needed, NULL, 0) != 0) + return NULL; + if (needed == 0) { + *size_out = 0; + return NULL; + } + + size_t capacity = needed + needed / 8u + 4096u; + uint8_t *buffer = malloc(capacity); + if (!buffer) + return NULL; + + size_t used = capacity; + if (sysctl(mib, mib_count, buffer, &used, NULL, 0) == 0) { + *size_out = used; + return buffer; + } + + int snapshot_errno = errno; + free(buffer); + if (snapshot_errno != ENOMEM) { + errno = snapshot_errno; + return NULL; + } + } + + errno = ENOMEM; + return NULL; +} + +static bool copy_record_path(char out[MAX_PATH], const uint8_t *record, + size_t record_size, size_t path_offset) { + out[0] = '\0'; + if (record_size <= path_offset) + return false; + + const char *path = (const char *)(record + path_offset); + size_t available = record_size - path_offset; + size_t length = strnlen(path, available); + if (length == 0 || length == available) + return false; + + (void)strlcpy(out, path, MAX_PATH); + return true; +} + +static void copy_process_name(char out[TDNAMLEN + 1], const uint8_t *record, + size_t record_size) { + size_t name_offset = offsetof(struct kinfo_proc, ki_tdname); + out[0] = '\0'; + if (record_size <= name_offset) + return; + + const char *name = (const char *)(record + name_offset); + size_t available = record_size - name_offset; + size_t length = strnlen(name, available); + if (length == available) + return; + if (length > TDNAMLEN) + length = TDNAMLEN; + memcpy(out, name, length); + out[length] = '\0'; +} + +static bool add_diag_root(mount_diag_roots_t *roots, const char *path) { + if (!path || path[0] == '\0') + return false; + for (size_t i = 0; i < roots->count; ++i) { + if (strcmp(roots->paths[i], path) == 0) + return true; + } + if (roots->count >= MOUNT_DIAG_MAX_ROOTS) + return false; + (void)strlcpy(roots->paths[roots->count++], path, MAX_PATH); + return true; +} + +static bool path_matches_diag_roots(const mount_diag_roots_t *roots, + const char *path) { + for (size_t i = 0; i < roots->count; ++i) { + if (path_matches_root_or_child(path, roots->paths[i])) + return true; + } + return false; +} + +static const char *normalized_mount_source(const char *source) { + return strncmp(source, ":", 8) == 0 ? source + 8 : source; +} + +static void collect_mount_dependencies(const char *mount_point, + mount_diag_roots_t *roots, + size_t *dependency_count_out) { + (void)add_diag_root(roots, mount_point); + + struct statfs *mounts = NULL; + int mount_count = getmntinfo(&mounts, MNT_NOWAIT); + if (mount_count <= 0 || !mounts) { + log_debug(" [MOUNT-BUSY] mount table unavailable for %s: %s", + mount_point, strerror(errno)); + return; + } + + size_t dependency_count = 0; + for (int i = 0; i < mount_count; ++i) { + const char *source = normalized_mount_source(mounts[i].f_mntfromname); + const char *target = mounts[i].f_mntonname; + bool consumes_mount = path_matches_root_or_child(source, mount_point); + bool child_mount = strcmp(target, mount_point) != 0 && + path_matches_root_or_child(target, mount_point); + if (!consumes_mount && !child_mount) + continue; + + dependency_count++; + log_debug(" [MOUNT-BUSY] dependent type=%s from=%s on=%s flags=0x%lX", + mounts[i].f_fstypename, source, target, + (unsigned long)mounts[i].f_flags); + if (consumes_mount) + (void)add_diag_root(roots, target); + } + *dependency_count_out = dependency_count; +} + +static void log_process_vm_refs(pid_t pid, const char *name, + const mount_diag_roots_t *roots, + size_t *hit_count) { + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_VMMAP, pid}; + size_t data_size = 0; + uint8_t *data = read_sysctl_snapshot(mib, 4, &data_size); + if (!data) + return; + + uint8_t *cursor = data; + uint8_t *end = data + data_size; + while ((size_t)(end - cursor) >= sizeof(int)) { + int record_size = 0; + memcpy(&record_size, cursor, sizeof(record_size)); + if (record_size <= 0 || (size_t)record_size > (size_t)(end - cursor)) + break; + + const struct kinfo_vmentry *entry = + (const struct kinfo_vmentry *)cursor; + if ((size_t)record_size > offsetof(struct kinfo_vmentry, kve_path) && + entry->kve_type == KVME_TYPE_VNODE) { + char path[MAX_PATH]; + if (copy_record_path(path, cursor, (size_t)record_size, + offsetof(struct kinfo_vmentry, kve_path)) && + path_matches_diag_roots(roots, path)) { + if (*hit_count < MOUNT_DIAG_MAX_VM_HITS) { + log_debug(" [MOUNT-BUSY] pid=%ld name=%s vm=0x%llX-0x%llX " + "prot=0x%X path=%s", + (long)pid, name, (unsigned long long)entry->kve_start, + (unsigned long long)entry->kve_end, + entry->kve_protection, path); + } + (*hit_count)++; + } + } + cursor += record_size; + } + + free(data); +} + +static void log_all_process_vm_refs(const mount_diag_roots_t *roots, + size_t *hit_count_out) { + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0}; + size_t data_size = 0; + uint8_t *data = read_sysctl_snapshot(mib, 4, &data_size); + if (!data) { + log_debug(" [MOUNT-BUSY] process list unavailable: %s", strerror(errno)); + return; + } + + size_t hit_count = 0; + uint8_t *cursor = data; + uint8_t *end = data + data_size; + while ((size_t)(end - cursor) >= sizeof(int)) { + int record_size = 0; + memcpy(&record_size, cursor, sizeof(record_size)); + if (record_size <= 0 || (size_t)record_size > (size_t)(end - cursor)) + break; + if ((size_t)record_size <= offsetof(struct kinfo_proc, ki_tdname)) + break; + + const struct kinfo_proc *process = (const struct kinfo_proc *)cursor; + char process_name[TDNAMLEN + 1]; + copy_process_name(process_name, cursor, (size_t)record_size); + const char *name = process_name[0] ? process_name : "?"; + if (process->ki_pid > 0) + log_process_vm_refs(process->ki_pid, name, roots, &hit_count); + cursor += record_size; + } + + free(data); + *hit_count_out = hit_count; + if (hit_count > MOUNT_DIAG_MAX_VM_HITS) { + log_debug(" [MOUNT-BUSY] %zu additional VM references suppressed", + hit_count - MOUNT_DIAG_MAX_VM_HITS); + } +} + +static void log_vnode_refs(const char *mount_point, size_t *hit_count_out) { + struct stat root_stat; + if (stat(mount_point, &root_stat) != 0) + return; + + int mib[2] = {CTL_KERN, KERN_VNODE}; + size_t data_size = 0; + uint8_t *data = read_sysctl_snapshot(mib, 2, &data_size); + if (!data) + return; + + void *mount_address = NULL; + uint8_t *cursor = data; + uint8_t *end = data + data_size; + while ((size_t)(end - cursor) >= sizeof(size_t)) { + const struct xvnode *vnode = (const struct xvnode *)cursor; + if (vnode->xv_size < sizeof(struct xvnode) || + vnode->xv_size > (size_t)(end - cursor)) { + break; + } + if (vnode->xv_dev == root_stat.st_dev && + vnode->xv_ino == root_stat.st_ino) { + mount_address = vnode->xv_mount; + break; + } + cursor += vnode->xv_size; + } + + size_t hit_count = 0; + size_t hold_only_count = 0; + if (mount_address) { + cursor = data; + while ((size_t)(end - cursor) >= sizeof(size_t)) { + const struct xvnode *vnode = (const struct xvnode *)cursor; + if (vnode->xv_size < sizeof(struct xvnode) || + vnode->xv_size > (size_t)(end - cursor)) { + break; + } + if (vnode->xv_mount == mount_address) { + bool active = vnode->xv_usecount > 0 || vnode->xv_writecount > 0 || + vnode->xv_numoutput > 0; + if (active) { + if (hit_count < MOUNT_DIAG_MAX_VNODE_HITS) { + log_debug(" [MOUNT-BUSY] vnode=%p dev=0x%llX ino=0x%llX " + "use=%d hold=%d write=%d output=%ld", + vnode->xv_vnode, (unsigned long long)vnode->xv_dev, + (unsigned long long)vnode->xv_ino, vnode->xv_usecount, + vnode->xv_holdcnt, vnode->xv_writecount, + vnode->xv_numoutput); + } + hit_count++; + } else if (vnode->xv_holdcnt > 0) { + hold_only_count++; + } + } + cursor += vnode->xv_size; + } + } + + free(data); + *hit_count_out = hit_count; + if (hit_count > MOUNT_DIAG_MAX_VNODE_HITS) { + log_debug(" [MOUNT-BUSY] %zu additional active vnodes suppressed", + hit_count - MOUNT_DIAG_MAX_VNODE_HITS); + } + if (hold_only_count > 0) { + log_debug(" [MOUNT-BUSY] hold-only cached vnodes=%zu", hold_only_count); + } +} + +void sm_mount_diag_log_busy(const char *mount_point) { + if (!mount_point || mount_point[0] == '\0') + return; + + int saved_errno = errno; + log_debug(" [MOUNT-BUSY] diagnostic begin: %s", mount_point); + + mount_diag_roots_t roots; + memset(&roots, 0, sizeof(roots)); + size_t dependency_count = 0; + collect_mount_dependencies(mount_point, &roots, &dependency_count); + + size_t vm_hit_count = 0; + log_all_process_vm_refs(&roots, &vm_hit_count); + + size_t vnode_hit_count = 0; + log_vnode_refs(mount_point, &vnode_hit_count); + + if (dependency_count == 0 && vm_hit_count == 0 && + vnode_hit_count == 0) { + log_debug(" [MOUNT-BUSY] no user-visible owner found; likely transient " + "VFS, buffer-cache, or backing-device reference"); + } + log_debug(" [MOUNT-BUSY] diagnostic end: dependencies=%zu vm_refs=%zu " + "active_vnodes=%zu", + dependency_count, vm_hit_count, vnode_hit_count); + errno = saved_errno; +} diff --git a/src/sm_scanner.c b/src/sm_scanner.c index fe5916b..699221c 100644 --- a/src/sm_scanner.c +++ b/src/sm_scanner.c @@ -572,6 +572,12 @@ static sm_scan_tree_dir_visit_t register_watch_directory_visit( const char *dir_path, unsigned int depth_from_root, void *ctx_ptr) { register_watch_tree_ctx_t *ctx = (register_watch_tree_ctx_t *)ctx_ptr; if (depth_from_root > 0u) { + // Runtime image descendants can be active PFS/PFSC mounts. Keeping a + // directory fd open there pins the mounted vnode and makes unmount return + // EBUSY. The managed mount-base watcher is sufficient: mounted images are + // read-only and every discovery pass rebuilds this tree. + if (is_under_image_mount_base(dir_path)) + return SM_SCAN_TREE_DIR_SKIP_DESCEND; if (depth_from_root >= ctx->scan_depth || directory_has_param_json(dir_path, NULL)) { return SM_SCAN_TREE_DIR_SKIP_DESCEND;