Skip to content

Commit e308235

Browse files
authored
patchpkg: port ELF patching to Go and use RPATH (#2248)
Move the patching functionality from glibc-patch.bash to Go while also updating it to set RPATH instead of RUNPATH.
1 parent 4677feb commit e308235

File tree

5 files changed

+190
-67
lines changed

5 files changed

+190
-67
lines changed

internal/boxcli/patch.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import (
66
)
77

88
func patchCmd() *cobra.Command {
9-
return &cobra.Command{
9+
var glibc string
10+
cmd := &cobra.Command{
1011
Use: "patch <store-path>",
1112
Short: "Apply Devbox patches to a package to fix common linker errors",
1213
Args: cobra.ExactArgs(1),
@@ -16,7 +17,10 @@ func patchCmd() *cobra.Command {
1617
if err != nil {
1718
return err
1819
}
20+
builder.Glibc = glibc
1921
return builder.Build(cmd.Context(), args[0])
2022
},
2123
}
24+
cmd.Flags().StringVar(&glibc, "glibc", "", "patch binaries to use a different glibc")
25+
return cmd
2226
}

internal/patchpkg/builder.go

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package patchpkg
33

44
import (
5+
"bufio"
56
"bytes"
67
"context"
78
_ "embed"
@@ -12,6 +13,7 @@ import (
1213
"log/slog"
1314
"os"
1415
"os/exec"
16+
"path"
1517
"path/filepath"
1618
)
1719

@@ -23,6 +25,12 @@ type DerivationBuilder struct {
2325
// Out is the output directory that will contain the built derivation.
2426
// If empty it defaults to $out, which is typically set by Nix.
2527
Out string
28+
29+
// Glibc is an optional store path to an alternative glibc version. If
30+
// it's set, the builder will patch ELF binaries to use its shared
31+
// libraries and dynamic linker.
32+
Glibc string
33+
glibcPatcher glibcPatcher
2634
}
2735

2836
// NewDerivationBuilder initializes a new DerivationBuilder from the current
@@ -42,6 +50,13 @@ func (d *DerivationBuilder) init() error {
4250
return fmt.Errorf("patchpkg: $out is empty (is this being run from a nix build?)")
4351
}
4452
}
53+
if d.Glibc != "" {
54+
var err error
55+
d.glibcPatcher, err = newGlibcPatcher(newPackageFS(d.Glibc))
56+
if err != nil {
57+
return fmt.Errorf("patchpkg: can't patch glibc using %s: %v", d.Glibc, err)
58+
}
59+
}
4560
return nil
4661
}
4762

@@ -53,7 +68,7 @@ func (d *DerivationBuilder) Build(ctx context.Context, pkgStorePath string) erro
5368
}
5469

5570
slog.DebugContext(ctx, "starting build to patch package",
56-
"pkg", pkgStorePath, "out", d.Out)
71+
"pkg", pkgStorePath, "glibc", d.Glibc, "out", d.Out)
5772
return d.build(ctx, newPackageFS(pkgStorePath), newPackageFS(d.Out))
5873
}
5974

@@ -70,7 +85,7 @@ func (d *DerivationBuilder) build(ctx context.Context, pkg, out *packageFS) erro
7085
case isSymlink(entry.Type()):
7186
err = d.copySymlink(pkg, out, path)
7287
default:
73-
err = d.copyFile(pkg, out, path)
88+
err = d.copyFile(ctx, pkg, out, path)
7489
}
7590

