blob: ae63dd65ef55ad46cc740450f3709a8cff4d1071 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2013 The ChromiumOS Authors
Mike Frysinger69cb41d2013-08-11 20:08:19 -04002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Generate minidump symbols for use by the Crash server.
6
7Note: This should be run inside the chroot.
8
9This produces files in the breakpad format required by minidump_stackwalk and
10the crash server to dump stack information.
11
12Basically it scans all the split .debug files in /build/$BOARD/usr/lib/debug/
13and converts them over using the `dump_syms` programs. Those plain text .sym
14files are then stored in /build/$BOARD/usr/lib/debug/breakpad/.
15
Mike Frysinger02e1e072013-11-10 22:11:34 -050016If you want to actually upload things, see upload_symbols.py.
17"""
Mike Frysinger69cb41d2013-08-11 20:08:19 -040018
19import collections
20import ctypes
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -080021import enum
Chris McDonaldb55b7032021-06-17 16:41:32 -060022import logging
Mike Frysinger69cb41d2013-08-11 20:08:19 -040023import multiprocessing
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -080024import multiprocessing.sharedctypes
Mike Frysinger69cb41d2013-08-11 20:08:19 -040025import os
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -080026import re
27from typing import Optional
Mike Frysinger69cb41d2013-08-11 20:08:19 -040028
Chris McDonaldb55b7032021-06-17 16:41:32 -060029from chromite.cbuildbot import cbuildbot_alerts
Mike Frysinger06a51c82021-04-06 11:39:17 -040030from chromite.lib import build_target_lib
Mike Frysinger69cb41d2013-08-11 20:08:19 -040031from chromite.lib import commandline
32from chromite.lib import cros_build_lib
33from chromite.lib import osutils
34from chromite.lib import parallel
Mike Frysinger96ad3f22014-04-24 23:27:27 -040035from chromite.lib import signals
Alex Klein1809f572021-09-09 11:28:37 -060036from chromite.utils import file_util
Mike Frysinger69cb41d2013-08-11 20:08:19 -040037
Mike Frysinger807d8282022-04-28 22:45:17 -040038
Stephen Boydfc1c8032021-10-06 20:58:37 -070039# Elf files that don't exist but have a split .debug file installed.
40ALLOWED_DEBUG_ONLY_FILES = {
Alex Klein1699fab2022-09-08 08:46:06 -060041 "boot/vmlinux",
Stephen Boydfc1c8032021-10-06 20:58:37 -070042}
Mike Frysinger69cb41d2013-08-11 20:08:19 -040043
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -080044# Allowlist of elf files that we know we can't symbolize in the normal way, but
45# which we don't have an automatic way to detect.
46EXPECTED_POOR_SYMBOLIZATION_FILES = ALLOWED_DEBUG_ONLY_FILES | {
47 # Git binaries are downloaded as binary blobs already stripped.
48 "usr/bin/git",
49 "usr/bin/git-receive-pack",
50 "usr/bin/git-upload-archive",
51 "usr/bin/git-upload-pack",
52 # Prebuild Android binary
53 "build/rootfs/opt/google/vms/android/etc/bin/XkbToKcmConverter",
Ian Barkley-Yeungab1dab12023-04-21 18:19:55 -070054 "build/rootfs/opt/google/containers/android/etc/bin/XkbToKcmConverter",
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -080055 # Pulled from
56 # https://skia.googlesource.com/buildbot/+/refs/heads/main/gold-client/, no
57 # need to resymbolize.
58 "usr/bin/goldctl",
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -080059}
60
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -080061# Allowlist of patterns for ELF files that symbolize (dump_syms exits with
62# success) but don't pass symbol file validation. Note that ELFs listed in
63# EXPECTED_POOR_SYMBOLIZATION_FILES do not have their symbol files validated and
64# do not need to be repeated here.
65ALLOWLIST_NO_SYMBOL_FILE_VALIDATION = {
66 # Built in a weird way, see comments at top of
67 # https://source.chromium.org/chromium/chromium/src/+/main:native_client/src/trusted/service_runtime/linux/nacl_bootstrap.x
68 "opt/google/chrome/nacl_helper_bootstrap",
69 # TODO(b/273543528): Investigate why this doesn't have stack records on
70 # kevin builds.
71 "build/rootfs/dlc-scaled/screen-ai/package/root/libchromescreenai.so",
Ian Barkley-Yeungab1dab12023-04-21 18:19:55 -070072 # TODO(b/279645511): Investigate why this doesn't have STACK records on
73 # jacuzzi, scarlet, kukui, etc.
74 "usr/bin/rma_reset",
75 # Virtual dynamic shared object, not expected to have STACK records.
76 "opt/google/containers/android/ndk_translation/lib/arm/libndk_translation_vdso.so",
77 # TODO(b/279665879): Figure out why this ndk_translation libraries is not
78 # getting STACK records.
79 "opt/google/containers/android/ndk_translation/lib/arm/libdexfile.so",
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -080080}
81# Same but patterns not exact paths.
82ALLOWLIST_NO_SYMBOL_FILE_VALIDATION_RE = tuple(
83 re.compile(x)
84 for x in (
85 # Only has code if use.camera_feature_effects. Otherwise will have no
86 # STACK records.
87 r"usr/lib[^/]*/libcros_ml_core\.so",
88 # Prebuilt closed-source library.
89 r"usr/lib[^/]*/python[0-9]\.[0-9]/site-packages/.*/x_ignore_nofocus.so",
90 # b/273577373: Rarely used, and only by few programs that do
91 # non-standard encoding conversions
92 r"lib[^/]*/libnss_files\.so\.[0-9.]+",
93 r"lib[^/]*/libnss_dns\.so\.[0-9.]+",
94 r"usr/lib[^/]*/gconv/libISOIR165\.so",
95 r"usr/lib[^/]*/gconv/libGB\.so",
96 r"usr/lib[^/]*/gconv/libKSC\.so",
97 r"usr/lib[^/]*/gconv/libCNS\.so",
98 r"usr/lib[^/]*/gconv/libJISX0213\.so",
99 r"usr/lib[^/]*/gconv/libJIS\.so",
100 # TODO(b/273579075): Figure out why libcares.so is not getting STACK
101 # records on kevin.
102 r"usr/lib[^/]*/libcares\.so[0-9.]*",
103 # libevent-2.1.so.7.0.1 does not have executable code and thus no
104 # STACK records. See libevent-2.1.12-libevent-shrink.patch.
105 r"usr/lib[^/]*/libevent-[0-9.]+\.so[0-9.]*",
106 # TODO(b/273599604): Figure out why libdcerpc-samr.so.0.0.1 is not
107 # getting STACK records on kevin.
108 r"usr/lib[^/]*/libdcerpc-samr\.so[0-9.]*",
109 # TODO(b/272613635): Figure out why these libabsl shared libraries are
110 # not getting STACK records.
111 r"usr/lib[^/]*/libabsl_bad_variant_access\.so\.[0-9.]+",
112 r"usr/lib[^/]*/libabsl_random_internal_platform\.so\.[0-9.]+",
113 r"usr/lib[^/]*/libabsl_flags\.so\.[0-9.]+",
114 r"usr/lib[^/]*/libabsl_bad_any_cast_impl\.so\.[0-9.]+",
115 r"usr/lib[^/]*/libabsl_bad_optional_access\.so\.[0-9.]+",
116 # TODO(b/273607289): Figure out why libgrpc++_error_details.so.1.43.0 is
117 # not getting STACK records on kevin.
118 r"usr/lib[^/]*/libgrpc\+\+_error_details\.so\.[0-9.]+",
Ian Barkley-Yeungab1dab12023-04-21 18:19:55 -0700119 # linux-gate.so is system call wrappers which don't always have enough
120 # code to get STACK entries.
121 r"lib/modules/[^/]+/vdso/linux-gate\.so",
122 # TODO(b/280503615): Figure out why libGLESv2.so.2.0.0 doesn't have
123 # STACK records on amd64-generic or betty-pi-arc.
124 r"usr/lib[^/]*/libGLESv[0-9]+\.so.*",
125 # This is just a backwards compatibility stub if ENABLE_HLSL is defined,
126 # see https://github.com/KhronosGroup/glslang/blob/main/hlsl/stub.cpp
127 r"usr/lib[^/]*/libHLSL.so",
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800128 )
129)
130
Alex Klein1699fab2022-09-08 08:46:06 -0600131SymbolHeader = collections.namedtuple(
132 "SymbolHeader",
133 (
134 "cpu",
135 "id",
136 "name",
137 "os",
138 ),
139)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400140
141
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800142class SymbolGenerationResult(enum.Enum):
143 """Result of running dump_syms
144
145 Return value of _DumpAllowingBasicFallback() and _DumpExpectingSymbols().
146 """
147
148 SUCCESS = 1
149 UNEXPECTED_FAILURE = 2
150 EXPECTED_FAILURE = 3
151
152
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800153class ExpectedFiles(enum.Enum):
154 """The files always expect to see dump_syms run on.
155
156 We do extra validation on a few, semi-randomly chosen files. If we do not
157 create symbol files for these ELFs, something is very wrong.
158 """
159
160 ASH_CHROME = enum.auto()
161 LIBC = enum.auto()
162 CRASH_REPORTER = enum.auto()
163 LIBMETRICS = enum.auto()
164
165
166ALL_EXPECTED_FILES = frozenset(
167 (
168 ExpectedFiles.ASH_CHROME,
169 ExpectedFiles.LIBC,
170 ExpectedFiles.CRASH_REPORTER,
171 ExpectedFiles.LIBMETRICS,
172 )
173)
174
175
Ian Barkley-Yeungab1dab12023-04-21 18:19:55 -0700176# Regular expression for ChromeOS's libc.so. Note that some containers have
177# their own libc.so file; we don't want to do the extra validation on those.
178# (They are often subsets of the full libc and will not pass STACK count tests.)
179LIBC_REGEX = re.compile(r"lib[^/]*/libc\.so\.[0-9.]+")
180
181
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800182class SymbolFileLineCounts:
183 """Counts of the various types of lines in a .sym file"""
184
185 LINE_NUMBER_REGEX = re.compile(r"^([0-9a-f]+)")
186
187 def __init__(self, sym_file: str, elf_file: str):
188 # https://chromium.googlesource.com/breakpad/breakpad/+/HEAD/docs/symbol_files.md
189 # explains what these line types are.
190 self.module_lines = 0
191 self.file_lines = 0
192 self.inline_origin_lines = 0
193 self.func_lines = 0
194 self.inline_lines = 0
195 self.line_number_lines = 0
196 self.public_lines = 0
197 self.stack_lines = 0
198 # Not listed in the documentation but still present.
199 self.info_lines = 0
200
201 with open(sym_file, mode="r", encoding="utf-8") as f:
202 for line in f:
203 words = line.split()
204 expected_words_max = None
205 if not words:
206 raise ValueError(
207 f"{elf_file}: symbol file has unexpected blank line"
208 )
209
210 line_type = words[0]
211 if line_type == "MODULE":
212 self.module_lines += 1
213 expected_words_min = 5
214 expected_words_max = 5
215 elif line_type == "FILE":
216 self.file_lines += 1
217 expected_words_min = 3
218 # No max, filenames can have spaces.
219 elif line_type == "INLINE_ORIGIN":
220 self.inline_origin_lines += 1
221 expected_words_min = 3
222 # No max, function parameter lists can have spaces.
223 elif line_type == "FUNC":
224 self.func_lines += 1
225 expected_words_min = 5
226 # No max, function parameter lists can have spaces.
227 elif line_type == "INLINE":
228 self.inline_lines += 1
229 expected_words_min = 5
230 # No max, INLINE can have multiple address pairs.
231 elif SymbolFileLineCounts.LINE_NUMBER_REGEX.match(line_type):
232 self.line_number_lines += 1
233 expected_words_min = 4
234 expected_words_max = 4
235 line_type = "line number"
236 elif line_type == "PUBLIC":
237 self.public_lines += 1
Ian Barkley-Yeungab1dab12023-04-21 18:19:55 -0700238 # TODO(b/251003272): expected_words_min should be 4;
239 # however, dump_syms sometimes produces PUBLIC records with
240 # no symbol name. This is an error but is not affecting our
241 # ability to decode stacks.
242 expected_words_min = 3
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800243 # No max, function parameter lists can have spaces.
244 elif line_type == "STACK":
245 self.stack_lines += 1
246 expected_words_min = 5
247 # No max, expressions can be complex.
248 elif line_type == "INFO":
249 self.info_lines += 1
250 # Not documented, so unclear what the min & max are
251 expected_words_min = None
252 else:
253 raise ValueError(
254 f"{elf_file}: symbol file has unknown line type "
255 f"{line_type}"
256 )
257
258 if expected_words_max is not None:
259 if not (
260 expected_words_min <= len(words) <= expected_words_max
261 ):
262 raise ValueError(
263 f"{elf_file}: symbol file has {line_type} line "
264 f"with {len(words)} words (expected "
265 f"{expected_words_min} - {expected_words_max})"
266 )
267 elif expected_words_min is not None:
268 if len(words) < expected_words_min:
269 raise ValueError(
270 f"{elf_file}: symbol file has {line_type} line "
271 f"with {len(words)} words (expected "
272 f"{expected_words_min} or more)"
273 )
274
275
276def ValidateSymbolFile(
277 sym_file: str,
278 elf_file: str,
279 sysroot: Optional[str],
280 found_files: Optional[multiprocessing.managers.ListProxy],
281) -> bool:
282 """Checks that the given sym_file has enough info for us to get good stacks.
283
284 Validates that the given sym_file has enough information for us to get
285 good error reports -- enough STACK records to unwind the stack and enough
286 FUNC or PUBLIC records to turn the function addresses into human-readable
287 names.
288
289 Args:
290 sym_file: The complete path to the breakpad symbol file to validate
291 elf_file: The complete path to the elf file which was the source of the
292 symbol file.
293 sysroot: If not None, the root of the build directory ('/build/eve', for
294 instance).
295 found_files: A multiprocessing.managers.ListProxy list containing
296 ExpectedFiles, representing which of the "should always be present"
297 files have been processed.
298
299 Returns:
300 True if the symbol file passes validation.
301 """
302 if sysroot is not None:
303 relative_path = os.path.relpath(elf_file, sysroot)
304 else:
305 relative_path = os.path.relpath(elf_file, "/")
306
307 if relative_path in ALLOWLIST_NO_SYMBOL_FILE_VALIDATION:
308 return True
309 for regex in ALLOWLIST_NO_SYMBOL_FILE_VALIDATION_RE:
310 if regex.match(relative_path):
311 return True
312
313 counts = SymbolFileLineCounts(sym_file, elf_file)
314
315 errors = False
316 if counts.stack_lines == 0:
317 # Use the elf_file in error messages; sym_file is still a temporary
318 # file with a meaningless-to-humans name right now.
319 logging.warning("%s: Symbol file has no STACK records", elf_file)
320 errors = True
321 if counts.module_lines != 1:
322 logging.warning(
323 "%s: Symbol file has %d MODULE lines", elf_file, counts.module_lines
324 )
325 errors = True
326 # Many shared object files have only PUBLIC functions. In theory,
327 # executables should always have at least one FUNC (main) and some line
328 # numbers, but for reasons I'm unclear on, C-based executables often just
329 # have PUBLIC records. dump_syms does not support line numbers after
330 # PUBLIC records, only FUNC records, so such executables will also have
331 # no line numbers.
332 if counts.public_lines == 0 and counts.func_lines == 0:
333 logging.warning(
334 "%s: Symbol file has no FUNC or PUBLIC records", elf_file
335 )
336 errors = True
337 # However, if we get a FUNC record, we do want line numbers for it.
338 if counts.func_lines > 0 and counts.line_number_lines == 0:
339 logging.warning(
340 "%s: Symbol file has FUNC records but no line numbers", elf_file
341 )
342 errors = True
343
344 if counts.line_number_lines > 0 and counts.file_lines == 0:
345 logging.warning(
346 "%s: Symbol file has line number records but no FILE records",
347 elf_file,
348 )
349 errors = True
350 if counts.inline_lines > 0 and counts.file_lines == 0:
351 logging.warning(
352 "%s: Symbol file has INLINE records but no FILE records", elf_file
353 )
354 errors = True
355
356 if counts.inline_lines > 0 and counts.inline_origin_lines == 0:
357 logging.warning(
358 "%s: Symbol file has INLINE records but no INLINE_ORIGIN records",
359 elf_file,
360 )
361 errors = True
362
363 def _AddFoundFile(files, found):
364 """Add another file to the list of expected files we've found."""
365 if files is not None:
366 files.append(found)
367
368 # Extra validation for a few ELF files which are special. Either these are
369 # unusually important to the system (chrome binary, which is where a large
370 # fraction of our crashes occur, and libc.so, which is in every stack), or
371 # they are some hand-chosen ELF files which stand in for "normal" platform2
372 # binaries. Not all ELF files would pass the extra validation, so we can't
373 # run these checks on every ELF, but we want to make sure we don't end up
374 # with, say, a chrome build or a platform2 build with just one or two FUNC
375 # records on every binary.
376 if relative_path == "opt/google/chrome/chrome":
377 _AddFoundFile(found_files, ExpectedFiles.ASH_CHROME)
378 if counts.func_lines < 100000:
379 logging.warning(
380 "chrome should have at least 100,000 FUNC records, found %d",
381 counts.func_lines,
382 )
383 errors = True
384 if counts.stack_lines < 1000000:
385 logging.warning(
386 "chrome should have at least 1,000,000 STACK records, found %d",
387 counts.stack_lines,
388 )
389 errors = True
390 if counts.line_number_lines < 1000000:
391 logging.warning(
392 "chrome should have at least 1,000,000 line number records, "
393 "found %d",
394 counts.line_number_lines,
395 )
396 errors = True
397 # Lacros symbol files are not generated as part of the ChromeOS build and
398 # can't be validated here.
399 # TODO(b/273836486): Add similar logic to the code that generates Lacros
400 # symbols.
Ian Barkley-Yeungab1dab12023-04-21 18:19:55 -0700401 elif LIBC_REGEX.fullmatch(relative_path):
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800402 _AddFoundFile(found_files, ExpectedFiles.LIBC)
403 if counts.public_lines < 100:
404 logging.warning(
405 "%s should have at least 100 PUBLIC records, found %d",
406 elf_file,
407 counts.public_lines,
408 )
409 errors = True
410 if counts.stack_lines < 10000:
411 logging.warning(
412 "%s should have at least 10000 STACK records, found %d",
413 elf_file,
414 counts.stack_lines,
415 )
416 errors = True
417 elif relative_path == "sbin/crash_reporter":
418 # Representative platform2 executable.
419 _AddFoundFile(found_files, ExpectedFiles.CRASH_REPORTER)
420 if counts.stack_lines < 1000:
421 logging.warning(
422 "crash_reporter should have at least 1000 STACK records, "
423 "found %d",
424 counts.stack_lines,
425 )
426 errors = True
427 if counts.func_lines < 1000:
428 logging.warning(
429 "crash_reporter should have at least 1000 FUNC records, "
430 "found %d",
431 counts.func_lines,
432 )
433 errors = True
434 if counts.line_number_lines < 10000:
435 logging.warning(
436 "crash_reporter should have at least 10,000 line number "
437 "records, found %d",
438 counts.line_number_lines,
439 )
440 errors = True
441 elif os.path.basename(relative_path) == "libmetrics.so":
442 # Representative platform2 shared library.
443 _AddFoundFile(found_files, ExpectedFiles.LIBMETRICS)
444 if counts.func_lines < 100:
445 logging.warning(
446 "libmetrics should have at least 100 FUNC records, found %d",
447 counts.func_lines,
448 )
449 errors = True
450 if counts.public_lines == 0:
451 logging.warning(
452 "libmetrics should have at least 1 PUBLIC record, found %d",
453 counts.public_lines,
454 )
455 errors = True
456 if counts.stack_lines < 1000:
457 logging.warning(
458 "libmetrics should have at least 1000 STACK records, found %d",
459 counts.stack_lines,
460 )
461 errors = True
462 if counts.line_number_lines < 5000:
463 logging.warning(
464 "libmetrics should have at least 5000 line number records, "
465 "found %d",
466 counts.line_number_lines,
467 )
468 errors = True
469
470 return not errors
471
472
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800473def _ExpectGoodSymbols(elf_file, sysroot):
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800474 """Determines if we expect dump_syms to create good symbols.
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800475
476 We know that certain types of files never generate good symbols. Distinguish
477 those from the majority of elf files which should generate good symbols.
478
479 Args:
480 elf_file: The complete path to the file which we will pass to dump_syms
481 sysroot: If not None, the root of the build directory ('/build/eve', for
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800482 instance)
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800483
484 Returns:
485 True if the elf file should generate good symbols, False if not.
486 """
487 # .ko files (kernel object files) never produce good symbols.
488 if elf_file.endswith(".ko"):
489 return False
490
491 # dump_syms doesn't understand Golang executables.
492 result = cros_build_lib.run(
493 ["/usr/bin/file", elf_file], print_cmd=False, stdout=True
494 )
495 if b"Go BuildID" in result.stdout:
496 return False
497
498 if sysroot is not None:
499 relative_path = os.path.relpath(elf_file, sysroot)
500 else:
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800501 relative_path = os.path.relpath(elf_file, "/")
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800502
503 if relative_path in EXPECTED_POOR_SYMBOLIZATION_FILES:
504 return False
505
Ian Barkley-Yeunge1785762023-03-08 18:32:18 -0800506 # Binaries in /usr/local are not actually shipped to end-users, so we
507 # don't care if they get good symbols -- we should never get crash reports
508 # for them anyways.
509 if relative_path.startswith("usr/local"):
510 return False
511
Ian Barkley-Yeung4ed8cee2023-05-05 11:20:52 -0700512 # TODO(b/279668555): tael and tatl libraries in
513 # /opt/google/cros-containers/lib have invalid .debug files (CRC32
514 # mismatches). Since only tael and tatl have
515 # /opt/google/cros-containers/lib, we can just allowlist the directory until
516 # the issue is fixed.
517 if relative_path.startswith("opt/google/cros-containers/lib"):
518 return False
519
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800520 return True
521
522
523def ReadSymsHeader(sym_file, name_for_errors):
Alex Klein1699fab2022-09-08 08:46:06 -0600524 """Parse the header of the symbol file
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400525
Alex Klein1699fab2022-09-08 08:46:06 -0600526 The first line of the syms file will read like:
Alex Klein8b444532023-04-11 16:35:24 -0600527 MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F40 blkid
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400528
Alex Klein1699fab2022-09-08 08:46:06 -0600529 https://code.google.com/p/google-breakpad/wiki/SymbolFiles
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400530
Alex Klein1699fab2022-09-08 08:46:06 -0600531 Args:
Alex Klein8b444532023-04-11 16:35:24 -0600532 sym_file: The symbol file to parse
533 name_for_errors: A name for error strings. Can be the name of the elf
534 file that generated the symbol file, or the name of the symbol file
535 if the symbol file has already been moved to a meaningful location.
Mike Frysinger1a736a82013-12-12 01:50:59 -0500536
Alex Klein1699fab2022-09-08 08:46:06 -0600537 Returns:
Alex Klein8b444532023-04-11 16:35:24 -0600538 A SymbolHeader object
Mike Frysinger1a736a82013-12-12 01:50:59 -0500539
Alex Klein1699fab2022-09-08 08:46:06 -0600540 Raises:
Alex Klein8b444532023-04-11 16:35:24 -0600541 ValueError if the first line of |sym_file| is invalid
Alex Klein1699fab2022-09-08 08:46:06 -0600542 """
543 with file_util.Open(sym_file, "rb") as f:
544 header = f.readline().decode("utf-8").split()
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400545
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800546 if len(header) != 5 or header[0] != "MODULE":
547 raise ValueError(
548 f"header of sym file from {name_for_errors} is invalid"
549 )
Mike Frysinger50cedd32014-02-09 23:03:18 -0500550
Alex Klein1699fab2022-09-08 08:46:06 -0600551 return SymbolHeader(
552 os=header[1], cpu=header[2], id=header[3], name=header[4]
553 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400554
555
Alex Klein1699fab2022-09-08 08:46:06 -0600556def GenerateBreakpadSymbol(
557 elf_file,
558 debug_file=None,
559 breakpad_dir=None,
560 strip_cfi=False,
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800561 sysroot=None,
Alex Klein1699fab2022-09-08 08:46:06 -0600562 num_errors=None,
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800563 found_files=None,
Alex Klein1699fab2022-09-08 08:46:06 -0600564 dump_syms_cmd="dump_syms",
565):
566 """Generate the symbols for |elf_file| using |debug_file|
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400567
Alex Klein1699fab2022-09-08 08:46:06 -0600568 Args:
Alex Klein8b444532023-04-11 16:35:24 -0600569 elf_file: The file to dump symbols for
570 debug_file: Split debug file to use for symbol information
571 breakpad_dir: The dir to store the output symbol file in
572 strip_cfi: Do not generate CFI data
573 sysroot: Path to the sysroot with the elf_file under it
574 num_errors: An object to update with the error count (needs a .value
575 member).
576 found_files: A multiprocessing.managers.ListProxy list containing
577 ExpectedFiles, representing which of the "should always be present"
578 files have been processed.
579 dump_syms_cmd: Command to use for dumping symbols.
Mike Frysinger1a736a82013-12-12 01:50:59 -0500580
Alex Klein1699fab2022-09-08 08:46:06 -0600581 Returns:
Alex Klein8b444532023-04-11 16:35:24 -0600582 The name of symbol file written out on success, or the failure count.
Alex Klein1699fab2022-09-08 08:46:06 -0600583 """
584 assert breakpad_dir
585 if num_errors is None:
Trent Apted1e2e4f32023-05-05 03:50:20 +0000586 num_errors = ctypes.c_int(0)
Alex Klein1699fab2022-09-08 08:46:06 -0600587 debug_file_only = not os.path.exists(elf_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400588
Manoj Gupta68546b02023-05-01 21:55:19 +0000589 cmd_base = [dump_syms_cmd, "-v"]
Alex Klein1699fab2022-09-08 08:46:06 -0600590 if strip_cfi:
591 cmd_base += ["-c"]
592 # Some files will not be readable by non-root (e.g. set*id /bin/su).
593 needs_sudo = not os.access(elf_file, os.R_OK)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400594
Alex Klein1699fab2022-09-08 08:46:06 -0600595 def _DumpIt(cmd_args):
596 if needs_sudo:
597 run_command = cros_build_lib.sudo_run
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400598 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600599 run_command = cros_build_lib.run
600 return run_command(
601 cmd_base + cmd_args,
602 stderr=True,
603 stdout=temp.name,
604 check=False,
605 debug_level=logging.DEBUG,
606 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400607
Alex Klein1699fab2022-09-08 08:46:06 -0600608 def _CrashCheck(result, file_or_files, msg):
609 if result.returncode:
610 cbuildbot_alerts.PrintBuildbotStepWarnings()
611 if result.returncode < 0:
612 logging.warning(
613 "dump_syms %s crashed with %s; %s",
614 file_or_files,
615 signals.StrSignal(-result.returncode),
616 msg,
617 )
618 else:
619 logging.warning(
620 "dump_syms %s returned %d; %s",
621 file_or_files,
622 result.returncode,
623 msg,
624 )
625 logging.warning("output:\n%s", result.stderr.decode("utf-8"))
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400626
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800627 def _DumpAllowingBasicFallback():
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800628 """Dump symbols for an ELF when we do NOT expect to get good symbols.
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800629
630 Returns:
631 A SymbolGenerationResult
632 """
Alex Klein1699fab2022-09-08 08:46:06 -0600633 if debug_file:
634 # Try to dump the symbols using the debug file like normal.
635 if debug_file_only:
636 cmd_args = [debug_file]
637 file_or_files = debug_file
638 else:
639 cmd_args = [elf_file, os.path.dirname(debug_file)]
640 file_or_files = [elf_file, debug_file]
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400641
Alex Klein1699fab2022-09-08 08:46:06 -0600642 result = _DumpIt(cmd_args)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400643
Alex Klein1699fab2022-09-08 08:46:06 -0600644 if result.returncode:
645 # Sometimes dump_syms can crash because there's too much info.
646 # Try dumping and stripping the extended stuff out. At least
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800647 # this way we'll get the extended symbols.
648 # https://crbug.com/266064
Alex Klein1699fab2022-09-08 08:46:06 -0600649 _CrashCheck(result, file_or_files, "retrying w/out CFI")
650 cmd_args = ["-c", "-r"] + cmd_args
651 result = _DumpIt(cmd_args)
652 _CrashCheck(result, file_or_files, "retrying w/out debug")
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700653
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800654 if not result.returncode:
655 return SymbolGenerationResult.SUCCESS
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700656
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800657 # If that didn't work (no debug, or dump_syms still failed), try
658 # dumping just the file itself directly.
659 result = _DumpIt([elf_file])
660 if result.returncode:
661 # A lot of files (like kernel files) contain no debug information,
662 # do not consider such occurrences as errors.
663 cbuildbot_alerts.PrintBuildbotStepWarnings()
664 if b"file contains no debugging information" in result.stderr:
665 logging.warning("dump_syms failed; giving up entirely.")
666 logging.warning("No symbols found for %s", elf_file)
667 return SymbolGenerationResult.EXPECTED_FAILURE
668 else:
669 _CrashCheck(result, elf_file, "counting as failure")
670 return SymbolGenerationResult.UNEXPECTED_FAILURE
671
672 return SymbolGenerationResult.SUCCESS
673
674 def _DumpExpectingSymbols():
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800675 """Dump symbols for an ELF when we expect to get good symbols.
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800676
677 Returns:
678 A SymbolGenerationResult. We never expect failure, so the result
679 will always be SUCCESS or UNEXPECTED_FAILURE.
680 """
681 if not debug_file:
682 logging.warning("%s must have debug file", elf_file)
683 return SymbolGenerationResult.UNEXPECTED_FAILURE
684
685 cmd_args = [elf_file, os.path.dirname(debug_file)]
686 result = _DumpIt(cmd_args)
687 if result.returncode:
Ian Barkley-Yeungab1dab12023-04-21 18:19:55 -0700688 _CrashCheck(
689 result,
690 [elf_file, debug_file],
691 "unexpected symbol generation failure",
692 )
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800693 return SymbolGenerationResult.UNEXPECTED_FAILURE
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800694
695 # TODO(b/270240549): Remove try/except, allow exceptions to just
696 # fail the script. The try/except is just here until we are sure this
697 # will not break the build.
698 try:
699 if not ValidateSymbolFile(
700 temp.name, elf_file, sysroot, found_files
701 ):
702 logging.warning("%s: symbol file failed validation", elf_file)
703 return SymbolGenerationResult.UNEXPECTED_FAILURE
704 except ValueError as e:
705 logging.warning(
706 "%s: symbol file failed validation due to exception %s",
707 elf_file,
708 e,
709 )
710 return SymbolGenerationResult.UNEXPECTED_FAILURE
711
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800712 return SymbolGenerationResult.SUCCESS
713
714 osutils.SafeMakedirs(breakpad_dir)
715 with cros_build_lib.UnbufferedNamedTemporaryFile(
716 dir=breakpad_dir, delete=False
717 ) as temp:
718 if _ExpectGoodSymbols(elf_file, sysroot):
719 result = _DumpExpectingSymbols()
720 # Until the EXPECTED_POOR_SYMBOLIZATION_FILES allowlist is
721 # completely set up for all boards, don't fail the build if
722 # _ExpectGoodSymbols is wrong.
723 # TODO(b/241470012): Remove the call to _DumpAllowingBasicFallback()
724 # and just error out if _DumpExpectingSymbols fails.
725 if result == SymbolGenerationResult.UNEXPECTED_FAILURE:
726 result = _DumpAllowingBasicFallback()
727 else:
728 result = _DumpAllowingBasicFallback()
729
730 if result == SymbolGenerationResult.UNEXPECTED_FAILURE:
731 num_errors.value += 1
732 os.unlink(temp.name)
733 return num_errors.value
734
735 if result == SymbolGenerationResult.EXPECTED_FAILURE:
736 os.unlink(temp.name)
737 return 0
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700738
Alex Klein1699fab2022-09-08 08:46:06 -0600739 # Move the dumped symbol file to the right place:
Alex Klein8b444532023-04-11 16:35:24 -0600740 # /$SYSROOT/usr/lib/debug/breakpad/<module-name>/<id>/<module-name>.sym
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800741 header = ReadSymsHeader(temp, elf_file)
Alex Klein1699fab2022-09-08 08:46:06 -0600742 logging.info("Dumped %s as %s : %s", elf_file, header.name, header.id)
743 sym_file = os.path.join(
744 breakpad_dir, header.name, header.id, header.name + ".sym"
745 )
746 osutils.SafeMakedirs(os.path.dirname(sym_file))
747 os.rename(temp.name, sym_file)
748 os.chmod(sym_file, 0o644)
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700749
Alex Klein1699fab2022-09-08 08:46:06 -0600750 return sym_file
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400751
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700752
Alex Klein1699fab2022-09-08 08:46:06 -0600753def GenerateBreakpadSymbols(
754 board,
755 breakpad_dir=None,
756 strip_cfi=False,
757 generate_count=None,
758 sysroot=None,
759 num_processes=None,
760 clean_breakpad=False,
761 exclude_dirs=(),
762 file_list=None,
763):
764 """Generate symbols for this board.
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700765
Alex Klein1699fab2022-09-08 08:46:06 -0600766 If |file_list| is None, symbols are generated for all executables, otherwise
767 only for the files included in |file_list|.
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700768
Alex Klein1699fab2022-09-08 08:46:06 -0600769 TODO(build):
770 This should be merged with buildbot_commands.GenerateBreakpadSymbols()
771 once we rewrite cros_generate_breakpad_symbols in python.
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400772
Alex Klein1699fab2022-09-08 08:46:06 -0600773 Args:
Alex Klein8b444532023-04-11 16:35:24 -0600774 board: The board whose symbols we wish to generate
775 breakpad_dir: The full path to the breakpad directory where symbols live
776 strip_cfi: Do not generate CFI data
777 generate_count: If set, only generate this many symbols (meant for
778 testing)
779 sysroot: The root where to find the corresponding ELFs
780 num_processes: Number of jobs to run in parallel
781 clean_breakpad: Should we `rm -rf` the breakpad output dir first; note:
782 we do not do any locking, so do not run more than one in parallel
783 when True
784 exclude_dirs: List of dirs (relative to |sysroot|) to not search
785 file_list: Only generate symbols for files in this list. Each file must
786 be a full path (including |sysroot| prefix).
787 TODO(build): Support paths w/o |sysroot|.
Alex Klein1699fab2022-09-08 08:46:06 -0600788
789 Returns:
Alex Klein8b444532023-04-11 16:35:24 -0600790 The number of errors that were encountered.
Alex Klein1699fab2022-09-08 08:46:06 -0600791 """
792 if sysroot is None:
793 sysroot = build_target_lib.get_default_sysroot_path(board)
794 if breakpad_dir is None:
795 breakpad_dir = FindBreakpadDir(board, sysroot=sysroot)
796 if clean_breakpad:
797 logging.info("cleaning out %s first", breakpad_dir)
798 osutils.RmDir(breakpad_dir, ignore_missing=True, sudo=True)
799 # Make sure non-root can write out symbols as needed.
800 osutils.SafeMakedirs(breakpad_dir, sudo=True)
801 if not os.access(breakpad_dir, os.W_OK):
802 cros_build_lib.sudo_run(["chown", "-R", str(os.getuid()), breakpad_dir])
803 debug_dir = FindDebugDir(board, sysroot=sysroot)
804 exclude_paths = [os.path.join(debug_dir, x) for x in exclude_dirs]
805 if file_list is None:
806 file_list = []
807 file_filter = dict.fromkeys([os.path.normpath(x) for x in file_list], False)
808
809 logging.info("generating breakpad symbols using %s", debug_dir)
810
811 # Let's locate all the debug_files and elfs first along with the debug file
Alex Klein8b444532023-04-11 16:35:24 -0600812 # sizes. This way we can start processing the largest files first in
813 # parallel with the small ones.
Alex Klein1699fab2022-09-08 08:46:06 -0600814 # If |file_list| was given, ignore all other files.
815 targets = []
816 for root, dirs, files in os.walk(debug_dir):
817 if root in exclude_paths:
818 logging.info("Skipping excluded dir %s", root)
819 del dirs[:]
820 continue
821
822 for debug_file in files:
823 debug_file = os.path.join(root, debug_file)
824 # Turn /build/$BOARD/usr/lib/debug/sbin/foo.debug into
825 # /build/$BOARD/sbin/foo.
826 elf_file = os.path.join(
827 sysroot, debug_file[len(debug_dir) + 1 : -6]
828 )
829
830 if file_filter:
831 if elf_file in file_filter:
832 file_filter[elf_file] = True
833 elif debug_file in file_filter:
834 file_filter[debug_file] = True
835 else:
836 continue
837
838 # Filter out files based on common issues with the debug file.
839 if not debug_file.endswith(".debug"):
840 continue
841
842 elif os.path.islink(debug_file):
843 # The build-id stuff is common enough to filter out by default.
844 if "/.build-id/" in debug_file:
845 msg = logging.debug
846 else:
847 msg = logging.warning
848 msg("Skipping symbolic link %s", debug_file)
849 continue
850
851 # Filter out files based on common issues with the elf file.
852 elf_path = os.path.relpath(elf_file, sysroot)
853 debug_only = elf_path in ALLOWED_DEBUG_ONLY_FILES
854 if not os.path.exists(elf_file) and not debug_only:
Alex Klein8b444532023-04-11 16:35:24 -0600855 # Sometimes we filter out programs from /usr/bin but leave
856 # behind the .debug file.
Alex Klein1699fab2022-09-08 08:46:06 -0600857 logging.warning("Skipping missing %s", elf_file)
858 continue
859
860 targets.append((os.path.getsize(debug_file), elf_file, debug_file))
861
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800862 with multiprocessing.Manager() as mp_manager:
863 bg_errors = parallel.WrapMultiprocessing(multiprocessing.Value, "i")
864 found_files = parallel.WrapMultiprocessing(mp_manager.list)
865 if file_filter:
866 files_not_found = [
867 x for x, found in file_filter.items() if not found
868 ]
869 bg_errors.value += len(files_not_found)
870 if files_not_found:
871 logging.error(
872 "Failed to find requested files: %s", files_not_found
873 )
Alex Klein1699fab2022-09-08 08:46:06 -0600874
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800875 # Now start generating symbols for the discovered elfs.
876 with parallel.BackgroundTaskRunner(
877 GenerateBreakpadSymbol,
878 breakpad_dir=breakpad_dir,
879 strip_cfi=strip_cfi,
880 num_errors=bg_errors,
881 processes=num_processes,
882 sysroot=sysroot,
883 found_files=found_files,
884 ) as queue:
885 for _, elf_file, debug_file in sorted(targets, reverse=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600886 if generate_count == 0:
887 break
888
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800889 queue.put([elf_file, debug_file])
890 if generate_count is not None:
891 generate_count -= 1
892 if generate_count == 0:
893 break
894
895 missing = ALL_EXPECTED_FILES - frozenset(found_files)
896 if missing and not file_filter and generate_count is None:
897 logging.warning(
898 "Not all expected files were processed successfully, "
899 "missing %s",
900 missing,
901 )
902 # TODO(b/270240549): Increment bg_errors.value here once we check
903 # that this isn't going to fail any current builds.
904
Alex Klein1699fab2022-09-08 08:46:06 -0600905 return bg_errors.value
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400906
907
Mike Frysinger3f571af2016-08-31 23:56:53 -0400908def FindDebugDir(board, sysroot=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600909 """Given a |board|, return the path to the split debug dir for it"""
910 if sysroot is None:
911 sysroot = build_target_lib.get_default_sysroot_path(board)
912 return os.path.join(sysroot, "usr", "lib", "debug")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400913
914
Mike Frysinger3f571af2016-08-31 23:56:53 -0400915def FindBreakpadDir(board, sysroot=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600916 """Given a |board|, return the path to the breakpad dir for it"""
917 return os.path.join(FindDebugDir(board, sysroot=sysroot), "breakpad")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400918
919
920def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600921 parser = commandline.ArgumentParser(description=__doc__)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400922
Alex Klein1699fab2022-09-08 08:46:06 -0600923 parser.add_argument(
924 "--board", default=None, help="board to generate symbols for"
925 )
926 parser.add_argument(
927 "--breakpad_root",
928 type="path",
929 default=None,
930 help="root output directory for breakpad symbols",
931 )
932 parser.add_argument(
933 "--sysroot",
934 type="path",
935 default=None,
936 help="root input directory for files",
937 )
938 parser.add_argument(
939 "--exclude-dir",
940 type=str,
941 action="append",
942 default=[],
943 help="directory (relative to |board| root) to not search",
944 )
945 parser.add_argument(
946 "--generate-count",
947 type=int,
948 default=None,
949 help="only generate # number of symbols",
950 )
951 parser.add_argument(
952 "--noclean",
953 dest="clean",
954 action="store_false",
955 default=True,
956 help="do not clean out breakpad dir before running",
957 )
958 parser.add_argument(
959 "--jobs", type=int, default=None, help="limit number of parallel jobs"
960 )
961 parser.add_argument(
962 "--strip_cfi",
963 action="store_true",
964 default=False,
965 help="do not generate CFI data (pass -c to dump_syms)",
966 )
967 parser.add_argument(
968 "file_list",
969 nargs="*",
970 default=None,
971 help="generate symbols for only these files "
972 "(e.g. /build/$BOARD/usr/bin/foo)",
973 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400974
Alex Klein1699fab2022-09-08 08:46:06 -0600975 opts = parser.parse_args(argv)
976 opts.Freeze()
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400977
Alex Klein1699fab2022-09-08 08:46:06 -0600978 if opts.board is None and opts.sysroot is None:
979 cros_build_lib.Die("--board or --sysroot is required")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400980
Alex Klein1699fab2022-09-08 08:46:06 -0600981 ret = GenerateBreakpadSymbols(
982 opts.board,
983 breakpad_dir=opts.breakpad_root,
984 strip_cfi=opts.strip_cfi,
985 generate_count=opts.generate_count,
986 sysroot=opts.sysroot,
987 num_processes=opts.jobs,
988 clean_breakpad=opts.clean,
989 exclude_dirs=opts.exclude_dir,
990 file_list=opts.file_list,
991 )
992 if ret:
993 logging.error("encountered %i problem(s)", ret)
Alex Klein8b444532023-04-11 16:35:24 -0600994 # Since exit(status) gets masked, clamp it to 1 so we don't
995 # inadvertently return 0 in case we are a multiple of the mask.
Alex Klein1699fab2022-09-08 08:46:06 -0600996 ret = 1
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400997
Alex Klein1699fab2022-09-08 08:46:06 -0600998 return ret