blob: 44f6be1fd96a7e245dc680d727832805af68858e [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-Yeungfdaaf2e2023-03-02 17:30:39 -0800512 return True
513
514
515def ReadSymsHeader(sym_file, name_for_errors):
Alex Klein1699fab2022-09-08 08:46:06 -0600516 """Parse the header of the symbol file
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400517
Alex Klein1699fab2022-09-08 08:46:06 -0600518 The first line of the syms file will read like:
Alex Klein8b444532023-04-11 16:35:24 -0600519 MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F40 blkid
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400520
Alex Klein1699fab2022-09-08 08:46:06 -0600521 https://code.google.com/p/google-breakpad/wiki/SymbolFiles
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400522
Alex Klein1699fab2022-09-08 08:46:06 -0600523 Args:
Alex Klein8b444532023-04-11 16:35:24 -0600524 sym_file: The symbol file to parse
525 name_for_errors: A name for error strings. Can be the name of the elf
526 file that generated the symbol file, or the name of the symbol file
527 if the symbol file has already been moved to a meaningful location.
Mike Frysinger1a736a82013-12-12 01:50:59 -0500528
Alex Klein1699fab2022-09-08 08:46:06 -0600529 Returns:
Alex Klein8b444532023-04-11 16:35:24 -0600530 A SymbolHeader object
Mike Frysinger1a736a82013-12-12 01:50:59 -0500531
Alex Klein1699fab2022-09-08 08:46:06 -0600532 Raises:
Alex Klein8b444532023-04-11 16:35:24 -0600533 ValueError if the first line of |sym_file| is invalid
Alex Klein1699fab2022-09-08 08:46:06 -0600534 """
535 with file_util.Open(sym_file, "rb") as f:
536 header = f.readline().decode("utf-8").split()
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400537
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800538 if len(header) != 5 or header[0] != "MODULE":
539 raise ValueError(
540 f"header of sym file from {name_for_errors} is invalid"
541 )
Mike Frysinger50cedd32014-02-09 23:03:18 -0500542
Alex Klein1699fab2022-09-08 08:46:06 -0600543 return SymbolHeader(
544 os=header[1], cpu=header[2], id=header[3], name=header[4]
545 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400546
547
Alex Klein1699fab2022-09-08 08:46:06 -0600548def GenerateBreakpadSymbol(
549 elf_file,
550 debug_file=None,
551 breakpad_dir=None,
552 strip_cfi=False,
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800553 sysroot=None,
Alex Klein1699fab2022-09-08 08:46:06 -0600554 num_errors=None,
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800555 found_files=None,
Alex Klein1699fab2022-09-08 08:46:06 -0600556 dump_syms_cmd="dump_syms",
557):
558 """Generate the symbols for |elf_file| using |debug_file|
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400559
Alex Klein1699fab2022-09-08 08:46:06 -0600560 Args:
Alex Klein8b444532023-04-11 16:35:24 -0600561 elf_file: The file to dump symbols for
562 debug_file: Split debug file to use for symbol information
563 breakpad_dir: The dir to store the output symbol file in
564 strip_cfi: Do not generate CFI data
565 sysroot: Path to the sysroot with the elf_file under it
566 num_errors: An object to update with the error count (needs a .value
567 member).
568 found_files: A multiprocessing.managers.ListProxy list containing
569 ExpectedFiles, representing which of the "should always be present"
570 files have been processed.
571 dump_syms_cmd: Command to use for dumping symbols.
Mike Frysinger1a736a82013-12-12 01:50:59 -0500572
Alex Klein1699fab2022-09-08 08:46:06 -0600573 Returns:
Alex Klein8b444532023-04-11 16:35:24 -0600574 The name of symbol file written out on success, or the failure count.
Alex Klein1699fab2022-09-08 08:46:06 -0600575 """
576 assert breakpad_dir
577 if num_errors is None:
578 num_errors = ctypes.c_int()
579 debug_file_only = not os.path.exists(elf_file)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400580
Manoj Gupta68546b02023-05-01 21:55:19 +0000581 cmd_base = [dump_syms_cmd, "-v"]
Alex Klein1699fab2022-09-08 08:46:06 -0600582 if strip_cfi:
583 cmd_base += ["-c"]
584 # Some files will not be readable by non-root (e.g. set*id /bin/su).
585 needs_sudo = not os.access(elf_file, os.R_OK)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400586
Alex Klein1699fab2022-09-08 08:46:06 -0600587 def _DumpIt(cmd_args):
588 if needs_sudo:
589 run_command = cros_build_lib.sudo_run
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400590 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600591 run_command = cros_build_lib.run
592 return run_command(
593 cmd_base + cmd_args,
594 stderr=True,
595 stdout=temp.name,
596 check=False,
597 debug_level=logging.DEBUG,
598 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400599
Alex Klein1699fab2022-09-08 08:46:06 -0600600 def _CrashCheck(result, file_or_files, msg):
601 if result.returncode:
602 cbuildbot_alerts.PrintBuildbotStepWarnings()
603 if result.returncode < 0:
604 logging.warning(
605 "dump_syms %s crashed with %s; %s",
606 file_or_files,
607 signals.StrSignal(-result.returncode),
608 msg,
609 )
610 else:
611 logging.warning(
612 "dump_syms %s returned %d; %s",
613 file_or_files,
614 result.returncode,
615 msg,
616 )
617 logging.warning("output:\n%s", result.stderr.decode("utf-8"))
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400618
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800619 def _DumpAllowingBasicFallback():
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800620 """Dump symbols for an ELF when we do NOT expect to get good symbols.
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800621
622 Returns:
623 A SymbolGenerationResult
624 """
Alex Klein1699fab2022-09-08 08:46:06 -0600625 if debug_file:
626 # Try to dump the symbols using the debug file like normal.
627 if debug_file_only:
628 cmd_args = [debug_file]
629 file_or_files = debug_file
630 else:
631 cmd_args = [elf_file, os.path.dirname(debug_file)]
632 file_or_files = [elf_file, debug_file]
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400633
Alex Klein1699fab2022-09-08 08:46:06 -0600634 result = _DumpIt(cmd_args)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400635
Alex Klein1699fab2022-09-08 08:46:06 -0600636 if result.returncode:
637 # Sometimes dump_syms can crash because there's too much info.
638 # Try dumping and stripping the extended stuff out. At least
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800639 # this way we'll get the extended symbols.
640 # https://crbug.com/266064
Alex Klein1699fab2022-09-08 08:46:06 -0600641 _CrashCheck(result, file_or_files, "retrying w/out CFI")
642 cmd_args = ["-c", "-r"] + cmd_args
643 result = _DumpIt(cmd_args)
644 _CrashCheck(result, file_or_files, "retrying w/out debug")
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700645
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800646 if not result.returncode:
647 return SymbolGenerationResult.SUCCESS
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700648
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800649 # If that didn't work (no debug, or dump_syms still failed), try
650 # dumping just the file itself directly.
651 result = _DumpIt([elf_file])
652 if result.returncode:
653 # A lot of files (like kernel files) contain no debug information,
654 # do not consider such occurrences as errors.
655 cbuildbot_alerts.PrintBuildbotStepWarnings()
656 if b"file contains no debugging information" in result.stderr:
657 logging.warning("dump_syms failed; giving up entirely.")
658 logging.warning("No symbols found for %s", elf_file)
659 return SymbolGenerationResult.EXPECTED_FAILURE
660 else:
661 _CrashCheck(result, elf_file, "counting as failure")
662 return SymbolGenerationResult.UNEXPECTED_FAILURE
663
664 return SymbolGenerationResult.SUCCESS
665
666 def _DumpExpectingSymbols():
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800667 """Dump symbols for an ELF when we expect to get good symbols.
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800668
669 Returns:
670 A SymbolGenerationResult. We never expect failure, so the result
671 will always be SUCCESS or UNEXPECTED_FAILURE.
672 """
673 if not debug_file:
674 logging.warning("%s must have debug file", elf_file)
675 return SymbolGenerationResult.UNEXPECTED_FAILURE
676
677 cmd_args = [elf_file, os.path.dirname(debug_file)]
678 result = _DumpIt(cmd_args)
679 if result.returncode:
Ian Barkley-Yeungab1dab12023-04-21 18:19:55 -0700680 _CrashCheck(
681 result,
682 [elf_file, debug_file],
683 "unexpected symbol generation failure",
684 )
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800685 return SymbolGenerationResult.UNEXPECTED_FAILURE
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800686
687 # TODO(b/270240549): Remove try/except, allow exceptions to just
688 # fail the script. The try/except is just here until we are sure this
689 # will not break the build.
690 try:
691 if not ValidateSymbolFile(
692 temp.name, elf_file, sysroot, found_files
693 ):
694 logging.warning("%s: symbol file failed validation", elf_file)
695 return SymbolGenerationResult.UNEXPECTED_FAILURE
696 except ValueError as e:
697 logging.warning(
698 "%s: symbol file failed validation due to exception %s",
699 elf_file,
700 e,
701 )
702 return SymbolGenerationResult.UNEXPECTED_FAILURE
703
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800704 return SymbolGenerationResult.SUCCESS
705
706 osutils.SafeMakedirs(breakpad_dir)
707 with cros_build_lib.UnbufferedNamedTemporaryFile(
708 dir=breakpad_dir, delete=False
709 ) as temp:
710 if _ExpectGoodSymbols(elf_file, sysroot):
711 result = _DumpExpectingSymbols()
712 # Until the EXPECTED_POOR_SYMBOLIZATION_FILES allowlist is
713 # completely set up for all boards, don't fail the build if
714 # _ExpectGoodSymbols is wrong.
715 # TODO(b/241470012): Remove the call to _DumpAllowingBasicFallback()
716 # and just error out if _DumpExpectingSymbols fails.
717 if result == SymbolGenerationResult.UNEXPECTED_FAILURE:
718 result = _DumpAllowingBasicFallback()
719 else:
720 result = _DumpAllowingBasicFallback()
721
722 if result == SymbolGenerationResult.UNEXPECTED_FAILURE:
723 num_errors.value += 1
724 os.unlink(temp.name)
725 return num_errors.value
726
727 if result == SymbolGenerationResult.EXPECTED_FAILURE:
728 os.unlink(temp.name)
729 return 0
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700730
Alex Klein1699fab2022-09-08 08:46:06 -0600731 # Move the dumped symbol file to the right place:
Alex Klein8b444532023-04-11 16:35:24 -0600732 # /$SYSROOT/usr/lib/debug/breakpad/<module-name>/<id>/<module-name>.sym
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800733 header = ReadSymsHeader(temp, elf_file)
Alex Klein1699fab2022-09-08 08:46:06 -0600734 logging.info("Dumped %s as %s : %s", elf_file, header.name, header.id)
735 sym_file = os.path.join(
736 breakpad_dir, header.name, header.id, header.name + ".sym"
737 )
738 osutils.SafeMakedirs(os.path.dirname(sym_file))
739 os.rename(temp.name, sym_file)
740 os.chmod(sym_file, 0o644)
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700741
Alex Klein1699fab2022-09-08 08:46:06 -0600742 return sym_file
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400743
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700744
Alex Klein1699fab2022-09-08 08:46:06 -0600745def GenerateBreakpadSymbols(
746 board,
747 breakpad_dir=None,
748 strip_cfi=False,
749 generate_count=None,
750 sysroot=None,
751 num_processes=None,
752 clean_breakpad=False,
753 exclude_dirs=(),
754 file_list=None,
755):
756 """Generate symbols for this board.
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700757
Alex Klein1699fab2022-09-08 08:46:06 -0600758 If |file_list| is None, symbols are generated for all executables, otherwise
759 only for the files included in |file_list|.
Prathmesh Prabhu9995e9b2013-10-31 16:43:55 -0700760
Alex Klein1699fab2022-09-08 08:46:06 -0600761 TODO(build):
762 This should be merged with buildbot_commands.GenerateBreakpadSymbols()
763 once we rewrite cros_generate_breakpad_symbols in python.
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400764
Alex Klein1699fab2022-09-08 08:46:06 -0600765 Args:
Alex Klein8b444532023-04-11 16:35:24 -0600766 board: The board whose symbols we wish to generate
767 breakpad_dir: The full path to the breakpad directory where symbols live
768 strip_cfi: Do not generate CFI data
769 generate_count: If set, only generate this many symbols (meant for
770 testing)
771 sysroot: The root where to find the corresponding ELFs
772 num_processes: Number of jobs to run in parallel
773 clean_breakpad: Should we `rm -rf` the breakpad output dir first; note:
774 we do not do any locking, so do not run more than one in parallel
775 when True
776 exclude_dirs: List of dirs (relative to |sysroot|) to not search
777 file_list: Only generate symbols for files in this list. Each file must
778 be a full path (including |sysroot| prefix).
779 TODO(build): Support paths w/o |sysroot|.
Alex Klein1699fab2022-09-08 08:46:06 -0600780
781 Returns:
Alex Klein8b444532023-04-11 16:35:24 -0600782 The number of errors that were encountered.
Alex Klein1699fab2022-09-08 08:46:06 -0600783 """
784 if sysroot is None:
785 sysroot = build_target_lib.get_default_sysroot_path(board)
786 if breakpad_dir is None:
787 breakpad_dir = FindBreakpadDir(board, sysroot=sysroot)
788 if clean_breakpad:
789 logging.info("cleaning out %s first", breakpad_dir)
790 osutils.RmDir(breakpad_dir, ignore_missing=True, sudo=True)
791 # Make sure non-root can write out symbols as needed.
792 osutils.SafeMakedirs(breakpad_dir, sudo=True)
793 if not os.access(breakpad_dir, os.W_OK):
794 cros_build_lib.sudo_run(["chown", "-R", str(os.getuid()), breakpad_dir])
795 debug_dir = FindDebugDir(board, sysroot=sysroot)
796 exclude_paths = [os.path.join(debug_dir, x) for x in exclude_dirs]
797 if file_list is None:
798 file_list = []
799 file_filter = dict.fromkeys([os.path.normpath(x) for x in file_list], False)
800
801 logging.info("generating breakpad symbols using %s", debug_dir)
802
803 # Let's locate all the debug_files and elfs first along with the debug file
Alex Klein8b444532023-04-11 16:35:24 -0600804 # sizes. This way we can start processing the largest files first in
805 # parallel with the small ones.
Alex Klein1699fab2022-09-08 08:46:06 -0600806 # If |file_list| was given, ignore all other files.
807 targets = []
808 for root, dirs, files in os.walk(debug_dir):
809 if root in exclude_paths:
810 logging.info("Skipping excluded dir %s", root)
811 del dirs[:]
812 continue
813
814 for debug_file in files:
815 debug_file = os.path.join(root, debug_file)
816 # Turn /build/$BOARD/usr/lib/debug/sbin/foo.debug into
817 # /build/$BOARD/sbin/foo.
818 elf_file = os.path.join(
819 sysroot, debug_file[len(debug_dir) + 1 : -6]
820 )
821
822 if file_filter:
823 if elf_file in file_filter:
824 file_filter[elf_file] = True
825 elif debug_file in file_filter:
826 file_filter[debug_file] = True
827 else:
828 continue
829
830 # Filter out files based on common issues with the debug file.
831 if not debug_file.endswith(".debug"):
832 continue
833
834 elif os.path.islink(debug_file):
835 # The build-id stuff is common enough to filter out by default.
836 if "/.build-id/" in debug_file:
837 msg = logging.debug
838 else:
839 msg = logging.warning
840 msg("Skipping symbolic link %s", debug_file)
841 continue
842
843 # Filter out files based on common issues with the elf file.
844 elf_path = os.path.relpath(elf_file, sysroot)
845 debug_only = elf_path in ALLOWED_DEBUG_ONLY_FILES
846 if not os.path.exists(elf_file) and not debug_only:
Alex Klein8b444532023-04-11 16:35:24 -0600847 # Sometimes we filter out programs from /usr/bin but leave
848 # behind the .debug file.
Alex Klein1699fab2022-09-08 08:46:06 -0600849 logging.warning("Skipping missing %s", elf_file)
850 continue
851
852 targets.append((os.path.getsize(debug_file), elf_file, debug_file))
853
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800854 with multiprocessing.Manager() as mp_manager:
855 bg_errors = parallel.WrapMultiprocessing(multiprocessing.Value, "i")
856 found_files = parallel.WrapMultiprocessing(mp_manager.list)
857 if file_filter:
858 files_not_found = [
859 x for x, found in file_filter.items() if not found
860 ]
861 bg_errors.value += len(files_not_found)
862 if files_not_found:
863 logging.error(
864 "Failed to find requested files: %s", files_not_found
865 )
Alex Klein1699fab2022-09-08 08:46:06 -0600866
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800867 # Now start generating symbols for the discovered elfs.
868 with parallel.BackgroundTaskRunner(
869 GenerateBreakpadSymbol,
870 breakpad_dir=breakpad_dir,
871 strip_cfi=strip_cfi,
872 num_errors=bg_errors,
873 processes=num_processes,
874 sysroot=sysroot,
875 found_files=found_files,
876 ) as queue:
877 for _, elf_file, debug_file in sorted(targets, reverse=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600878 if generate_count == 0:
879 break
880
Ian Barkley-Yeungc5b6f582023-03-08 18:23:07 -0800881 queue.put([elf_file, debug_file])
882 if generate_count is not None:
883 generate_count -= 1
884 if generate_count == 0:
885 break
886
887 missing = ALL_EXPECTED_FILES - frozenset(found_files)
888 if missing and not file_filter and generate_count is None:
889 logging.warning(
890 "Not all expected files were processed successfully, "
891 "missing %s",
892 missing,
893 )
894 # TODO(b/270240549): Increment bg_errors.value here once we check
895 # that this isn't going to fail any current builds.
896
Alex Klein1699fab2022-09-08 08:46:06 -0600897 return bg_errors.value
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400898
899
Mike Frysinger3f571af2016-08-31 23:56:53 -0400900def FindDebugDir(board, sysroot=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600901 """Given a |board|, return the path to the split debug dir for it"""
902 if sysroot is None:
903 sysroot = build_target_lib.get_default_sysroot_path(board)
904 return os.path.join(sysroot, "usr", "lib", "debug")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400905
906
Mike Frysinger3f571af2016-08-31 23:56:53 -0400907def FindBreakpadDir(board, sysroot=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600908 """Given a |board|, return the path to the breakpad dir for it"""
909 return os.path.join(FindDebugDir(board, sysroot=sysroot), "breakpad")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400910
911
912def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600913 parser = commandline.ArgumentParser(description=__doc__)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400914
Alex Klein1699fab2022-09-08 08:46:06 -0600915 parser.add_argument(
916 "--board", default=None, help="board to generate symbols for"
917 )
918 parser.add_argument(
919 "--breakpad_root",
920 type="path",
921 default=None,
922 help="root output directory for breakpad symbols",
923 )
924 parser.add_argument(
925 "--sysroot",
926 type="path",
927 default=None,
928 help="root input directory for files",
929 )
930 parser.add_argument(
931 "--exclude-dir",
932 type=str,
933 action="append",
934 default=[],
935 help="directory (relative to |board| root) to not search",
936 )
937 parser.add_argument(
938 "--generate-count",
939 type=int,
940 default=None,
941 help="only generate # number of symbols",
942 )
943 parser.add_argument(
944 "--noclean",
945 dest="clean",
946 action="store_false",
947 default=True,
948 help="do not clean out breakpad dir before running",
949 )
950 parser.add_argument(
951 "--jobs", type=int, default=None, help="limit number of parallel jobs"
952 )
953 parser.add_argument(
954 "--strip_cfi",
955 action="store_true",
956 default=False,
957 help="do not generate CFI data (pass -c to dump_syms)",
958 )
959 parser.add_argument(
960 "file_list",
961 nargs="*",
962 default=None,
963 help="generate symbols for only these files "
964 "(e.g. /build/$BOARD/usr/bin/foo)",
965 )
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400966
Alex Klein1699fab2022-09-08 08:46:06 -0600967 opts = parser.parse_args(argv)
968 opts.Freeze()
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400969
Alex Klein1699fab2022-09-08 08:46:06 -0600970 if opts.board is None and opts.sysroot is None:
971 cros_build_lib.Die("--board or --sysroot is required")
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400972
Alex Klein1699fab2022-09-08 08:46:06 -0600973 ret = GenerateBreakpadSymbols(
974 opts.board,
975 breakpad_dir=opts.breakpad_root,
976 strip_cfi=opts.strip_cfi,
977 generate_count=opts.generate_count,
978 sysroot=opts.sysroot,
979 num_processes=opts.jobs,
980 clean_breakpad=opts.clean,
981 exclude_dirs=opts.exclude_dir,
982 file_list=opts.file_list,
983 )
984 if ret:
985 logging.error("encountered %i problem(s)", ret)
Alex Klein8b444532023-04-11 16:35:24 -0600986 # Since exit(status) gets masked, clamp it to 1 so we don't
987 # inadvertently return 0 in case we are a multiple of the mask.
Alex Klein1699fab2022-09-08 08:46:06 -0600988 ret = 1
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400989
Alex Klein1699fab2022-09-08 08:46:06 -0600990 return ret