Skip to content

Commit 9973aa3

Browse files
authored
Merge pull request #1579 from DataDog/nsavoire/fix_elf_dynamic_symbols
Fix upload of ELF dynamic symbols
2 parents 72cdf62 + 37dcfdf commit 9973aa3

File tree

6 files changed

+77
-39
lines changed

6 files changed

+77
-39
lines changed

src/commands/elf-symbols/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,4 @@ If location is a file, command will split debug info from it and upload them to
5050
| `--disable-git` | Optional | Prevents the command from invoking Git in the current working directory and sending repository-related data to Datadog (such as the hash, remote URL, and paths within the repository of sources referenced in the source map). |
5151
| `--repository-url` | Optional | Overrides the remote repository with a custom URL. For example, `https://github.com/my-company/my-project`. |
5252
| `--replace-existing` | Optional | If symbol information with the same build ID is already present on Datadog side, discard it and use the newly uploaded information.<br>Default behavior is to only replace existing debug information if the newly uploaded information is considered a better source with the following ordering: debug info > symbol table > dynamic symbol table. |
53-
| `--upload-dynamic-symbols` | Optional | Upload dynamic symbol information if neither debug information nor symbol table are present but a dynamic symbol table is available.<br>Default behavior is to upload symbol information only when a symbol table or debug information are present, since dynamic symbol table as less information and only contains exported symbols. |
53+
| `--upload-dynamic-symbols` | Optional | Upload dynamic symbol information if neither debug information nor symbol table are present but a dynamic symbol table is available.<br>Default behavior is to upload symbol information only when a symbol table or debug information are present, since dynamic symbol table has less information and only contains exported symbols. |

src/commands/elf-symbols/__tests__/elf.test.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,7 @@ describe('elf', () => {
736736
goBuildId: '',
737737
elfType: 'DYN',
738738
hasDebugInfo: true,
739-
hasDynamicSymbolTable: true,
739+
hasDynamicSymbolTable: false,
740740
hasSymbolTable: true,
741741
hasCode: false,
742742
})
@@ -831,15 +831,19 @@ describe('elf', () => {
831831
const filename = path.basename(elfFile)
832832
const outputFilename = `${tmpDirectory}/${filename}.debug`
833833
const elfFileMetadata = await getElfFileMetadata(elfFile)
834-
await copyElfDebugInfo(elfFile, outputFilename, elfFileMetadata, false)
834+
await copyElfDebugInfo(elfFile, outputFilename, elfFileMetadata, true)
835835
const debugInfoMetadata = await getElfFileMetadata(outputFilename)
836836

837837
// check that elf and debug info metadata are equal except for hasCode and filename
838+
// dynamic symbol table is kept only if there is no debug info nor symbol table
839+
const hasDynamicSymbolTable =
840+
!elfFileMetadata.hasDebugInfo && !elfFileMetadata.hasSymbolTable && elfFileMetadata.hasDynamicSymbolTable
838841
expect(debugInfoMetadata).toEqual({
839842
...elfFileMetadata,
840843
hasCode: false,
841844
filename: outputFilename,
842845
fileHash: debugInfoMetadata.fileHash,
846+
hasDynamicSymbolTable,
843847
})
844848
}
845849

Binary file not shown.

src/commands/elf-symbols/__tests__/upload.test.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ describe('elf-symbols upload', () => {
114114
`${fixtureDir}/exec_arm_little`,
115115
`${fixtureDir}/go_x86_64_both_gnu_and_go_build_id`,
116116
`${fixtureDir}/go_x86_64_only_go_build_id`,
117-
`${fixtureDir}/go_x86_64_only_go_build_id.debug`,
118117
])
119118
})
120119

