link-doctor 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- link_doctor-0.5.0/.github/workflows/ci.yml +57 -0
- link_doctor-0.5.0/LICENSE +21 -0
- link_doctor-0.5.0/MANIFEST.in +8 -0
- link_doctor-0.5.0/PKG-INFO +394 -0
- link_doctor-0.5.0/README.md +384 -0
- link_doctor-0.5.0/pyproject.toml +34 -0
- link_doctor-0.5.0/scripts/ci-local.sh +49 -0
- link_doctor-0.5.0/scripts/inspect-distributions.py +109 -0
- link_doctor-0.5.0/scripts/verify-package.sh +84 -0
- link_doctor-0.5.0/setup.cfg +4 -0
- link_doctor-0.5.0/src/link_doctor/__init__.py +3 -0
- link_doctor-0.5.0/src/link_doctor/__main__.py +79 -0
- link_doctor-0.5.0/src/link_doctor/diagnose.py +386 -0
- link_doctor-0.5.0/src/link_doctor/driver.py +595 -0
- link_doctor-0.5.0/src/link_doctor/examples/01-basic-order/README.md +21 -0
- link_doctor-0.5.0/src/link_doctor/examples/01-basic-order/build.sh +8 -0
- link_doctor-0.5.0/src/link_doctor/examples/01-basic-order/helper.c +4 -0
- link_doctor-0.5.0/src/link_doctor/examples/01-basic-order/main.c +6 -0
- link_doctor-0.5.0/src/link_doctor/examples/01-basic-order/provider.c +6 -0
- link_doctor-0.5.0/src/link_doctor/examples/02-cycle/README.md +20 -0
- link_doctor-0.5.0/src/link_doctor/examples/02-cycle/a_finish.c +4 -0
- link_doctor-0.5.0/src/link_doctor/examples/02-cycle/a_start.c +6 -0
- link_doctor-0.5.0/src/link_doctor/examples/02-cycle/b_middle.c +6 -0
- link_doctor-0.5.0/src/link_doctor/examples/02-cycle/build.sh +9 -0
- link_doctor-0.5.0/src/link_doctor/examples/02-cycle/main.c +6 -0
- link_doctor-0.5.0/src/link_doctor/examples/03-missing-provider/README.md +19 -0
- link_doctor-0.5.0/src/link_doctor/examples/03-missing-provider/build.sh +6 -0
- link_doctor-0.5.0/src/link_doctor/examples/03-missing-provider/main.c +6 -0
- link_doctor-0.5.0/src/link_doctor/examples/04-ambiguity/README.md +21 -0
- link_doctor-0.5.0/src/link_doctor/examples/04-ambiguity/build.sh +8 -0
- link_doctor-0.5.0/src/link_doctor/examples/04-ambiguity/first.c +6 -0
- link_doctor-0.5.0/src/link_doctor/examples/04-ambiguity/main.c +6 -0
- link_doctor-0.5.0/src/link_doctor/examples/04-ambiguity/second.c +4 -0
- link_doctor-0.5.0/src/link_doctor/examples/05-unsupported/README.md +19 -0
- link_doctor-0.5.0/src/link_doctor/examples/05-unsupported/build.sh +6 -0
- link_doctor-0.5.0/src/link_doctor/examples/05-unsupported/main.c +4 -0
- link_doctor-0.5.0/src/link_doctor/examples/README.md +24 -0
- link_doctor-0.5.0/src/link_doctor/outcomes.py +37 -0
- link_doctor-0.5.0/src/link_doctor/repair.py +90 -0
- link_doctor-0.5.0/src/link_doctor.egg-info/PKG-INFO +394 -0
- link_doctor-0.5.0/src/link_doctor.egg-info/SOURCES.txt +56 -0
- link_doctor-0.5.0/src/link_doctor.egg-info/dependency_links.txt +1 -0
- link_doctor-0.5.0/src/link_doctor.egg-info/entry_points.txt +2 -0
- link_doctor-0.5.0/src/link_doctor.egg-info/top_level.txt +1 -0
- link_doctor-0.5.0/tests/fixtures/archive_order/answer.c +2 -0
- link_doctor-0.5.0/tests/fixtures/archive_order/main.c +3 -0
- link_doctor-0.5.0/tests/fixtures/two_archives/leaf.c +1 -0
- link_doctor-0.5.0/tests/fixtures/two_archives/main.c +1 -0
- link_doctor-0.5.0/tests/fixtures/two_archives/middle.c +1 -0
- link_doctor-0.5.0/tests/phase3_acceptance.py +78 -0
- link_doctor-0.5.0/tests/test_common_definition_contrasts.py +121 -0
- link_doctor-0.5.0/tests/test_diagnose.py +286 -0
- link_doctor-0.5.0/tests/test_driver.py +322 -0
- link_doctor-0.5.0/tests/test_noop_wording.py +26 -0
- link_doctor-0.5.0/tests/test_notype_common.py +121 -0
- link_doctor-0.5.0/tests/test_outcomes.py +88 -0
- link_doctor-0.5.0/tests/test_phase4.py +316 -0
- link_doctor-0.5.0/tests/test_phase5.py +227 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Static Link Order Doctor CI.
|
|
2
|
+
#
|
|
3
|
+
# The equivalent checks can also be run locally with scripts/ci-local.sh.
|
|
4
|
+
# Check the Actions run for the current hosted result; local success is not
|
|
5
|
+
# a substitute for hosted evidence. The suite was recorded against CPython 3.14.9, GNU binutils
|
|
6
|
+
# 2.46 (ld.bfd/readelf/ar) and GCC 15.2.0 on Linux 7.0. Hosted runners ship
|
|
7
|
+
# different compiler versions; this file records toolchain versions in the
|
|
8
|
+
# job log rather than asserting them.
|
|
9
|
+
|
|
10
|
+
name: ci
|
|
11
|
+
|
|
12
|
+
on: [push, pull_request]
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
regression:
|
|
16
|
+
runs-on: ubuntu-24.04
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.14"
|
|
22
|
+
- name: Record toolchain
|
|
23
|
+
run: |
|
|
24
|
+
python --version
|
|
25
|
+
gcc --version | head -1
|
|
26
|
+
g++ --version | head -1
|
|
27
|
+
ld.bfd --version | head -1
|
|
28
|
+
readelf --version | head -1
|
|
29
|
+
ar --version | head -1
|
|
30
|
+
- name: Full regression and differential native suite
|
|
31
|
+
run: PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python -m unittest discover -s tests -v
|
|
32
|
+
|
|
33
|
+
package:
|
|
34
|
+
runs-on: ubuntu-24.04
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- uses: actions/setup-python@v5
|
|
38
|
+
with:
|
|
39
|
+
python-version: "3.14"
|
|
40
|
+
- name: Record toolchain
|
|
41
|
+
run: |
|
|
42
|
+
python --version
|
|
43
|
+
gcc --version | head -1
|
|
44
|
+
ld.bfd --version | head -1
|
|
45
|
+
readelf --version | head -1
|
|
46
|
+
ar --version | head -1
|
|
47
|
+
- name: Build wheel and sdist
|
|
48
|
+
run: |
|
|
49
|
+
python -m venv /tmp/build-venv
|
|
50
|
+
/tmp/build-venv/bin/pip install --upgrade pip build
|
|
51
|
+
/tmp/build-venv/bin/python -m build
|
|
52
|
+
- name: Clean install, CLI surface and shipped examples outside the checkout
|
|
53
|
+
run: |
|
|
54
|
+
python -m venv /tmp/clean-venv
|
|
55
|
+
scripts/verify-package.sh dist/link_doctor-*.whl /tmp/clean-venv/bin/python /tmp/package-check dist/link_doctor-*.tar.gz
|
|
56
|
+
- name: Distribution content inspection
|
|
57
|
+
run: /tmp/build-venv/bin/python scripts/inspect-distributions.py dist
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 krisleighmiller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
recursive-include src *.py
|
|
2
|
+
recursive-include src/link_doctor/examples *.c *.sh *.md
|
|
3
|
+
include tests/*.py
|
|
4
|
+
include tests/fixtures/*/*.c
|
|
5
|
+
include .github/workflows/ci.yml
|
|
6
|
+
include scripts/ci-local.sh
|
|
7
|
+
include scripts/verify-package.sh
|
|
8
|
+
include scripts/inspect-distributions.py
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: link-doctor
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Diagnose deliberately restricted GNU static-archive link order failures and propose constrained repairs.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# Static Link Order Doctor
|
|
12
|
+
|
|
13
|
+
Diagnose supported GNU static-archive link-order failures, explain the missing
|
|
14
|
+
extraction, and propose a constrained repair. Optional verification runs the
|
|
15
|
+
linker against isolated copies without overwriting your output.
|
|
16
|
+
|
|
17
|
+
Licensed under the [MIT License](LICENSE). The implementation, tests and examples
|
|
18
|
+
are freshly authored; no private benchmark source, prompts, graders or fixtures
|
|
19
|
+
are included or required.
|
|
20
|
+
|
|
21
|
+
## Installation and usage
|
|
22
|
+
|
|
23
|
+
The Python package and CLI are named `link-doctor`. GNU/Linux and GNU binutils
|
|
24
|
+
are required; see the supported command and toolchain boundaries below.
|
|
25
|
+
|
|
26
|
+
Install into a virtual environment from a checkout or a built wheel:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
git clone https://github.com/krisleighmiller/link-doctor.git
|
|
30
|
+
cd link-doctor
|
|
31
|
+
python3 -m venv .venv
|
|
32
|
+
.venv/bin/pip install .
|
|
33
|
+
# or: .venv/bin/pip install dist/link_doctor-*.whl
|
|
34
|
+
.venv/bin/link-doctor --help
|
|
35
|
+
.venv/bin/link-doctor --version
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Command intake and working directory
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
link-doctor diagnose [--verify] [--json] [--cwd DIR] -- ld -e main -o app main.o ...
|
|
42
|
+
link-doctor diagnose [--verify] [--json] [--cwd DIR] -- gcc main.o -L. -lfoo -o app
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Pass a linker **argv after `--`**, never a shell string; the tool performs no
|
|
46
|
+
shell evaluation. Input paths in the supplied argv resolve against `--cwd`
|
|
47
|
+
(or the current directory), which is also where `@response` files resolve.
|
|
48
|
+
`--help` and `--version` work without a linker argv.
|
|
49
|
+
|
|
50
|
+
### Result states and exit codes
|
|
51
|
+
|
|
52
|
+
Exit code 0 means the report's status is `candidate` (a model-valid repair
|
|
53
|
+
within the constrained search) or `already_satisfied` (the supplied order
|
|
54
|
+
needed no repair). Exit code 2 covers **everything else**, including all usage
|
|
55
|
+
errors and the statuses `unknown` (bounded search found no candidate — never a
|
|
56
|
+
proof of impossibility), `unsupported` (outside the documented grammar) and
|
|
57
|
+
`execution_failure` (missing tools/files, malformed native output or
|
|
58
|
+
timeouts). Human and `--json` output carry the same status, verification state,
|
|
59
|
+
reasons and provider-change warnings; JSON reports are versioned via
|
|
60
|
+
`schema_version`.
|
|
61
|
+
|
|
62
|
+
Verification is opt-in (`--verify`): the supplied command is never executed
|
|
63
|
+
during diagnosis; verification links byte snapshots in a disposable directory,
|
|
64
|
+
replaces both `-o` destinations (even output/input aliases), never runs the
|
|
65
|
+
produced binary and never overwrites your inputs or existing outputs. A
|
|
66
|
+
model-valid candidate is not a linker verdict, and **link success is never
|
|
67
|
+
proof of program equivalence** — especially when a repair changes which member
|
|
68
|
+
supplies a symbol, which the report always calls out.
|
|
69
|
+
|
|
70
|
+
### Public examples
|
|
71
|
+
|
|
72
|
+
Five self-contained examples ship inside the installed package:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
python3 -c "from pathlib import Path; import link_doctor; print(Path(link_doctor.__file__).parent / 'examples')"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
They cover a basic order error, a cross-archive cycle, a missing provider, a
|
|
79
|
+
provider-changing ambiguity and an unsupported command. Each has a `README.md`
|
|
80
|
+
and a `build.sh` that compiles with the installed GNU toolchain.
|
|
81
|
+
|
|
82
|
+
### Compatibility boundary
|
|
83
|
+
|
|
84
|
+
Intended: CPython >= 3.11 on GNU/Linux with GNU binutils `ld.bfd`/`readelf` at
|
|
85
|
+
`/usr/bin`. Actually exercised for this build: CPython 3.14.9 and 3.14.4, GNU
|
|
86
|
+
binutils 2.46, GCC/G++ 15.2.0 on Linux 7.0 (x86-64). No broader platform claim
|
|
87
|
+
is made. No third-party Python dependencies; standard library only.
|
|
88
|
+
|
|
89
|
+
### Regression and packaging checks
|
|
90
|
+
|
|
91
|
+
The full suite (unit, native differential and CLI contract tests) runs with:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
95
|
+
scripts/ci-local.sh # build wheel+sdist, clean-install and inspect them
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`.github/workflows/ci.yml` mirrors `scripts/ci-local.sh`. See the
|
|
99
|
+
[GitHub Actions runs](https://github.com/krisleighmiller/link-doctor/actions)
|
|
100
|
+
for hosted results; the recorded toolchain above describes local checks.
|
|
101
|
+
Build evidence stays in `.build/` and is deliberately excluded from the
|
|
102
|
+
distributions.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## Phase 4: constrained cycles and static provider resolution
|
|
106
|
+
|
|
107
|
+
Accepted after independent review and a full separate re-review. Both direct
|
|
108
|
+
`ld.bfd` and ordinary GCC/G++ entry paths reuse the inspected member model and
|
|
109
|
+
the same bounded repair engine.
|
|
110
|
+
|
|
111
|
+
Repairs never delete or replace library inputs, or insert whole-archive.
|
|
112
|
+
Preference is deterministic for the same input bytes/argv:
|
|
113
|
+
|
|
114
|
+
1. Original order, then permutations within contiguous user archive spans, in
|
|
115
|
+
input occurrence order (not lexical pathname order).
|
|
116
|
+
2. Starting from the original order, breadth-first demand-directed repetitions,
|
|
117
|
+
at most four added archive occurrences. Append only within the provider's
|
|
118
|
+
span, for unresolved demand introduced by a member of that same span.
|
|
119
|
+
3. One contiguous subspan group, shortest width first, then left-to-right. A long
|
|
120
|
+
cyclic chain can therefore receive `--start-group A.a B.a --end-group`
|
|
121
|
+
without grouping an unrelated `C.a`.
|
|
122
|
+
|
|
123
|
+
All strategies share a **720 model-evaluation budget**, including the original.
|
|
124
|
+
Permutations can consume the budget before repeats/groups are tried. Repeat
|
|
125
|
+
search is intentionally incomplete; it does not combine every permutation with
|
|
126
|
+
every repetition, rewrite existing groups, or move inputs across barriers.
|
|
127
|
+
Exhaustion is `unknown`, never proof that no repair exists. Group search starts
|
|
128
|
+
from the original order and tries one new group at a time, not combinations of
|
|
129
|
+
groups in multiple spans or groups plus repeats. No private formal
|
|
130
|
+
memo pruning or impossibility certificates are used.
|
|
131
|
+
|
|
132
|
+
Ordinary static weak/common/strong definitions are supported. Weak undefined
|
|
133
|
+
references do not trigger extraction; a resolved weak definition does not demand
|
|
134
|
+
a stronger archive provider. Common definitions coalesce; a common demand can
|
|
135
|
+
extract a strong defined OBJECT or NOTYPE symbol, regardless of section, but not
|
|
136
|
+
a weak, FUNC or common archive definition. ABS symbols remain unsupported.
|
|
137
|
+
Reports list coalesced common members, not a prediction of allocated layout.
|
|
138
|
+
Loaded strong/strong conflicts invalidate a candidate; unextracted alternatives
|
|
139
|
+
do not. Archive/member order determines which eligible alternative is extracted.
|
|
140
|
+
JSON `ambiguity`, `selected_providers`, `provider_changes` and `repair` fields,
|
|
141
|
+
and human warnings expose those choices. A provider-changing repair is a visible
|
|
142
|
+
proposal, **not a claim of program equivalence**, even when the link succeeds.
|
|
143
|
+
|
|
144
|
+
Fresh differential tests and receipts:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`tests/test_phase4.py` writes native argv/status/trace and report receipts to
|
|
151
|
+
`.build/phase4-acceptance/`. It contrasts standalone native GCC/G++/ld links
|
|
152
|
+
with snapshot verification, compares archive extraction traces, tests a seeded
|
|
153
|
+
24-case weak/common/strong matrix, targeted repeats/groups, alternative choices,
|
|
154
|
+
conflicts, barriers and preservation. Provider winners are also checked against
|
|
155
|
+
native cross-reference/common-allocation maps for direct ld and ordinary GCC/G++.
|
|
156
|
+
Failure/timeout injection is explicitly
|
|
157
|
+
synthetic. Red test receipts and the native common-symbol probe are retained
|
|
158
|
+
there separately. No produced executable is run.
|
|
159
|
+
|
|
160
|
+
## Ordinary GCC/G++ link commands
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
# Source checkout; paths in the supplied argv are relative to --cwd (or your cwd).
|
|
164
|
+
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 -m link_doctor \
|
|
165
|
+
diagnose --cwd '/path/to/build with spaces' --verify --json -- \
|
|
166
|
+
gcc main.o -L. -lleaf -lmiddle -o 'OUT space'
|
|
167
|
+
|
|
168
|
+
# Fresh C/libc and C++/iostream reversed-order demos, actual GCC/G++ contrasts,
|
|
169
|
+
# preservation hashes and human/JSON receipts in .build/phase3-acceptance/:
|
|
170
|
+
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 tests/phase3_acceptance.py
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Command-entry contract
|
|
174
|
+
|
|
175
|
+
- Pass an **argv**, after `--`; the caller's shell handles its own quoting. Never
|
|
176
|
+
supply a shell command string. No shell evaluation, whitespace splitting,
|
|
177
|
+
redirection, pipes or command substitution is performed by the tool.
|
|
178
|
+
- Bare `gcc` and `g++` select trusted `/usr/bin` drivers, **not** caller PATH
|
|
179
|
+
wrappers. Link-only `.o`/indexed `.a` inputs, `-o PATH`, joined/separate `-L`
|
|
180
|
+
and `-l` (including `-l:filename`) are supported. Paths may contain spaces.
|
|
181
|
+
- `-Wl,` and `-Xlinker` forward only the same allowlisted linker grammar.
|
|
182
|
+
`--as-needed`, `--no-as-needed`, `--push-state`, `--pop-state`, `-Bstatic`,
|
|
183
|
+
`-Bdynamic`, `--start-group` and `--end-group` are preserved. States/groups
|
|
184
|
+
must balance; restore dynamic mode before the compiler runtime. Existing
|
|
185
|
+
groups are evaluated to a fixed point and left intact. Only contiguous user
|
|
186
|
+
archive spans outside existing groups can be reordered, repeated or grouped;
|
|
187
|
+
objects, options, runtime inputs and groups remain barriers.
|
|
188
|
+
- GNU-style `@response` expansion supports quotes, backslash escapes and nested
|
|
189
|
+
files relative to the command cwd. Limits: 8 levels, 64 KiB cumulative file
|
|
190
|
+
contents, 256 expanded arguments, 8192 characters per argument. Cycles,
|
|
191
|
+
malformed quotes/escapes, empty arguments and embedded control characters
|
|
192
|
+
fail closed. `-Wl,@file` is rejected; use top-level `@file`. Repairs are fully
|
|
193
|
+
expanded canonical argv, not edits to response files. Reports preserve both
|
|
194
|
+
original and normalized commands plus response-file hashes.
|
|
195
|
+
- Library selection searches all explicit `-L` directories before discovered
|
|
196
|
+
driver paths, in order; within each directory `.so` precedes `.a` unless
|
|
197
|
+
static mode is active. A selected unsupported `.so` is rejected, never
|
|
198
|
+
skipped in favor of a convenient archive. Missing library names are reported
|
|
199
|
+
explicitly. A default output name need not be supplied: verification always
|
|
200
|
+
sets its own disposable output.
|
|
201
|
+
|
|
202
|
+
### Runtime discovery and safety policy
|
|
203
|
+
|
|
204
|
+
The driver receives only fixed synthetic `-### DOCTOR_INPUT.o -o DOCTOR_OUTPUT`
|
|
205
|
+
arguments in a disposable cwd and sanitized environment. **No supplied command
|
|
206
|
+
is executed during diagnosis**, even as a driver dry run. The trusted driver's
|
|
207
|
+
startup inputs, search paths and fixed linker flags are parsed, not guessed.
|
|
208
|
+
Unknown template options fail closed. User specs, plugins, wrappers, source
|
|
209
|
+
compilation, `-B` compiler overrides, alternate linkers, sysroots, maps,
|
|
210
|
+
dependency outputs, script options and arbitrary output-producing options
|
|
211
|
+
are rejected before discovery. LTO, bitcode and disguised user scripts are
|
|
212
|
+
rejected during file inspection. No install or Python package dependency is needed.
|
|
213
|
+
|
|
214
|
+
GCC's driver-owned `liblto_plugin.so` must resolve to a root-owned path with
|
|
215
|
+
non-writable ancestors. Its discovery options are removed: **no plugin,
|
|
216
|
+
collect2 or compiler is executed for verification**. Every selected object and
|
|
217
|
+
archive member is checked for ELF/LTO metadata first; opt-in verification uses
|
|
218
|
+
trusted `ld.bfd` on snapshots of the normalized non-LTO link.
|
|
219
|
+
|
|
220
|
+
Restricted root-owned compiler-runtime `GROUP`, `INPUT`, `AS_NEEDED` and
|
|
221
|
+
`OUTPUT_FORMAT(elf64-x86-64)` scripts are recursively resolved, validated and
|
|
222
|
+
snapshotted. Group/as-needed semantics are preserved rather than flattened.
|
|
223
|
+
Nested `-l` selections use the actual library search paths, without an implicit
|
|
224
|
+
cwd entry. Raw script filenames retain their spelling: relative names (including
|
|
225
|
+
nested relative paths) try the original command cwd first, then those search
|
|
226
|
+
paths, not the script's parent directory; absolute names are used directly.
|
|
227
|
+
A selected invalid cwd file is rejected, never replaced by a system copy.
|
|
228
|
+
A system script's child does **not** inherit trust: cwd and user `-L` overrides
|
|
229
|
+
receive ordinary user ELF checks, and valid archives are accepted regardless of
|
|
230
|
+
a `.so` suffix. Inventory receipts retain spelling, lookup context, selected path
|
|
231
|
+
and byte hash; verification renders the selected snapshots. Explicit `-lm`, `-lc`, `-lgcc`, `-lgcc_s` and `-lstdc++` may use this same
|
|
232
|
+
restricted runtime path. General user scripts/shared libraries remain outside
|
|
233
|
+
scope. Runtime DSOs' `DT_NEEDED` closure is inspected and snapshotted; complete
|
|
234
|
+
dynamic-table row counts are checked, and RPATH/RUNPATH/filter/audit semantics
|
|
235
|
+
are rejected. Verification resolves dependency aliases in the snapshot directory.
|
|
236
|
+
The interpreter path is a validated system path, not an executed program.
|
|
237
|
+
|
|
238
|
+
The minimum model handles CRT weak references, weak definitions, default and
|
|
239
|
+
versioned shared-provider names, runtime IFUNC/TLS/unique provider *presence*,
|
|
240
|
+
and initializer sections without discarding their ordinary symbol references.
|
|
241
|
+
Weak undefined references do not extract archive members. It does **not** model
|
|
242
|
+
runtime dynamic binding, execute IFUNC resolvers, validate relocations/layout,
|
|
243
|
+
or establish behavioral equivalence. COMDAT/section groups, user TLS/IFUNC,
|
|
244
|
+
user/runtime provider overlap and unsupported special sections cannot yield a
|
|
245
|
+
confident repair. Trusted runtime libraries remain a fixed baseline, not general
|
|
246
|
+
mixed shared/static or dynamic-binding repair support. These boundaries retain
|
|
247
|
+
ordinary static object/archive repair through the compiler runtime; they do not
|
|
248
|
+
claim arbitrary C++ COMDAT, section-GC or user-DSO semantics.
|
|
249
|
+
|
|
250
|
+
Reports include recursive input structure, selected file hashes and trust
|
|
251
|
+
classification, discovery argv/template, search paths, missing-symbol consumers,
|
|
252
|
+
archive extraction evidence and positional repairs. `missing_provider`,
|
|
253
|
+
`duplicate_definitions`, `provider_ambiguity` and `constrained_search_exhausted`
|
|
254
|
+
are non-ordering diagnosis categories; unsupported/incompatible input and tool
|
|
255
|
+
failures retain their reasons. Model-valid is not linker-verified. Native
|
|
256
|
+
verification disagreement withholds the command and returns `unknown`.
|
|
257
|
+
|
|
258
|
+
Limits additionally include 16 MiB per file, 128 MiB total selected files, 2048
|
|
259
|
+
files, 256 members per user archive/1024 per trusted runtime archive, 16 nested
|
|
260
|
+
runtime-script expressions, 720 candidate evaluations and 10 seconds per native
|
|
261
|
+
call. This is **not a sandbox or a hard disk/process-wide time quota**. The
|
|
262
|
+
runtime grammar is intentionally specific to the discovered GNU x86-64 toolchain;
|
|
263
|
+
static executables through the driver, non-PIE driver variants, general shared
|
|
264
|
+
linking and GC remain unsupported. Tested native acceptance: GCC/G++ 15.2.0
|
|
265
|
+
and GNU binutils 2.46. Other templates may be explicitly rejected.
|
|
266
|
+
|
|
267
|
+
## Phase 2: explicit-input diagnosis and repair
|
|
268
|
+
|
|
269
|
+
The original internal vertical milestone remains supported with its narrower
|
|
270
|
+
explicit-input grammar. Run from source with a CPython interpreter inside the
|
|
271
|
+
declared compatibility boundary (see above), on Linux, with GNU readelf and
|
|
272
|
+
ld.bfd installed at `/usr/bin`. The tests also
|
|
273
|
+
require GNU gcc and ar. No Python dependencies are required.
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Reproduce the fresh two-archive case
|
|
280
|
+
|
|
281
|
+
From this directory (builds only in a disposable directory):
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
root="$PWD"
|
|
285
|
+
build="$(mktemp -d "${TMPDIR:-/tmp}/link-doctor-demo.XXXXXX")"
|
|
286
|
+
(
|
|
287
|
+
cd "$build" || exit 1
|
|
288
|
+
for name in main middle leaf; do
|
|
289
|
+
gcc -c -fno-pie -fno-stack-protector \
|
|
290
|
+
"$root/tests/fixtures/two_archives/$name.c" -o "$name.o" || exit 1
|
|
291
|
+
done
|
|
292
|
+
ar rcs libmiddle.a middle.o
|
|
293
|
+
ar rcs libleaf.a leaf.o
|
|
294
|
+
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH="$root/src" python3 -m link_doctor \
|
|
295
|
+
diagnose --verify -- ld.bfd -e main -o OUT main.o libleaf.a libmiddle.a
|
|
296
|
+
)
|
|
297
|
+
rm -rf -- "$build"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The original command leaves `leaf` unresolved: `libleaf.a(leaf.o)` was visited
|
|
301
|
+
when only `middle` was needed. Later, `libmiddle.a(middle.o)` introduces the
|
|
302
|
+
reference to `leaf`. The repair is:
|
|
303
|
+
|
|
304
|
+
```sh
|
|
305
|
+
ld.bfd -e main -o OUT main.o libmiddle.a libleaf.a
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Add `--json` before `--` for a schema-versioned report with original argv,
|
|
309
|
+
ordered inputs/members and their definitions/references, extraction events,
|
|
310
|
+
archive-visit evidence, unresolved symbols/consumers, missed providers, search
|
|
311
|
+
budget, positional changes, candidate model result and native verification
|
|
312
|
+
receipts. Human output includes the same central evidence, proposed command,
|
|
313
|
+
changes and verification status. Exit 0 means model-valid candidate or already
|
|
314
|
+
satisfied; exit 2 means unsupported/error/unknown, **not** proven impossible.
|
|
315
|
+
`execution_failure` distinguishes missing tools/files and timeouts from unsupported
|
|
316
|
+
semantics; native rejection of a proposed repair returns `unknown` with
|
|
317
|
+
`verification.status = failed` and no usable command.
|
|
318
|
+
|
|
319
|
+
Without `--verify`, only inspection runs; the supplied linker command is never
|
|
320
|
+
executed. With `--verify` and a model-valid candidate, both original and candidate
|
|
321
|
+
orders are linked against byte snapshots in a fresh temporary directory. Both
|
|
322
|
+
`-o` destinations are replaced, even when the requested output aliases an input.
|
|
323
|
+
No linked binary is executed. Original files and existing output sentinels are
|
|
324
|
+
untouched. Temporary files are removed on completion. Receipt argv paths therefore
|
|
325
|
+
refer to now-disposed snapshots, not reusable executables. Native failure,
|
|
326
|
+
timeout or disagreement with the model withholds the usable candidate command.
|
|
327
|
+
A proposed command is evidence about the inspected snapshots, not a promise about
|
|
328
|
+
files that a user subsequently edits; executing it manually has normal linker
|
|
329
|
+
output-overwrite behavior.
|
|
330
|
+
|
|
331
|
+
## Direct-ld semantics and deliberate rejection boundaries
|
|
332
|
+
|
|
333
|
+
- Bare `ld` or `ld.bfd` argv only, mapped to trusted `/usr/bin/ld.bfd` rather than
|
|
334
|
+
resolved through the user's PATH. Exactly one separate `-e SYMBOL` and `-o PATH`
|
|
335
|
+
are required. Balanced `--start-group`/`--end-group` are also supported.
|
|
336
|
+
Other flags, absolute executable paths, wrappers, response files,
|
|
337
|
+
scripts, libraries by `-l`, search paths, plugin options, maps/dependency outputs,
|
|
338
|
+
whole-archive/GC modes are rejected before spawning. GCC/G++ use the separate
|
|
339
|
+
driver grammar above.
|
|
340
|
+
- Explicit regular `.o` and normal indexed GNU `.a` files only; ELF64,
|
|
341
|
+
little-endian, x86-64 relocatables. Ordinary default-visibility strong, weak
|
|
342
|
+
and common globals with simple C-style names only. Unique/IFUNC/TLS, LTO/bitcode,
|
|
343
|
+
COMDAT/groups, dynamic symbols and special linker sections handled by the
|
|
344
|
+
rejection rules are unsupported. This is a symbol-extraction model, not a
|
|
345
|
+
relocation/layout/ABI validator. A model-valid command is not native success.
|
|
346
|
+
- Every member is inspected, even unused members. Duplicate member names, thin
|
|
347
|
+
archives, BSD/64-bit indexes, non-ELF members and `__.LIBDEP` dependency metadata
|
|
348
|
+
are rejected. GNU long member names and path spaces are supported. Archive
|
|
349
|
+
indexes must exactly match the inspected symbol-to-member-offset relationships
|
|
350
|
+
and use nondecreasing member offsets. Noncanonical index order is explicitly
|
|
351
|
+
refused: native probes show it can change an alternative provider without
|
|
352
|
+
changing link success. Repeated archive paths share member identity; repeated
|
|
353
|
+
explicit objects are rejected, not silently deduplicated.
|
|
354
|
+
- ELF facts come from `readelf -W -h -S -s`, not a new ELF parser. No debug dumps
|
|
355
|
+
or debug-link following are requested. A small bounded GNU ar **container**
|
|
356
|
+
reader preserves member order/identity, checks index consistency and rejects
|
|
357
|
+
metadata that whole-archive readelf can skip. It is not an archive extraction
|
|
358
|
+
utility: original names are never used as filesystem destinations. `nm` and
|
|
359
|
+
BFD `ar` are deliberately not run for inspection because of implicit plugins.
|
|
360
|
+
- Each archive repeatedly extracts needed members until stable. Unused members
|
|
361
|
+
do not inject their references. A later reference does not rescan an earlier
|
|
362
|
+
archive. The explicit `-e` symbol roots archive extraction on the supported
|
|
363
|
+
GNU ld.bfd, as covered by a native archive-only entry-point test.
|
|
364
|
+
- Only contiguous archive-only argv spans may be repaired using the Phase 4
|
|
365
|
+
policy above. Objects and every flag stay fixed and act as barriers. Search
|
|
366
|
+
never materializes factorial permutation pools. No valid candidate means
|
|
367
|
+
`unknown`, never an impossibility proof.
|
|
368
|
+
- Limits: 64 inputs, 256 members per archive, 16 MiB total input bytes, 8192 chars
|
|
369
|
+
per argv token, 10 seconds per native invocation; captured tool output above
|
|
370
|
+
16 MiB is rejected after the invocation. Tools must resolve to root-owned,
|
|
371
|
+
non-group/world-writable regular files. Subprocesses use argv, `shell=False`,
|
|
372
|
+
a sanitized environment, and only the two hardcoded system tools. Missing
|
|
373
|
+
files/tools, malformed native output, stderr diagnostics and timeouts fail
|
|
374
|
+
closed. **This is not an OS sandbox**: do not treat native binutils as safe
|
|
375
|
+
against arbitrary hostile ELF exploits. Output size checks are not a hard
|
|
376
|
+
disk quota, and there is no process-wide wall-clock budget.
|
|
377
|
+
|
|
378
|
+
The Phase 1 `outcomes.assess_candidate` boundary is reused: unmet requirements
|
|
379
|
+
withhold a command, and model satisfaction alone never sets `verified`.
|
|
380
|
+
|
|
381
|
+
## Tests
|
|
382
|
+
|
|
383
|
+
`tests/test_diagnose.py` builds fresh C in temporary directories, never runs linked
|
|
384
|
+
programs, and tests two/three-archive chains, correct-order contrast, intra-archive
|
|
385
|
+
fixed points, unused members, object/flag barriers, missing providers/entry,
|
|
386
|
+
bounded search, byte preservation including output/input aliasing, real GNU
|
|
387
|
+
verification, long names/path spaces, unsupported inputs/options, metadata,
|
|
388
|
+
malformed indexes, FIFO rejection, partial parser output, and injected tool
|
|
389
|
+
failures/timeouts. Mocked failures are fault-injection tests, not claimed native
|
|
390
|
+
link evidence. The original seven outcome/native tests remain unchanged.
|
|
391
|
+
|
|
392
|
+
Canonical plan, provenance, TDD and independent verification evidence are kept
|
|
393
|
+
outside this repository, in the author's private planning notes. This project
|
|
394
|
+
contains only the fresh implementation, its public examples and its tests.
|