Skip to content

Commit 470e552

Browse files
committed
wip
1 parent 9c758fd commit 470e552

File tree

4 files changed

+471
-306
lines changed

4 files changed

+471
-306
lines changed

internal/patchpkg/elf.go

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
package patchpkg
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"debug/elf"
7+
"errors"
8+
"fmt"
9+
"io"
10+
"io/fs"
11+
"iter"
12+
"log/slog"
13+
"os"
14+
"os/exec"
15+
"path"
16+
"path/filepath"
17+
"slices"
18+
"strings"
19+
)
20+
21+
// libPatcher patches ELF binaries to use an alternative version of glibc.
22+
type libPatcher struct {
23+
// ld is the absolute path to the new dynamic linker (ld.so).
24+
ld string
25+
26+
// rpath is the new RPATH with the directories containing the new libc
27+
// shared objects (libc.so) and other libraries.
28+
rpath []string
29+
30+
// needed are shared libraries to add as dependencies (DT_NEEDED).
31+
needed []string
32+
}
33+
34+
// setGlibc configures the patcher to use the dynamic linker and libc libraries
35+
// in pkg.
36+
func (p *libPatcher) setGlibc(pkg *packageFS) error {
37+
// Verify that we can find a directory with libc in it.
38+
glob := "lib*/libc.so*"
39+
matches, _ := fs.Glob(pkg, glob)
40+
if len(matches) == 0 {
41+
return fmt.Errorf("cannot find libc.so file matching %q", glob)
42+
}
43+
for i := range matches {
44+
matches[i] = path.Dir(matches[i])
45+
}
46+
// Pick the shortest name: lib < lib32 < lib64 < libx32
47+
//
48+
// - lib is usually a symlink to the correct arch (e.g., lib -> lib64)
49+
// - *.so is usually a symlink to the correct version (e.g., foo.so -> foo.so.2)
50+
slices.Sort(matches)
51+
52+
lib, err := pkg.OSPath(matches[0])
53+
if err != nil {
54+
return err
55+
}
56+
p.rpath = append(p.rpath, lib)
57+
slog.Debug("found new libc directory", "path", lib)
58+
59+
// Verify that we can find the new dynamic linker.
60+
glob = "lib*/ld-linux*.so*"
61+
matches, _ = fs.Glob(pkg, glob)
62+
if len(matches) == 0 {
63+
return fmt.Errorf("cannot find ld.so file matching %q", glob)
64+
}
65+
slices.Sort(matches)
66+
p.ld, err = pkg.OSPath(matches[0])
67+
if err != nil {
68+
return err
69+
}
70+
slog.Debug("found new dynamic linker", "path", p.ld)
71+
return nil
72+
}
73+
74+
// setGlibc configures the patcher to use the standard C++ and gcc libraries in
75+
// pkg.
76+
func (p *libPatcher) setGcc(pkg *packageFS) error {
77+
// Verify that we can find a directory with libstdc++.so in it.
78+
glob := "lib*/libstdc++.so*"
79+
matches, _ := fs.Glob(pkg, glob)
80+
if len(matches) == 0 {
81+
return fmt.Errorf("cannot find libstdc++.so file matching %q", glob)
82+
}
83+
for i := range matches {
84+
matches[i] = path.Dir(matches[i])
85+
}
86+
// Pick the shortest name: lib < lib32 < lib64 < libx32
87+
//
88+
// - lib is usually a symlink to the correct arch (e.g., lib -> lib64)
89+
// - *.so is usually a symlink to the correct version (e.g., foo.so -> foo.so.2)
90+
slices.Sort(matches)
91+
92+
lib, err := pkg.OSPath(matches[0])
93+
if err != nil {
94+
return err
95+
}
96+
p.rpath = append(p.rpath, lib)
97+
p.needed = append(p.needed, "libstdc++.so")
98+
slog.Debug("found new libstdc++ directory", "path", lib)
99+
return nil
100+
}
101+
102+
func (p *libPatcher) prependRPATH(libPkg *packageFS) {
103+
glob := "lib*/*.so*"
104+
matches, _ := fs.Glob(libPkg, glob)
105+
if len(matches) == 0 {
106+
slog.Debug("not prepending package to RPATH because no shared libraries were found", "pkg", libPkg.storePath)
107+
return
108+
}
109+
for i := range matches {
110+
matches[i] = path.Dir(matches[i])
111+
}
112+
slices.Sort(matches)
113+
matches = slices.Compact(matches)
114+
for i := range matches {
115+
var err error
116+
matches[i], err = libPkg.OSPath(matches[i])
117+
if err != nil {
118+
continue
119+
}
120+
}
121+
p.rpath = append(p.rpath, matches...)
122+
slog.Debug("prepended package lib dirs to RPATH", "pkg", libPkg.storePath, "dirs", matches)
123+
}
124+
125+
// patch applies glibc patches to a binary and writes the patched result to
126+
// outPath. It does not modify the original binary in-place.
127+
func (p *libPatcher) patch(ctx context.Context, path, outPath string) error {
128+
cmd := &patchelf{PrintInterpreter: true}
129+
out, err := cmd.run(ctx, path)
130+
if err != nil {
131+
return err
132+
}
133+
oldInterp := string(out)
134+
135+
cmd = &patchelf{PrintRPATH: true}
136+
out, err = cmd.run(ctx, path)
137+
if err != nil {
138+
return err
139+
}
140+
oldRpath := strings.Split(string(out), ":")
141+
142+
cmd = &patchelf{
143+
SetInterpreter: p.ld,
144+
SetRPATH: append(p.rpath, oldRpath...),
145+
AddNeeded: p.needed,
146+
Output: outPath,
147+
}
148+
slog.Debug("patching glibc on binary",
149+
"path", path, "outPath", cmd.Output,
150+
"old_interp", oldInterp, "new_interp", cmd.SetInterpreter,
151+
"old_rpath", oldRpath, "new_rpath", cmd.SetRPATH,
152+
)
153+
_, err = cmd.run(ctx, path)
154+
return err
155+
}
156+
157+
// patchelf runs the patchelf command.
158+
type patchelf struct {
159+
SetRPATH []string
160+
PrintRPATH bool
161+
162+
SetInterpreter string
163+
PrintInterpreter bool
164+
165+
AddNeeded []string
166+
167+
Output string
168+
}
169+
170+
// run runs patchelf on an ELF binary and returns its output.
171+
func (p *patchelf) run(ctx context.Context, elf string) ([]byte, error) {
172+
cmd := exec.CommandContext(ctx, lookPath("patchelf"))
173+
if len(p.SetRPATH) != 0 {
174+
cmd.Args = append(cmd.Args, "--force-rpath", "--set-rpath", strings.Join(p.SetRPATH, ":"))
175+
}
176+
if p.PrintRPATH {
177+
cmd.Args = append(cmd.Args, "--print-rpath")
178+
}
179+
if p.SetInterpreter != "" {
180+
cmd.Args = append(cmd.Args, "--set-interpreter", p.SetInterpreter)
181+
}
182+
if p.PrintInterpreter {
183+
cmd.Args = append(cmd.Args, "--print-interpreter")
184+
}
185+
for _, needed := range p.AddNeeded {
186+
cmd.Args = append(cmd.Args, "--add-needed", needed)
187+
}
188+
if p.Output != "" {
189+
cmd.Args = append(cmd.Args, "--output", p.Output)
190+
}
191+
cmd.Args = append(cmd.Args, elf)
192+
out, err := cmd.Output()
193+
return bytes.TrimSpace(out), err
194+
}
195+
196+
var (
197+
// SystemLibSearchPaths match the system library paths for common Linux
198+
// distributions.
199+
SystemLibSearchPaths = []string{
200+
"/lib*/*-linux-gnu", // Debian
201+
"/lib*", // Red Hat
202+
"/var/lib*/*/lib*", // Docker
203+
}
204+
205+
// EnvLibrarySearchPath matches the paths in the LIBRARY_PATH
206+
// environment variable.
207+
EnvLibrarySearchPath = filepath.SplitList(os.Getenv("LIBRARY_PATH"))
208+
209+
// EnvLDLibrarySearchPath matches the paths in the LD_LIBRARY_PATH
210+
// environment variable.
211+
EnvLDLibrarySearchPath = filepath.SplitList(os.Getenv("LD_LIBRARY_PATH"))
212+
213+
// CUDALibSearchPaths match the common installation directories for CUDA
214+
// libraries.
215+
CUDALibSearchPaths = []string{
216+
// Common non-package manager locations.
217+
"/opt/cuda/lib*",
218+
"/opt/nvidia/lib*",
219+
"/usr/local/cuda/lib*",
220+
"/usr/local/nvidia/lib*",
221+
222+
// Unlikely, but might as well try.
223+
"/lib*/nvidia",
224+
"/lib*/cuda",
225+
"/usr/lib*/nvidia",
226+
"/usr/lib*/cuda",
227+
"/usr/local/lib*",
228+
"/usr/local/lib*/nvidia",
229+
"/usr/local/lib*/cuda",
230+
}
231+
)
232+
233+
// SharedLibrary describes an ELF shared library (object).
234+
//
235+
// Note that the various name fields document the common naming and versioning
236+
// conventions, but it is possible for a library to deviate from them.
237+
//
238+
// For an introduction to Linux shared libraries, see
239+
// https://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html
240+
type SharedLibrary struct {
241+
*os.File
242+
243+
// LinkerName is the soname without any version suffix (libfoo.so). It
244+
// is typically a symlink pointing to Soname. The build-time linker
245+
// looks for this name by default.
246+
LinkerName string
247+
248+
// Soname is the shared object name from the library's DT_SONAME field.
249+
// It usually includes a version number suffix (libfoo.so.1). Other ELF
250+
// binaries that depend on this library typically specify this name in
251+
// the DT_NEEDED field.
252+
Soname string
253+
254+
// RealName is the absolute path to the file that actually contains the
255+
// library code. It is typically the soname plus a minor version and
256+
// release number (libfoo.so.1.0.0).
257+
RealName string
258+
}
259+
260+
// OpenSharedLibrary opens a shared library file. Unlike with ld, name must be
261+
// an exact path. To search for a library in the usual locations, use
262+
// [FindSharedLibrary] instead.
263+
func OpenSharedLibrary(name string) (SharedLibrary, error) {
264+
lib := SharedLibrary{}
265+
var err error
266+
lib.File, err = os.Open(name)
267+
if err != nil {
268+
return lib, err
269+
}
270+
271+
dir, file := filepath.Split(name)
272+
i := strings.Index(file, ".so")
273+
if i != -1 {
274+
lib.LinkerName = dir + file[:i+3]
275+
}
276+
277+
elfFile, err := elf.NewFile(lib)
278+
if err == nil {
279+
soname, _ := elfFile.DynString(elf.DT_SONAME)
280+
if len(soname) != 0 {
281+
lib.Soname = soname[0]
282+
}
283+
}
284+
285+
real, err := filepath.EvalSymlinks(name)
286+
if err == nil {
287+
lib.RealName, _ = filepath.Abs(real)
288+
}
289+
return lib, nil
290+
}
291+
292+
// FindSharedLibrary searches the directories in searchPath for a shared
293+
// library. It yields any libraries in the search path directories that have
294+
// name as a prefix. For example, "libcuda.so" will match "libcuda.so",
295+
// "libcuda.so.1", and "libcuda.so.550.107.02". The underlying file is only
296+
// valid for a single iteration, after which it is closed.
297+
//
298+
// The search path may contain [filepath.Glob] patterns. See
299+
// [SystemLibSearchPaths] for some predefined search paths. If name is an
300+
// absolute path, then FindSharedLibrary opens it directly and doesn't perform
301+
// any searching.
302+
func FindSharedLibrary(name string, searchPath ...string) iter.Seq[SharedLibrary] {
303+
return func(yield func(SharedLibrary) bool) {
304+
if filepath.IsAbs(name) {
305+
lib, err := OpenSharedLibrary(name)
306+
if err == nil {
307+
yield(lib)
308+
}
309+
return
310+
}
311+
312+
if libPath := os.Getenv("LD_LIBRARY_PATH"); libPath != "" {
313+
searchPath = append(searchPath, filepath.SplitList(os.Getenv("LD_LIBRARY_PATH"))...)
314+
}
315+
if libPath := os.Getenv("LIBRARY_PATH"); libPath != "" {
316+
searchPath = append(searchPath, filepath.SplitList(libPath)...)
317+
}
318+
searchPath = append(searchPath,
319+
"/lib*/*-linux-gnu", // Debian
320+
"/lib*", // Red Hat
321+
)
322+
323+
suffix := globEscape(name) + "*"
324+
patterns := make([]string, len(searchPath))
325+
for i := range searchPath {
326+
patterns[i] = filepath.Join(searchPath[i], suffix)
327+
}
328+
for match := range searchGlobs(patterns) {
329+
lib, err := OpenSharedLibrary(match)
330+
if err != nil {
331+
continue
332+
}
333+
ok := yield(lib)
334+
_ = lib.Close()
335+
if !ok {
336+
return
337+
}
338+
}
339+
}
340+
}
341+
342+
// CopyAndLink copies the shared library to dir and creates the LinkerName and
343+
// Soname symlinks for it. It creates dir if it doesn't already exist.
344+
func (lib SharedLibrary) CopyAndLink(dir string) error {
345+
err := os.MkdirAll(dir, 0o755)
346+
if err != nil {
347+
return err
348+
}
349+
350+
dstPath := filepath.Join(dir, filepath.Base(lib.RealName))
351+
dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o666)
352+
if err != nil {
353+
return err
354+
}
355+
defer dst.Close()
356+
357+
_, err = io.Copy(dst, lib)
358+
if err != nil {
359+
return err
360+
}
361+
err = dst.Close()
362+
if err != nil {
363+
return err
364+
}
365+
366+
sonameLink := filepath.Join(dir, lib.Soname)
367+
var sonameErr error
368+
if lib.Soname != "" {
369+
sonameErr = os.Symlink(dstPath, sonameLink)
370+
}
371+
372+
linkerNameLink := filepath.Join(dir, lib.LinkerName)
373+
var linkerNameErr error
374+
if lib.LinkerName != "" {
375+
if sonameErr == nil {
376+
linkerNameErr = os.Symlink(sonameLink, linkerNameLink)
377+
} else {
378+
linkerNameErr = os.Symlink(dstPath, linkerNameLink)
379+
}
380+
}
381+
382+
err = errors.Join(sonameErr, linkerNameErr)
383+
if err != nil {
384+
return fmt.Errorf("patchpkg: create symlinks for shared library: %w", err)
385+
}
386+
return nil
387+
}
388+
389+
func (lib SharedLibrary) LogValue() slog.Value {
390+
return slog.GroupValue(
391+
slog.String("lib.path", lib.Name()),
392+
slog.String("lib.linkername", lib.LinkerName),
393+
slog.String("lib.soname", lib.Soname),
394+
slog.String("lib.realname", lib.RealName),
395+
)
396+
}

0 commit comments

Comments
 (0)