7691
if err != nil {
@@ -93,14 +108,28 @@ func (d *DerivationBuilder) copyDir(out *packageFS, path string) error {
93108
return os.Mkdir(path, 0o777)
94109
}
95110

96-
func (d *DerivationBuilder) copyFile(pkg, out *packageFS, path string) error {
97-
src, err := pkg.Open(path)
111+
func (d *DerivationBuilder) copyFile(ctx context.Context, pkg, out *packageFS, path string) error {
112+
srcFile, err := pkg.Open(path)
98113
if err != nil {
99114
return err
100115
}
101-
defer src.Close()
116+
defer srcFile.Close()
102117

103-
stat, err := src.Stat()
118+
src := bufio.NewReader(srcFile)
119+
if d.needsGlibcPatch(src, path) {
120+
srcPath, err := pkg.OSPath(path)
121+
if err != nil {
122+
return err
123+
}
124+
dstPath, err := out.OSPath(path)
125+
if err != nil {
126+
return err
127+
}
128+
// No need to copy the file, patchelf will do it for us.
129+
return d.glibcPatcher.patch(ctx, srcPath, dstPath)
130+
}
131+
132+
stat, err := srcFile.Stat()
104133
if err != nil {
105134
return err
106135
}
@@ -142,6 +171,23 @@ func (d *DerivationBuilder) copySymlink(pkg, out *packageFS, path string) error
142171
return os.Symlink(target, link)
143172
}
144173

174+
func (d *DerivationBuilder) needsGlibcPatch(file *bufio.Reader, filePath string) bool {
175+
if d.Glibc == "" {
176+
return false
177+
}
178+
if path.Dir(filePath) != "bin" {
179+
return false
180+
}
181+
182+
// ELF binaries are identifiable by the first 4 magic bytes:
183+
// 0x7F E L F
184+
magic, err := file.Peek(4)
185+
if err != nil {
186+
return false
187+
}
188+
return magic[0] == 0x7F && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'
189+
}
190+
145191
// packageFS is the tree of files for a package in the Nix store.
146192
type packageFS struct {
147193
fs.FS

internal/patchpkg/glibc-patch.bash

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,76 +2,21 @@
22

33
set -euo pipefail
44

5-
declare -r pkg # package that we're patching
6-
declare -r glibc # new glibc that we're patching in
7-
declare -r out # nix output path that will contain the patched package
5+
declare -r pkg # package that we're patching
6+
declare -r out # nix output path that will contain the patched package
87

98
# Paths to this script's dependencies set by nix.
10-
declare -r coreutils file findutils gnused patchelf ripgrep
9+
declare -r coreutils gnused ripgrep
1110

1211
# Explicitly declare the specific commands that this script depends on.
13-
hash -p "$coreutils/bin/cp" cp
1412
hash -p "$coreutils/bin/chmod" chmod
1513
hash -p "$coreutils/bin/dirname" dirname
1614
hash -p "$coreutils/bin/echo" echo
17-
hash -p "$coreutils/bin/head" head
1815
hash -p "$coreutils/bin/stat" stat
1916
hash -p "$coreutils/bin/wc" wc
20-
hash -p "$file/bin/file" file
21-
hash -p "$findutils/bin/find" find
2217
hash -p "$gnused/bin/sed" sed
23-
hash -p "$patchelf/bin/patchelf" patchelf
2418
hash -p "$ripgrep/bin/rg" rg
2519

26-
# Find the new linker that we'll patch into all of the package's executables as
27-
# the interpreter.
28-
interp="$(find "$glibc/lib" -type f -maxdepth 1 -executable -name 'ld-linux-*.so*' | head -n1)"
29-
readonly interp
30-
31-
patch() {
32-
declare -r binary="$1" # ELF binary to patch
33-
34-
perm=$(stat -c "%a" "$binary")
35-
old_rpath="$(patchelf --print-rpath "$binary")"
36-
new_rpath="$glibc/lib${old_rpath:+:$old_rpath}"
37-
38-
echo "running patchelf file=\"$binary\" rpath=\"$new_rpath\" perm=\"$perm\""
39-
chmod u+w "$binary"
40-
patchelf --set-rpath "$new_rpath" \
41-
--add-needed libBrokenLocale.so.1 \
42-
--add-needed libanl.so.1 \
43-
--add-needed libc.so.6 \
44-
--add-needed libdl.so.2 \
45-
--add-needed libgcc_s.so.1 \
46-
--add-needed libm.so.6 \
47-
--add-needed libmvec.so.1 \
48-
--add-needed libnsl.so.1 \
49-
--add-needed libnss_compat.so.2 \
50-
--add-needed libnss_db.so.2 \
51-
--add-needed libnss_dns.so.2 \
52-
--add-needed libnss_files.so.2 \
53-
--add-needed libnss_hesiod.so.2 \
54-
--add-needed libpcprofile.so \
55-
--add-needed libpthread.so.0 \
56-
--add-needed libresolv.so.2 \
57-
--add-needed librt.so.1 \
58-
--add-needed libutil.so.1 \
59-
--set-interpreter "$interp" \
60-
"$binary"
61-
62-
# Neaten the runpath by removing extraneous paths. This will likely remove any old glibc.
63-
patchelf --shrink-rpath "$binary"
64-
chmod "$perm" "$binary"
65-
}
66-
67-
# Search for any files that look like ELF binaries and patch them.
68-
elves="$(find "$out" -type f -exec "$file/bin/file" {} \+ | rg --replace '$1' '^(.*): .*ELF.*executable.*dynamically linked.*$')"
69-
count="$(echo "$elves" | wc -l)"
70-
echo "patching elf binaries count=$count"
71-
for binary in $elves; do
72-
patch "$binary" exe
73-
done
74-
7520
patch_store_path() {
7621
declare -r path="$1"
7722
declare -r perm=$(stat -c "%a" "$path")

internal/patchpkg/patch.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package patchpkg
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io/fs"
8+
"log/slog"
9+
"os/exec"
10+
"path"
11+
"slices"
12+
)
13+
14+
// glibcPatcher patches ELF binaries to use an alternative version of glibc.
15+
type glibcPatcher struct {
16+
// ld is the absolute path to the new dynamic linker (ld.so).
17+
ld string
18+
19+
// lib is the absolute path to the lib directory containing the new libc
20+
// shared objects (libc.so).
21+
lib string
22+
}
23+
24+
// newGlibcPatcher creates a new glibcPatcher and verifies that it can find the
25+
// shared object files in glibc.
26+
func newGlibcPatcher(glibc *packageFS) (patcher glibcPatcher, err error) {
27+
// Verify that we can find a directory with libc in it.
28+
glob := "lib*/libc.so*"
29+
matches, _ := fs.Glob(glibc, glob)
30+
if len(matches) == 0 {
31+
return glibcPatcher{}, fmt.Errorf("cannot find libc.so file matching %q", glob)
32+
}
33+
for i := range matches {
34+
matches[i] = path.Dir(matches[i])
35+
}
36+
slices.Sort(matches) // pick the shortest name: lib < lib32 < lib64 < libx32
37+
patcher.lib, err = glibc.OSPath(matches[0])
38+
if err != nil {
39+
return glibcPatcher{}, err
40+
}
41+
slog.Debug("found new libc directory", "path", patcher.lib)
42+
43+
// Verify that we can find the new dynamic linker.
44+
glob = "lib*/ld-linux*.so*"
45+
matches, _ = fs.Glob(glibc, glob)
46+
if len(matches) == 0 {
47+
return glibcPatcher{}, fmt.Errorf("cannot find ld.so file matching %q", glob)
48+
}
49+
slices.Sort(matches)
50+
patcher.ld, err = glibc.OSPath(matches[0])
51+
if err != nil {
52+
return glibcPatcher{}, err
53+
}
54+
slog.Debug("found new dynamic linker", "path", patcher.ld)
55+
56+
return patcher, nil
57+
}
58+
59+
// patch applies glibc patches to a binary and writes the patched result to
60+
// outPath. It does not modify the original binary in-place.
61+
func (g glibcPatcher) patch(ctx context.Context, path, outPath string) error {
62+
cmd := &patchelf{PrintInterpreter: true}
63+
out, err := cmd.run(ctx, path)
64+
if err != nil {
65+
return err
66+
}
67+
oldInterp := string(out)
68+
69+
cmd = &patchelf{PrintRPATH: true}
70+
out, err = cmd.run(ctx, path)
71+
if err != nil {
72+
return err
73+
}
74+
oldRpath := string(out)
75+
76+
cmd = &patchelf{
77+
SetInterpreter: g.ld,
78+
Output: outPath,
79+
}
80+
if len(oldRpath) == 0 {
81+
cmd.SetRPATH = g.lib
82+
} else {
83+
cmd.SetRPATH = g.lib + ":" + oldRpath
84+
}
85+
86+
slog.Debug("patching glibc on binary",
87+
"path", path, "outPath", cmd.Output,
88+
"old_interp", oldInterp, "new_interp", cmd.SetInterpreter,
89+
"old_rpath", oldRpath, "new_rpath", cmd.SetRPATH,
90+
)
91+
_, err = cmd.run(ctx, path)
92+
return err
93+
}
94+
95+
// patchelf runs the patchelf command.
96+
type patchelf struct {
97+
SetRPATH string
98+
PrintRPATH bool
99+
100+
SetInterpreter string
101+
PrintInterpreter bool
102+
103+
Output string
104+
}
105+
106+
// run runs patchelf on an ELF binary and returns its output.
107+
func (p *patchelf) run(ctx context.Context, elf string) ([]byte, error) {
108+
cmd := exec.CommandContext(ctx, lookPath("patchelf"))
109+
if p.SetRPATH != "" {
110+
cmd.Args = append(cmd.Args, "--force-rpath", "--set-rpath", p.SetRPATH)
111+
}
112+
if p.PrintRPATH {
113+
cmd.Args = append(cmd.Args, "--print-rpath")
114+
}
115+
if p.SetInterpreter != "" {
116+
cmd.Args = append(cmd.Args, "--set-interpreter", p.SetInterpreter)
117+
}
118+
if p.PrintInterpreter {
119+
cmd.Args = append(cmd.Args, "--print-interpreter")
120+
}
121+
if p.Output != "" {
122+
cmd.Args = append(cmd.Args, "--output", p.Output)
123+
}
124+
cmd.Args = append(cmd.Args, elf)
125+
out, err := cmd.Output()
126+
return bytes.TrimSpace(out), err
127+
}

internal/shellgen/tmpl/glibc-patch.nix.tmpl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
system = pkg.system;
4646

4747
# Programs needed by glibc-patch.bash.
48-
inherit (nixpkgs-glibc.legacyPackages."${system}") bash coreutils file findutils glibc gnused patchelf ripgrep;
48+
inherit (nixpkgs-glibc.legacyPackages."${system}") bash coreutils glibc gnused patchelf ripgrep;
4949

5050
# Create a package that puts the local devbox binary in the conventional
5151
# bin subdirectory. This also ensures that the executable is named
@@ -62,8 +62,9 @@
6262
args = [ "-c" "${coreutils}/bin/mkdir -p $out/bin && ${coreutils}/bin/cp ${local-devbox} $out/bin/devbox && exit 0" ];
6363
};
6464

65+
DEVBOX_DEBUG = 1;
6566
builder = "${devbox}/bin/devbox";
66-
args = [ "patch" pkg ];
67+
args = [ "patch" "--glibc" glibc pkg ];
6768
};
6869
in
6970
{

0 commit comments

Comments
 (0)