/* SPDX-License-Identifier: Apache-2.0 * (c) 2025, Konstantin Demin */ #include "overlay.hh" #include #include #include #include #include extern "C" { #include #include #include #include #include #include } struct x_dirspec_t { dev_t dev; ino_t ino; int fd; }; typedef std::vector dirspec_vec; typedef std::set> dev_ino_set; struct x_entry_t { int32_t root_id; uint32_t type; xxhash_t name_hash; dev_t dev; ino_t ino; ovl_name_t name; }; static size_t procfs_fd2name(int fd, char * buffer, size_t size); template static inline bool set_contains(const std::set & set, const T & needle); static inline bool filter_out_dots(const char * name); static inline bool handle_file_type(int type, bool allow_symlinks = false); static inline ovl_entry_kind dirent_to_entry_kind(typeof(dirent::d_type) d_type); bool arg_to_rootspec(const char * arg, ovl_dirspec_t * rootspec, uint32_t flags) { if ((!arg) || (!rootspec)) return false; if (!(arg[0])) return false; int fd, fd_real; fd = open(arg, O_DIRECTORY | O_RDONLY); // here is relatively small window for race/TOCTOU fd_real = open(arg, O_PATH | O_NOFOLLOW | O_RDONLY); if (fd < 0) { if (fd_real >= 0) (void) close(fd_real); return false; } (void) memset(rootspec, 0, sizeof(ovl_dirspec_t)); do { struct stat st; (void) memset(&st, 0, sizeof(st)); if (fstat(fd, &st) != 0) break; if (!S_ISDIR(st.st_mode)) break; rootspec->dev = st.st_dev; rootspec->ino = st.st_ino; rootspec->fd = fd; rootspec->root.len = procfs_fd2name((fd_real < 0) ? fd : fd_real, rootspec->root.str, ovl_path_max); } while (false); if ((fd_real >= 0) && (fd_real != fd)) { (void) close(fd_real); } // basic sanity checks: // - path length is non-zero // - either dev_t or ino_t are non-zero if ((!rootspec->root.len) || ((!rootspec->dev) && (!rootspec->ino))) { // not needed anymore (void) close(fd); (void) memset(rootspec, 0, sizeof(ovl_dirspec_t)); return false; } // try to adjust/shorten path against current working directory do { bool relative = !!(flags & ovl_rootspec_flags::relative); if (!relative) break; ovl_path_t cwd; (void) memset(&cwd, 0, sizeof(cwd)); if (!getcwd(cwd.str, ovl_path_max)) break; cwd.len = strnlen(cwd.str, ovl_path_max); if ((!cwd.len) || (cwd.len == ovl_path_max)) break; // cwd is root - nothing to do here if ((cwd.len == 1) && (cwd.str[0] == '/')) break; // ensure that rootpath has prefix of cwd + "/" if (cwd.len >= rootspec->root.len) break; if (strncmp(rootspec->root.str, cwd.str, cwd.len) != 0) break; if (rootspec->root.str[cwd.len] != '/') break; // "cwd" isn't needed anymore so reuse it as temporary container (void) memset(cwd.str, 0, cwd.len); size_t prefix_len = cwd.len + 1; cwd.len = rootspec->root.len - prefix_len; (void) memcpy(cwd.str, &rootspec->root.str[prefix_len], cwd.len); // finally replace with adjusted value (void) memcpy(&rootspec->root, &cwd, sizeof(ovl_path_t)); } while (false); return true; } bool is_empty_dir(int dirfd) { // internal method, parameters are already checked /* if (dirfd < 0) return false; */ // reset handle to beginning (void) lseek(dirfd, 0, SEEK_SET); int fd_dir = dup(dirfd); if (fd_dir < 0) return false; DIR * p_dir = fdopendir(fd_dir); if (!p_dir) { (void) close(fd_dir); return false; } // read current directory entries bool empty = true; for (struct dirent * dent = nullptr; (dent = readdir(p_dir)) != nullptr; ) { if (!filter_out_dots(dent->d_name)) continue; empty = false; break; } // rewind directory rewinddir(p_dir); (void) lseek(fd_dir, 0, SEEK_SET); // close handles (void) closedir(p_dir); // handle was reowned [by libc] via fdopendir() but why not to try to close it [again] (void) close(fd_dir); return empty; } class overlay_processor { sort_mode _sort; unsigned int _depth; ovl_dirspec_vec _roots; // we should report problems when they arise but there's common problem: // name is relative to almost unknown path and we don't know the full path. // furthermore we won't track path due to performance reasons. ovl_entry_vec _impl(const dirspec_vec & paths, unsigned int depth) { ovl_entry_vec rv = ovl_entry_vec(); if (!depth) return rv; int n_usable_paths = 0; for (const auto & p : paths) { if (p.fd < 0) continue; n_usable_paths++; } if (!n_usable_paths) return rv; bool multiple_layers = n_usable_paths > 1; auto seen_paths = dev_ino_set(); auto dirs = std::list(); auto nondirs = std::list(); for (size_t i = 0; i < paths.size(); i++) { const auto & p = paths[i]; if (p.fd < 0) continue; if (multiple_layers) { auto needle = std::make_tuple(p.dev, p.ino); if (set_contains(seen_paths, needle)) { continue; } (void) seen_paths.emplace(needle); } // reset handle to beginning (void) lseek(p.fd, 0, SEEK_SET); int fd_dir = dup(p.fd); if (fd_dir < 0) { (void) fprintf(stderr, "overlaydirs: dup(%d): %s\n", p.fd, strerror(errno)); continue; } DIR * p_dir = fdopendir(fd_dir); if (!p_dir) { (void) fprintf(stderr, "overlaydirs: fdopendir(%d): %s\n", fd_dir, strerror(errno)); (void) close(fd_dir); continue; } // read current directory entries auto ent_all = std::vector(); for (struct dirent * dent = nullptr; (dent = readdir(p_dir)) != nullptr; ) { if (!filter_out_dots(dent->d_name)) continue; if (!handle_file_type(dent->d_type)) continue; x_entry_t t; (void) memset(&t, 0, sizeof(t)); // 1. only basic info is filled - name and type // 2. directories and symlinks are handled later // 3. dev_t and ino_t are relevant only for directories t.name.len = strnlen(dent->d_name, sizeof(dirent::d_name)); if (t.name.len == sizeof(dirent::d_name)) { // TODO: report warning about too long name // (see function' comment) continue; } t.type = dent->d_type; (void) memcpy(t.name.str, dent->d_name, t.name.len); ent_all.push_back(t); } // rewind directory rewinddir(p_dir); (void) lseek(fd_dir, 0, SEEK_SET); // close handles (void) closedir(p_dir); /* p_dir = nullptr; */ // handle was reowned [by libc] via fdopendir() but why not to try to close it [again] (void) close(fd_dir); /* fd_dir = -1; */ // process entries auto hash_skip = std::set(); auto ent_dirs = std::list(); auto ent_nondirs = std::list(); for (auto & x : ent_all) { bool is_skip = false; do { if (x.name.len < 2) break; /* if (strncmp(&x.name.name[x.name.len - 2], ".-", 2) != 0) break; */ if (x.name.str[x.name.len - 2] != '.') break; if (x.name.str[x.name.len - 1] != '-') break; // current entry is "skip" entry is_skip = true; } while (false); if (is_skip) { // skip file is found but we're not using them if (!multiple_layers) continue; // don't use "bare skip" entries (e.g. named exactly ".-") if (x.name.len == 2) continue; (void) hash_skip.emplace(xxhash(x.name.str, x.name.len - 2)); continue; } if ((x.type == DT_DIR) || (x.type == DT_LNK)) { struct stat st; (void) memset(&st, 0, sizeof(st)); if (fstatat(p.fd, x.name.str, &st, 0) != 0) { // TODO: report warning about unavailable entry // (see function' comment) continue; } // handle (new) entry type typeof(x_entry_t::type) new_type = IFTODT(st.st_mode); if ((x.type == DT_DIR) && (new_type != DT_DIR)) { // TODO: report warning about inconsistent entry type // (see function' comment) // PS: this is probably a bug continue; } // skip dangling symlinks along with unknown file types if (!handle_file_type(new_type, true)) { // TODO: report warning about unapplicable entry type // (see function' comment) continue; } // adjust entry type x.type = new_type; if (x.type == DT_DIR) { if (depth > 1) { // we should track precise dev_t and ino_t for directories x.dev = st.st_dev; x.ino = st.st_ino; } else { // end-of-depth directories are handled as regular files x.type = DT_REG; } } } x.root_id = i; x.name_hash = xxhash(x.name.str, x.name.len); if (x.type == DT_DIR) { ent_dirs.push_back(x); } else { ent_nondirs.push_back(x); } } // not needed anymore ent_all.clear(); ent_all.shrink_to_fit(); // remove "skip" entries if (!hash_skip.empty()) { nondirs.remove_if( [&hash_skip](const x_entry_t & x) -> bool { return set_contains(hash_skip, x.name_hash); } ); dirs.remove_if( [&hash_skip](const x_entry_t & x) -> bool { return set_contains(hash_skip, x.name_hash); } ); // not needed anymore hash_skip.clear(); } // "hash_skip" is now empty and doesn't contain exactly "skip" entries // but is going to be reused further // override all entries with current "non-dir" entries if (!ent_nondirs.empty()) { for (const auto & x : ent_nondirs) { (void) hash_skip.emplace(x.name_hash); } nondirs.remove_if( [&hash_skip](const x_entry_t & x) -> bool { return set_contains(hash_skip, x.name_hash); } ); dirs.remove_if( [&hash_skip](const x_entry_t & x) -> bool { return set_contains(hash_skip, x.name_hash); } ); // not needed anymore hash_skip.clear(); } // override "non-dir" entries with "dir" entries if (!ent_dirs.empty()) { for (const auto & x : ent_dirs) { (void) hash_skip.emplace(x.name_hash); } nondirs.remove_if( [&hash_skip](const x_entry_t & x) -> bool { return set_contains(hash_skip, x.name_hash); } ); // not needed anymore hash_skip.clear(); } // merge current entries with accumulated "all" entries for (const auto & x : ent_nondirs) { nondirs.push_back(x); } ent_nondirs.clear(); for (const auto & x : ent_dirs) { dirs.push_back(x); } ent_dirs.clear(); } // sort "dir" entries if (_sort != sort_mode::none) { dirs.sort( [this](const x_entry_t & a, const x_entry_t & b) -> bool { if (a.name_hash == b.name_hash) return a.root_id < b.root_id; return custom_sort(a.name.str, b.name.str, _sort) < 0; } ); } // merge entries auto ent_all = std::list(); for (const auto & x : dirs) { ent_all.push_back(x); } for (const auto & x : nondirs) { ent_all.push_back(x); } // not needed anymore nondirs.clear(); // sort entries if (_sort != sort_mode::none) { ent_all.sort( [this](const x_entry_t & a, const x_entry_t & b) -> bool { // in case of directory entries if (a.name_hash == b.name_hash) return a.root_id < b.root_id; return custom_sort(a.name.str, b.name.str, _sort) < 0; } ); } auto seen_dirs = std::set(); for (const auto & x : ent_all) { if (x.root_id < 0) continue; ovl_entry_kind etype = dirent_to_entry_kind(x.type); if (etype == ovl_entry_kind::unknown) continue; if (etype == ovl_entry_kind::directory) { // skip if directory with the same name is already handled if (set_contains(seen_dirs, x.name_hash)) continue; (void) seen_dirs.emplace(x.name_hash); } ovl_entry_t t; t.type = etype; t.root_id = x.root_id; t.name_hash = x.name_hash; t.name.len = x.name.len; (void) memcpy(t.name.str, x.name.str, t.name.len); if (t.type == ovl_entry_kind::directory) { // root_id is meaningless for directories t.root_id = 0; auto child_paths = dirspec_vec(); for (size_t i = 0; i < paths.size(); i++) { child_paths.push_back(x_dirspec_t{ .dev = 0, .ino = 0, .fd = -1 }); } for (const auto & x : dirs) { if (x.name_hash != t.name_hash) continue; int fd = openat(paths[x.root_id].fd, t.name.str, O_DIRECTORY | O_RDONLY); if (fd < 0) { // TODO: report warning about unsuccessful openat() // (see function' comment) continue; } child_paths[x.root_id].dev = x.dev; child_paths[x.root_id].ino = x.ino; child_paths[x.root_id].fd = fd; } t.children = _impl(child_paths, depth - 1); for (const auto & x : child_paths) { if (x.fd < 0) continue; (void) close(x.fd); } child_paths.clear(); child_paths.shrink_to_fit(); } rv.push_back(t); } // not needed anymore dirs.clear(); return rv; } public: overlay_processor(const ovl_dirspec_vec & roots, unsigned int depth, sort_mode sort) { _roots = ovl_dirspec_vec(); for (const auto & r : roots) { _roots.push_back(r); } _sort = sort; _depth = (depth > ovl_depth_max) ? ovl_depth_max : depth; } ovl_result run(void) { auto rv = ovl_result(); auto paths = dirspec_vec(); for (const auto & r : _roots) { rv.roots.push_back(r.root); paths.push_back(x_dirspec_t{ .dev = r.dev, .ino = r.ino, .fd = r.fd }); } rv.entries = _impl(paths, _depth); return rv; } }; ovl_result process_overlay(ovl_dirspec_vec & roots, sort_mode sort, unsigned int depth) { auto rv = ovl_result(); if (!depth) return rv; if (roots.empty()) return rv; rv = overlay_processor(roots, depth, sort).run(); // close all open file descriptors for (auto & r : roots) { (void) close(r.fd); r.fd = -1; } return rv; } class overlay_printer { print_mode _print; ovl_result _ovl; void _impl(const ovl_entry_vec & entries, const ovl_path_t * subpath = nullptr) { for (const auto & e : entries) { switch (e.type) { case ovl_entry_kind::file: print_path(_print, &(_ovl.roots[e.root_id]), subpath, &(e.name)); break; case ovl_entry_kind::directory: // NOT printing directory name ovl_path_t n_subpath; (void) memset(&n_subpath, 0, sizeof(n_subpath)); if (subpath) { n_subpath.len = subpath->len + 1 + e.name.len; if (n_subpath.len >= ovl_path_max) { (void) fprintf(stderr, "subpath too long: /%s/%s/\n", subpath->str, e.name.str); continue; } /* (void) snprintf(n_subpath.str, ovl_path_max, "%s/%s", subpath->str, e.name.str); */ size_t off = 0; (void) memcpy(n_subpath.str, subpath->str, subpath->len); off = subpath->len; n_subpath.str[off++] = '/'; (void) memcpy(n_subpath.str + off, e.name.str, e.name.len); } else { // ovl_name_max is definitely smaller than ovl_path_max n_subpath.len = e.name.len; (void) memcpy(n_subpath.str, e.name.str, e.name.len); } _impl(e.children, &n_subpath); break; default: break; } } } public: overlay_printer(const ovl_result & ovl, print_mode print) { _ovl = ovl; _print = print; } void run(void) { _impl(_ovl.entries); } }; void list_overlay(const ovl_result & overlay, print_mode print) { overlay_printer(overlay, print).run(); } class overlay_merger { ovl_result _ovl; ovl_dirspec_t _target; int _impl(const ovl_entry_vec & entries, int dirfd, const ovl_path_t * subpath = nullptr) { int rv = 0; int new_fd; ovl_path_t t; for (const auto & e : entries) { switch (e.type) { case ovl_entry_kind::file: (void) memset(&t, 0, sizeof(t)); if (subpath) { t.len = _ovl.roots[e.root_id].len + 1 + subpath->len + 1 + e.name.len; if (t.len >= ovl_path_max) { (void) fprintf(stderr, "name too long: %s/%s/%s\n", _ovl.roots[e.root_id].str, subpath->str, e.name.str); return ENAMETOOLONG; } /* (void) snprintf(t.str, ovl_path_max, "%s/%s/%s\n", _ovl.roots[e.root_id].str, subpath->str, e.name.str); */ size_t off = 0; (void) memcpy(t.str, _ovl.roots[e.root_id].str, _ovl.roots[e.root_id].len); off = _ovl.roots[e.root_id].len; t.str[off++] = '/'; (void) memcpy(t.str + off, subpath->str, subpath->len); off += subpath->len; t.str[off++] = '/'; (void) memcpy(t.str + off, e.name.str, e.name.len); } else { t.len = _ovl.roots[e.root_id].len + 1 + e.name.len; if (t.len >= ovl_path_max) { (void) fprintf(stderr, "name too long: %s/%s\n", _ovl.roots[e.root_id].str, e.name.str); return ENAMETOOLONG; } /* (void) snprintf(t.str, ovl_path_max, "%s/%s\n", _ovl.roots[e.root_id].str, e.name.str); */ size_t off = 0; (void) memcpy(t.str, _ovl.roots[e.root_id].str, _ovl.roots[e.root_id].len); off = _ovl.roots[e.root_id].len; t.str[off++] = '/'; (void) memcpy(t.str + off, e.name.str, e.name.len); } if (symlinkat(t.str, dirfd, e.name.str) < 0) { rv = errno; if (subpath) { (void) fprintf(stderr, "symlinkat() error: \"%s\" with %s/%s/%s -> %s\n", strerror(rv), _target.root.str, subpath->str, e.name.str, t.str); } else { (void) fprintf(stderr, "symlinkat() error: \"%s\" with %s/%s -> %s\n", strerror(rv), _target.root.str, e.name.str, t.str); } return rv; } break; case ovl_entry_kind::directory: (void) memset(&t, 0, sizeof(t)); if (subpath) { t.len = subpath->len + 1 /* "/" */ + e.name.len; if (t.len >= ovl_path_max) { (void) fprintf(stderr, "subpath too long: /%s/%s/\n", subpath->str, e.name.str); return ENAMETOOLONG; } /* (void) snprintf(t.str, ovl_path_max, "%s/%s", subpath->str, e.name.str); */ (void) memcpy(t.str, subpath->str, subpath->len); t.str[subpath->len] = '/'; (void) memcpy(t.str + subpath->len + 1, e.name.str, e.name.len); } else { // ovl_name_max is definitely smaller than ovl_path_max t.len = e.name.len; (void) memcpy(t.str, e.name.str, e.name.len); } if (mkdirat(dirfd, e.name.str, 0755) < 0) { rv = errno; if (subpath) { (void) fprintf(stderr, "mkdirat() error: \"%s\" with path %s/%s/%s\n", strerror(rv), _target.root.str, subpath->str, e.name.str); } else { (void) fprintf(stderr, "mkdirat() error: \"%s\" with path %s/%s\n", strerror(rv), _target.root.str, e.name.str); } return rv; } new_fd = openat(dirfd, e.name.str, O_DIRECTORY | O_RDONLY); if (new_fd < 0) { rv = errno; if (subpath) { (void) fprintf(stderr, "openat() error: \"%s\" with path %s/%s/%s\n", strerror(rv), _target.root.str, subpath->str, e.name.str); } else { (void) fprintf(stderr, "openat() error: \"%s\" with path %s/%s\n", strerror(rv), _target.root.str, e.name.str); } return rv; } rv = _impl(e.children, new_fd, &t); (void) close(new_fd); if (rv != 0) return rv; break; default: break; } } return rv; } public: overlay_merger(const ovl_result & ovl, const ovl_dirspec_t & target) { _ovl = ovl; _target = target; } int run(void) { return _impl(_ovl.entries, _target.fd); } }; int merge_overlay(const ovl_result & overlay, const ovl_dirspec_t & target) { if (!is_empty_dir(target.fd)) return -1; return overlay_merger(overlay, target).run(); } static size_t procfs_fd2name(int fd, char * buffer, size_t size) { // internal method, parameters are already checked /* if ((fd < 0) || (!buffer) || (!size)) return 0; */ // "/proc/self/fd/" - 14, "%d" - up to 10 char procfs_link[32]; (void) memset(procfs_link, 0, sizeof(procfs_link)); (void) snprintf(procfs_link, sizeof(procfs_link) - 1, "/proc/self/fd/%d", fd); (void) memset(buffer, 0, size); ssize_t x = readlink(procfs_link, buffer, size - 1); x = (x > 0) ? x : 0; // not really needed buffer[x] = 0; // already clamped return (size_t) x; } #ifndef HAVE_CPP_SET_CONTAINS #ifdef __cplusplus #if __cplusplus >= 202002L #define HAVE_CPP_SET_CONTAINS 1 #endif #endif #endif #ifndef HAVE_CPP_SET_CONTAINS #define HAVE_CPP_SET_CONTAINS 0 #endif template static inline bool set_contains(const std::set & set, const T & needle) { #if HAVE_CPP_SET_CONTAINS return set.contains(needle); #else return (set.find(needle) != set.cend()); #endif } static inline bool filter_out_dots(const char * name) { /* if (strcmp(name, ".") == 0) return false; if (strcmp(name, "..") == 0) return false; return true; */ if (name[0] != '.') return true; switch (name[1]) { case 0: return false; case '.': return (name[2] != 0); } return true; } static inline bool handle_file_type(int type, bool allow_symlinks) { switch (type) { // common file types case DT_REG: /* -fallthrough */ case DT_DIR: /* -fallthrough */ // less common types (but also allowed) case DT_BLK: /* -fallthrough */ case DT_CHR: /* -fallthrough */ case DT_FIFO: /* -fallthrough */ case DT_SOCK: return true; // symlinks should be handled separately! case DT_LNK: return !allow_symlinks; default: return false; } } static inline ovl_entry_kind dirent_to_entry_kind(typeof(dirent::d_type) d_type) { switch (d_type) { case DT_DIR: return ovl_entry_kind::directory; case DT_REG: /* -fallthrough */ case DT_BLK: /* -fallthrough */ case DT_CHR: /* -fallthrough */ case DT_FIFO: /* -fallthrough */ case DT_SOCK: return ovl_entry_kind::file; // symlinks should be already handled! } return ovl_entry_kind::unknown; }