summaryrefslogtreecommitdiff
path: root/lib/spack/spack/test/util/elf.py
blob: db826df1730fab9d3c7d47d7cfaf3aaaacbd216a (plain) (blame)
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-2023 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)