-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtry_dlopen.py
More file actions
executable file
·156 lines (121 loc) · 5.47 KB
/
Copy pathtry_dlopen.py
File metadata and controls
executable file
·156 lines (121 loc) · 5.47 KB
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
#!/usr/bin/env python3
import ctypes
import ctypes.util
import os
import sys
from typing import Optional
def _load_libdl() -> ctypes.CDLL:
# In normal glibc-based Linux environments, find_library("dl") should return
# something like "libdl.so.2". In minimal or stripped-down environments
# (no ldconfig/gcc, incomplete linker cache), this can return None even
# though libdl is present. In that case, we fall back to the stable SONAME.
name = ctypes.util.find_library("dl") or "libdl.so.2"
try:
return ctypes.CDLL(name)
except OSError as e:
raise RuntimeError(f"Could not load {name!r} (required for dlinfo/dlerror on Linux)") from e
LIBDL = _load_libdl()
# dlinfo
LIBDL.dlinfo.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p]
LIBDL.dlinfo.restype = ctypes.c_int
# dlerror (thread-local error string; cleared after read)
LIBDL.dlerror.argtypes = []
LIBDL.dlerror.restype = ctypes.c_char_p
# First appeared in 2004-era glibc. Universally correct on Linux for all practical purposes.
RTLD_DI_LINKMAP = 2
RTLD_DI_ORIGIN = 6
class _LinkMapLNameView(ctypes.Structure):
"""
Prefix-only view of glibc's `struct link_map` used **solely** to read `l_name`.
Background:
- `dlinfo(handle, RTLD_DI_LINKMAP, ...)` returns a `struct link_map*`.
- The first few members of `struct link_map` (including `l_name`) have been
stable on glibc for decades and are documented as debugger-visible.
- We only need the offset/layout of `l_name`, not the full struct.
Safety constraints:
- This is a **partial** definition (prefix). It must only be used via a pointer
returned by `dlinfo(...)`.
- Do **not** instantiate it or pass it **by value** to any C function.
- Do **not** access any members beyond those declared here.
- Do **not** rely on `ctypes.sizeof(LinkMapPrefix)` for allocation.
Rationale:
- Defining only the leading fields avoids depending on internal/unstable
tail members while keeping code more readable than raw pointer arithmetic.
"""
_fields_ = (
("l_addr", ctypes.c_void_p), # ElfW(Addr)
("l_name", ctypes.c_char_p), # char*
)
# Defensive assertions, mainly to document the invariants we depend on
assert _LinkMapLNameView.l_addr.offset == 0
assert _LinkMapLNameView.l_name.offset == ctypes.sizeof(ctypes.c_void_p)
def _dl_last_error() -> Optional[str]:
msg_bytes = cast(Optional[bytes], LIBDL.dlerror())
if not msg_bytes:
return None # no pending error
# Never raises; undecodable bytes are mapped to U+DC80..U+DCFF
return msg_bytes.decode("utf-8", "surrogateescape")
def l_name_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> str:
lm_view = ctypes.POINTER(_LinkMapLNameView)()
rc = LIBDL.dlinfo(ctypes.c_void_p(handle._handle), RTLD_DI_LINKMAP, ctypes.byref(lm_view))
if rc != 0:
err = _dl_last_error()
raise OSError(f"dlinfo failed for {libname=!r} (rc={rc})" + (f": {err}" if err else ""))
if not lm_view: # NULL link_map**
raise OSError(f"dlinfo returned NULL link_map pointer for {libname=!r}")
l_name_bytes = lm_view.contents.l_name
if not l_name_bytes:
raise OSError(f"dlinfo returned empty link_map->l_name for {libname=!r}")
path = os.fsdecode(l_name_bytes)
if not path:
raise OSError(f"dlinfo returned empty l_name string for {libname=!r}")
return path
def l_origin_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> str:
l_origin_buf = ctypes.create_string_buffer(4096)
rc = LIBDL.dlinfo(ctypes.c_void_p(handle._handle), RTLD_DI_ORIGIN, l_origin_buf)
if rc != 0:
err = _dl_last_error()
raise OSError(f"dlinfo failed for {libname=!r} (rc={rc})" + (f": {err}" if err else ""))
path = os.fsdecode(l_origin_buf.value)
if not path:
raise OSError(f"dlinfo returned empty l_origin string for {libname=!r}")
return path
def abs_path_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> str:
l_name = l_name_for_dynamic_library(libname, handle)
l_origin = l_origin_for_dynamic_library(libname, handle)
return os.path.join(l_origin, os.path.basename(l_name))
def main(argv: list[str]) -> int:
if not argv:
prog = sys.argv[0] if sys.argv else "try_dlopen.py"
print(f"Usage: {prog} <libpath> [<libpath> ...]", file=sys.stderr)
return 2
status = 0
for libpath in argv:
try:
handle = ctypes.CDLL(libpath)
except OSError as e:
print(f"dlopen failed: {libpath!r}: {e}")
status = max(status, 1)
continue
print(f"dlopen succeeded: {libpath!r}")
try:
l_name = l_name_for_dynamic_library(libpath, handle)
print(f" l_name: {l_name!r}")
except OSError as e:
print(f" could not determine l_name via dlinfo: {e}")
status = max(status, 1)
try:
l_origin = l_origin_for_dynamic_library(libpath, handle)
print(f" l_origin: {l_origin!r}")
except OSError as e:
print(f" could not determine l_origin via dlinfo: {e}")
status = max(status, 1)
try:
abs_path = abs_path_for_dynamic_library(libpath, handle)
print(f" abs_path: {abs_path!r}")
except OSError as e:
print(f" could not determine abs_path via dlinfo: {e}")
status = max(status, 1)
return status
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))