summaryrefslogtreecommitdiffstats
path: root/src/basic/build-path.c
blob: b5972658dfea11101640a2b4c06db41eb6c3e00c (plain)
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <elf.h>
#include <link.h>
#include <sys/auxv.h>

#include "build-path.h"
#include "errno-list.h"
#include "errno-util.h"
#include "macro.h"
#include "path-util.h"
#include "process-util.h"
#include "unistd.h"

static int get_runpath_from_dynamic(const ElfW(Dyn) *d, ElfW(Addr) bias, const char **ret) {
        size_t runpath_index = SIZE_MAX, rpath_index = SIZE_MAX;
        const char *strtab = NULL;

        assert(d);

        /* Iterates through the PT_DYNAMIC section to find the DT_RUNPATH/DT_RPATH entries */

        for (; d->d_tag != DT_NULL; d++) {

                switch (d->d_tag) {

                case DT_RUNPATH:
                        runpath_index = (size_t) d->d_un.d_val;
                        break;

                case DT_RPATH:
                        rpath_index = (size_t) d->d_un.d_val;
                        break;

                case DT_STRTAB:
                        /* On MIPS and RISC-V DT_STRTAB records an offset, not a valid address, so it has to be adjusted
                         * using the bias calculated earlier. */
                        if (d->d_un.d_val != 0)
                                strtab = (const char *) ((uintptr_t) d->d_un.d_val
#if defined(__mips__) || defined(__riscv)
                                         + bias
#endif
                                );
                        break;
                }

                /* runpath wins, hence if we have the table and runpath we can exit the loop early */
                if (strtab && runpath_index != SIZE_MAX)
                        break;
        }

        if (!strtab)
                return -ENOTRECOVERABLE;

        /* According to ld.so runpath wins if both runpath and rpath are defined. */
        if (runpath_index != SIZE_MAX) {
                if (ret)
                        *ret = strtab + runpath_index;
                return 1;
        }

        if (rpath_index != SIZE_MAX) {
                if (ret)
                        *ret = strtab + rpath_index;
                return 1;
        }

        if (ret)
                *ret = NULL;

        return 0;
}

static int get_runpath(const char **ret) {
        unsigned long phdr, phent, phnum;

        /* Finds the rpath/runpath in the program headers of the main executable we are running in */

        phdr = getauxval(AT_PHDR);      /* Start offset of phdr */
        if (phdr == 0)
                return -ENOTRECOVERABLE;

        phnum = getauxval(AT_PHNUM);    /* Number of entries in phdr */
        if (phnum == 0)
                return -ENOTRECOVERABLE;

        phent = getauxval(AT_PHENT);    /* Size of entries in phdr */
        if (phent < sizeof(ElfW(Phdr))) /* Safety check, that our idea of the structure matches the file */
                return -ENOTRECOVERABLE;

        ElfW(Addr) bias = 0, dyn = 0;
        bool found_bias = false, found_dyn = false;

        /* Iterate through the Phdr structures to find the PT_PHDR and PT_DYNAMIC sections */
        for (unsigned long i = 0; i < phnum; i++) {
                const ElfW(Phdr) *p = (const ElfW(Phdr)*) (phdr + (i * phent));

                switch (p->p_type) {

                case PT_PHDR:
                        if (p->p_vaddr > phdr) /* safety overflow check */
                                return -ENOTRECOVERABLE;

                        bias = (ElfW(Addr)) phdr - p->p_vaddr;
                        found_bias = true;
                        break;

                case PT_DYNAMIC:
                        dyn = p->p_vaddr;
                        found_dyn = true;
                        break;
                }

                if (found_bias && found_dyn)
                        break;
        }

        if (!found_dyn)
                return -ENOTRECOVERABLE;

        return get_runpath_from_dynamic((const ElfW(Dyn)*) (bias + dyn), bias, ret);
}

