1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
|
# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import io
from collections import OrderedDict
import pytest
import llnl.util.filesystem as fs
import spack.platforms
import spack.util.elf as elf
import spack.util.executable
from spack.hooks.drop_redundant_rpaths import drop_redundant_rpaths
# note that our elf parser is platform independent... but I guess creating an elf file
# is slightly more difficult with system tools on non-linux.
def skip_unless_linux(f):
return pytest.mark.skipif(
str(spack.platforms.real_host()) != "linux",
reason="implementation currently requires linux",
)(f)
@pytest.mark.requires_executables("gcc")
@skip_unless_linux
@pytest.mark.parametrize(
"linker_flag,is_runpath",
[("-Wl,--disable-new-dtags", False), ("-Wl,--enable-new-dtags", True)],
)
def test_elf_parsing_shared_linking(linker_flag, is_runpath, tmpdir):
gcc = spack.util.executable.which("gcc")
with fs.working_dir(str(tmpdir)):
# Create a library to link to so we can force a dynamic section in an ELF file
with open("foo.c", "w") as f:
f.write("int foo(){return 0;}")
with open("bar.c", "w") as f:
f.write("int foo(); int _start(){return foo();}")
# Create library and executable linking to it.
gcc("-shared", "-o", "libfoo.so", "-Wl,-soname,libfoo.so.1", "-nostdlib", "foo.c")
gcc(
"-o",
"bar",
linker_flag,
"-Wl,-rpath,/first",
"-Wl,-rpath,/second",
"-Wl,--no-as-needed",
"-nostdlib",
"libfoo.so",
"bar.c",
"-o",
"bar",
)
with open("libfoo.so", "rb") as f:
foo_parsed = elf.parse_elf(f, interpreter=True, dynamic_section=True)
assert not foo_parsed.has_pt_interp
assert foo_parsed.has_pt_dynamic
assert not foo_parsed.has_rpath
assert not foo_parsed.has_needed
assert foo_parsed.has_soname
assert foo_parsed.dt_soname_str == b"libfoo.so.1"
with open("bar", "rb") as f:
bar_parsed = elf.parse_elf(f, interpreter=True, dynamic_section=True)
assert bar_parsed.has_pt_interp
assert bar_parsed.has_pt_dynamic
assert bar_parsed.has_rpath
assert bar_parsed.has_needed
assert not bar_parsed.has_soname
assert bar_parsed.dt_rpath_str == b"/first:/second"
assert bar_parsed.dt_needed_strs == [b"libfoo.so.1"]
def test_broken_elf():
# No elf magic
with pytest.raises(elf.ElfParsingError, match="Not an ELF file"):
elf.parse_elf(io.BytesIO(b"x"))
# Incomplete ELF header
with pytest.raises(elf.ElfParsingError, match="Not an ELF file"):
elf.parse_elf(io.BytesIO(b"\x7fELF"))
# Invalid class
with pytest.raises(elf.ElfParsingError, match="Invalid class"):
elf.parse_elf(io.BytesIO(b"\x7fELF\x09\x01" + b"\x00" * 10))
# Invalid data type
with pytest.raises(elf.ElfParsingError, match="Invalid data type"):
elf.parse_elf(io.BytesIO(b"\x7fELF\x01\x09" + b"\x00" * 10))
# 64-bit needs at least 64 bytes of header; this is only 56 bytes
with pytest.raises(elf.ElfParsingError, match="ELF header malformed"):
elf.parse_elf(io.BytesIO(b"\x7fELF\x02\x01" + b"\x00" * 50))
# 32-bit needs at least 52 bytes of header; this is only 46 bytes
with pytest.raises(elf.ElfParsingError, match="ELF header malformed"):
elf.parse_elf(io.BytesIO(b"\x7fELF\x01\x01" + b"\x00" * 40))
# Not a ET_DYN/ET_EXEC on a 32-bit LE ELF
with pytest.raises(elf.ElfParsingError, match="Not an ET_DYN or ET_EXEC"):
elf.parse_elf(io.BytesIO(b"\x7fELF\x01\x01" + (b"\x00" * 10) + b"\x09" + (b"\x00" * 35)))
def test_parser_doesnt_deal_with_nonzero_offset():
# Currently we don't have logic to parse ELF files at nonzero offsets in a file
# This could be useful when e.g. modifying an ELF file inside a tarball or so,
# but currently we cannot.
elf_at_offset_one = io.BytesIO(b"\x00\x7fELF\x01\x01" + b"\x00" * 10)
elf_at_offset_one.read(1)
with pytest.raises(elf.ElfParsingError, match="Cannot parse at a nonzero offset"):
elf.parse_elf(elf_at_offset_one)
def test_only_header():
# When passing only_header=True parsing a file that is literally just a header
# without any sections/segments should not error.
# 32 bit
elf_32 = elf.parse_elf(io.BytesIO(b"\x7fELF\x01\x01" + b"\x00" * 46), only_header=True)
assert not elf_32.is_64_bit
assert elf_32.is_little_endian
# 64 bit
elf_64 = elf.parse_elf(io.BytesIO(b"\x7fELF\x02\x01" + b"\x00" * 58), only_header=True)
assert elf_64.is_64_bit
assert elf_64.is_little_endian
@pytest.mark.requires_executables("gcc")
@skip_unless_linux
def test_elf_get_and_replace_rpaths(binary_with_rpaths):
long_rpaths = ["/very/long/prefix-a/x", "/very/long/prefix-b/y"]
executable = str(binary_with_rpaths(rpaths=long_rpaths))
# Before
assert elf.get_rpaths(executable) == long_rpaths
replacements = OrderedDict(
[
(b"/very/long/prefix-a", b"/short-a"),
(b"/very/long/prefix-b", b"/short-b"),
(b"/very/long", b"/dont"),
]
)
# Replace once: should modify the file.
assert elf.replace_rpath_in_place_or_raise(executable, replacements)
# Replace twice: nothing to be done.
assert not elf.replace_rpath_in_place_or_raise(executable, replacements)
# Verify the rpaths were modified correctly
assert elf.get_rpaths(executable) == ["/short-a/x", "/short-b/y"]
# Going back to long rpaths should fail, since we've added trailing \0
# bytes, and replacement can't assume it can write back in repeated null
# bytes -- it may correspond to zero-length strings for example.
with pytest.raises(
elf.ElfDynamicSectionUpdateFailed,
match="New rpath /very/long/prefix-a/x:/very/long/prefix-b/y is "
"longer than old rpath /short-a/x:/short-b/y",
):
elf.replace_rpath_in_place_or_raise(
executable,
OrderedDict(
[(b"/short-a", b"/very/long/prefix-a"), (b"/short-b", b"/very/long/prefix-b")]
),
)
@pytest.mark.requires_executables("gcc")
@skip_unless_linux
def test_drop_redundant_rpath(tmpdir, binary_with_rpaths):
"""Test the post install hook that drops redundant rpath entries"""
# Use existing and non-existing dirs in tmpdir
non_existing_dirs = [str(tmpdir.join("a")), str(tmpdir.join("b"))]
existing_dirs = [str(tmpdir.join("c")), str(tmpdir.join("d"))]
all_dirs = non_existing_dirs + existing_dirs
tmpdir.ensure("c", dir=True)
tmpdir.ensure("d", dir=True)
# Create a binary with rpaths to both existing and non-existing dirs
binary = binary_with_rpaths(rpaths=all_dirs)
# Verify that the binary has all the rpaths
# sometimes compilers add extra rpaths, so we test for a subset
assert set(all_dirs).issubset(elf.get_rpaths(binary))
# Test whether the right rpaths are dropped
drop_redundant_rpaths(binary)
new_rpaths = elf.get_rpaths(binary)
assert set(existing_dirs).issubset(new_rpaths)
assert set(non_existing_dirs).isdisjoint(new_rpaths)
|