| /** @file |
| |
| Shared "enumerate and unlink the shm segments for a prefix" primitive, used by |
| both the cache subsystem (purge-on-disabled-start) and `traffic_ctl cache shm |
| clear`. Header-only since traffic_ctl does not link the cache library; tscore is |
| fine here, both consumers link it. |
| purge_segments() does no logging; it returns a report each caller formats itself. |
| |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| #pragma once |
| |
| #include "shared/cache_shm/Layout.h" |
| |
| #include <fcntl.h> |
| #include <sys/file.h> |
| #include <sys/mman.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| |
| #include <csignal> |
| #include <algorithm> |
| #include <cerrno> |
| #include <cstdint> |
| #include <cstring> |
| #include <string> |
| #include <vector> |
| |
| namespace cache_shm |
| { |
| |
| /// True if `pid` names a live process. EPERM counts as alive: it exists, we just may not signal it. |
| inline bool |
| process_is_alive(int32_t pid) |
| { |
| if (pid <= 0) { |
| return false; |
| } |
| return ::kill(static_cast<pid_t>(pid), 0) == 0 || errno == EPERM; |
| } |
| |
| /// Outcome of trying to take the control segment's exclusive lock. |
| enum class LockResult { |
| Acquired, ///< We hold the exclusive lock; no other process does. |
| HeldByOther, ///< Another live process holds it (flock returned EWOULDBLOCK). |
| Unsupported, ///< flock is not honored for this fd (e.g. macOS POSIX shm). |
| }; |
| |
| /// Authoritative on Linux/tmpfs, where the lock is auto-released on crash. macOS POSIX shm returns Unsupported, so |
| /// callers fall back to owner_pid liveness; `unexpected_errno` distinguishes that expected case from EBADF/EINVAL/ENOLCK. |
| inline LockResult |
| try_lock_control(int fd, int *unexpected_errno = nullptr) |
| { |
| int rc = 0; |
| while ((rc = ::flock(fd, LOCK_EX | LOCK_NB)) != 0 && errno == EINTR) { |
| ; // retry: a signal, not a real lock failure |
| } |
| if (rc == 0) { |
| return LockResult::Acquired; |
| } |
| // EWOULDBLOCK is the only errno meaning "another process holds it"; anything else |
| // means flock is unusable here -> fall back to the owner_pid backstop. |
| if (errno == EWOULDBLOCK) { |
| return LockResult::HeldByOther; |
| } |
| if (unexpected_errno != nullptr) { |
| *unexpected_errno = errno; |
| } |
| return LockResult::Unsupported; |
| } |
| |
| /// Bounded by the field size: the fixed char[] may be un-terminated in a tampered or stale segment. |
| inline std::string |
| read_shm_name(const char (&field)[MAX_SHM_NAME_LEN + 1]) |
| { |
| return std::string(field, ::strnlen(field, sizeof(field))); |
| } |
| |
| /// Derived from the control-table index so the purge path can sweep the whole name space without a trustworthy table. |
| inline std::string |
| stripe_segment_name(const std::string &prefix, uint32_t stripe_index) |
| { |
| std::string name = prefix + "s" + std::to_string(stripe_index); |
| if (name.size() >= MAX_SHM_NAME_LEN) { |
| name.resize(MAX_SHM_NAME_LEN - 1); |
| } |
| return name; |
| } |
| |
| /// Everything but Purged/TooSmall means nothing was unlinked. |
| enum class PurgeOutcome { |
| BadPrefix, ///< Prefix is empty or does not start with '/'. Nothing attempted. |
| NotPresent, ///< No <prefix>control segment exists (shm_open ENOENT). Nothing to do. |
| OpenFailed, ///< shm_open failed for a reason other than ENOENT; cannot read safely. |
| MapFailed, ///< The control segment exists but could not be mmap'd. |
| StatFailed, ///< fstat on the control fd failed; size/validity unknown, nothing unlinked. |
| TooSmall, ///< Control segment is smaller than CacheShmControl; table not walked, name space swept instead. |
| OwnedByLive, ///< A live process owns the segment; nothing was unlinked. |
| Purged, ///< The stripe table was walked and its segments unlinked (possibly zero stripes). |
| }; |
| |
| /// One shm_unlink attempt, so callers can log each name in their own format. |
| struct PurgeUnlink { |
| std::string name; |
| bool is_control; ///< true for the <prefix>control object, false for a stripe. |
| int error; ///< 0 on success; otherwise the errno from shm_unlink (ENOENT == already gone). |
| }; |
| |
| /// `unlinked` lists every shm_unlink attempted, stripes first, then the control object. |
| struct PurgeReport { |
| PurgeOutcome outcome = PurgeOutcome::NotPresent; |
| std::string control_name; ///< the <prefix>control name (set whenever the prefix was valid). |
| int sys_errno = 0; ///< errno behind OpenFailed / MapFailed. |
| long long segment_size = -1; ///< control segment size in bytes; set whenever fstat succeeded. |
| int32_t owner_pid = 0; ///< the recorded owner pid, for OwnedByLive. |
| bool table_untrusted = false; ///< the stripe table could not be walked; swept `<prefix>s<N>` by name instead. |
| std::vector<PurgeUnlink> unlinked; |
| |
| /// Segments successfully removed (a shm_unlink that returned 0). |
| unsigned |
| removed() const |
| { |
| unsigned n = 0; |
| for (const auto &u : unlinked) { |
| if (u.error == 0) { |
| ++n; |
| } |
| } |
| return n; |
| } |
| |
| /// ENOENT is not counted: the segment being already gone is the desired end state. |
| unsigned |
| failures() const |
| { |
| unsigned n = 0; |
| for (const auto &u : unlinked) { |
| if (u.error != 0 && u.error != ENOENT) { |
| ++n; |
| } |
| } |
| return n; |
| } |
| }; |
| |
| namespace detail |
| { |
| /// Close an fd on scope exit (the mmap survives the close). |
| struct FdGuard { |
| int fd; |
| ~FdGuard() |
| { |
| if (fd >= 0) { |
| ::close(fd); |
| } |
| } |
| }; |
| } // namespace detail |
| |
| /// Only names under `prefix` are touched, so a corrupt but magic-valid table cannot drive shm_unlink on unrelated objects. |
| inline void |
| unlink_table_stripes(const std::string &prefix, const CacheShmControl *table, std::vector<PurgeUnlink> &out) |
| { |
| const uint32_t stripe_count = std::min<uint32_t>(table->stripe_count, MAX_STRIPES); |
| |
| for (uint32_t i = 0; i < stripe_count; ++i) { |
| std::string name = read_shm_name(table->stripes[i].shm_name); |
| if (name.empty() || !name.starts_with(prefix)) { |
| continue; |
| } |
| int e = ::shm_unlink(name.c_str()) == 0 ? 0 : errno; |
| out.push_back({std::move(name), false, e}); |
| } |
| } |
| |
| /// Fallback for an unreadable stripe table: the names are ours by construction, and leaving a stripe segment behind would |
| /// leak the whole directory in it. Absent indices just ENOENT. |
| inline void |
| unlink_stripe_name_space(const std::string &prefix, std::vector<PurgeUnlink> &out) |
| { |
| for (uint32_t i = 0; i < MAX_STRIPES; ++i) { |
| std::string name = stripe_segment_name(prefix, i); |
| if (::shm_unlink(name.c_str()) == 0) { |
| out.push_back({std::move(name), false, 0}); |
| } |
| } |
| } |
| |
| /// Unlinks the stripe segments plus the control object, unless a live process still owns it. No logging -- callers format |
| /// the report. The stripe table is trusted only when the magic matches and the size is ours; otherwise the name space is |
| /// swept instead (report.table_untrusted). See "Operator tooling" in the shm-fast-restart developer guide. |
| inline PurgeReport |
| purge_segments(const std::string &prefix) |
| { |
| PurgeReport report; |
| |
| if (prefix.empty() || prefix[0] != '/') { |
| report.outcome = PurgeOutcome::BadPrefix; |
| return report; |
| } |
| report.control_name = control_segment_name(prefix); |
| |
| // Records the name-sweep fallback where the fact is established, so the flag cannot |
| // disagree with the outcome that led to it. |
| auto sweep_stripe_name_space = [&report, &prefix]() { |
| report.table_untrusted = true; |
| unlink_stripe_name_space(prefix, report.unlinked); |
| }; |
| |
| int fd = ::shm_open(report.control_name.c_str(), O_RDONLY, 0); |
| if (fd < 0) { |
| report.sys_errno = errno; |
| report.outcome = (errno == ENOENT) ? PurgeOutcome::NotPresent : PurgeOutcome::OpenFailed; |
| return report; |
| } |
| detail::FdGuard guard{fd}; |
| |
| // clang-format off |
| struct stat sb{}; |
| // clang-format on |
| if (::fstat(fd, &sb) < 0) { |
| // Size and validity are unknown: report the error and leave the segment |
| // alone rather than unlink a control object we failed to stat. |
| report.sys_errno = errno; |
| report.outcome = PurgeOutcome::StatFailed; |
| return report; |
| } |
| report.segment_size = static_cast<long long>(sb.st_size); |
| |
| // Before any size branch: flock needs only the fd, and a segment too small for *our* layout can still be a live older |
| // build's -- unlinking it would strip the names out from under the very upgrade the frozen header exists to support. |
| const LockResult lock = try_lock_control(fd); |
| |
| // Map only what is really there. A foreign build's segment may be shorter than CONTROL_SIZE, and mapping past the last |
| // page of the object faults on access; the frozen header prefix is all the owner guard needs. |
| const std::size_t map_len = std::min(static_cast<std::size_t>(sb.st_size), CONTROL_SIZE); |
| void *addr = nullptr; |
| if (map_len >= CONTROL_HEADER_SIZE) { |
| addr = ::mmap(nullptr, map_len, PROT_READ, MAP_SHARED, fd, 0); |
| if (addr == MAP_FAILED) { |
| report.sys_errno = errno; |
| report.outcome = PurgeOutcome::MapFailed; |
| return report; |
| } |
| } |
| const auto *ctrl = static_cast<const CacheShmControl *>(addr); |
| // Too short to even hold the frozen header: no build of ours wrote it, so there is no owner to protect. |
| const bool magic_ok = ctrl != nullptr && std::memcmp(ctrl->magic, CACHE_SHM_MAGIC, sizeof(CACHE_SHM_MAGIC)) == 0; |
| |
| if (lock == LockResult::HeldByOther || (lock == LockResult::Unsupported && magic_ok && process_is_alive(ctrl->owner_pid))) { |
| report.owner_pid = magic_ok ? ctrl->owner_pid : 0; |
| report.outcome = PurgeOutcome::OwnedByLive; |
| if (addr != nullptr) { |
| ::munmap(addr, map_len); |
| } |
| return report; |
| } |
| |
| if (static_cast<std::size_t>(sb.st_size) < CONTROL_SIZE) { |
| // Too small to hold this build's header/table: there is no table to walk, so |
| // sweep the stripe name space and unlink the control object itself. |
| report.outcome = PurgeOutcome::TooSmall; |
| sweep_stripe_name_space(); |
| int e = ::shm_unlink(report.control_name.c_str()) == 0 ? 0 : errno; |
| report.unlinked.push_back({report.control_name, true, e}); |
| if (addr != nullptr) { |
| ::munmap(addr, map_len); |
| } |
| return report; |
| } |
| |
| // A control size this build does not accept means a build with a different sizeof(CacheShmControl) wrote it. The frozen |
| // header prefix is still readable (so the owner guard above applies), but stripes[] may have a different stride |
| // entirely, so its names must not drive shm_unlink. |
| if (magic_ok && is_own_control_size(static_cast<std::size_t>(sb.st_size))) { |
| unlink_table_stripes(prefix, ctrl, report.unlinked); |
| } else { |
| // Bad magic, or a foreign sizeof(CacheShmControl): the table cannot be walked. |
| sweep_stripe_name_space(); |
| } |
| ::munmap(addr, map_len); |
| |
| int e = ::shm_unlink(report.control_name.c_str()) == 0 ? 0 : errno; |
| report.unlinked.push_back({report.control_name, true, e}); |
| |
| report.outcome = PurgeOutcome::Purged; |
| return report; |
| } |
| |
| } // namespace cache_shm |