Skip to content

Commit 3370e23

Browse files
authored
Merge pull request #38 from amartani/readelf
Implement ELF library dependency resolution
2 parents 8fb7bf3 + 4c0ba0d commit 3370e23

File tree

3 files changed

+377
-29
lines changed

3 files changed

+377
-29
lines changed

cmd/landrun/main.go

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/urfave/cli/v2"
9+
"github.com/zouuup/landrun/internal/elfdeps"
910
"github.com/zouuup/landrun/internal/exec"
1011
"github.com/zouuup/landrun/internal/log"
1112
"github.com/zouuup/landrun/internal/sandbox"
@@ -14,33 +15,6 @@ import (
1415
// Version is the current version of landrun
1516
const Version = "0.1.15"
1617

17-
// getLibraryDependencies returns a list of library paths that the given binary depends on
18-
func getLibraryDependencies(binary string) ([]string, error) {
19-
cmd := osexec.Command("ldd", binary)
20-
output, err := cmd.Output()
21-
if err != nil {
22-
return nil, err
23-
}
24-
25-
var libPaths []string
26-
lines := strings.Split(string(output), "\n")
27-
for _, line := range lines {
28-
// Skip empty lines and the first line (usually the binary name)
29-
if line == "" || !strings.Contains(line, "=>") {
30-
continue
31-
}
32-
// Extract the library path
33-
parts := strings.Fields(line)
34-
if len(parts) >= 3 {
35-
libPath := strings.Trim(parts[2], "()")
36-
if libPath != "" {
37-
libPaths = append(libPaths, libPath)
38-
}
39-
}
40-
}
41-
return libPaths, nil
42-
}
43-
4418
func main() {
4519
app := &cli.App{
4620
Name: "landrun",
@@ -137,15 +111,15 @@ func main() {
137111
log.Fatal("Failed to find binary: %v", err)
138112
}
139113

140-
// Add command's directory to readOnlyExecutablePaths
114+
// Add command to readOnlyExecutablePaths
141115
if c.Bool("add-exec") {
142116
readOnlyExecutablePaths = append(readOnlyExecutablePaths, binary)
143117
log.Debug("Added executable path: %v", binary)
144118
}
145119

146120
// If --ldd flag is set, detect and add library dependencies
147121
if c.Bool("ldd") {
148-
libPaths, err := getLibraryDependencies(binary)
122+
libPaths, err := elfdeps.GetLibraryDependencies(binary)
149123
if err != nil {
150124
log.Fatal("Failed to detect library dependencies: %v", err)
151125
}

internal/elfdeps/elfdeps.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package elfdeps
2+
3+
import (
4+
"debug/elf"
5+
"fmt"
6+
"io"
7+
"os"
8+
osexec "os/exec"
9+
"path/filepath"
10+
"strings"
11+
)
12+
13+
// ldconfigRunner runs `ldconfig -p` and returns its output. Tests may override
14+
// this variable to inject fake output. It is unexported on purpose to allow
15+
// test injection within the package.
16+
var ldconfigRunner = func() ([]byte, error) {
17+
return osexec.Command("ldconfig", "-p").Output()
18+
}
19+
20+
// getLdmap runs `ldconfig -p` and returns a map of soname -> path.
21+
func getLdmap() map[string]string {
22+
m := map[string]string{}
23+
out, err := ldconfigRunner()
24+
if err != nil {
25+
return m
26+
}
27+
lines := strings.Split(string(out), "\n")
28+
for _, line := range lines {
29+
if !strings.Contains(line, "=>") {
30+
continue
31+
}
32+
parts := strings.Split(line, "=>")
33+
if len(parts) < 2 {
34+
continue
35+
}
36+
path := strings.TrimSpace(parts[len(parts)-1])
37+
left := strings.TrimSpace(parts[0])
38+
toks := strings.Fields(left)
39+
if len(toks) == 0 {
40+
continue
41+
}
42+
soname := toks[0]
43+
if path == "" || soname == "" {
44+
continue
45+
}
46+
if _, err := os.Stat(path); err == nil {
47+
if _, exists := m[soname]; !exists {
48+
m[soname] = path
49+
}
50+
}
51+
}
52+
return m
53+
}
54+
55+
// parseInterp extracts the PT_INTERP interpreter path from an ELF file.
56+
func parseInterp(f *elf.File) string {
57+
for _, prog := range f.Progs {
58+
if prog.Type == elf.PT_INTERP {
59+
r := prog.Open()
60+
if r == nil {
61+
// Can't read interpreter
62+
return ""
63+
}
64+
if data, err := io.ReadAll(r); err == nil {
65+
return strings.TrimRight(string(data), "\x00")
66+
}
67+
}
68+
}
69+
return ""
70+
}
71+
72+
// parseDynamic extracts DT_NEEDED and RPATH/RUNPATH entries from the .dynamic section.
73+
func parseDynamic(f *elf.File) (needed []string, rpaths []string) {
74+
needed = []string{}
75+
rpaths = []string{}
76+
77+
if libs, err := f.DynString(elf.DT_NEEDED); err == nil {
78+
needed = append(needed, libs...)
79+
}
80+
81+
// DT_RPATH and DT_RUNPATH may both be present; split on ':' and append
82+
if rp, err := f.DynString(elf.DT_RPATH); err == nil {
83+
for _, v := range rp {
84+
if v == "" {
85+
continue
86+
}
87+
rpaths = append(rpaths, strings.Split(v, ":")...)
88+
}
89+
}
90+
if rp, err := f.DynString(elf.DT_RUNPATH); err == nil {
91+
for _, v := range rp {
92+
if v == "" {
93+
continue
94+
}
95+
rpaths = append(rpaths, strings.Split(v, ":")...)
96+
}
97+
}
98+
return
99+
}
100+
101+
// normalizeRpaths expands common tokens like $ORIGIN and makes relative
102+
// rpath entries absolute using the provided origin directory.
103+
func normalizeRpaths(rpaths []string, origin string) []string {
104+
out := []string{}
105+
for _, rp := range rpaths {
106+
if rp == "" {
107+
continue
108+
}
109+
// expand $ORIGIN (common token in RPATH/RUNPATH)
110+
rp = strings.ReplaceAll(rp, "$ORIGIN", origin)
111+
rp = strings.ReplaceAll(rp, "${ORIGIN}", origin)
112+
// make relative rpath entries absolute using origin
113+
if !filepath.IsAbs(rp) {
114+
rp = filepath.Join(origin, rp)
115+
}
116+
out = append(out, rp)
117+
}
118+
return out
119+
}
120+
121+
// resolveSingleSoname attempts to resolve a single soname using rpaths,
122+
// standard dirs and ldconfig fallback. It takes a pointer to ldmap so the
123+
// caller can lazily populate and reuse it.
124+
func resolveSingleSoname(soname string, rpaths []string, stdDirs []string, ldmap *map[string]string) string {
125+
// check rpaths first
126+
for _, rp := range rpaths {
127+
candidate := filepath.Join(rp, soname)
128+
if _, err := os.Stat(candidate); err == nil {
129+
return candidate
130+
}
131+
}
132+
133+
// then check standard dirs
134+
for _, d := range stdDirs {
135+
candidate := filepath.Join(d, soname)
136+
if _, err := os.Stat(candidate); err == nil {
137+
return candidate
138+
}
139+
}
140+
141+
// fallback: consult parsed ldconfig map (populate lazily)
142+
if *ldmap == nil {
143+
*ldmap = getLdmap()
144+
}
145+
if p, ok := (*ldmap)[soname]; ok {
146+
return p
147+
}
148+
149+
return ""
150+
}
151+
152+
// resolveSonames attempts to resolve sonames to absolute paths using rpaths,
153+
// standard library directories and falling back to parsing `ldconfig -p` output.
154+
func resolveSonames(needed []string, rpaths []string) []string {
155+
resolved := map[string]string{}
156+
stdDirs := []string{"/lib", "/lib64", "/usr/lib", "/usr/lib64", "/usr/local/lib"}
157+
var ldmap map[string]string
158+
159+
for _, soname := range needed {
160+
if _, ok := resolved[soname]; ok {
161+
continue
162+
}
163+
resolved[soname] = resolveSingleSoname(soname, rpaths, stdDirs, &ldmap)
164+
}
165+
166+
out := []string{}
167+
for _, r := range resolved {
168+
if r != "" {
169+
out = append(out, r)
170+
}
171+
}
172+
return out
173+
}
174+
175+
// GetLibraryDependencies returns a list of library paths that the given binary depends on
176+
func GetLibraryDependencies(binary string) ([]string, error) {
177+
f, err := elf.Open(binary)
178+
if err != nil {
179+
return nil, fmt.Errorf("open ELF %s: %w", binary, err)
180+
}
181+
defer f.Close()
182+
183+
interpPath := parseInterp(f)
184+
needed, rpaths := parseDynamic(f)
185+
origin := filepath.Dir(binary)
186+
rpaths = normalizeRpaths(rpaths, origin)
187+
libPaths := resolveSonames(needed, rpaths)
188+
189+
// Dedupe using a map
190+
finalMap := map[string]struct{}{}
191+
if interpPath != "" {
192+
finalMap[interpPath] = struct{}{}
193+
}
194+
for _, p := range libPaths {
195+
finalMap[p] = struct{}{}
196+
}
197+
// Add /etc/ld.so.cache if present
198+
if _, err := os.Stat("/etc/ld.so.cache"); err == nil {
199+
finalMap["/etc/ld.so.cache"] = struct{}{}
200+
}
201+
202+
out := make([]string, 0, len(finalMap))
203+
for p := range finalMap {
204+
out = append(out, p)
205+
}
206+
207+
return out, nil
208+
}

0 commit comments

Comments
 (0)