@@ -131,12 +130,17 @@ describe('elf-symbols upload', () => {
131130
test('should not throw an error when a directory (except top-level) is not readable', async () => {
132131
const command = createCommand(UploadCommand)
133132
let tmpDir
133+
let tmpSubDir
134134
try {
135135
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'elf-tests-'))
136-
const tmpSubDir = fs.mkdtempSync(path.join(tmpDir, 'unreadable-'))
136+
tmpSubDir = fs.mkdtempSync(path.join(tmpDir, 'unreadable-'))
137137
fs.chmodSync(tmpSubDir, 0o200)
138138
await expect(command['getElfSymbolFiles'](tmpDir)).resolves.toEqual([])
139139
} finally {
140+
if (tmpSubDir) {
141+
// node 23 fails to remove the directory if it's not readable
142+
fs.chmodSync(tmpSubDir, 0o700)
143+
}
140144
if (tmpDir) {
141145
fs.rmSync(tmpDir, {recursive: true})
142146
}
@@ -263,17 +267,17 @@ describe('elf-symbols upload', () => {
263267
},
264268
{
265269
...commonMetadata,
266-
file_hash: '',
270+
file_hash: '5ba2907faebb8002de89711d5f0f005c',
267271
gnu_build_id: '90aef8b4a3cd45d758501e49d1d9844736c872cd',
268272
go_build_id: '',
269273
arch: 'aarch64',
270-
filename: 'dyn_aarch64.debug',
274+
filename: 'dyn_aarch64',
271275
symbol_source: 'debug_info',
272276
},
273277
{
274278
...commonMetadata,
275-
file_hash: 'e8a12b7f5702d7a4f92da4983d75e9af',
276-
gnu_build_id: 'a8ac08faa0d114aa65f1ee0730af38903ac506de',
279+
file_hash: '40a0f8378cf61c89c325a397edaa0dd2',
280+
gnu_build_id: '90aef8b4a3cd45d758501e49d1d9844736c872cd',
277281
go_build_id: '',
278282
arch: 'x86_64',
279283
filename: 'dyn_x86_64',
@@ -317,7 +321,7 @@ describe('elf-symbols upload', () => {
317321

318322
return JSON.parse((payload.content.get('event') as MultipartStringValue).value)
319323
})
320-
const getId = (m: any) => m.gnu_build_id || m.go_build_id || m.file_hash
324+
const getId = (m: any) => ((m.gnu_build_id || m.go_build_id || m.file_hash) as string) + '-' + (m.arch as string)
321325
metadata.sort((a, b) => getId(a).localeCompare(getId(b)))
322326
expectedMetadata.sort((a, b) => getId(a).localeCompare(getId(b)))
323327
expect(metadata).toEqual(expectedMetadata)

src/commands/elf-symbols/elf.ts

+55-25
Original file line numberDiff line numberDiff line change
@@ -421,12 +421,16 @@ export const isSupportedElfType = (type: string): boolean => {
421421
return SUPPORTED_ELF_TYPES.includes(type)
422422
}
423423

424+
export const hasNonEmptySection = (sectionHeaders: SectionHeader[], name: string): boolean => {
425+
return sectionHeaders.some((section) => section.name === name && section.sh_type !== SectionHeaderType.SHT_NOBITS)
426+
}
427+
424428
export const getSectionInfo = (
425429
sections: SectionHeader[]
426430
): {hasDebugInfo: boolean; hasSymbolTable: boolean; hasDynamicSymbolTable: boolean; hasCode: boolean} => {
427-
const hasDebugInfo = sections.some((section) => section.name === '.debug_info')
428-
const hasSymbolTable = sections.some((section) => section.name === '.symtab')
429-
const hasDynamicSymbolTable = sections.some((section) => section.name === '.dynsym')
431+
const hasDebugInfo = hasNonEmptySection(sections, '.debug_info') || hasNonEmptySection(sections, '.zdebug_info')
432+
const hasSymbolTable = hasNonEmptySection(sections, '.symtab')
433+
const hasDynamicSymbolTable = hasNonEmptySection(sections, '.dynsym')
430434
const hasCode = sections.some(
431435
(section) => section.name === '.text' && section.sh_type === SectionHeaderType.SHT_PROGBITS
432436
)
@@ -523,29 +527,32 @@ export const computeFileHash = async (filename: string): Promise<string> => {
523527
}
524528
}
525529

526-
const getSupportedBfdTargetsInternal = async (): Promise<string[]> => {
530+
const hasZstdSupport = async (): Promise<boolean> => {
531+
const {stdout} = await execute('objcopy --help')
532+
533+
return /--compress-debug-sections.*zstd/.test(stdout.toString())
534+
}
535+
536+
const memoize = <T>(fn: () => Promise<T>): (() => Promise<T>) => {
537+
let promise: Promise<T> | undefined
538+
539+
return () => (promise = promise || fn())
540+
}
541+
542+
const hasZstdSupportCached = memoize(hasZstdSupport)
543+
544+
const getSupportedBfdTargets = async (): Promise<string[]> => {
527545
const {stdout} = await execute('objcopy --help')
528546

529547
const groups = /supported targets: (?<targets>.*)$/m.exec(stdout.toString())?.groups
530548
if (groups) {
531-
return groups.targets.split(/\s*/)
549+
return groups.targets.split(/\s+/)
532550
}
533551

534552
return []
535553
}
536554

537-
const getSupportedBfdTargets = (() => {
538-
let promise: Promise<string[]> | undefined
539-
540-
return () =>
541-
(promise =
542-
promise ||
543-
(async () => {
544-
const targets = await getSupportedBfdTargetsInternal()
545-
546-
return targets
547-
})())
548-
})()
555+
const getSupportedBfdTargetsCached = memoize(getSupportedBfdTargets)
549556

550557
const replaceElfHeader = async (targetFilename: string, sourceFilename: string): Promise<void> => {
551558
const sourceElfHeader = await getElfHeaderStart(sourceFilename)
@@ -560,7 +567,7 @@ export const copyElfDebugInfo = async (
560567
elfFileMetadata: ElfFileMetadata,
561568
compressDebugSections: boolean
562569
): Promise<void> => {
563-
const supportedTargets = await getSupportedBfdTargets()
570+
const supportedTargets = await getSupportedBfdTargetsCached()
564571

565572
let bfdTargetOption = ''
566573
const bfdTarget = getBFDTargetForArch(elfFileMetadata.arch, elfFileMetadata.littleEndian, elfFileMetadata.elfClass)
@@ -574,15 +581,38 @@ export const copyElfDebugInfo = async (
574581
bfdTargetOption = `-I ${genericBfdTarget}`
575582
}
576583

577-
const compressDebugSectionsOption = compressDebugSections ? '--compress-debug-sections' : ''
584+
let compressDebugSectionsOption = ''
585+
if (compressDebugSections) {
586+
compressDebugSectionsOption = '--compress-debug-sections'
587+
if (await hasZstdSupportCached()) {
588+
compressDebugSectionsOption += '=zstd'
589+
}
590+
}
591+
592+
const keepDynamicSymbolTable =
593+
elfFileMetadata.hasDynamicSymbolTable && !elfFileMetadata.hasSymbolTable && !elfFileMetadata.hasDebugInfo
578594

579595
// Remove .gdb_index section as it is not needed and can be quite big
580-
await execute(
581-
`objcopy ${bfdTargetOption} --only-keep-debug ${compressDebugSectionsOption} --remove-section=.gdb_index ${filename} ${outputFile}`
582-
)
596+
let options = `${bfdTargetOption} --only-keep-debug ${compressDebugSectionsOption} --remove-section=.gdb_index`
597+
598+
if (keepDynamicSymbolTable) {
599+
// If the file has only a dynamic symbol table, preserve it
600+
// `objcopy --only-keep-debug` would remove it, so we need to dump it separately with `objcopy --dump-section` and then merge it back with `objcopy --add-section`
601+
await execute(
602+
`objcopy ${bfdTargetOption} --dump-section .dynsym=${outputFile}.dynsym --dump-section .dynstr=${outputFile}.dynstr ${filename} ${outputFile}`
603+
)
604+
options = `--remove-section .dynsym --remove-section .dynstr ${options} --add-section .dynsym=${outputFile}.dynsym --add-section .dynstr=${outputFile}.dynstr`
605+
}
606+
607+
await execute(`objcopy ${options} ${filename} ${outputFile}`).finally(() => {
608+
if (keepDynamicSymbolTable) {
609+
fs.unlinkSync(`${outputFile}.dynsym`)
610+
fs.unlinkSync(`${outputFile}.dynstr`)
611+
}
612+
})
583613

584614
if (bfdTargetOption) {
585-
// Replace the ELF header in the extracted debug info file with the one from the initial file
615+
// Replace the ELF header in the extracted debug info file with the one from the initial file to keep the original architecture
586616
await replaceElfHeader(outputFile, filename)
587617
}
588618
}
@@ -592,6 +622,6 @@ export const getOutputFilenameFromBuildId = (buildId: string): string => {
592622
return buildId.replace(/\//g, '-')
593623
}
594624

595-
export const getBuildId = (fileMetadata: ElfFileMetadata): string => {
596-
return fileMetadata.gnuBuildId || fileMetadata.goBuildId || fileMetadata.fileHash
625+
export const getBuildIdWithArch = (fileMetadata: ElfFileMetadata): string => {
626+
return (fileMetadata.gnuBuildId || fileMetadata.goBuildId || fileMetadata.fileHash) + '-' + fileMetadata.arch
597627
}

src/commands/elf-symbols/upload.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
ElfFileMetadata,
2424
getElfFileMetadata,
2525
isSupportedElfType,
26-
getBuildId,
26+
getBuildIdWithArch,
2727
getOutputFilenameFromBuildId,
2828
copyElfDebugInfo,
2929
isSupportedArch,
@@ -282,7 +282,7 @@ export class UploadCommand extends Command {
282282
private removeBuildIdDuplicates(filesMetadata: ElfFileMetadata[]): ElfFileMetadata[] {
283283
const buildIds = new Map<string, ElfFileMetadata>()
284284
for (const metadata of filesMetadata) {
285-
const buildId = getBuildId(metadata)
285+
const buildId = getBuildIdWithArch(metadata)
286286
const existing = buildIds.get(buildId)
287287
if (existing) {
288288
if (
@@ -328,9 +328,9 @@ export class UploadCommand extends Command {
328328
try {
329329
const results = await doWithMaxConcurrency(this.maxConcurrency, elfFilesMetadata, async (fileMetadata) => {
330330
const metadata = this.getMappingMetadata(fileMetadata)
331-
const outputFilename = getOutputFilenameFromBuildId(getBuildId(fileMetadata))
331+
const outputFilename = getOutputFilenameFromBuildId(getBuildIdWithArch(fileMetadata))
332332
const outputFilePath = buildPath(tmpDirectory, outputFilename)
333-
await copyElfDebugInfo(fileMetadata.filename, outputFilePath, fileMetadata, false)
333+
await copyElfDebugInfo(fileMetadata.filename, outputFilePath, fileMetadata, true)
334334

335335
if (this.dryRun) {
336336
this.context.stdout.write(`[DRYRUN] ${renderUpload(fileMetadata.filename, metadata)}`)

0 commit comments

Comments
 (0)