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.
Files changed (58) hide show
  1. link_doctor-0.5.0/.github/workflows/ci.yml +57 -0
  2. link_doctor-0.5.0/LICENSE +21 -0
  3. link_doctor-0.5.0/MANIFEST.in +8 -0
  4. link_doctor-0.5.0/PKG-INFO +394 -0
  5. link_doctor-0.5.0/README.md +384 -0
  6. link_doctor-0.5.0/pyproject.toml +34 -0
  7. link_doctor-0.5.0/scripts/ci-local.sh +49 -0
  8. link_doctor-0.5.0/scripts/inspect-distributions.py +109 -0
  9. link_doctor-0.5.0/scripts/verify-package.sh +84 -0
  10. link_doctor-0.5.0/setup.cfg +4 -0
  11. link_doctor-0.5.0/src/link_doctor/__init__.py +3 -0
  12. link_doctor-0.5.0/src/link_doctor/__main__.py +79 -0
  13. link_doctor-0.5.0/src/link_doctor/diagnose.py +386 -0
  14. link_doctor-0.5.0/src/link_doctor/driver.py +595 -0
  15. link_doctor-0.5.0/src/link_doctor/examples/01-basic-order/README.md +21 -0
  16. link_doctor-0.5.0/src/link_doctor/examples/01-basic-order/build.sh +8 -0
  17. link_doctor-0.5.0/src/link_doctor/examples/01-basic-order/helper.c +4 -0
  18. link_doctor-0.5.0/src/link_doctor/examples/01-basic-order/main.c +6 -0
  19. link_doctor-0.5.0/src/link_doctor/examples/01-basic-order/provider.c +6 -0
  20. link_doctor-0.5.0/src/link_doctor/examples/02-cycle/README.md +20 -0
  21. link_doctor-0.5.0/src/link_doctor/examples/02-cycle/a_finish.c +4 -0
  22. link_doctor-0.5.0/src/link_doctor/examples/02-cycle/a_start.c +6 -0
  23. link_doctor-0.5.0/src/link_doctor/examples/02-cycle/b_middle.c +6 -0
  24. link_doctor-0.5.0/src/link_doctor/examples/02-cycle/build.sh +9 -0
  25. link_doctor-0.5.0/src/link_doctor/examples/02-cycle/main.c +6 -0
  26. link_doctor-0.5.0/src/link_doctor/examples/03-missing-provider/README.md +19 -0
  27. link_doctor-0.5.0/src/link_doctor/examples/03-missing-provider/build.sh +6 -0
  28. link_doctor-0.5.0/src/link_doctor/examples/03-missing-provider/main.c +6 -0
  29. link_doctor-0.5.0/src/link_doctor/examples/04-ambiguity/README.md +21 -0
  30. link_doctor-0.5.0/src/link_doctor/examples/04-ambiguity/build.sh +8 -0
  31. link_doctor-0.5.0/src/link_doctor/examples/04-ambiguity/first.c +6 -0
  32. link_doctor-0.5.0/src/link_doctor/examples/04-ambiguity/main.c +6 -0
  33. link_doctor-0.5.0/src/link_doctor/examples/04-ambiguity/second.c +4 -0
  34. link_doctor-0.5.0/src/link_doctor/examples/05-unsupported/README.md +19 -0
  35. link_doctor-0.5.0/src/link_doctor/examples/05-unsupported/build.sh +6 -0
  36. link_doctor-0.5.0/src/link_doctor/examples/05-unsupported/main.c +4 -0
  37. link_doctor-0.5.0/src/link_doctor/examples/README.md +24 -0
  38. link_doctor-0.5.0/src/link_doctor/outcomes.py +37 -0
  39. link_doctor-0.5.0/src/link_doctor/repair.py +90 -0
  40. link_doctor-0.5.0/src/link_doctor.egg-info/PKG-INFO +394 -0
  41. link_doctor-0.5.0/src/link_doctor.egg-info/SOURCES.txt +56 -0
  42. link_doctor-0.5.0/src/link_doctor.egg-info/dependency_links.txt +1 -0
  43. link_doctor-0.5.0/src/link_doctor.egg-info/entry_points.txt +2 -0
  44. link_doctor-0.5.0/src/link_doctor.egg-info/top_level.txt +1 -0
  45. link_doctor-0.5.0/tests/fixtures/archive_order/answer.c +2 -0
  46. link_doctor-0.5.0/tests/fixtures/archive_order/main.c +3 -0
  47. link_doctor-0.5.0/tests/fixtures/two_archives/leaf.c +1 -0
  48. link_doctor-0.5.0/tests/fixtures/two_archives/main.c +1 -0
  49. link_doctor-0.5.0/tests/fixtures/two_archives/middle.c +1 -0
  50. link_doctor-0.5.0/tests/phase3_acceptance.py +78 -0
  51. link_doctor-0.5.0/tests/test_common_definition_contrasts.py +121 -0
  52. link_doctor-0.5.0/tests/test_diagnose.py +286 -0
  53. link_doctor-0.5.0/tests/test_driver.py +322 -0
  54. link_doctor-0.5.0/tests/test_noop_wording.py +26 -0
  55. link_doctor-0.5.0/tests/test_notype_common.py +121 -0
  56. link_doctor-0.5.0/tests/test_outcomes.py +88 -0
  57. link_doctor-0.5.0/tests/test_phase4.py +316 -0
  58. 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.