Coverage Report

Created: 2026-10-23 06:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/trafficserver/include/shared/cache_shm/Purge.h
Line
Count
Source
1
/** @file
2
3
  Shared "enumerate and unlink the shm segments for a prefix" primitive, used by
4
  both the cache subsystem (purge-on-disabled-start) and `traffic_ctl cache shm
5
  clear`. Header-only since traffic_ctl does not link the cache library; tscore is
6
  fine here, both consumers link it.
7
  purge_segments() does no logging; it returns a report each caller formats itself.
8
9
  @section license License
10
11
  Licensed to the Apache Software Foundation (ASF) under one
12
  or more contributor license agreements.  See the NOTICE file
13
  distributed with this work for additional information
14
  regarding copyright ownership.  The ASF licenses this file
15
  to you under the Apache License, Version 2.0 (the
16
  "License"); you may not use this file except in compliance
17
  with the License.  You may obtain a copy of the License at
18
19
      http://www.apache.org/licenses/LICENSE-2.0
20
21
  Unless required by applicable law or agreed to in writing, software
22
  distributed under the License is distributed on an "AS IS" BASIS,
23
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
  See the License for the specific language governing permissions and
25
  limitations under the License.
26
 */
27
28
#pragma once
29
30
#include "shared/cache_shm/Layout.h"
31
32
#include <fcntl.h>
33
#include <sys/file.h>
34
#include <sys/mman.h>
35
#include <sys/stat.h>
36
#include <unistd.h>
37
38
#include <csignal>
39
#include <algorithm>
40
#include <cerrno>
41
#include <cstdint>
42
#include <cstring>
43
#include <string>
44
#include <vector>
45
46
namespace cache_shm
47
{
48
49
/// True if `pid` names a live process. EPERM counts as alive: it exists, we just may not signal it.
50
inline bool
51
process_is_alive(int32_t pid)
52
0
{
53
0
  if (pid <= 0) {
54
0
    return false;
55
0
  }
56
0
  return ::kill(static_cast<pid_t>(pid), 0) == 0 || errno == EPERM;
57
0
}
58
59
/// Outcome of trying to take the control segment's exclusive lock.
60
enum class LockResult {
61
  Acquired,    ///< We hold the exclusive lock; no other process does.
62
  HeldByOther, ///< Another live process holds it (flock returned EWOULDBLOCK).
63
  Unsupported, ///< flock is not honored for this fd (e.g. macOS POSIX shm).
64
};
65
66
/// Authoritative on Linux/tmpfs, where the lock is auto-released on crash. macOS POSIX shm returns Unsupported, so
67
/// callers fall back to owner_pid liveness; `unexpected_errno` distinguishes that expected case from EBADF/EINVAL/ENOLCK.
68
inline LockResult
69
try_lock_control(int fd, int *unexpected_errno = nullptr)
70
0
{
71
0
  int rc = 0;
72
0
  while ((rc = ::flock(fd, LOCK_EX | LOCK_NB)) != 0 && errno == EINTR) {
73
0
    ; // retry: a signal, not a real lock failure
74
0
  }
75
0
  if (rc == 0) {
76
0
    return LockResult::Acquired;
77
0
  }
78
  // EWOULDBLOCK is the only errno meaning "another process holds it"; anything else
79
  // means flock is unusable here -> fall back to the owner_pid backstop.
80
0
  if (errno == EWOULDBLOCK) {
81
0
    return LockResult::HeldByOther;
82
0
  }
83
0
  if (unexpected_errno != nullptr) {
84
0
    *unexpected_errno = errno;
85
0
  }
86
0
  return LockResult::Unsupported;
87
0
}
88
89
/// Bounded by the field size: the fixed char[] may be un-terminated in a tampered or stale segment.
90
inline std::string
91
read_shm_name(const char (&field)[MAX_SHM_NAME_LEN + 1])
92
0
{
93
0
  return std::string(field, ::strnlen(field, sizeof(field)));
94
0
}
95
96
/// Derived from the control-table index so the purge path can sweep the whole name space without a trustworthy table.
97
inline std::string
98
stripe_segment_name(const std::string &prefix, uint32_t stripe_index)
99
0
{
100
0
  std::string name = prefix + "s" + std::to_string(stripe_index);
101
0
  if (name.size() >= MAX_SHM_NAME_LEN) {
102
0
    name.resize(MAX_SHM_NAME_LEN - 1);
103
0
  }
104
0
  return name;
105
0
}
106
107
/// Everything but Purged/TooSmall means nothing was unlinked.
108
enum class PurgeOutcome {
109
  BadPrefix,   ///< Prefix is empty or does not start with '/'. Nothing attempted.
110
  NotPresent,  ///< No <prefix>control segment exists (shm_open ENOENT). Nothing to do.
111
  OpenFailed,  ///< shm_open failed for a reason other than ENOENT; cannot read safely.
112
  MapFailed,   ///< The control segment exists but could not be mmap'd.
113
  StatFailed,  ///< fstat on the control fd failed; size/validity unknown, nothing unlinked.
114
  TooSmall,    ///< Control segment is smaller than CacheShmControl; table not walked, name space swept instead.
115
  OwnedByLive, ///< A live process owns the segment; nothing was unlinked.
116
  Purged,      ///< The stripe table was walked and its segments unlinked (possibly zero stripes).
117
};
118
119
/// One shm_unlink attempt, so callers can log each name in their own format.
120
struct PurgeUnlink {
121
  std::string name;
122
  bool        is_control; ///< true for the <prefix>control object, false for a stripe.
123
  int         error;      ///< 0 on success; otherwise the errno from shm_unlink (ENOENT == already gone).
124
};
125
126
/// `unlinked` lists every shm_unlink attempted, stripes first, then the control object.
127
struct PurgeReport {
128
  PurgeOutcome             outcome = PurgeOutcome::NotPresent;
129
  std::string              control_name;            ///< the <prefix>control name (set whenever the prefix was valid).
130
  int                      sys_errno       = 0;     ///< errno behind OpenFailed / MapFailed.
131
  long long                segment_size    = -1;    ///< control segment size in bytes; set whenever fstat succeeded.
132
  int32_t                  owner_pid       = 0;     ///< the recorded owner pid, for OwnedByLive.
133
  bool                     table_untrusted = false; ///< the stripe table could not be walked; swept `<prefix>s<N>` by name instead.
134
  std::vector<PurgeUnlink> unlinked;
135
136
  /// Segments successfully removed (a shm_unlink that returned 0).
137
  unsigned
138
  removed() const
139
0
  {
140
0
    unsigned n = 0;
141
0
    for (const auto &u : unlinked) {
142
0
      if (u.error == 0) {
143
0
        ++n;
144
0
      }
145
0
    }
146
0
    return n;
147
0
  }
148
149
  /// ENOENT is not counted: the segment being already gone is the desired end state.
150
  unsigned
151
  failures() const
152
0
  {
153
0
    unsigned n = 0;
154
0
    for (const auto &u : unlinked) {
155
0
      if (u.error != 0 && u.error != ENOENT) {
156
0
        ++n;
157
0
      }
158
0
    }
159
0
    return n;
160
0
  }
161
};
162
163
namespace detail
164
{
165
  /// Close an fd on scope exit (the mmap survives the close).
166
  struct FdGuard {
167
    int fd;
168
    ~FdGuard()
169
0
    {
170
0
      if (fd >= 0) {
171
0
        ::close(fd);
172
0
      }
173
0
    }
174
  };
175
} // namespace detail
176
177
/// Only names under `prefix` are touched, so a corrupt but magic-valid table cannot drive shm_unlink on unrelated objects.
178
inline void
179
unlink_table_stripes(const std::string &prefix, const CacheShmControl *table, std::vector<PurgeUnlink> &out)
180
0
{
181
0
  const uint32_t stripe_count = std::min<uint32_t>(table->stripe_count, MAX_STRIPES);
182
183
0
  for (uint32_t i = 0; i < stripe_count; ++i) {
184
0
    std::string name = read_shm_name(table->stripes[i].shm_name);
185
0
    if (name.empty() || !name.starts_with(prefix)) {
186
0
      continue;
187
0
    }
188
0
    int e = ::shm_unlink(name.c_str()) == 0 ? 0 : errno;
189
0
    out.push_back({std::move(name), false, e});
190
0
  }
191
0
}
192
193
/// Fallback for an unreadable stripe table: the names are ours by construction, and leaving a stripe segment behind would
194
/// leak the whole directory in it. Absent indices just ENOENT.
195
inline void
196
unlink_stripe_name_space(const std::string &prefix, std::vector<PurgeUnlink> &out)
197
0
{
198
0
  for (uint32_t i = 0; i < MAX_STRIPES; ++i) {
199
0
    std::string name = stripe_segment_name(prefix, i);
200
0
    if (::shm_unlink(name.c_str()) == 0) {
201
0
      out.push_back({std::move(name), false, 0});
202
0
    }
203
0
  }
204
0
}
205
206
/// Unlinks the stripe segments plus the control object, unless a live process still owns it. No logging -- callers format
207
/// the report. The stripe table is trusted only when the magic matches and the size is ours; otherwise the name space is
208
/// swept instead (report.table_untrusted). See "Operator tooling" in the shm-fast-restart developer guide.
209
inline PurgeReport
210
purge_segments(const std::string &prefix)
211
0
{
212
0
  PurgeReport report;
213
214
0
  if (prefix.empty() || prefix[0] != '/') {
215
0
    report.outcome = PurgeOutcome::BadPrefix;
216
0
    return report;
217
0
  }
218
0
  report.control_name = control_segment_name(prefix);
219
220
  // Records the name-sweep fallback where the fact is established, so the flag cannot
221
  // disagree with the outcome that led to it.
222
0
  auto sweep_stripe_name_space = [&report, &prefix]() {
223
0
    report.table_untrusted = true;
224
0
    unlink_stripe_name_space(prefix, report.unlinked);
225
0
  };
226
227
0
  int fd = ::shm_open(report.control_name.c_str(), O_RDONLY, 0);
228
0
  if (fd < 0) {
229
0
    report.sys_errno = errno;
230
0
    report.outcome   = (errno == ENOENT) ? PurgeOutcome::NotPresent : PurgeOutcome::OpenFailed;
231
0
    return report;
232
0
  }
233
0
  detail::FdGuard guard{fd};
234
235
  // clang-format off
236
0
  struct stat sb{};
237
  // clang-format on
238
0
  if (::fstat(fd, &sb) < 0) {
239
    // Size and validity are unknown: report the error and leave the segment
240
    // alone rather than unlink a control object we failed to stat.
241
0
    report.sys_errno = errno;
242
0
    report.outcome   = PurgeOutcome::StatFailed;
243
0
    return report;
244
0
  }
245
0
  report.segment_size = static_cast<long long>(sb.st_size);
246
247
  // Before any size branch: flock needs only the fd, and a segment too small for *our* layout can still be a live older
248
  // build's -- unlinking it would strip the names out from under the very upgrade the frozen header exists to support.
249
0
  const LockResult lock = try_lock_control(fd);
250
251
  // Map only what is really there. A foreign build's segment may be shorter than CONTROL_SIZE, and mapping past the last
252
  // page of the object faults on access; the frozen header prefix is all the owner guard needs.
253
0
  const std::size_t map_len = std::min(static_cast<std::size_t>(sb.st_size), CONTROL_SIZE);
254
0
  void             *addr    = nullptr;
255
0
  if (map_len >= CONTROL_HEADER_SIZE) {
256
0
    addr = ::mmap(nullptr, map_len, PROT_READ, MAP_SHARED, fd, 0);
257
0
    if (addr == MAP_FAILED) {
258
0
      report.sys_errno = errno;
259
0
      report.outcome   = PurgeOutcome::MapFailed;
260
0
      return report;
261
0
    }
262
0
  }
263
0
  const auto *ctrl = static_cast<const CacheShmControl *>(addr);
264
  // Too short to even hold the frozen header: no build of ours wrote it, so there is no owner to protect.
265
0
  const bool magic_ok = ctrl != nullptr && std::memcmp(ctrl->magic, CACHE_SHM_MAGIC, sizeof(CACHE_SHM_MAGIC)) == 0;
266
267
0
  if (lock == LockResult::HeldByOther || (lock == LockResult::Unsupported && magic_ok && process_is_alive(ctrl->owner_pid))) {
268
0
    report.owner_pid = magic_ok ? ctrl->owner_pid : 0;
269
0
    report.outcome   = PurgeOutcome::OwnedByLive;
270
0
    if (addr != nullptr) {
271
0
      ::munmap(addr, map_len);
272
0
    }
273
0
    return report;
274
0
  }
275
276
0
  if (static_cast<std::size_t>(sb.st_size) < CONTROL_SIZE) {
277
    // Too small to hold this build's header/table: there is no table to walk, so
278
    // sweep the stripe name space and unlink the control object itself.
279
0
    report.outcome = PurgeOutcome::TooSmall;
280
0
    sweep_stripe_name_space();
281
0
    int e = ::shm_unlink(report.control_name.c_str()) == 0 ? 0 : errno;
282
0
    report.unlinked.push_back({report.control_name, true, e});
283
0
    if (addr != nullptr) {
284
0
      ::munmap(addr, map_len);
285
0
    }
286
0
    return report;
287
0
  }
288
289
  // A control size this build does not accept means a build with a different sizeof(CacheShmControl) wrote it. The frozen
290
  // header prefix is still readable (so the owner guard above applies), but stripes[] may have a different stride
291
  // entirely, so its names must not drive shm_unlink.
292
0
  if (magic_ok && is_own_control_size(static_cast<std::size_t>(sb.st_size))) {
293
0
    unlink_table_stripes(prefix, ctrl, report.unlinked);
294
0
  } else {
295
    // Bad magic, or a foreign sizeof(CacheShmControl): the table cannot be walked.
296
0
    sweep_stripe_name_space();
297
0
  }
298
0
  ::munmap(addr, map_len);
299
300
0
  int e = ::shm_unlink(report.control_name.c_str()) == 0 ? 0 : errno;
301
0
  report.unlinked.push_back({report.control_name, true, e});
302
303
0
  report.outcome = PurgeOutcome::Purged;
304
0
  return report;
305
0
}
306
307
} // namespace cache_shm