int get_build_exec_dir(char **ret) {
        int r;

        /* Returns the build execution directory if we are invoked in a build environment. Specifically, this
         * checks if the main program binary has an rpath/runpath set (i.e. an explicit directory where to
         * look for shared libraries) to $ORIGIN. If so we know that this is not a regular installed binary,
         * but one which shall acquire its libraries from below a directory it is located in, i.e. a build
         * directory or similar. In that case it typically makes sense to also search for our auxiliary
         * executables we fork() off in a directory close to our main program binary, rather than in the
         * system.
         *
         * This function is supposed to be used when looking for "callout" binaries that are closely related
         * to the main program (i.e. speak a specific protocol between each other). And where it's generally
         * a good idea to use the binary from the build tree (if there is one) instead of the system.
         *
         * Note that this does *not* actually return the rpath/runpath but the instead the directory the main
         * executable was found in. This follows the logic that the result is supposed to be used for
         * executable binaries (i.e. stuff in bindir), not for shared libraries (i.e. stuff in libdir), and
         * hence the literal shared library path would just be wrong.
         *
         * TLDR: if we look for callouts in this dir first, running binaries from the meson build tree
         * automatically uses the right callout.
         *
         * Returns:
         *     -ENOEXEC         → We are not running in an rpath/runpath $ORIGIN environment
         *     -ENOENT          → We don't know our own binary path
         *     -NOTRECOVERABLE  → Dynamic binary information missing?
         */

        static int runpath_cached = -ERRNO_MAX-1;
        if (runpath_cached == -ERRNO_MAX-1) {
                const char *runpath = NULL;

                runpath_cached = get_runpath(&runpath);

                /* We only care if the runpath starts with $ORIGIN/ */
                if (runpath_cached > 0 && !startswith(runpath, "$ORIGIN/"))
                        runpath_cached = 0;
        }
        if (runpath_cached < 0)
                return runpath_cached;
        if (runpath_cached == 0)
                return -ENOEXEC;

        _cleanup_free_ char *exe = NULL;
        r = get_process_exe(0, &exe);
        if (r < 0)
                return runpath_cached = r;

        return path_extract_directory(exe, ret);
}

static int find_build_dir_binary(const char *fn, char **ret) {
        int r;

        assert(fn);
        assert(ret);

        _cleanup_free_ char *build_dir = NULL;
        r = get_build_exec_dir(&build_dir);
        if (r < 0)
                return r;

        _cleanup_free_ char *np = path_join(build_dir, fn);
        if (!np)
                return -ENOMEM;

        *ret = TAKE_PTR(np);
        return 0;
}

static int find_environment_binary(const char *fn, const char **ret) {

        /* If a path such as /usr/lib/systemd/systemd-foobar is specified, then this will check for an
         * environment variable SYSTEMD_FOOBAR_PATH and return it if set. */

        _cleanup_free_ char *s = strdup(fn);
        if (!s)
                return -ENOMEM;

        ascii_strupper(s);
        string_replace_char(s, '-', '_');

        if (!strextend(&s, "_PATH"))
                return -ENOMEM;

        const char *e;
        e = secure_getenv(s);
        if (!e)
                return -ENXIO;

        *ret = e;
        return 0;
}

int invoke_callout_binary(const char *path, char *const argv[]) {
        int r;

        assert(path);

        /* Just like execv(), but tries to execute the specified binary in the build dir instead, if known */

        _cleanup_free_ char *fn = NULL;
        r = path_extract_filename(path, &fn);
        if (r < 0)
                return r;
        if (r == O_DIRECTORY) /* Uh? */
                return -EISDIR;

        const char *e;
        if (find_environment_binary(fn, &e) >= 0) {
                /* If there's an explicit environment variable set for this binary, prefer it */
                execv(e, argv);
                return -errno; /* The environment variable counts, let's fail otherwise */
        }

        _cleanup_free_ char *np = NULL;
        if (find_build_dir_binary(fn, &np) >= 0)
                execv(np, argv);

        execv(path, argv);
        return -errno;
}

int pin_callout_binary(const char *path) {
        int r;

        assert(path);

        /* Similar to invoke_callout_binary(), but pins (i.e. O_PATH opens) the binary instead of executing it. */

        _cleanup_free_ char *fn = NULL;
        r = path_extract_filename(path, &fn);
        if (r < 0)
                return r;
        if (r == O_DIRECTORY) /* Uh? */
                return -EISDIR;

        const char *e;
        if (find_environment_binary(fn, &e) >= 0)
                return RET_NERRNO(open(e, O_CLOEXEC|O_PATH));

        _cleanup_free_ char *np = NULL;
        if (find_build_dir_binary(fn, &np) >= 0) {
                r = RET_NERRNO(open(np, O_CLOEXEC|O_PATH));
                if (r >= 0)
                        return r;
        }

        return RET_NERRNO(open(path, O_CLOEXEC|O_PATH));
}