1
// Container-aware CPU detection utility for Envoy
2
// Inspired by Go's runtime `cgroup` CPU limit detection
3
// See: https://github.com/golang/go/blob/go1.23.4/src/internal/cgroup/cgroup_linux.go
4

            
5
#include "source/server/cgroup_cpu_util.h"
6

            
7
#include <algorithm>
8
#include <cmath>
9

            
10
#include "source/common/common/logger.h"
11

            
12
#include "absl/strings/match.h"
13
#include "absl/strings/numbers.h"
14
#include "absl/strings/str_cat.h"
15
#include "absl/strings/str_split.h"
16
#include "absl/strings/strip.h"
17

            
18
namespace Envoy {
19

            
20
// Implementation of CgroupDetector interface
21
1
absl::optional<uint32_t> CgroupDetectorImpl::getCpuLimit(Filesystem::Instance& fs) {
22
1
  return CgroupCpuUtil::getCpuLimit(fs);
23
1
}
24

            
25
// Returns the CPU limit from `cgroup` subsystem, following Go runtime behavior.
26
// This function prioritizes `cgroup` `v1` over `v2` when both are available,
27
// as `v1` CPU controllers take precedence in hybrid environments.
28
//
29
// Return values:
30
//   Valid uint32_t: Actual CPU limit (number of CPUs, rounded up)
31
//   absl::nullopt: No limit detected (unlimited CPU usage allowed)
32
1
absl::optional<uint32_t> CgroupCpuUtil::getCpuLimit(Filesystem::Instance& fs) {
33
  // Step 1: Mount Discovery - call once and reuse
34
1
  absl::optional<std::string> mount_opt = discoverCgroupMount(fs);
35
1
  if (!mount_opt.has_value()) {
36
    // No `cgroup` filesystem found
37
    return absl::nullopt;
38
  }
39
1
  const std::string& mount_point = mount_opt.value();
40

            
41
  // Steps 2-3: Process Assignment + Path Construction
42
1
  absl::optional<CgroupInfo> cgroup_info_opt = constructCgroupPath(mount_point, fs);
43
1
  if (!cgroup_info_opt.has_value()) {
44
    // No valid `cgroup` path found
45
    return absl::nullopt;
46
  }
47
1
  const CgroupInfo& cgroup_info = cgroup_info_opt.value();
48

            
49
  // Step 4: File Access - append version-specific filenames and validate access
50
1
  absl::optional<CpuFiles> cpu_files_opt = accessCgroupFiles(cgroup_info, fs);
51
1
  if (!cpu_files_opt.has_value()) {
52
    // File access failed - fallback to "no `cgroup`"
53
    return absl::nullopt;
54
  }
55
1
  const CpuFiles& cpu_files = cpu_files_opt.value();
56

            
57
  // Step 5: Read Actual Limits using cached file paths
58
1
  absl::optional<double> cpu_ratio = readActualLimits(cpu_files, fs);
59
1
  if (!cpu_ratio.has_value()) {
60
    // No valid limit found or unlimited
61
1
    return absl::nullopt;
62
1
  }
63

            
64
  // Convert float64 ratio to uint32_t CPU count (rounded down, minimum 1)
65
  const uint32_t cpu_limit = std::max(1U, static_cast<uint32_t>(std::floor(cpu_ratio.value())));
66
  return cpu_limit;
67
1
}
68

            
69
// Validates `cgroup` file content following strict requirements.
70
// This centralizes the validation logic used by both `v1` and `v2` `cgroup` file parsers.
71
//
72
// Validation requirements:
73
// - Newline requirement: Content must end with '\n'
74
//
75
// Returns string_view without trailing newline on success, absl::nullopt on validation failure.
76
absl::optional<absl::string_view>
77
3
CgroupCpuUtil::validateCgroupFileContent(const std::string& content, const std::string& file_path) {
78
  // ✅ Newline Validation: Require trailing newline
79
3
  if (content.empty() || content.back() != '\n') {
80
2
    ENVOY_LOG_MISC(warn, "Malformed `cgroup` file {}: missing trailing newline", file_path);
81
2
    return absl::nullopt;
82
2
  }
83

            
84
  // Return content without trailing newline
85
1
  return absl::string_view(content.data(), content.size() - 1);
86
3
}
87

            
88
// Parses `/proc/self/cgroup` to find the current process's `cgroup` path with priority handling.
89
//
90
// File format (one line per hierarchy):
91
//   `cgroup` `v2`: "0::/path/to/cgroup"
92
//   `cgroup` `v1`: "N:controller,list:/path/to/cgroup"
93
//
94
// Priority handling logic:
95
//   - If hierarchy "0": Save v2 path, continue searching
96
//   - If v1 hierarchy + containsCPU(): Return immediately (v1 wins)
97
//   - Result: Single relative path + version with highest priority
98
//
99
// Returns CgroupPathInfo with relative path and version, or absl::nullopt if no suitable `cgroup`
100
// found.
101
8
absl::optional<CgroupPathInfo> CgroupCpuUtil::getCurrentCgroupPath(Filesystem::Instance& fs) {
102
8
  const auto result = fs.fileReadToEnd(std::string(PROC_CGROUP_PATH));
103
8
  if (!result.ok()) {
104
    // `/proc/self/cgroup` doesn't exist - not in a `cgroup`
105
    ENVOY_LOG_MISC(warn,
106
                   "Cannot read `/proc/self/cgroup`: not in a `cgroup` or file doesn't exist");
107
    return absl::nullopt;
108
  }
109

            
110
8
  const std::string content = result.value();
111
8
  const std::vector<std::string> lines = absl::StrSplit(content, '\n');
112

            
113
8
  std::string v2_path;   // Save v2 path in case no v1 found
114
8
  bool found_v2 = false; // Track if we found any v2 hierarchy
115

            
116
  // Parse /proc/self/cgroup line by line
117
15
  for (const std::string& line : lines) {
118
15
    if (line.empty()) {
119
6
      continue;
120
6
    }
121

            
122
    // Extract hierarchy ID, controllers, path from hierarchy:controllers:path format
123
9
    size_t first_colon = line.find(':');
124
9
    if (first_colon == std::string::npos) {
125
1
      ENVOY_LOG_MISC(warn, "Skipping malformed cgroup line: no colon separator");
126
1
      continue;
127
1
    }
128

            
129
8
    size_t second_colon = line.find(':', first_colon + 1);
130
8
    if (second_colon == std::string::npos) {
131
      ENVOY_LOG_MISC(warn, "Skipping malformed cgroup line: missing second colon");
132
      continue;
133
    }
134

            
135
8
    absl::string_view hierarchy_id = absl::string_view(line).substr(0, first_colon);
136
8
    absl::string_view controllers =
137
8
        absl::string_view(line).substr(first_colon + 1, second_colon - first_colon - 1);
138
8
    absl::string_view path = absl::string_view(line).substr(second_colon + 1);
139

            
140
    // Priority handling: If hierarchy "0": Save v2 path, continue searching
141
8
    if (hierarchy_id == "0") {
142
4
      v2_path = std::string(path); // Save v2 path but keep searching for v1
143
4
      found_v2 = true;             // Mark that we found v2 hierarchy
144
4
      continue;
145
4
    }
146

            
147
    // Priority handling: If v1 hierarchy + containsCPU(): Return immediately (v1 wins)
148
4
    if (absl::StrContains(controllers, "cpu")) {
149
      // Found cgroup v1 with CPU controller - return immediately (highest priority)
150
2
      return CgroupPathInfo{std::string(path), "v1"};
151
2
    }
152
4
  }
153

            
154
  // Result: Single relative path with highest priority
155
  // Return v2 path if we found v2 hierarchy, or nullopt if no valid cgroup found
156
6
  if (!found_v2) {
157
2
    return absl::nullopt;
158
2
  }
159
4
  return CgroupPathInfo{v2_path, "v2"};
160
6
}
161

            
162
// Constructs complete cgroup path by combining mount point and process assignment.
163
absl::optional<CgroupInfo> CgroupCpuUtil::constructCgroupPath(const std::string& mount_point,
164
1
                                                              Filesystem::Instance& fs) {
165

            
166
  // Process Assignment - get relative path and determine version
167
1
  absl::optional<CgroupPathInfo> path_info_opt = getCurrentCgroupPath(fs);
168
1
  if (!path_info_opt.has_value()) {
169
    // No cgroup path found for this process
170
    return absl::nullopt;
171
  }
172
1
  const CgroupPathInfo& path_info = path_info_opt.value();
173
1
  const std::string& relative_path = path_info.relative_path;
174
1
  const std::string& version = path_info.version;
175

            
176
  // Path Construction - combine mount point and relative path
177
1
  CgroupInfo info;
178

            
179
  // Construct full path using absl::StrCat (efficient concatenation)
180
1
  if (!relative_path.empty() && relative_path[0] != '/') {
181
    info.full_path = absl::StrCat(mount_point, "/", relative_path);
182
1
  } else {
183
1
    info.full_path = absl::StrCat(mount_point, relative_path);
184
1
  }
185

            
186
  // Version determination from getCurrentCgroupPath
187
  // Version is now determined by parsing /proc/self/cgroup, not by trial and error
188
1
  info.version = version;
189

            
190
1
  ENVOY_LOG_MISC(debug, "Constructed cgroup path: {} (version: {})", info.full_path, info.version);
191

            
192
  // Result: Combined path in single buffer + final version
193
1
  return info;
194
1
}
195

            
196
// Accesses cgroup v1 CPU files (quota and period).
197
absl::optional<CpuFiles> CgroupCpuUtil::accessCgroupV1Files(const CgroupInfo& cgroup_info,
198
4
                                                            Filesystem::Instance& fs) {
199
  // Read v1 files directly - no trial and error needed
200
4
  std::string v1_quota_path = absl::StrCat(cgroup_info.full_path, CGROUP_V1_QUOTA_FILE);
201
4
  std::string v1_period_path = absl::StrCat(cgroup_info.full_path, CGROUP_V1_PERIOD_FILE);
202

            
203
4
  const auto quota_result = fs.fileReadToEnd(v1_quota_path);
204
4
  const auto period_result = fs.fileReadToEnd(v1_period_path);
205

            
206
4
  if (quota_result.ok() && period_result.ok()) {
207
1
    CpuFiles cpu_files;
208
1
    cpu_files.version = "v1";
209
1
    cpu_files.quota_content = quota_result.value();
210
1
    cpu_files.period_content = period_result.value();
211
1
    ENVOY_LOG_MISC(debug, "Using cgroup v1 files at {}", cgroup_info.full_path);
212
1
    return cpu_files;
213
3
  } else {
214
    // Expected v1 files don't exist - this is an error
215
3
    ENVOY_LOG_MISC(warn, "Expected cgroup v1 files not accessible at {}", cgroup_info.full_path);
216
3
    return absl::nullopt;
217
3
  }
218
4
}
219

            
220
// Accesses cgroup v2 CPU file (cpu.max).
221
absl::optional<CpuFiles> CgroupCpuUtil::accessCgroupV2Files(const CgroupInfo& cgroup_info,
222
4
                                                            Filesystem::Instance& fs) {
223
  // Read v2 file directly - no trial and error needed
224
4
  std::string v2_cpu_max_path = absl::StrCat(cgroup_info.full_path, CGROUP_V2_CPU_MAX_FILE);
225
4
  const auto result = fs.fileReadToEnd(v2_cpu_max_path);
226

            
227
4
  if (result.ok()) {
228
3
    CpuFiles cpu_files;
229
3
    cpu_files.version = "v2";
230
3
    cpu_files.quota_content = result.value();
231
3
    cpu_files.period_content = ""; // v2 doesn't use separate period file
232
3
    ENVOY_LOG_MISC(debug, "Using cgroup v2 file at {}", cgroup_info.full_path);
233
3
    return cpu_files;
234
3
  } else {
235
    // Expected v2 file doesn't exist - this is an error
236
1
    ENVOY_LOG_MISC(warn, "Expected cgroup v2 file not accessible at {}", cgroup_info.full_path);
237
1
    return absl::nullopt;
238
1
  }
239
4
}
240

            
241
// Accesses cgroup CPU files with version-specific filename appending and validation.
242
//
243
// Logic:
244
//   1. Get combined path from Step 3
245
//   2. Append version-specific filenames
246
//   3. Validate file access via filesystem interface
247
//   4. Error handling: File not found → return absl::nullopt
248
//   5. Result: CPU struct with cached file content for reading
249
absl::optional<CpuFiles> CgroupCpuUtil::accessCgroupFiles(const CgroupInfo& cgroup_info,
250
1
                                                          Filesystem::Instance& fs) {
251
  // Version is already determined by getCurrentCgroupPath() from /proc/self/cgroup parsing.
252
  // No need for fallback logic - we know exactly which files to read based on the version.
253

            
254
1
  if (cgroup_info.version == "v1") {
255
    return accessCgroupV1Files(cgroup_info, fs);
256
1
  } else if (cgroup_info.version == "v2") {
257
1
    return accessCgroupV2Files(cgroup_info, fs);
258
1
  } else {
259
    // Unknown version - this shouldn't happen
260
    ENVOY_LOG_MISC(warn, "Unknown cgroup version '{}' at {}", cgroup_info.version,
261
                   cgroup_info.full_path);
262
    return absl::nullopt;
263
  }
264
1
}
265

            
266
// Reads actual CPU limits from cgroup v1 files with quota/period parsing.
267
7
absl::optional<double> CgroupCpuUtil::readActualLimitsV1(const CpuFiles& cpu_files) {
268
  // v1: Use cached quota and period content (no re-reading)
269
7
  const std::string quota_str = std::string(absl::StripAsciiWhitespace(cpu_files.quota_content));
270
7
  const std::string period_str = std::string(absl::StripAsciiWhitespace(cpu_files.period_content));
271

            
272
7
  int64_t quota, period;
273
7
  if (!absl::SimpleAtoi(quota_str, &quota) || !absl::SimpleAtoi(period_str, &period)) {
274
2
    ENVOY_LOG_MISC(warn, "Failed to parse cgroup v1 values: quota='{}' period='{}'", quota_str,
275
2
                   period_str);
276
2
    return absl::nullopt;
277
2
  }
278

            
279
  // Handle special case: v1 quota = -1 means no limit
280
5
  if (quota == -1) {
281
1
    ENVOY_LOG_MISC(debug, "cgroup v1 unlimited CPU (quota = -1)");
282
1
    return absl::nullopt; // Unlimited - return nullopt
283
1
  }
284

            
285
  // Validate values
286
4
  if (period <= 0 || quota <= 0) {
287
2
    ENVOY_LOG_MISC(warn, "Invalid cgroup v1 values: quota={} period={}", quota, period);
288
2
    return absl::nullopt;
289
2
  }
290

            
291
  // Calculate CPU ratio as float64
292
2
  double cpu_ratio = static_cast<double>(quota) / static_cast<double>(period);
293

            
294
2
  ENVOY_LOG_MISC(debug, "cgroup v1 CPU ratio: {} (quota={}, period={})", cpu_ratio, quota, period);
295

            
296
2
  return cpu_ratio;
297
4
}
298

            
299
// Reads actual CPU limits from cgroup v2 files with "quota period" parsing.
300
9
absl::optional<double> CgroupCpuUtil::readActualLimitsV2(const CpuFiles& cpu_files) {
301
  // v2: Use cached cpu.max content (no re-reading)
302
9
  const std::string content = std::string(absl::StripAsciiWhitespace(cpu_files.quota_content));
303

            
304
  // Parse "quota period" format
305
9
  const std::vector<std::string> parts = absl::StrSplit(content, ' ');
306

            
307
9
  if (parts.size() != 2) {
308
2
    ENVOY_LOG_MISC(warn, "Malformed cgroup v2 cpu.max: expected 'quota period', got '{}'", content);
309
2
    return absl::nullopt;
310
2
  }
311

            
312
  // Handle special case: v2 quota = "max" means no limit
313
7
  if (parts[0] == "max") {
314
2
    ENVOY_LOG_MISC(debug, "cgroup v2 unlimited CPU (quota = max)");
315
2
    return absl::nullopt; // Unlimited - return nullopt
316
2
  }
317

            
318
  // Parse quota and period values
319
5
  uint64_t quota, period;
320
5
  if (!absl::SimpleAtoi(parts[0], &quota) || !absl::SimpleAtoi(parts[1], &period)) {
321
2
    ENVOY_LOG_MISC(warn, "Failed to parse cgroup v2 values: quota='{}' period='{}'", parts[0],
322
2
                   parts[1]);
323
2
    return absl::nullopt;
324
2
  }
325

            
326
  // Validate values
327
3
  if (period == 0) {
328
1
    ENVOY_LOG_MISC(warn, "Invalid cgroup v2 period: cannot be zero");
329
1
    return absl::nullopt;
330
1
  }
331

            
332
  // Calculate CPU ratio as float64
333
2
  double cpu_ratio = static_cast<double>(quota) / static_cast<double>(period);
334

            
335
2
  ENVOY_LOG_MISC(debug, "cgroup v2 CPU ratio: {} (quota={}, period={})", cpu_ratio, quota, period);
336

            
337
2
  return cpu_ratio;
338
3
}
339

            
340
// Reads actual CPU limits from cgroup files with version-specific parsing.
341
//
342
// Logic:
343
//   1. Use cached file paths from Step 4
344
//   2. Read files using filesystem interface
345
//   3. Version-specific parsing:
346
//      - v1: Read two separate files, divide quota/period
347
//      - v2: Parse "quota period" from single file
348
//   4. Handle special cases:
349
//      - v1: quota = -1 means no limit
350
//      - v2: quota = "max" means no limit
351
//   5. Result: CPU limit as float64 ratio
352
absl::optional<double> CgroupCpuUtil::readActualLimits(const CpuFiles& cpu_files,
353
1
                                                       Filesystem::Instance& /* fs */) {
354
1
  if (cpu_files.version == "v1") {
355
    return readActualLimitsV1(cpu_files);
356
1
  } else if (cpu_files.version == "v2") {
357
1
    return readActualLimitsV2(cpu_files);
358
1
  } else {
359
    ENVOY_LOG_MISC(warn, "Unknown cgroup version: {}", cpu_files.version);
360
    return absl::nullopt;
361
  }
362
1
}
363

            
364
// Discovers cgroup filesystem mounts by parsing /proc/self/mountinfo line by line.
365
// Implements proper priority handling where cgroup v1 with CPU controller wins over v2.
366
//
367
// /proc/self/mountinfo format:
368
// mountID parentID major:minor root mountPoint options - fsType source superOptions
369
// (1)     (2)      (3)        (4)  (5)       (6)     (7)(8)    (9)    (10)
370
//
371
// Priority logic:
372
// - If cgroup v1 + CPU controller: return immediately (highest priority)
373
// - If cgroup v2: save mount point, continue searching
374
// - Result: single mount point with highest priority
375
//
376
6
absl::optional<std::string> CgroupCpuUtil::discoverCgroupMount(Filesystem::Instance& fs) {
377
6
  const auto result = fs.fileReadToEnd(std::string(PROC_MOUNTINFO_PATH));
378
6
  if (!result.ok()) {
379
    // /proc/self/mountinfo doesn't exist - not in a cgroup
380
    ENVOY_LOG_MISC(warn, "Cannot read /proc/self/mountinfo: not in a cgroup or file doesn't exist");
381
    return absl::nullopt;
382
  }
383

            
384
6
  const std::string content = result.value();
385
6
  const std::vector<std::string> lines = absl::StrSplit(content, '\n');
386

            
387
6
  std::string v2_mount_point; // Save v2 mount in case no v1 found
388

            
389
50
  for (const std::string& line_str : lines) {
390
50
    if (line_str.empty()) {
391
4
      continue;
392
4
    }
393

            
394
    // Work with string_view for efficient parsing
395
46
    absl::string_view line = line_str;
396
46
    bool line_valid = true;
397

            
398
    // Skip first four fields
399
230
    for (int field = 0; field < 4; field++) {
400
184
      size_t space_pos = line.find(' ');
401
184
      if (space_pos == absl::string_view::npos) {
402
        ENVOY_LOG_MISC(warn, "Malformed mountinfo line: not enough fields");
403
        line_valid = false;
404
        break;
405
      }
406
184
      line = line.substr(space_pos + 1);
407
184
    }
408
46
    if (!line_valid)
409
      continue;
410

            
411
    // (5) mount point: extract mount point
412
46
    size_t mount_end = line.find(' ');
413
46
    if (mount_end == absl::string_view::npos) {
414
      ENVOY_LOG_MISC(warn, "Malformed mountinfo line: no mount point");
415
      continue;
416
    }
417
46
    absl::string_view mount_point_escaped = line.substr(0, mount_end);
418
46
    line = line.substr(mount_end + 1);
419

            
420
    // Skip ahead past optional fields, delimited by " - "
421
46
    bool separator_found = false;
422
48
    while (true) {
423
48
      size_t space_pos = line.find(' ');
424
48
      if (space_pos == absl::string_view::npos) {
425
        ENVOY_LOG_MISC(warn, "Malformed mountinfo line: no separator found");
426
        line_valid = false;
427
        break;
428
      }
429

            
430
48
      if (space_pos + 3 >= line.length()) {
431
        ENVOY_LOG_MISC(warn, "Malformed mountinfo line: separator position invalid");
432
        line_valid = false;
433
        break;
434
      }
435

            
436
48
      absl::string_view delim = line.substr(space_pos, 3);
437
48
      if (delim == " - ") {
438
46
        line = line.substr(space_pos + 3);
439
46
        separator_found = true;
440
46
        break;
441
46
      }
442
2
      line = line.substr(space_pos + 1);
443
2
    }
444
46
    if (!line_valid || !separator_found)
445
      continue;
446

            
447
    // (9) filesystem type: extract filesystem type
448
46
    size_t fs_type_end = line.find(' ');
449
46
    if (fs_type_end == absl::string_view::npos) {
450
      ENVOY_LOG_MISC(warn, "Malformed mountinfo line: no filesystem type");
451
      continue;
452
    }
453
46
    absl::string_view fs_type = line.substr(0, fs_type_end);
454
46
    line = line.substr(fs_type_end + 1);
455

            
456
    // Check if this is a cgroup filesystem
457
46
    if (fs_type != "cgroup" && fs_type != "cgroup2") {
458
36
      continue;
459
36
    }
460

            
461
    // Unescape mount point
462
10
    std::string mount_point = unescapePath(std::string(mount_point_escaped));
463

            
464
    // As in Go: cgroup v1 with a CPU controller takes precedence over cgroup v2
465
10
    if (fs_type == "cgroup2") {
466
      // v2 hierarchy - save mount point but keep searching
467
4
      v2_mount_point = mount_point;
468
4
      ENVOY_LOG_MISC(debug, "Found cgroup v2 at {}, continuing search for v1", mount_point);
469
4
      continue; // Keep searching, we might find a v1 hierarchy with CPU controller
470
4
    }
471

            
472
    // For cgroup v1, check for CPU controller in super options
473

            
474
    // (10) mount source: skip it
475
6
    size_t source_end = line.find(' ');
476
6
    if (source_end == absl::string_view::npos) {
477
      ENVOY_LOG_MISC(warn, "Malformed mountinfo line: no mount source");
478
      continue;
479
    }
480
6
    line = line.substr(source_end + 1);
481

            
482
    // (11) super options: check for CPU controller
483
6
    absl::string_view super_options = line;
484

            
485
    // v1 hierarchy - check for CPU controller
486
6
    if (absl::StrContains(super_options, "cpu")) {
487
      // Found a v1 CPU controller. This must be the only one, so we're done
488
2
      ENVOY_LOG_MISC(debug, "Found cgroup v1 with CPU controller at {}", mount_point);
489
2
      return mount_point; // Return immediately - v1 CPU wins
490
2
    }
491
6
  }
492

            
493
  // Return v2 mount if no v1 with CPU found
494
4
  if (!v2_mount_point.empty()) {
495
3
    ENVOY_LOG_MISC(debug, "Using cgroup v2 mount at {}", v2_mount_point);
496
3
    return v2_mount_point;
497
3
  }
498

            
499
  // No cgroup filesystem found
500
1
  ENVOY_LOG_MISC(debug, "No cgroup filesystem mounts found");
501
1
  return absl::nullopt;
502
4
}
503

            
504
// Unescapes octal escape sequences in paths from /proc/self/mountinfo.
505
// Linux's show_path converts '\', ' ', '\t', and '\n' to octal escape sequences
506
// like '\040' for space, '\134' for backslash, '\011' for tab, '\012' for newline.
507
//
508
// This matches the Go runtime implementation:
509
// https://github.com/golang/go/blob/master/src/internal/runtime/cgroup/cgroup_linux.go
510
32
std::string CgroupCpuUtil::unescapePath(const std::string& path) {
511
32
  std::string result;
512
32
  result.reserve(path.length()); // Pre-allocate to avoid `reallocations`
513

            
514
452
  for (size_t i = 0; i < path.length(); ++i) {
515
420
    char c = path[i];
516

            
517
    // Check for escape sequence start
518
420
    if (c != '\\') {
519
395
      result += c;
520
395
      continue;
521
395
    }
522

            
523
    // Start of escape sequence: backslash followed by 3 octal digits
524
    // Escape sequence is always 4 characters: one backslash and three digits
525
25
    if (i + 3 >= path.length()) {
526
      // Invalid escape sequence - not enough characters
527
1
      ENVOY_LOG_MISC(warn, "Invalid escape sequence in path '{}' at position {}", path, i);
528
1
      result += c; // Keep the backslash as-is
529
1
      continue;
530
1
    }
531

            
532
    // Parse three octal digits using `std::strtol`
533
    // Extract exactly 3 characters after the backslash
534
24
    if (i + 3 >= path.length()) {
535
      // Not enough characters for complete octal sequence
536
      ENVOY_LOG_MISC(warn, "Incomplete octal escape sequence in path '{}' at position {}", path, i);
537
      result += c; // Keep the backslash as-is
538
      continue;
539
    }
540

            
541
24
    std::string octal_str = path.substr(i + 1, 3);
542

            
543
    // Validate all characters are valid octal digits (0-7)
544
24
    bool valid = std::all_of(octal_str.begin(), octal_str.end(),
545
70
                             [](char c) { return c >= '0' && c <= '7'; });
546

            
547
24
    if (!valid) {
548
      // Invalid octal digits found
549
1
      ENVOY_LOG_MISC(warn, "Invalid octal escape sequence in path '{}' at position {}", path, i);
550
1
      result += c; // Keep the backslash as-is
551
1
      continue;
552
1
    }
553

            
554
    // Convert octal string to integer
555
23
    char* end;
556
23
    long decoded = std::strtol(octal_str.c_str(), &end, 8);
557

            
558
    // Verify conversion was successful and complete
559
23
    if (end != octal_str.c_str() + 3 || decoded > 255) {
560
      ENVOY_LOG_MISC(warn, "Invalid octal escape sequence in path '{}' at position {}", path, i);
561
      result += c; // Keep the backslash as-is
562
      continue;
563
    }
564

            
565
    // Valid escape sequence - add decoded character
566
23
    result += static_cast<char>(decoded);
567
23
    i += 3; // Skip the three digits (loop will increment i by 1)
568
23
  }
569

            
570
32
  return result;
571
32
}
572

            
573
// Parses a single line from /proc/self/mountinfo to extract cgroup mount point.
574
// Format: mountID parentID major:minor root mountPoint options - fsType source superOptions
575
//
576
// Example lines:
577
// 25 21 0:21 / /sys/fs/cgroup/cpu rw,`relatime` - cgroup cgroup rw,cpu
578
// 26 21 0:22 / /sys/fs/cgroup cgroup2 rw,`relatime` - cgroup2 cgroup2 rw
579
//
580
// We extract field 5 (mount point) for cgroup/cgroup2 filesystem only.
581
//
582
// NOTE: Mount points may contain escaped characters (\040 for space, \134 for backslash, etc.)
583
// and must be unescaped before use.
584
7
absl::optional<std::string> CgroupCpuUtil::parseMountInfoLine(const std::string& line) {
585
7
  const std::vector<std::string> fields = absl::StrSplit(line, ' ');
586

            
587
  // Find the separator "-" to locate filesystem type field
588
7
  size_t separator_pos = 0;
589
48
  for (size_t i = 0; i < fields.size(); i++) {
590
47
    if (fields[i] == "-") {
591
6
      separator_pos = i;
592
6
      break;
593
6
    }
594
47
  }
595

            
596
7
  if (separator_pos == 0 || separator_pos + 1 >= fields.size()) {
597
    // Malformed line or separator not found
598
1
    ENVOY_LOG_MISC(warn, "Malformed mountinfo line: separator '-' not found or invalid position");
599
1
    return absl::nullopt;
600
1
  }
601

            
602
  // Extract mount point (field 5, 0-indexed = 4) and filesystem type (separator + 1)
603
6
  if (fields.size() < 5 || separator_pos + 1 >= fields.size()) {
604
    // Insufficient fields
605
1
    ENVOY_LOG_MISC(warn,
606
1
                   "Malformed mountinfo line: expected at least 5 fields and filesystem type after "
607
1
                   "separator, got {} fields",
608
1
                   fields.size());
609
1
    return absl::nullopt;
610
1
  }
611

            
612
5
  const std::string& mount_point_escaped = fields[4];
613
5
  const std::string& fs_type = fields[separator_pos + 1];
614

            
615
  // Check if this is a cgroup filesystem
616
5
  if (fs_type != "cgroup" && fs_type != "cgroup2") {
617
1
    return absl::nullopt;
618
1
  }
619

            
620
  // Unescape mount point - Linux's show_path escapes special characters
621
4
  std::string mount_point = unescapePath(mount_point_escaped);
622

            
623
4
  ENVOY_LOG_MISC(trace, "Parsed cgroup mount: {} ({})", mount_point, fs_type);
624

            
625
4
  return mount_point;
626
5
}
627

            
628
} // namespace Envoy