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
use goblin::{
    elf::{Dyn, Elf},
    elf64::{
        dynamic::{DF_1_PIE, DT_FLAGS_1},
        program_header::PT_DYNAMIC,
    },
    error::Error as GoblinError,
};
use scroll::{Pread, Pwrite};
use std::{
    fs, mem,
    path::{Path, PathBuf},
};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum Error {
    #[error("Unable to parse ELF file")]
    UnableToParseElf(GoblinError),

    #[error("Unable to serialize ELF file")]
    UnableToSerializeElf(GoblinError),

    #[error("Unable to found DT_FLAGS_1 position")]
    NoDTFlags1Found,

    #[error("Unable to find PT_DYNAMIC section")]
    NoDynamicSectionFound,

    #[error("DT_FLAGS_1 flag crosscheck failed")]
    FlagCrosscheckFailed,

    #[error("IOError")]
    IOError(#[from] std::io::Error),
}

/// Patches executable file for new version of glibc dynamic loader
///
/// After glibc 2.29 on linux `dlopen` is explicitly denying loading
/// PIE executables as a shared library. The following error might appear:
///
/// ```console
/// dlopen error: cannot dynamically load position-independent executable
/// ```
///
/// From 2.29 [dynamic loader throws an error](glibc) if `DF_1_PIE` flag is set in
/// `DT_FLAG_1` tag on the `PT_DYNAMIC` section. Although the loading of executable
/// files as a shared library was never an intended use case, through the years
/// some applications adopted this technique and it is very convenient in the context
/// of paired benchmarking.
///
/// Following method check if this flag is set and patch binary at runtime
/// (writing patched version in a different file). As far as I am aware
/// this is safe modification because `DF_1_PIE` is purely informational and doesn't
/// changle the dynamic linking process in any way. Theoretically in the future this modification
/// could prevent ASLR ramndomization on the OS level which is irrelevant for benchmark
/// executables.
///
/// [glibc]: https://github.com/bminor/glibc/blob/2e0c0ff95ca0e3122eb5b906ee26a31f284ce5ab/elf/dl-load.c#L1280-L1282
pub fn patch_pie_binary_if_needed(
    #[allow(unused_variables)] path: impl AsRef<Path>,
) -> Result<Option<PathBuf>, Error> {
    let mut bytes = fs::read(path.as_ref())?;
    let elf = Elf::parse(&bytes).map_err(Error::UnableToParseElf)?;

    let Some(dynamic) = elf.dynamic else {
        return Ok(None);
    };
    if dynamic.info.flags_1 & DF_1_PIE == 0 {
        return Ok(None);
    }

    let (dyn_idx, _) = dynamic
        .dyns
        .iter()
        .enumerate()
        .find(|(_, d)| d.d_tag == DT_FLAGS_1)
        .ok_or(Error::NoDTFlags1Found)?;

    // Finding PT_DYNAMIC section offset
    let header = elf
        .program_headers
        .iter()
        .find(|h| h.p_type == PT_DYNAMIC)
        .ok_or(Error::NoDynamicSectionFound)?;

    // Finding target Dyn item offset
    let dyn_offset = header.p_offset as usize + dyn_idx * mem::size_of::<Dyn>();

    // Crosschecking we found right dyn tag
    let mut dyn_item = bytes
        .pread::<Dyn>(dyn_offset)
        .map_err(Error::UnableToSerializeElf)?;

    if dyn_item.d_tag != DT_FLAGS_1 || dyn_item.d_val != dynamic.info.flags_1 {
        return Err(Error::FlagCrosscheckFailed);
    }

    // clearing DF_1_PIE bit and writing patched binary
    dyn_item.d_val &= !DF_1_PIE;
    bytes
        .pwrite(dyn_item, dyn_offset)
        .map_err(Error::UnableToSerializeElf)?;

    let path = path.as_ref().with_extension("patched");
    fs::write(&path, bytes)?;

    Ok(Some(path))
}