lib/tldr/src/formats/elf/relink.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const allocators = @import("alloc");
   3 const root = @import("../../root.zig");
   4 const addressing = @import("address/root.zig");
   5 const archive_selection = @import("archive/root.zig");
   6 const format = @import("format.zig");
   7 const object_writer = @import("object/root.zig");
   8 const parser = @import("parser.zig");
   9 const relocation = @import("relocation/root.zig");
  10 
  11 const Allocator = std.mem.Allocator;
  12 const archive = root.archive;
  13 const checked = relocation.value;
  14 const incremental = root.incremental;
  15 const model = root.model;
  16 const parallel = root.parallel;
  17 
  18 const ContributionKind = incremental.ContributionKind;
  19 const ContributionRecord = incremental.ContributionRecord;
  20 const InputChangeStorage = incremental.InputChangeStorage;
  21 const InputChanges = incremental.InputChanges;
  22 const Manifest = incremental.Manifest;
  23 const RecordedInputs = incremental.RecordedInputs;
  24 const DirectRelinkEvidence = incremental.DirectRelinkEvidence;
  25 const ReplacementContribution = incremental.ReplacementContribution;
  26 const ObjectFile = parser.ObjectFile;
  27 const ObjectRelocation = object_writer.Relocation;
  28 const ObjectSection = object_writer.Section;
  29 const ObjectSymbol = object_writer.Symbol;
  30 const SectionHeader = format.SectionHeader;
  31 const Symbol = format.Symbol;
  32 const Rela = format.Rela;
  33 const buildObject = object_writer.build;
  34 const sectionBytes = format.sectionBytes;
  35 const sectionName = parser.sectionName;
  36 const writeU16 = format.writeU16;
  37 const writeU32 = format.writeU32;
  38 const writeU64 = format.writeU64;
  39 
  40 pub fn linkageHash(object: ObjectFile) model.Error!u64 {
  41     var hasher = std.hash.Wyhash.init(0x544c44524c494e4b);
  42     try hashSections(&hasher, object);
  43     hashSymbols(&hasher, object.symbols);
  44     hashRelocations(&hasher, object.relocations);
  45     return hasher.final();
  46 }
  47 
  48 pub fn recordInputLinkEvidence(
  49     builder: *incremental.Builder,
  50     allocator: Allocator,
  51     inputs: []const model.Input,
  52     objects: []const ObjectFile,
  53     archive_state: *const archive_selection.ExtractionState,
  54 ) model.Error!void {
  55     if (inputs.len == 0) return;
  56 
  57     const states = try allocator.alloc(InputLinkHashState, inputs.len);
  58     defer allocator.free(states);
  59     for (states) |*state| state.* = InputLinkHashState.init();
  60 
  61     for (objects) |object| {
  62         if (object.input_index >= states.len) continue;
  63         try states[object.input_index].update(object);
  64     }
  65 
  66     for (states, inputs, 0..) |*state, input, input_index| {
  67         if (archive.isArchive(input.bytes)) {
  68             builder.setInputSelectionHash(input_index, try selectionHashForInput(allocator, input));
  69         }
  70         if (state.count == 0) continue;
  71         builder.setInputLinkHash(input_index, state.final());
  72     }
  73 
  74     for (objects) |object| {
  75         if (object.input_index >= inputs.len) continue;
  76         if (!archive.isArchive(inputs[object.input_index].bytes)) continue;
  77         try builder.addArchiveMember(
  78             object.input_index,
  79             object.name,
  80             incremental.hashBytes(object.bytes),
  81             try linkageHash(object),
  82             true,
  83         );
  84     }
  85     for (archive_state.candidates.items) |candidate| {
  86         if (candidate.extracted) continue;
  87         if (candidate.object == null) continue;
  88         try builder.addArchiveMember(
  89             candidate.archive_input_index,
  90             candidate.member.name,
  91             incremental.hashBytes(candidate.member.bytes),
  92             0,
  93             false,
  94         );
  95     }
  96 }
  97 
  98 pub fn directEvidenceAlloc(
  99     allocator: Allocator,
 100     manifest: Manifest,
 101     inputs: []const model.Input,
 102     input_changes: InputChanges,
 103     options: model.LinkOptions,
 104 ) model.Error!DirectRelinkEvidence {
 105     if (!directReplacementsEnabled(options)) return .{};
 106 
 107     const changed_inputs = try changedInputFilterAlloc(allocator, manifest, input_changes);
 108     defer allocator.free(changed_inputs);
 109 
 110     var context = try DirectRelocationContext.init(allocator, manifest, changed_inputs);
 111     defer context.deinit(allocator);
 112     try context.seedGotEntries(allocator, manifest.got_entries);
 113     try context.seedMergePieces(allocator, manifest.merge_pieces);
 114     const inputs_proven = try context.addProvenInputs(allocator, manifest, inputs, input_changes, options);
 115     if (!inputs_proven) return .{};
 116 
 117     const member_updates = try context.member_updates.toOwnedSlice(allocator);
 118     errdefer if (member_updates.len != 0) allocator.free(member_updates);
 119 
 120     if (input_changes.summary.changed == 0 or manifest.contributions.len == 0) {
 121         return .{ .member_updates = member_updates, .inputs_proven = true };
 122     }
 123 
 124     var replacements: std.ArrayListUnmanaged(ReplacementContribution) = .empty;
 125     errdefer freeReplacementList(allocator, &replacements);
 126 
 127     for (input_changes.changes) |change| {
 128         if (change.kind != .changed) continue;
 129         const recorded_index = change.recorded_index orelse continue;
 130         if (recorded_index >= manifest.inputs.len) continue;
 131         try context.appendProvenInputReplacements(allocator, &replacements, recorded_index);
 132     }
 133 
 134     if (replacements.items.len == 0) {
 135         replacements.deinit(allocator);
 136         return .{ .member_updates = member_updates, .inputs_proven = true };
 137     }
 138     return .{
 139         .replacements = try replacements.toOwnedSlice(allocator),
 140         .member_updates = member_updates,
 141         .inputs_proven = true,
 142     };
 143 }
 144 
 145 fn directReplacementsEnabled(options: model.LinkOptions) bool {
 146     return options.icf == .off;
 147 }
 148 
 149 fn appendInputReplacements(
 150     allocator: Allocator,
 151     replacements: *std.ArrayListUnmanaged(ReplacementContribution),
 152     context: *const DirectRelocationContext,
 153     object: ObjectFile,
 154     recorded_index: usize,
 155 ) model.Error!void {
 156     for (context.contributionIndexesForInputName(object.name, recorded_index)) |contribution_index| {
 157         if (contribution_index >= context.contributions.len) continue;
 158         const contribution = context.contributions[contribution_index];
 159         if (contribution.input_index != recorded_index or !std.mem.eql(u8, context.manifest.string(contribution.input_name_id), object.name)) continue;
 160         const replacement = try replacementForContribution(allocator, context, contribution, object, recorded_index) orelse continue;
 161         errdefer freeReplacementPayload(allocator, replacement);
 162         try replacements.append(allocator, replacement);
 163     }
 164 }
 165 
 166 fn replacementForContribution(
 167     allocator: Allocator,
 168     context: *const DirectRelocationContext,
 169     contribution: ContributionRecord,
 170     object: ObjectFile,
 171     recorded_index: usize,
 172 ) model.Error!?ReplacementContribution {
 173     if (contribution.kind == .common_symbol) return replacementForCommonContribution(context.manifest, contribution, object);
 174     if (contribution.kind != .section) return null;
 175 
 176     const section_index: usize = contribution.ordinal;
 177     if (section_index >= object.sections.len) return null;
 178     const section = object.sections[section_index];
 179     const name = try sectionName(object, section_index);
 180     if (!std.mem.eql(u8, name, context.manifest.string(contribution.name_id))) return null;
 181     if (section.size != contribution.size) return null;
 182     if (@max(section.alignment, 1) > @max(contribution.alignment, 1)) return null;
 183 
 184     const relocations = object.relocationsForSection(section_index);
 185     if (hashCoveredPayloadSection(section, name)) return hashCoveredReplacement(context.manifest, contribution);
 186     const payload = if (section.section_type == std.elf.SHT_NOBITS) payload: {
 187         if (!filelessPayloadSection(section)) return null;
 188         break :payload &.{};
 189     } else if (relocations.len == 0) payload: {
 190         if (contribution.file_size != contribution.size) return null;
 191         break :payload try sectionBytes(object.bytes, section);
 192     } else payload: {
 193         if (contribution.file_size != contribution.size) return null;
 194         break :payload (try relocatedPayloadAlloc(allocator, context, contribution, object, recorded_index, section, relocations)) orelse return null;
 195     };
 196 
 197     return .{
 198         .input_name = context.manifest.string(contribution.input_name_id),
 199         .input_index = @intCast(contribution.input_index),
 200         .kind = contribution.kind,
 201         .name = context.manifest.string(contribution.name_id),
 202         .ordinal = contribution.ordinal,
 203         .size = section.size,
 204         .alignment = @max(section.alignment, 1),
 205         .output_section_name = context.manifest.string(contribution.output_section_name_id),
 206         .address = contribution.address,
 207         .file_offset = contribution.file_offset,
 208         .reserved_size = contribution.reserved_size,
 209         .payload = payload,
 210         .payload_owned = section.section_type != std.elf.SHT_NOBITS and relocations.len != 0,
 211     };
 212 }
 213 
 214 fn hashCoveredPayloadSection(section: SectionHeader, name: []const u8) bool {
 215     if (section.section_type == std.elf.SHT_NOBITS) return false;
 216     return !directPayloadSection(section, name);
 217 }
 218 
 219 fn hashCoveredReplacement(manifest: Manifest, contribution: ContributionRecord) ReplacementContribution {
 220     return .{
 221         .input_name = manifest.string(contribution.input_name_id),
 222         .input_index = @intCast(contribution.input_index),
 223         .kind = contribution.kind,
 224         .name = manifest.string(contribution.name_id),
 225         .ordinal = contribution.ordinal,
 226         .size = contribution.size,
 227         .alignment = contribution.alignment,
 228         .output_section_name = manifest.string(contribution.output_section_name_id),
 229         .address = contribution.address,
 230         .file_offset = contribution.file_offset,
 231         .reserved_size = contribution.reserved_size,
 232         .unchanged = true,
 233     };
 234 }
 235 
 236 fn replacementForCommonContribution(
 237     manifest: Manifest,
 238     contribution: ContributionRecord,
 239     object: ObjectFile,
 240 ) ?ReplacementContribution {
 241     const symbol_index: usize = contribution.ordinal;
 242     if (symbol_index >= object.symbols.len) return null;
 243     const symbol = object.symbols[symbol_index];
 244     if (!symbol.isCommon()) return null;
 245     if (!std.mem.eql(u8, symbol.name, manifest.string(contribution.name_id))) return null;
 246     if (symbol.size != contribution.size) return null;
 247     if (@max(symbol.value, 1) > @max(contribution.alignment, 1)) return null;
 248     return .{
 249         .input_name = manifest.string(contribution.input_name_id),
 250         .input_index = @intCast(contribution.input_index),
 251         .kind = contribution.kind,
 252         .name = manifest.string(contribution.name_id),
 253         .ordinal = contribution.ordinal,
 254         .size = symbol.size,
 255         .alignment = @max(symbol.value, 1),
 256         .output_section_name = manifest.string(contribution.output_section_name_id),
 257         .address = contribution.address,
 258         .file_offset = contribution.file_offset,
 259         .reserved_size = contribution.reserved_size,
 260     };
 261 }
 262 
 263 fn freeReplacementList(allocator: Allocator, replacements: *std.ArrayListUnmanaged(ReplacementContribution)) void {
 264     for (replacements.items) |replacement| freeReplacementPayload(allocator, replacement);
 265     replacements.deinit(allocator);
 266 }
 267 
 268 fn freeReplacementPayload(allocator: Allocator, replacement: ReplacementContribution) void {
 269     if (replacement.payload_owned) allocator.free(replacement.payload);
 270 }
 271 
 272 fn directPayloadSection(section: SectionHeader, name: []const u8) bool {
 273     return patchablePayloadSection(section, name) or stableRelocatedPayloadSection(section, name);
 274 }
 275 
 276 fn filelessPayloadSection(section: SectionHeader) bool {
 277     if ((section.flags & std.elf.SHF_ALLOC) == 0) return false;
 278     if ((section.flags & std.elf.SHF_MERGE) != 0) return false;
 279     return section.section_type == std.elf.SHT_NOBITS;
 280 }
 281 
 282 fn patchablePayloadSection(section: SectionHeader, name: []const u8) bool {
 283     if ((section.flags & std.elf.SHF_ALLOC) == 0) return false;
 284     if ((section.flags & std.elf.SHF_MERGE) != 0) return false;
 285     if (section.section_type != std.elf.SHT_PROGBITS) return false;
 286     if (std.mem.eql(u8, name, ".eh_frame")) return false;
 287     return true;
 288 }
 289 
 290 fn stableRelocatedPayloadSection(section: SectionHeader, name: []const u8) bool {
 291     if ((section.flags & std.elf.SHF_ALLOC) == 0) return false;
 292     if ((section.flags & std.elf.SHF_MERGE) != 0) return false;
 293     return section.section_type == std.elf.SHT_X86_64_UNWIND or std.mem.eql(u8, name, ".eh_frame");
 294 }
 295 
 296 fn relocatedPayloadAlloc(
 297     allocator: Allocator,
 298     context: *const DirectRelocationContext,
 299     contribution: ContributionRecord,
 300     object: ObjectFile,
 301     recorded_index: usize,
 302     section: SectionHeader,
 303     relocations: []const Rela,
 304 ) model.Error!?[]u8 {
 305     const bytes = try sectionBytes(object.bytes, section);
 306     const payload = try allocator.dupe(u8, bytes);
 307     var keep_payload = false;
 308     defer if (!keep_payload) allocator.free(payload);
 309 
 310     for (relocations) |entry| {
 311         if (relocation.isNone(entry)) continue;
 312         if (!try applyDirectRelocation(payload, context, contribution, object, recorded_index, entry)) return null;
 313     }
 314 
 315     keep_payload = true;
 316     return payload;
 317 }
 318 
 319 const DirectRelocationTarget = struct {
 320     address: i128,
 321     size: u64,
 322 };
 323 
 324 fn applyDirectRelocation(
 325     payload: []u8,
 326     context: *const DirectRelocationContext,
 327     contribution: ContributionRecord,
 328     object: ObjectFile,
 329     recorded_index: usize,
 330     entry: Rela,
 331 ) model.Error!bool {
 332     const symbol_index: usize = @intCast(entry.symbolIndex());
 333     if (symbol_index >= object.symbols.len) return false;
 334     const relocation_type = entry.relocationType();
 335     switch (relocation_type) {
 336         @backingInt(std.elf.R_X86_64.GOTPCREL),
 337         @backingInt(std.elf.R_X86_64.GOTPCRELX),
 338         @backingInt(std.elf.R_X86_64.REX_GOTPCRELX),
 339         => return applyDirectGotRelocation(payload, context, contribution, object, recorded_index, entry, relocation_type),
 340         else => {},
 341     }
 342 
 343     const symbol = object.symbols[symbol_index];
 344     var addend = @as(i128, entry.addend);
 345     const target = merged: {
 346         if (symbol.isSection() and entry.addend >= 0) {
 347             if (context.mergePieceAddress(object.name, recorded_index, symbol.section_index, @intCast(entry.addend))) |piece_address| {
 348                 addend = 0;
 349                 break :merged DirectRelocationTarget{ .address = piece_address, .size = 0 };
 350             }
 351         }
 352         break :merged directRelocationTarget(context, object, recorded_index, symbol_index) orelse return false;
 353     };
 354 
 355     switch (relocation_type) {
 356         @backingInt(std.elf.R_X86_64.@"64"),
 357         @backingInt(std.elf.R_X86_64.@"32"),
 358         @backingInt(std.elf.R_X86_64.@"32S"),
 359         @backingInt(std.elf.R_X86_64.@"16"),
 360         @backingInt(std.elf.R_X86_64.@"8"),
 361         => {
 362             const write_size: u64 = switch (relocation_type) {
 363                 @backingInt(std.elf.R_X86_64.@"64") => 8,
 364                 @backingInt(std.elf.R_X86_64.@"32"),
 365                 @backingInt(std.elf.R_X86_64.@"32S"),
 366                 => 4,
 367                 @backingInt(std.elf.R_X86_64.@"16") => 2,
 368                 @backingInt(std.elf.R_X86_64.@"8") => 1,
 369                 else => unreachable,
 370             };
 371             const offset = directRelocationOffset(payload, entry, write_size) orelse return false;
 372             const value = target.address + addend;
 373             switch (relocation_type) {
 374                 @backingInt(std.elf.R_X86_64.@"64") => writeU64(payload, offset, try checked.checkedX8664U64(value)),
 375                 @backingInt(std.elf.R_X86_64.@"32") => writeU32(payload, offset, try checked.checkedU32(value)),
 376                 @backingInt(std.elf.R_X86_64.@"32S") => writeU32(payload, offset, @bitCast(try checked.checkedI32(value))),
 377                 @backingInt(std.elf.R_X86_64.@"16") => writeU16(payload, offset, try checked.checkedX8664U16(value)),
 378                 @backingInt(std.elf.R_X86_64.@"8") => payload[offset] = try checked.checkedX8664U8(value),
 379                 else => unreachable,
 380             }
 381             return true;
 382         },
 383         @backingInt(std.elf.R_X86_64.PC16),
 384         @backingInt(std.elf.R_X86_64.PC8),
 385         @backingInt(std.elf.R_X86_64.PC32),
 386         @backingInt(std.elf.R_X86_64.PLT32),
 387         @backingInt(std.elf.R_X86_64.PC64),
 388         => {
 389             const write_size: u64 = switch (relocation_type) {
 390                 @backingInt(std.elf.R_X86_64.PC64) => 8,
 391                 @backingInt(std.elf.R_X86_64.PC16) => 2,
 392                 @backingInt(std.elf.R_X86_64.PC8) => 1,
 393                 else => 4,
 394             };
 395             const offset = directRelocationOffset(payload, entry, write_size) orelse return false;
 396             const place = @as(i128, @intCast(contribution.address)) + @as(i128, @intCast(entry.offset));
 397             const value = target.address + addend - place;
 398             switch (relocation_type) {
 399                 @backingInt(std.elf.R_X86_64.PC64) => writeU64(payload, offset, @bitCast(try checked.checkedI64(value))),
 400                 @backingInt(std.elf.R_X86_64.PC32),
 401                 @backingInt(std.elf.R_X86_64.PLT32),
 402                 => writeU32(payload, offset, @bitCast(try checked.checkedI32(value))),
 403                 @backingInt(std.elf.R_X86_64.PC16) => writeU16(payload, offset, @bitCast(try checked.checkedI16(value))),
 404                 @backingInt(std.elf.R_X86_64.PC8) => payload[offset] = @bitCast(try checked.checkedI8(value)),
 405                 else => unreachable,
 406             }
 407             return true;
 408         },
 409         @backingInt(std.elf.R_X86_64.SIZE32),
 410         @backingInt(std.elf.R_X86_64.SIZE64),
 411         => {
 412             const write_size: u64 = if (relocation_type == @backingInt(std.elf.R_X86_64.SIZE64)) 8 else 4;
 413             const offset = directRelocationOffset(payload, entry, write_size) orelse return false;
 414             const value = @as(i128, @intCast(target.size)) + addend;
 415             if (relocation_type == @backingInt(std.elf.R_X86_64.SIZE64)) {
 416                 writeU64(payload, offset, try checked.checkedU64(value));
 417             } else {
 418                 writeU32(payload, offset, try checked.checkedU32(value));
 419             }
 420             return true;
 421         },
 422         else => return false,
 423     }
 424 }
 425 
 426 fn applyDirectGotRelocation(
 427     payload: []u8,
 428     context: *const DirectRelocationContext,
 429     contribution: ContributionRecord,
 430     object: ObjectFile,
 431     recorded_index: usize,
 432     entry: Rela,
 433     relocation_type: u32,
 434 ) model.Error!bool {
 435     const symbol_index: usize = @intCast(entry.symbolIndex());
 436     const symbol = object.symbols[symbol_index];
 437     const offset = directRelocationOffset(payload, entry, 4) orelse return false;
 438     const place = @as(i128, @intCast(contribution.address)) + @as(i128, @intCast(entry.offset));
 439 
 440     if (relocation_type == @backingInt(std.elf.R_X86_64.GOTPCREL) and symbol.isWeakUndefined()) {
 441         if (addressing.boundaryForSymbol(symbol.name) != null) return false;
 442         if (context.externalTarget(symbol.name) == null) {
 443             if (relocation.relax.gotpcrelWeakUndefinedNullCheck(payload, offset)) return true;
 444         }
 445     }
 446 
 447     const relaxation = relocation.relax.gotpcrelxInstruction(payload, offset, relocation_type);
 448     if (relocation_type == @backingInt(std.elf.R_X86_64.GOTPCREL)) {
 449         if (relaxation) |relaxed| {
 450             const target = directRelocationTarget(context, object, recorded_index, symbol_index) orelse return false;
 451             const value = switch (relaxed) {
 452                 .pc_relative => target.address + entry.addend - place,
 453                 .absolute_signed_32 => target.address + entry.addend + 4,
 454             };
 455             writeU32(payload, offset, @bitCast(try checked.checkedI32(value)));
 456             return true;
 457         }
 458         const slot = context.gotSlotAddress(object, recorded_index, symbol_index) orelse return false;
 459         const value = @as(i128, @intCast(slot)) + entry.addend - place;
 460         writeU32(payload, offset, @bitCast(try checked.checkedI32(value)));
 461         return true;
 462     }
 463 
 464     const relaxed = relaxation orelse return false;
 465     const target = directRelocationTarget(context, object, recorded_index, symbol_index) orelse return false;
 466     const value = switch (relaxed) {
 467         .pc_relative => target.address + entry.addend - place,
 468         .absolute_signed_32 => target.address + entry.addend + 4,
 469     };
 470     writeU32(payload, offset, @bitCast(try checked.checkedI32(value)));
 471     return true;
 472 }
 473 
 474 fn directRelocationOffset(payload: []const u8, entry: Rela, write_size: u64) ?usize {
 475     if (entry.offset > payload.len or write_size > payload.len - entry.offset) return null;
 476     return @intCast(entry.offset);
 477 }
 478 
 479 fn directRelocationTarget(
 480     context: *const DirectRelocationContext,
 481     object: ObjectFile,
 482     recorded_index: usize,
 483     symbol_index: usize,
 484 ) ?DirectRelocationTarget {
 485     if (symbol_index >= object.symbols.len) return null;
 486     const symbol = object.symbols[symbol_index];
 487     if (symbol.isUndefined()) {
 488         return context.externalTarget(symbol.name) orelse if (symbol.isWeakUndefined()) .{
 489             .address = 0,
 490             .size = 0,
 491         } else null;
 492     }
 493     if (!directSymbolBindingProven(symbol)) return null;
 494     if (symbol.isAbsolute()) return .{
 495         .address = @as(i64, @bitCast(symbol.value)),
 496         .size = symbol.size,
 497     };
 498     if (symbol.isCommon()) {
 499         const contribution = context.contributionForSymbol(object.name, recorded_index, symbol_index) orelse return null;
 500         return .{
 501             .address = @intCast(contribution.address),
 502             .size = symbol.size,
 503         };
 504     }
 505     if (context.contributionForSection(object.name, recorded_index, symbol.section_index)) |contribution| {
 506         return .{
 507             .address = @as(i128, @intCast(contribution.address)) + @as(i128, @intCast(symbol.value)),
 508             .size = symbol.size,
 509         };
 510     }
 511     const piece_address = context.mergePieceAddress(object.name, recorded_index, symbol.section_index, symbol.value) orelse return null;
 512     return .{
 513         .address = piece_address,
 514         .size = symbol.size,
 515     };
 516 }
 517 
 518 fn directSymbolBindingProven(symbol: Symbol) bool {
 519     return symbol.binding() == std.elf.STB_LOCAL or format.symbolBindingIsExternalDefinition(symbol.binding());
 520 }
 521 
 522 const DirectExternalMap = std.StringHashMapUnmanaged(DirectRelocationTarget);
 523 
 524 const DirectArchiveMemberRecord = struct {
 525     member: archive.Member,
 526     ambiguous: bool = false,
 527 };
 528 
 529 const DirectArchiveMemberMap = std.StringHashMapUnmanaged(DirectArchiveMemberRecord);
 530 
 531 const DirectArchiveMemberIndex = struct {
 532     members: DirectArchiveMemberMap = .{},
 533 
 534     fn init(allocator: Allocator, members: []const archive.Member) Allocator.Error!DirectArchiveMemberIndex {
 535         var index: DirectArchiveMemberIndex = .{};
 536         errdefer index.deinit(allocator);
 537         try index.members.ensureTotalCapacity(allocator, @intCast(members.len));
 538         for (members) |member| {
 539             const gop = index.members.getOrPutAssumeCapacity(member.name);
 540             if (gop.found_existing) {
 541                 gop.value_ptr.ambiguous = true;
 542             } else {
 543                 gop.value_ptr.* = .{ .member = member };
 544             }
 545         }
 546         return index;
 547     }
 548 
 549     fn deinit(self: *DirectArchiveMemberIndex, allocator: Allocator) void {
 550         self.members.deinit(allocator);
 551         self.* = .{};
 552     }
 553 
 554     fn uniqueMember(self: DirectArchiveMemberIndex, name: []const u8) ?archive.Member {
 555         const indexed = self.members.get(name) orelse return null;
 556         if (indexed.ambiguous) return null;
 557         return indexed.member;
 558     }
 559 };
 560 
 561 const DirectContributionKey = struct {
 562     input_name: []const u8,
 563     input_index: usize,
 564     kind: ContributionKind,
 565     ordinal: u32,
 566 
 567     fn fromContribution(manifest: Manifest, contribution: ContributionRecord) DirectContributionKey {
 568         return .{
 569             .input_name = manifest.string(contribution.input_name_id),
 570             .input_index = @intCast(contribution.input_index),
 571             .kind = contribution.kind,
 572             .ordinal = contribution.ordinal,
 573         };
 574     }
 575 
 576     fn section(input_name: []const u8, input_index: usize, section_index: u16) DirectContributionKey {
 577         return .{
 578             .input_name = input_name,
 579             .input_index = input_index,
 580             .kind = .section,
 581             .ordinal = section_index,
 582         };
 583     }
 584 
 585     fn common(input_name: []const u8, input_index: usize, symbol_index: u32) DirectContributionKey {
 586         return .{
 587             .input_name = input_name,
 588             .input_index = input_index,
 589             .kind = .common_symbol,
 590             .ordinal = symbol_index,
 591         };
 592     }
 593 };
 594 
 595 const DirectContributionKeyContext = struct {
 596     pub fn hash(_: DirectContributionKeyContext, key: DirectContributionKey) u64 {
 597         var hasher = std.hash.Wyhash.init(0x544c445244495245);
 598         hashBytes(&hasher, key.input_name);
 599         hashU64(&hasher, key.input_index);
 600         hashU8(&hasher, @backingInt(key.kind));
 601         hashU64(&hasher, key.ordinal);
 602         return hasher.final();
 603     }
 604 
 605     pub fn eql(_: DirectContributionKeyContext, a: DirectContributionKey, b: DirectContributionKey) bool {
 606         return a.input_index == b.input_index and
 607             a.kind == b.kind and
 608             a.ordinal == b.ordinal and
 609             std.mem.eql(u8, a.input_name, b.input_name);
 610     }
 611 };
 612 
 613 const direct_contribution_key_context = DirectContributionKeyContext{};
 614 const DirectContributionMap = std.HashMapUnmanaged(DirectContributionKey, usize, DirectContributionKeyContext, 80);
 615 
 616 const DirectInputKey = struct {
 617     input_name: []const u8,
 618     input_index: usize,
 619 
 620     fn fromContribution(manifest: Manifest, contribution: ContributionRecord) DirectInputKey {
 621         return .{
 622             .input_name = manifest.string(contribution.input_name_id),
 623             .input_index = @intCast(contribution.input_index),
 624         };
 625     }
 626 
 627     fn init(input_name: []const u8, input_index: usize) DirectInputKey {
 628         return .{
 629             .input_name = input_name,
 630             .input_index = input_index,
 631         };
 632     }
 633 };
 634 
 635 const DirectInputKeyContext = struct {
 636     pub fn hash(_: DirectInputKeyContext, key: DirectInputKey) u64 {
 637         var hasher = std.hash.Wyhash.init(0x544c445244494e50);
 638         hashBytes(&hasher, key.input_name);
 639         hashU64(&hasher, key.input_index);
 640         return hasher.final();
 641     }
 642 
 643     pub fn eql(_: DirectInputKeyContext, a: DirectInputKey, b: DirectInputKey) bool {
 644         return a.input_index == b.input_index and std.mem.eql(u8, a.input_name, b.input_name);
 645     }
 646 };
 647 
 648 const direct_input_key_context = DirectInputKeyContext{};
 649 
 650 const DirectOrdinalKey = struct {
 651     input_name: []const u8,
 652     input_index: usize,
 653     ordinal: u32,
 654 };
 655 
 656 const DirectOrdinalKeyContext = struct {
 657     pub fn hash(_: DirectOrdinalKeyContext, key: DirectOrdinalKey) u64 {
 658         var hasher = std.hash.Wyhash.init(0x544c44524f52444e);
 659         hashBytes(&hasher, key.input_name);
 660         hashU64(&hasher, key.input_index);
 661         hashU64(&hasher, key.ordinal);
 662         return hasher.final();
 663     }
 664 
 665     pub fn eql(_: DirectOrdinalKeyContext, a: DirectOrdinalKey, b: DirectOrdinalKey) bool {
 666         return a.input_index == b.input_index and
 667             a.ordinal == b.ordinal and
 668             std.mem.eql(u8, a.input_name, b.input_name);
 669     }
 670 };
 671 
 672 const direct_ordinal_key_context = DirectOrdinalKeyContext{};
 673 const DirectOrdinalValueMap = std.HashMapUnmanaged(DirectOrdinalKey, u64, DirectOrdinalKeyContext, 80);
 674 const DirectOrdinalSliceMap = std.HashMapUnmanaged(DirectOrdinalKey, []const incremental.MergePieceRecord, DirectOrdinalKeyContext, 80);
 675 const DirectInputContributionMap = std.HashMapUnmanaged(DirectInputKey, std.ArrayListUnmanaged(usize), DirectInputKeyContext, 80);
 676 
 677 const ProvenInputTask = struct {
 678     recorded_index: usize,
 679     current_index: usize,
 680     proven: bool = true,
 681     member_updates: std.ArrayListUnmanaged(incremental.MemberHashUpdate) = .empty,
 682 };
 683 
 684 const ProvenInputFanout = struct {
 685     context: *DirectRelocationContext,
 686     allocator: Allocator,
 687     manifest: Manifest,
 688     inputs: []const model.Input,
 689     options: model.LinkOptions,
 690     tasks: []ProvenInputTask,
 691     out_of_memory: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
 692 
 693     fn run(self: *ProvenInputFanout, worker: usize, item: usize) void {
 694         _ = worker;
 695         if (item == 0) {
 696             self.context.seedExternalTargets(self.allocator, self.manifest.external_targets) catch {
 697                 self.out_of_memory.store(true, .monotonic);
 698             };
 699             return;
 700         }
 701         const task = &self.tasks[item - 1];
 702         const input = self.inputs[task.current_index];
 703         const result = if (archive.isArchive(input.bytes))
 704             self.context.addProvenArchive(self.allocator, self.manifest, input, task.recorded_index, self.options, &task.member_updates)
 705         else
 706             self.context.addProvenObjectInput(self.allocator, self.manifest, input, task.recorded_index, self.options);
 707         if (result) |proven| {
 708             task.proven = proven;
 709         } else |err| {
 710             if (err == error.OutOfMemory) self.out_of_memory.store(true, .monotonic);
 711             task.proven = false;
 712         }
 713     }
 714 };
 715 
 716 const DirectRelocationContext = struct {
 717     manifest: Manifest,
 718     contributions: []const ContributionRecord,
 719     changed_inputs: []const bool,
 720     contribution_indexes: DirectContributionMap = .{},
 721     input_contribution_indexes: DirectInputContributionMap = .{},
 722     proven_input_objects: []std.ArrayListUnmanaged(ObjectFile) = &.{},
 723     proven_unchanged_members: []std.ArrayListUnmanaged([]const u8) = &.{},
 724     member_updates: std.ArrayListUnmanaged(incremental.MemberHashUpdate) = .empty,
 725     externals: DirectExternalMap = .{},
 726     got_named: std.StringHashMapUnmanaged(u64) = .{},
 727     got_local: DirectOrdinalValueMap = .{},
 728     merge_piece_groups: DirectOrdinalSliceMap = .{},
 729 
 730     fn init(
 731         allocator: Allocator,
 732         manifest: Manifest,
 733         changed_inputs: []const bool,
 734     ) Allocator.Error!DirectRelocationContext {
 735         const contributions = manifest.contributions;
 736         var context = DirectRelocationContext{
 737             .manifest = manifest,
 738             .contributions = contributions,
 739             .changed_inputs = changed_inputs,
 740         };
 741         errdefer context.deinit(allocator);
 742         var changed_count: usize = 0;
 743         for (contributions) |contribution| {
 744             if (context.inputChanged(contribution.input_index)) changed_count += 1;
 745         }
 746         try context.contribution_indexes.ensureTotalCapacity(allocator, @intCast(changed_count));
 747         try context.input_contribution_indexes.ensureTotalCapacity(allocator, @intCast(changed_count));
 748         for (contributions, 0..) |contribution, contribution_index| {
 749             if (!context.inputChanged(contribution.input_index)) continue;
 750             const gop = context.contribution_indexes.getOrPutAssumeCapacityContext(
 751                 DirectContributionKey.fromContribution(manifest, contribution),
 752                 direct_contribution_key_context,
 753             );
 754             if (!gop.found_existing) gop.value_ptr.* = contribution_index;
 755             try context.addInputContributionIndex(allocator, contribution, contribution_index);
 756         }
 757         return context;
 758     }
 759 
 760     fn inputChanged(self: *const DirectRelocationContext, input_index: u64) bool {
 761         return input_index < self.changed_inputs.len and self.changed_inputs[@intCast(input_index)];
 762     }
 763 
 764     fn deinit(self: *DirectRelocationContext, allocator: Allocator) void {
 765         var input_iterator = self.input_contribution_indexes.iterator();
 766         while (input_iterator.next()) |entry| entry.value_ptr.deinit(allocator);
 767         for (self.proven_input_objects) |*objects| {
 768             for (objects.items) |*object| object.deinit(allocator);
 769             objects.deinit(allocator);
 770         }
 771         if (self.proven_input_objects.len != 0) allocator.free(self.proven_input_objects);
 772         for (self.proven_unchanged_members) |*members| members.deinit(allocator);
 773         if (self.proven_unchanged_members.len != 0) allocator.free(self.proven_unchanged_members);
 774         self.member_updates.deinit(allocator);
 775         self.contribution_indexes.deinit(allocator);
 776         self.input_contribution_indexes.deinit(allocator);
 777         self.externals.deinit(allocator);
 778         self.got_named.deinit(allocator);
 779         self.got_local.deinit(allocator);
 780         self.merge_piece_groups.deinit(allocator);
 781         self.* = .{ .manifest = Manifest.empty(), .contributions = &.{}, .changed_inputs = &.{} };
 782     }
 783 
 784     fn addInputContributionIndex(
 785         self: *DirectRelocationContext,
 786         allocator: Allocator,
 787         contribution: ContributionRecord,
 788         contribution_index: usize,
 789     ) Allocator.Error!void {
 790         const gop = self.input_contribution_indexes.getOrPutAssumeCapacityContext(
 791             DirectInputKey.fromContribution(self.manifest, contribution),
 792             direct_input_key_context,
 793         );
 794         if (!gop.found_existing) gop.value_ptr.* = .empty;
 795         try gop.value_ptr.append(allocator, contribution_index);
 796     }
 797 
 798     fn seedExternalTargets(
 799         self: *DirectRelocationContext,
 800         allocator: Allocator,
 801         external_targets: []const incremental.ExternalTargetRecord,
 802     ) Allocator.Error!void {
 803         try self.externals.ensureTotalCapacity(allocator, std.math.cast(u32, external_targets.len) orelse return error.OutOfMemory);
 804         for (external_targets) |record| {
 805             self.externals.putAssumeCapacity(self.manifest.string(record.name_id), .{
 806                 .address = record.address(),
 807                 .size = record.size,
 808             });
 809         }
 810     }
 811 
 812     fn seedGotEntries(
 813         self: *DirectRelocationContext,
 814         allocator: Allocator,
 815         got_entries: []const incremental.GotEntryRecord,
 816     ) Allocator.Error!void {
 817         for (got_entries) |record| {
 818             const name = self.manifest.string(record.name_id);
 819             if (name.len != 0) {
 820                 try self.got_named.put(allocator, name, record.address);
 821                 continue;
 822             }
 823             if (!self.inputChanged(record.input_index)) continue;
 824             try self.got_local.putContext(allocator, .{
 825                 .input_name = self.manifest.string(record.input_name_id),
 826                 .input_index = @intCast(record.input_index),
 827                 .ordinal = record.ordinal,
 828             }, record.address, direct_ordinal_key_context);
 829         }
 830     }
 831 
 832     fn seedMergePieces(
 833         self: *DirectRelocationContext,
 834         allocator: Allocator,
 835         merge_pieces: []const incremental.MergePieceRecord,
 836     ) Allocator.Error!void {
 837         var group_start: usize = 0;
 838         for (merge_pieces, 0..) |piece, piece_index| {
 839             const start_piece = merge_pieces[group_start];
 840             if (piece.input_index == start_piece.input_index and
 841                 piece.ordinal == start_piece.ordinal and
 842                 piece.input_name_id == start_piece.input_name_id) continue;
 843             try self.addMergePieceGroup(allocator, merge_pieces[group_start..piece_index]);
 844             group_start = piece_index;
 845         }
 846         if (group_start < merge_pieces.len) {
 847             try self.addMergePieceGroup(allocator, merge_pieces[group_start..]);
 848         }
 849     }
 850 
 851     fn addMergePieceGroup(
 852         self: *DirectRelocationContext,
 853         allocator: Allocator,
 854         group: []const incremental.MergePieceRecord,
 855     ) Allocator.Error!void {
 856         const first = group[0];
 857         if (!self.inputChanged(first.input_index)) return;
 858         try self.merge_piece_groups.putContext(allocator, .{
 859             .input_name = self.manifest.string(first.input_name_id),
 860             .input_index = @intCast(first.input_index),
 861             .ordinal = first.ordinal,
 862         }, group, direct_ordinal_key_context);
 863     }
 864 
 865     fn gotSlotAddress(
 866         self: *const DirectRelocationContext,
 867         object: ObjectFile,
 868         recorded_index: usize,
 869         symbol_index: usize,
 870     ) ?u64 {
 871         if (symbol_index >= object.symbols.len) return null;
 872         const symbol = object.symbols[symbol_index];
 873         if ((symbol.isUndefined() or symbol.isCommon()) and symbol.name.len != 0) {
 874             return self.got_named.get(symbol.name);
 875         }
 876         if (symbol.name.len != 0) {
 877             if (self.got_named.get(symbol.name)) |address| return address;
 878         }
 879         const ordinal = std.math.cast(u32, symbol_index) orelse return null;
 880         return self.got_local.getContext(.{
 881             .input_name = object.name,
 882             .input_index = recorded_index,
 883             .ordinal = ordinal,
 884         }, direct_ordinal_key_context);
 885     }
 886 
 887     fn mergePieceAddress(
 888         self: *const DirectRelocationContext,
 889         input_name: []const u8,
 890         input_index: usize,
 891         section_index: u16,
 892         offset: u64,
 893     ) ?u64 {
 894         const group = self.merge_piece_groups.getContext(.{
 895             .input_name = input_name,
 896             .input_index = input_index,
 897             .ordinal = section_index,
 898         }, direct_ordinal_key_context) orelse return null;
 899         for (group) |piece| {
 900             if (offset < piece.input_offset) continue;
 901             if (offset >= piece.input_offset + piece.size) continue;
 902             return piece.address + (offset - piece.input_offset);
 903         }
 904         return null;
 905     }
 906 
 907     fn addProvenInputs(
 908         self: *DirectRelocationContext,
 909         allocator: Allocator,
 910         manifest: Manifest,
 911         inputs: []const model.Input,
 912         input_changes: InputChanges,
 913         options: model.LinkOptions,
 914     ) model.Error!bool {
 915         if (input_changes.summary.changed != 0) try self.ensureProvenInputObjects(allocator, manifest.inputs.len);
 916 
 917         var changed_inputs_proven = true;
 918         var proving = std.ArrayListUnmanaged(ProvenInputTask).empty;
 919         defer {
 920             for (proving.items) |*task| task.member_updates.deinit(allocator);
 921             proving.deinit(allocator);
 922         }
 923         for (input_changes.changes) |change| {
 924             if (change.kind != .changed) continue;
 925             const recorded_index = change.recorded_index orelse {
 926                 changed_inputs_proven = false;
 927                 continue;
 928             };
 929             const current_index = change.current_index orelse {
 930                 changed_inputs_proven = false;
 931                 continue;
 932             };
 933             if (recorded_index >= manifest.inputs.len or current_index >= inputs.len) {
 934                 changed_inputs_proven = false;
 935                 continue;
 936             }
 937             try proving.append(allocator, .{
 938                 .recorded_index = recorded_index,
 939                 .current_index = current_index,
 940             });
 941         }
 942 
 943         var locked = allocators.LockedAllocator.init(allocator);
 944         var fanout = ProvenInputFanout{
 945             .context = self,
 946             .allocator = locked.allocator(),
 947             .manifest = manifest,
 948             .inputs = inputs,
 949             .options = options,
 950             .tasks = proving.items,
 951         };
 952         parallel.forItems(proving.items.len + 1, 0, &fanout, ProvenInputFanout.run);
 953         if (fanout.out_of_memory.load(.monotonic)) return error.OutOfMemory;
 954 
 955         for (proving.items) |*task| {
 956             try self.member_updates.appendSlice(allocator, task.member_updates.items);
 957             if (!task.proven) changed_inputs_proven = false;
 958         }
 959         return changed_inputs_proven;
 960     }
 961 
 962     fn addProvenObjectInput(
 963         self: *DirectRelocationContext,
 964         allocator: Allocator,
 965         manifest: Manifest,
 966         input: model.Input,
 967         recorded_index: usize,
 968         options: model.LinkOptions,
 969     ) model.Error!bool {
 970         var object = parser.parseObjectWithOptions(allocator, input, .{ .strip_debug = options.strip_debug }) catch |err| switch (err) {
 971             error.OutOfMemory => return error.OutOfMemory,
 972             else => return false,
 973         };
 974         var owns_object = true;
 975         defer if (owns_object) object.deinit(allocator);
 976         if ((try inputLinkHashForObject(object)) != manifest.inputs[recorded_index].link_hash) return false;
 977         if (try self.addProvenObject(allocator, recorded_index, object)) owns_object = false;
 978         return true;
 979     }
 980 
 981     fn addProvenArchive(
 982         self: *DirectRelocationContext,
 983         allocator: Allocator,
 984         manifest: Manifest,
 985         input: model.Input,
 986         recorded_index: usize,
 987         options: model.LinkOptions,
 988         member_updates: *std.ArrayListUnmanaged(incremental.MemberHashUpdate),
 989     ) model.Error!bool {
 990         var parsed = archive.parseDetailed(allocator, input) catch |err| switch (err) {
 991             error.OutOfMemory => return error.OutOfMemory,
 992             else => return false,
 993         };
 994         defer parsed.deinit(allocator);
 995         if (manifest.inputs[recorded_index].selection_hash != structuralSelectionHash(parsed)) return false;
 996 
 997         var member_index = try DirectArchiveMemberIndex.init(allocator, parsed.members);
 998         defer member_index.deinit(allocator);
 999 
1000         var objects = std.ArrayListUnmanaged(ObjectFile).empty;
1001         var transferred_objects = false;
1002         defer {
1003             if (!transferred_objects) for (objects.items) |*object| object.deinit(allocator);
1004             objects.deinit(allocator);
1005         }
1006 
1007         for (manifest.archive_members, 0..) |record, record_index| {
1008             if (record.input_index != recorded_index) continue;
1009             const record_name = manifest.string(record.name_id);
1010             const member = member_index.uniqueMember(record_name) orelse return false;
1011             const member_hash = incremental.hashBytes(member.bytes);
1012             if (!record.selected) {
1013                 if (member_hash != record.hash) return false;
1014                 continue;
1015             }
1016             if (member_hash == record.hash) {
1017                 try self.addProvenUnchangedMember(allocator, recorded_index, record_name);
1018                 continue;
1019             }
1020             var object = parser.parseObjectWithOptions(allocator, .{
1021                 .name = member.name,
1022                 .bytes = member.bytes,
1023             }, .{ .strip_debug = options.strip_debug }) catch |err| switch (err) {
1024                 error.OutOfMemory => return error.OutOfMemory,
1025                 else => return false,
1026             };
1027             errdefer object.deinit(allocator);
1028             const member_link_hash = linkageHash(object) catch |err| switch (err) {
1029                 error.OutOfMemory => return error.OutOfMemory,
1030                 else => {
1031                     object.deinit(allocator);
1032                     return false;
1033                 },
1034             };
1035             if (member_link_hash != record.link_hash) {
1036                 object.deinit(allocator);
1037                 return false;
1038             }
1039             object.input_index = recorded_index;
1040             try member_updates.append(allocator, .{ .member_index = record_index, .hash = member_hash });
1041             try objects.append(allocator, object);
1042         }
1043 
1044         if (objects.items.len == 0) return true;
1045         if (try self.addProvenObjects(allocator, recorded_index, objects.items)) transferred_objects = true;
1046         return true;
1047     }
1048 
1049     fn ensureProvenInputObjects(
1050         self: *DirectRelocationContext,
1051         allocator: Allocator,
1052         input_count: usize,
1053     ) Allocator.Error!void {
1054         if (input_count > self.proven_input_objects.len) {
1055             const proven_input_objects = try allocator.alloc(std.ArrayListUnmanaged(ObjectFile), input_count);
1056             @memset(proven_input_objects, .empty);
1057             for (self.proven_input_objects, 0..) |objects, input_index| proven_input_objects[input_index] = objects;
1058             if (self.proven_input_objects.len != 0) allocator.free(self.proven_input_objects);
1059             self.proven_input_objects = proven_input_objects;
1060         }
1061         if (input_count > self.proven_unchanged_members.len) {
1062             const proven_unchanged_members = try allocator.alloc(std.ArrayListUnmanaged([]const u8), input_count);
1063             @memset(proven_unchanged_members, .empty);
1064             for (self.proven_unchanged_members, 0..) |members, input_index| proven_unchanged_members[input_index] = members;
1065             if (self.proven_unchanged_members.len != 0) allocator.free(self.proven_unchanged_members);
1066             self.proven_unchanged_members = proven_unchanged_members;
1067         }
1068     }
1069 
1070     fn addProvenUnchangedMember(
1071         self: *DirectRelocationContext,
1072         allocator: Allocator,
1073         input_index: usize,
1074         member_name: []const u8,
1075     ) Allocator.Error!void {
1076         if (input_index >= self.proven_unchanged_members.len) return;
1077         try self.proven_unchanged_members[input_index].append(allocator, member_name);
1078     }
1079 
1080     fn addProvenObject(
1081         self: *DirectRelocationContext,
1082         allocator: Allocator,
1083         input_index: usize,
1084         object: ObjectFile,
1085     ) Allocator.Error!bool {
1086         if (input_index >= self.proven_input_objects.len) return false;
1087         try self.proven_input_objects[input_index].append(allocator, object);
1088         return true;
1089     }
1090 
1091     fn addProvenObjects(
1092         self: *DirectRelocationContext,
1093         allocator: Allocator,
1094         input_index: usize,
1095         objects: []const ObjectFile,
1096     ) Allocator.Error!bool {
1097         if (input_index >= self.proven_input_objects.len) return false;
1098         try self.proven_input_objects[input_index].appendSlice(allocator, objects);
1099         return true;
1100     }
1101 
1102     fn appendProvenInputReplacements(
1103         self: *const DirectRelocationContext,
1104         allocator: Allocator,
1105         replacements: *std.ArrayListUnmanaged(ReplacementContribution),
1106         input_index: usize,
1107     ) model.Error!void {
1108         if (input_index < self.proven_unchanged_members.len) {
1109             for (self.proven_unchanged_members[input_index].items) |member_name| {
1110                 try self.appendUnchangedMemberReplacements(allocator, replacements, member_name, input_index);
1111             }
1112         }
1113         if (input_index >= self.proven_input_objects.len) return;
1114         for (self.proven_input_objects[input_index].items) |object| {
1115             try appendInputReplacements(allocator, replacements, self, object, input_index);
1116         }
1117     }
1118 
1119     fn appendUnchangedMemberReplacements(
1120         self: *const DirectRelocationContext,
1121         allocator: Allocator,
1122         replacements: *std.ArrayListUnmanaged(ReplacementContribution),
1123         member_name: []const u8,
1124         input_index: usize,
1125     ) model.Error!void {
1126         for (self.contributionIndexesForInputName(member_name, input_index)) |contribution_index| {
1127             if (contribution_index >= self.contributions.len) continue;
1128             const contribution = self.contributions[contribution_index];
1129             if (contribution.input_index != input_index or !std.mem.eql(u8, self.manifest.string(contribution.input_name_id), member_name)) continue;
1130             try replacements.append(allocator, .{
1131                 .input_name = self.manifest.string(contribution.input_name_id),
1132                 .input_index = @intCast(contribution.input_index),
1133                 .kind = contribution.kind,
1134                 .name = self.manifest.string(contribution.name_id),
1135                 .ordinal = contribution.ordinal,
1136                 .size = contribution.size,
1137                 .alignment = contribution.alignment,
1138                 .output_section_name = self.manifest.string(contribution.output_section_name_id),
1139                 .address = contribution.address,
1140                 .file_offset = contribution.file_offset,
1141                 .reserved_size = contribution.reserved_size,
1142                 .unchanged = true,
1143             });
1144         }
1145     }
1146 
1147     fn externalTarget(self: *const DirectRelocationContext, name: []const u8) ?DirectRelocationTarget {
1148         return self.externals.get(name);
1149     }
1150 
1151     fn contributionIndexesForInputName(
1152         self: *const DirectRelocationContext,
1153         input_name: []const u8,
1154         input_index: usize,
1155     ) []const usize {
1156         const indexes = self.input_contribution_indexes.getContext(
1157             DirectInputKey.init(input_name, input_index),
1158             direct_input_key_context,
1159         ) orelse return &.{};
1160         return indexes.items;
1161     }
1162 
1163     fn contributionForSection(
1164         self: *const DirectRelocationContext,
1165         input_name: []const u8,
1166         input_index: usize,
1167         section_index: u16,
1168     ) ?ContributionRecord {
1169         return self.contributionForKey(DirectContributionKey.section(input_name, input_index, section_index));
1170     }
1171 
1172     fn contributionForSymbol(
1173         self: *const DirectRelocationContext,
1174         input_name: []const u8,
1175         input_index: usize,
1176         symbol_index: usize,
1177     ) ?ContributionRecord {
1178         const ordinal = std.math.cast(u32, symbol_index) orelse return null;
1179         return self.contributionForKey(DirectContributionKey.common(input_name, input_index, ordinal));
1180     }
1181 
1182     fn contributionForKey(self: *const DirectRelocationContext, key: DirectContributionKey) ?ContributionRecord {
1183         const contribution_index = self.contribution_indexes.getContext(
1184             key,
1185             direct_contribution_key_context,
1186         ) orelse return null;
1187         if (contribution_index >= self.contributions.len) return null;
1188         const contribution = self.contributions[contribution_index];
1189         if (!direct_contribution_key_context.eql(key, DirectContributionKey.fromContribution(self.manifest, contribution))) return null;
1190         return contribution;
1191     }
1192 };
1193 
1194 fn changedInputFilterAlloc(
1195     allocator: Allocator,
1196     manifest: Manifest,
1197     input_changes: InputChanges,
1198 ) Allocator.Error![]bool {
1199     const changed = try allocator.alloc(bool, manifest.inputs.len);
1200     @memset(changed, false);
1201     for (input_changes.changes) |change| {
1202         if (change.kind != .changed) continue;
1203         const recorded_index = change.recorded_index orelse continue;
1204         if (recorded_index >= changed.len) continue;
1205         changed[recorded_index] = true;
1206     }
1207     return changed;
1208 }
1209 
1210 fn inputLinkHashForObject(object: ObjectFile) model.Error!u64 {
1211     var state = InputLinkHashState.init();
1212     try state.update(object);
1213     return state.final();
1214 }
1215 
1216 fn selectionHashForInput(allocator: Allocator, input: model.Input) model.Error!u64 {
1217     var parsed = archive.parseDetailed(allocator, input) catch |err| switch (err) {
1218         error.OutOfMemory => return error.OutOfMemory,
1219         else => return incremental.hashBytes(input.bytes),
1220     };
1221     defer parsed.deinit(allocator);
1222     return structuralSelectionHash(parsed);
1223 }
1224 
1225 fn structuralSelectionHash(parsed: archive.ParsedArchive) u64 {
1226     var hasher = std.hash.Wyhash.init(0x544c445241524348);
1227     hashU8(&hasher, if (parsed.has_symbol_index) 1 else 0);
1228     hashU64(&hasher, parsed.symbol_index.len);
1229     for (parsed.symbol_index) |entry| {
1230         hashBytes(&hasher, entry.name);
1231         hashU64(&hasher, memberOrdinalByOffset(parsed.members, entry.member_offset) orelse std.math.maxInt(u64));
1232     }
1233     hashU64(&hasher, parsed.members.len);
1234     for (parsed.members) |member| hashBytes(&hasher, member.name);
1235     return hasher.final();
1236 }
1237 
1238 fn memberOrdinalByOffset(members: []const archive.Member, offset: u64) ?u64 {
1239     var low: usize = 0;
1240     var high: usize = members.len;
1241     while (low < high) {
1242         const middle = low + (high - low) / 2;
1243         const member_offset = members[middle].header_offset;
1244         if (member_offset == offset) return @intCast(middle);
1245         if (offset < member_offset) {
1246             high = middle;
1247         } else {
1248             low = middle + 1;
1249         }
1250     }
1251     return null;
1252 }
1253 
1254 const InputLinkHashState = struct {
1255     hasher: std.hash.Wyhash,
1256     count: u64,
1257 
1258     fn init() InputLinkHashState {
1259         return .{
1260             .hasher = std.hash.Wyhash.init(0x544c4452494e5055),
1261             .count = 0,
1262         };
1263     }
1264 
1265     fn update(self: *InputLinkHashState, object: ObjectFile) model.Error!void {
1266         hashBytes(&self.hasher, object.name);
1267         hashU64(&self.hasher, try linkageHash(object));
1268         self.count += 1;
1269     }
1270 
1271     fn final(self: *InputLinkHashState) u64 {
1272         hashU64(&self.hasher, self.count);
1273         return self.hasher.final();
1274     }
1275 };
1276 
1277 fn hashSections(hasher: *std.hash.Wyhash, object: ObjectFile) model.Error!void {
1278     hashU64(hasher, object.sections.len);
1279     for (object.sections, 0..) |section, section_index| {
1280         const name = try sectionName(object, section_index);
1281         hashBytes(hasher, name);
1282         hashU32(hasher, section.section_type);
1283         hashU64(hasher, section.flags);
1284         hashU64(hasher, section.size);
1285         hashU32(hasher, section.link);
1286         hashU32(hasher, section.info);
1287         hashU64(hasher, section.alignment);
1288         hashU64(hasher, section.entry_size);
1289         if (!patchablePayloadSection(section, name)) hashBytes(hasher, try sectionBytes(object.bytes, section));
1290     }
1291 }
1292 
1293 fn hashSymbols(hasher: *std.hash.Wyhash, symbols: []const Symbol) void {
1294     hashU64(hasher, symbols.len);
1295     for (symbols) |symbol| {
1296         hashBytes(hasher, symbol.name);
1297         hashU8(hasher, symbol.info);
1298         hashU8(hasher, symbol.other);
1299         hashU16(hasher, symbol.section_index);
1300         hashU64(hasher, symbol.value);
1301         hashU64(hasher, symbol.size);
1302     }
1303 }
1304 
1305 fn hashRelocations(hasher: *std.hash.Wyhash, relocations: []const Rela) void {
1306     hashU64(hasher, relocations.len);
1307     for (relocations) |entry| {
1308         hashU64(hasher, entry.offset);
1309         hashU64(hasher, entry.info);
1310         hashI64(hasher, entry.addend);
1311     }
1312 }
1313 
1314 fn hashBytes(hasher: *std.hash.Wyhash, bytes: []const u8) void {
1315     hashU64(hasher, bytes.len);
1316     hasher.update(bytes);
1317 }
1318 
1319 fn hashU8(hasher: *std.hash.Wyhash, value: u8) void {
1320     hasher.update(&.{value});
1321 }
1322 
1323 fn hashU16(hasher: *std.hash.Wyhash, value: u16) void {
1324     var bytes: [2]u8 = undefined;
1325     std.mem.writeInt(u16, &bytes, value, .little);
1326     hasher.update(&bytes);
1327 }
1328 
1329 fn hashU32(hasher: *std.hash.Wyhash, value: u32) void {
1330     var bytes: [4]u8 = undefined;
1331     std.mem.writeInt(u32, &bytes, value, .little);
1332     hasher.update(&bytes);
1333 }
1334 
1335 fn hashU64(hasher: *std.hash.Wyhash, value: anytype) void {
1336     var bytes: [8]u8 = undefined;
1337     std.mem.writeInt(u64, &bytes, @intCast(value), .little);
1338     hasher.update(&bytes);
1339 }
1340 
1341 fn hashI64(hasher: *std.hash.Wyhash, value: i64) void {
1342     var bytes: [8]u8 = undefined;
1343     std.mem.writeInt(i64, &bytes, value, .little);
1344     hasher.update(&bytes);
1345 }
1346 
1347 const TestInputClassification = struct {
1348     bytes: InputChangeStorage.Storage,
1349     storage: InputChangeStorage,
1350     changes: InputChanges,
1351 
1352     fn deinit(self: *TestInputClassification, allocator: Allocator) void {
1353         const bytes = self.storage.deinit();
1354         std.debug.assert(bytes.ptr == self.bytes.ptr);
1355         std.debug.assert(bytes.len == self.bytes.len);
1356         allocator.free(bytes);
1357     }
1358 };
1359 
1360 fn classifyTestInputs(
1361     allocator: Allocator,
1362     manifest: Manifest,
1363     inputs: []const model.Input,
1364 ) !TestInputClassification {
1365     const limits = InputChangeStorage.Limits.inspect(
1366         RecordedInputs.fromManifest(&manifest),
1367         inputs,
1368     );
1369     const capacity = try InputChangeStorage.Capacity.derive(limits);
1370     const bytes = try allocator.alignedAlloc(
1371         u8,
1372         .fromByteUnits(InputChangeStorage.storage_alignment),
1373         capacity.storage_bytes,
1374     );
1375     errdefer allocator.free(bytes);
1376     var storage = try InputChangeStorage.init(bytes, limits);
1377     storage.activate();
1378     const changes = try storage.classify(RecordedInputs.fromManifest(&manifest), inputs);
1379     return .{
1380         .bytes = bytes,
1381         .storage = storage,
1382         .changes = changes,
1383     };
1384 }
1385 
1386 /// Builds a relocatable object with one allocated, executable section at
1387 /// alignment 16 that holds `text`, plus the symbols and relocations the caller
1388 /// gives. The relink tests (tests of a later link) build their small old and
1389 /// new objects with it. That section has file index 1, so a symbol list starts
1390 /// with `ObjectSymbol.section(1)` and later entries take indices from 2.
1391 fn relocatedTextObject(
1392     allocator: Allocator,
1393     name: []const u8,
1394     text: []const u8,
1395     symbols: []const ObjectSymbol,
1396     relocations: []const ObjectRelocation,
1397 ) ![]u8 {
1398     const sections = [_]ObjectSection{
1399         ObjectSection.progbits(name, text, std.elf.SHF_EXECINSTR, 16),
1400     };
1401     return try buildObject(allocator, .{
1402         .sections = &sections,
1403         .symbols = symbols,
1404         .relocations = relocations,
1405     });
1406 }
1407 
1408 /// Calls `relocatedTextObject` with an empty relocation list. The relink tests
1409 /// use it for objects that need no relocation.
1410 fn textObject(
1411     allocator: Allocator,
1412     name: []const u8,
1413     text: []const u8,
1414     symbols: []const ObjectSymbol,
1415 ) ![]u8 {
1416     return try relocatedTextObject(allocator, name, text, symbols, &.{});
1417 }
1418 
1419 test "ELF relink linkage hash ignores simple alloc payload bytes" {
1420     const allocator = std.testing.allocator;
1421 
1422     const first_text = [_]u8{ 0xc3, 0x90, 0x90 };
1423     const first_object = try textObject(allocator, ".text.patch", &first_text, &.{ObjectSymbol.section(1)});
1424     defer allocator.free(first_object);
1425 
1426     const second_text = [_]u8{ 0xc3, 0x90, 0xcc };
1427     const second_object = try textObject(allocator, ".text.patch", &second_text, &.{ObjectSymbol.section(1)});
1428     defer allocator.free(second_object);
1429 
1430     var first = try parser.parseObject(allocator, .{ .name = "patch.o", .bytes = first_object });
1431     defer first.deinit(allocator);
1432     var second = try parser.parseObject(allocator, .{ .name = "patch.o", .bytes = second_object });
1433     defer second.deinit(allocator);
1434 
1435     try std.testing.expectEqual(try linkageHash(first), try linkageHash(second));
1436 }
1437 
1438 test "ELF direct relink context indexes contribution targets" {
1439     const allocator = std.testing.allocator;
1440 
1441     var builder = try incremental.Builder.init(allocator, .{});
1442     defer builder.deinit();
1443     try builder.addContribution("other.o", 1, .section, ".text.other", 9, ".text", 0x401080, 32, 4, 4, 16);
1444     try builder.addContribution("target.o", 2, .common_symbol, "scratch", 5, ".bss", 0x402000, 48, 8, 16, 8);
1445     try builder.addContribution("target.o", 2, .section, ".text.target", 4, ".text", 0x401000, 4, 3, 8, 16);
1446 
1447     var manifest = try builder.finish();
1448     defer manifest.deinit(allocator);
1449 
1450     var context = try DirectRelocationContext.init(allocator, manifest, &.{ false, true, true });
1451     defer context.deinit(allocator);
1452 
1453     const section = context.contributionForSection("target.o", 2, 4) orelse return error.MissingSection;
1454     try std.testing.expectEqualStrings(".text.target", context.manifest.string(section.name_id));
1455     try std.testing.expectEqual(@as(u64, 0x401000), section.address);
1456 
1457     const common = context.contributionForSymbol("target.o", 2, 5) orelse return error.MissingCommon;
1458     try std.testing.expectEqualStrings("scratch", context.manifest.string(common.name_id));
1459     try std.testing.expectEqual(@as(u64, 0x402000), common.address);
1460 
1461     try std.testing.expectEqual(@as(?ContributionRecord, null), context.contributionForSection("target.o", 2, 9));
1462     try std.testing.expectEqual(@as(?ContributionRecord, null), context.contributionForSection("missing.o", 2, 4));
1463     try std.testing.expectEqual(@as(?ContributionRecord, null), context.contributionForSymbol("target.o", 2, std.math.maxInt(usize)));
1464 }
1465 
1466 test "ELF direct relink context indexes input contributions" {
1467     const allocator = std.testing.allocator;
1468 
1469     var builder = try incremental.Builder.init(allocator, .{});
1470     defer builder.deinit();
1471     try builder.addContribution("target.o", 2, .section, ".text.target", 4, ".text", 0x401000, 4, 3, 8, 16);
1472     try builder.addContribution("sibling.o", 2, .section, ".text.sibling", 6, ".text", 0x401080, 32, 4, 4, 16);
1473     try builder.addContribution("target.o", 2, .common_symbol, "scratch", 5, ".bss", 0x402000, 48, 8, 16, 8);
1474     try builder.addContribution("target.o", 3, .section, ".text.other-input", 4, ".text", 0x403000, 96, 3, 8, 16);
1475 
1476     var manifest = try builder.finish();
1477     defer manifest.deinit(allocator);
1478 
1479     var context = try DirectRelocationContext.init(allocator, manifest, &.{ false, false, true, true });
1480     defer context.deinit(allocator);
1481 
1482     const target_indexes = context.contributionIndexesForInputName("target.o", 2);
1483     try std.testing.expectEqual(@as(usize, 2), target_indexes.len);
1484     try std.testing.expectEqualStrings(".text.target", context.manifest.string(context.contributions[target_indexes[0]].name_id));
1485     try std.testing.expectEqualStrings("scratch", context.manifest.string(context.contributions[target_indexes[1]].name_id));
1486 
1487     const sibling_indexes = context.contributionIndexesForInputName("sibling.o", 2);
1488     try std.testing.expectEqual(@as(usize, 1), sibling_indexes.len);
1489     try std.testing.expectEqualStrings(".text.sibling", context.manifest.string(context.contributions[sibling_indexes[0]].name_id));
1490 
1491     const same_name_other_input = context.contributionIndexesForInputName("target.o", 3);
1492     try std.testing.expectEqual(@as(usize, 1), same_name_other_input.len);
1493     try std.testing.expectEqualStrings(".text.other-input", context.manifest.string(context.contributions[same_name_other_input[0]].name_id));
1494 
1495     try std.testing.expectEqual(@as(usize, 0), context.contributionIndexesForInputName("missing.o", 2).len);
1496 }
1497 
1498 test "ELF direct relink archive member index keeps duplicate names ambiguous" {
1499     const allocator = std.testing.allocator;
1500     const first = [_]u8{1};
1501     const second = [_]u8{2};
1502     const third = [_]u8{3};
1503     const members = [_]archive.Member{
1504         .{ .name = "dup.o", .bytes = &first, .header_offset = 8 },
1505         .{ .name = "unique.o", .bytes = &second, .header_offset = 80 },
1506         .{ .name = "dup.o", .bytes = &third, .header_offset = 152 },
1507     };
1508 
1509     var index = try DirectArchiveMemberIndex.init(allocator, &members);
1510     defer index.deinit(allocator);
1511 
1512     const unique = index.uniqueMember("unique.o") orelse return error.MissingUniqueMember;
1513     try std.testing.expectEqual(@as(u64, 80), unique.header_offset);
1514     try std.testing.expectEqualSlices(u8, &second, unique.bytes);
1515     try std.testing.expectEqual(@as(?archive.Member, null), index.uniqueMember("dup.o"));
1516     try std.testing.expectEqual(@as(?archive.Member, null), index.uniqueMember("missing.o"));
1517 }
1518 
1519 test "ELF direct relink replacements copy changed section payloads" {
1520     const allocator = std.testing.allocator;
1521 
1522     const old_text = [_]u8{ 0xc3, 0x90, 0x90 };
1523     const section_index: u16 = 1;
1524     const old_object = try textObject(allocator, ".text.patch", &old_text, &.{ObjectSymbol.section(section_index)});
1525     defer allocator.free(old_object);
1526 
1527     const new_text = [_]u8{ 0xc3, 0x90, 0xcc };
1528     const new_object = try textObject(allocator, ".text.patch", &new_text, &.{ObjectSymbol.section(section_index)});
1529     defer allocator.free(new_object);
1530 
1531     var parsed_old = try parser.parseObject(allocator, .{ .name = "patch.o", .bytes = old_object });
1532     defer parsed_old.deinit(allocator);
1533 
1534     var builder = try incremental.Builder.init(allocator, .{});
1535     defer builder.deinit();
1536     try builder.addInput(.{ .name = "patch.o", .bytes = old_object });
1537     builder.setInputLinkHash(0, try inputLinkHashForObject(parsed_old));
1538     try builder.addContribution("patch.o", 0, .section, ".text.patch", @intCast(section_index), ".text", 0x401000, 4, old_text.len, 16, 16);
1539 
1540     var manifest = try builder.finish();
1541     defer manifest.deinit(allocator);
1542 
1543     var state = try incremental.PreparedState.fromOwnedManifest(allocator, manifest.take());
1544     defer state.deinit(allocator);
1545 
1546     const inputs = [_]model.Input{.{ .name = "patch.o", .bytes = new_object }};
1547     var classified = try classifyTestInputs(
1548         allocator,
1549         state.manifest,
1550         &inputs,
1551     );
1552     defer classified.deinit(allocator);
1553     const changes = classified.changes;
1554 
1555     var evidence = try directEvidenceAlloc(allocator, state.manifest, &inputs, changes, .{});
1556     defer evidence.deinit(allocator);
1557     const replacements = evidence.replacements;
1558 
1559     try std.testing.expectEqual(@as(usize, 1), replacements.len);
1560     try std.testing.expectEqualStrings(".text.patch", replacements[0].name);
1561     try std.testing.expectEqual(@as(u64, new_text.len), replacements[0].size);
1562     try std.testing.expectEqualSlices(u8, &new_text, replacements[0].payload);
1563 
1564     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
1565     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
1566 }
1567 
1568 test "ELF direct relink replacements account for common symbols" {
1569     const allocator = std.testing.allocator;
1570     const options = model.LinkOptions{};
1571 
1572     const old_text = [_]u8{
1573         0x48, 0xb8,
1574         0,    0,
1575         0,    0,
1576         0,    0,
1577         0,    0,
1578         0xc3,
1579     };
1580     const old_object = try relocatedTextObject(
1581         allocator,
1582         ".text",
1583         &old_text,
1584         &.{
1585             ObjectSymbol.section(1),
1586             ObjectSymbol.function("_start", 1, 0, old_text.len),
1587             ObjectSymbol.commonObject("scratch", 16, 24),
1588         },
1589         &.{ObjectRelocation.x86_64(1, 2, 3, .@"64", 0)},
1590     );
1591     defer allocator.free(old_object);
1592 
1593     const new_text = [_]u8{
1594         0x48, 0xb8,
1595         0,    0,
1596         0,    0,
1597         0,    0,
1598         0,    0,
1599         0xcc,
1600     };
1601     const new_object = try relocatedTextObject(
1602         allocator,
1603         ".text",
1604         &new_text,
1605         &.{
1606             ObjectSymbol.section(1),
1607             ObjectSymbol.function("_start", 1, 0, new_text.len),
1608             ObjectSymbol.commonObject("scratch", 16, 24),
1609         },
1610         &.{ObjectRelocation.x86_64(1, 2, 3, .@"64", 0)},
1611     );
1612     defer allocator.free(new_object);
1613 
1614     const old_inputs = [_]model.Input{.{ .name = "common.o", .bytes = old_object }};
1615     const new_inputs = [_]model.Input{.{ .name = "common.o", .bytes = new_object }};
1616 
1617     var old_linked = try root.link(allocator, &old_inputs, options);
1618     defer old_linked.deinit(allocator);
1619     var state = try old_linked.prepareIncrementalState(allocator);
1620     defer state.deinit(allocator);
1621 
1622     var classified = try classifyTestInputs(
1623         allocator,
1624         state.manifest,
1625         &new_inputs,
1626     );
1627     defer classified.deinit(allocator);
1628     const changes = classified.changes;
1629 
1630     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, options);
1631     defer evidence.deinit(allocator);
1632     const replacements = evidence.replacements;
1633 
1634     try std.testing.expectEqual(@as(usize, 2), replacements.len);
1635     var common_replacements: usize = 0;
1636     for (replacements) |replacement| {
1637         if (replacement.kind != .common_symbol) continue;
1638         common_replacements += 1;
1639         try std.testing.expectEqualStrings("scratch", replacement.name);
1640         try std.testing.expectEqual(@as(u64, 24), replacement.size);
1641         try std.testing.expectEqual(@as(usize, 0), replacement.payload.len);
1642     }
1643     try std.testing.expectEqual(@as(usize, 1), common_replacements);
1644 
1645     const plan = state.planChangedInputRelinkFromInputChanges(options, changes, replacements);
1646     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
1647 
1648     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
1649     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
1650     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, options);
1651 
1652     var candidate = try root.link(allocator, &new_inputs, options);
1653     defer candidate.deinit(allocator);
1654 
1655     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
1656 }
1657 
1658 test "ELF direct relink patches garbage collected retained payloads" {
1659     const allocator = std.testing.allocator;
1660     const options = model.LinkOptions{ .gc_sections = true };
1661 
1662     const old_start_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0x90 };
1663     const old_helper_text = [_]u8{ 0xc3, 0x90 };
1664     const old_dead_text = [_]u8{ 0xcc, 0x90 };
1665     const old_start_index: u16 = 1;
1666     const old_helper_index: u16 = 2;
1667     const old_dead_index: u16 = 3;
1668     const old_sections = [_]ObjectSection{
1669         ObjectSection.progbits(".text.start", &old_start_text, std.elf.SHF_EXECINSTR, 16),
1670         ObjectSection.progbits(".text.helper", &old_helper_text, std.elf.SHF_EXECINSTR, 16),
1671         ObjectSection.progbits(".text.dead", &old_dead_text, std.elf.SHF_EXECINSTR, 16),
1672     };
1673     const old_symbols = [_]ObjectSymbol{
1674         ObjectSymbol.section(old_start_index),
1675         ObjectSymbol.section(old_helper_index),
1676         ObjectSymbol.section(old_dead_index),
1677         ObjectSymbol.function("_start", old_start_index, 0, old_start_text.len),
1678         ObjectSymbol.function("helper", old_helper_index, 0, old_helper_text.len),
1679         ObjectSymbol.function("dead", old_dead_index, 0, old_dead_text.len),
1680     };
1681     const old_helper_symbol: u32 = 5;
1682     const old_relocations = [_]ObjectRelocation{
1683         ObjectRelocation.x86_64(old_start_index, 1, old_helper_symbol, .PLT32, -4),
1684     };
1685     const old_object = try buildObject(allocator, .{
1686         .sections = &old_sections,
1687         .symbols = &old_symbols,
1688         .relocations = &old_relocations,
1689     });
1690     defer allocator.free(old_object);
1691 
1692     const new_start_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xcc };
1693     const new_helper_text = [_]u8{ 0xc3, 0xcc };
1694     const new_dead_text = [_]u8{ 0xcc, 0xcc };
1695     const new_start_index: u16 = 1;
1696     const new_helper_index: u16 = 2;
1697     const new_dead_index: u16 = 3;
1698     const new_sections = [_]ObjectSection{
1699         ObjectSection.progbits(".text.start", &new_start_text, std.elf.SHF_EXECINSTR, 16),
1700         ObjectSection.progbits(".text.helper", &new_helper_text, std.elf.SHF_EXECINSTR, 16),
1701         ObjectSection.progbits(".text.dead", &new_dead_text, std.elf.SHF_EXECINSTR, 16),
1702     };
1703     const new_symbols = [_]ObjectSymbol{
1704         ObjectSymbol.section(new_start_index),
1705         ObjectSymbol.section(new_helper_index),
1706         ObjectSymbol.section(new_dead_index),
1707         ObjectSymbol.function("_start", new_start_index, 0, new_start_text.len),
1708         ObjectSymbol.function("helper", new_helper_index, 0, new_helper_text.len),
1709         ObjectSymbol.function("dead", new_dead_index, 0, new_dead_text.len),
1710     };
1711     const new_helper_symbol: u32 = 5;
1712     const new_relocations = [_]ObjectRelocation{
1713         ObjectRelocation.x86_64(new_start_index, 1, new_helper_symbol, .PLT32, -4),
1714     };
1715     const new_object = try buildObject(allocator, .{
1716         .sections = &new_sections,
1717         .symbols = &new_symbols,
1718         .relocations = &new_relocations,
1719     });
1720     defer allocator.free(new_object);
1721 
1722     const old_inputs = [_]model.Input{.{ .name = "gc.o", .bytes = old_object }};
1723     const new_inputs = [_]model.Input{.{ .name = "gc.o", .bytes = new_object }};
1724 
1725     var old_linked = try root.link(allocator, &old_inputs, options);
1726     defer old_linked.deinit(allocator);
1727 
1728     try std.testing.expect(metadataContributionPresent(old_linked.manifest, "gc.o", ".text.start", old_start_index));
1729     try std.testing.expect(metadataContributionPresent(old_linked.manifest, "gc.o", ".text.helper", old_helper_index));
1730     try std.testing.expect(!metadataContributionPresent(old_linked.manifest, "gc.o", ".text.dead", old_dead_index));
1731 
1732     var state = try old_linked.prepareIncrementalState(allocator);
1733     defer state.deinit(allocator);
1734 
1735     var classified = try classifyTestInputs(
1736         allocator,
1737         state.manifest,
1738         &new_inputs,
1739     );
1740     defer classified.deinit(allocator);
1741     const changes = classified.changes;
1742 
1743     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, options);
1744     defer evidence.deinit(allocator);
1745     const replacements = evidence.replacements;
1746 
1747     try std.testing.expectEqual(@as(usize, 2), replacements.len);
1748 
1749     const plan = state.planChangedInputRelinkFromInputChanges(options, changes, replacements);
1750     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
1751 
1752     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
1753     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
1754     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, options);
1755 
1756     var candidate = try root.link(allocator, &new_inputs, options);
1757     defer candidate.deinit(allocator);
1758 
1759     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
1760 }
1761 
1762 test "ELF direct relink proves garbage collected discarded-only changes" {
1763     const allocator = std.testing.allocator;
1764     const options = model.LinkOptions{ .gc_sections = true };
1765 
1766     const start_text = [_]u8{0xc3};
1767     const start_object = try textObject(allocator, ".text.start", &start_text, &.{
1768         ObjectSymbol.section(1),
1769         ObjectSymbol.function("_start", 1, 0, start_text.len),
1770     });
1771     defer allocator.free(start_object);
1772 
1773     const old_dead_text = [_]u8{ 0xcc, 0x90 };
1774     const old_dead_index: u16 = 1;
1775     const old_dead_object = try textObject(allocator, ".text.dead", &old_dead_text, &.{
1776         ObjectSymbol.section(old_dead_index),
1777         ObjectSymbol.function("dead", old_dead_index, 0, old_dead_text.len),
1778     });
1779     defer allocator.free(old_dead_object);
1780 
1781     const new_dead_text = [_]u8{ 0xcc, 0xcc };
1782     const new_dead_object = try textObject(allocator, ".text.dead", &new_dead_text, &.{
1783         ObjectSymbol.section(1),
1784         ObjectSymbol.function("dead", 1, 0, new_dead_text.len),
1785     });
1786     defer allocator.free(new_dead_object);
1787 
1788     const old_inputs = [_]model.Input{
1789         .{ .name = "start.o", .bytes = start_object },
1790         .{ .name = "dead.o", .bytes = old_dead_object },
1791     };
1792     const new_inputs = [_]model.Input{
1793         .{ .name = "start.o", .bytes = start_object },
1794         .{ .name = "dead.o", .bytes = new_dead_object },
1795     };
1796 
1797     var old_linked = try root.link(allocator, &old_inputs, options);
1798     defer old_linked.deinit(allocator);
1799     try std.testing.expect(!metadataContributionPresent(old_linked.manifest, "dead.o", ".text.dead", old_dead_index));
1800 
1801     var state = try old_linked.prepareIncrementalState(allocator);
1802     defer state.deinit(allocator);
1803 
1804     var classified = try classifyTestInputs(
1805         allocator,
1806         state.manifest,
1807         &new_inputs,
1808     );
1809     defer classified.deinit(allocator);
1810     const changes = classified.changes;
1811 
1812     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, options);
1813     defer evidence.deinit(allocator);
1814     const replacements = evidence.replacements;
1815     try std.testing.expectEqual(@as(usize, 0), replacements.len);
1816     try std.testing.expect(evidence.inputs_proven);
1817 
1818     const plan = state.planChangedInputRelinkFromInputChanges(options, changes, replacements);
1819     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
1820 
1821     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
1822     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
1823     try std.testing.expectEqual(@as(usize, 0), application.contributions_written);
1824     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, options);
1825     try state.updateManifestForAcceptedChangedInputRelinkFromInputChanges(&new_inputs, changes, replacements, evidence.member_updates, 0, plan);
1826 
1827     var candidate = try root.link(allocator, &new_inputs, options);
1828     defer candidate.deinit(allocator);
1829 
1830     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
1831     try std.testing.expect(state.canReuseFor(options, &new_inputs));
1832 }
1833 
1834 test "ELF direct relink proves unselected archive payload changes" {
1835     const allocator = std.testing.allocator;
1836     const options = model.LinkOptions{};
1837 
1838     const start_text = [_]u8{0xc3};
1839     const start_object = try textObject(allocator, ".text.start", &start_text, &.{
1840         ObjectSymbol.section(1),
1841         ObjectSymbol.function("_start", 1, 0, start_text.len),
1842     });
1843     defer allocator.free(start_object);
1844 
1845     const old_member_text = [_]u8{ 0x90, 0xc3 };
1846     const old_member_object = try textObject(allocator, ".text.unused", &old_member_text, &.{
1847         ObjectSymbol.section(1),
1848         ObjectSymbol.function("unused", 1, 0, old_member_text.len),
1849     });
1850     defer allocator.free(old_member_object);
1851 
1852     const new_member_text = [_]u8{ 0xcc, 0xc3 };
1853     const new_member_object = try textObject(allocator, ".text.unused", &new_member_text, &.{
1854         ObjectSymbol.section(1),
1855         ObjectSymbol.function("unused", 1, 0, new_member_text.len),
1856     });
1857     defer allocator.free(new_member_object);
1858 
1859     const old_archive = try archive.build(allocator, &.{.{ .name = "unused.o", .bytes = old_member_object, .symbols = &.{"unused"} }});
1860     defer allocator.free(old_archive);
1861     const new_archive = try archive.build(allocator, &.{.{ .name = "unused.o", .bytes = new_member_object, .symbols = &.{"unused"} }});
1862     defer allocator.free(new_archive);
1863 
1864     const old_inputs = [_]model.Input{
1865         .{ .name = "start.o", .bytes = start_object },
1866         .{ .name = "libunused.a", .bytes = old_archive },
1867     };
1868     const new_inputs = [_]model.Input{
1869         .{ .name = "start.o", .bytes = start_object },
1870         .{ .name = "libunused.a", .bytes = new_archive },
1871     };
1872 
1873     var old_linked = try root.link(allocator, &old_inputs, options);
1874     defer old_linked.deinit(allocator);
1875     try std.testing.expectEqual(@as(usize, 1), old_linked.manifest.contributions.len);
1876     try std.testing.expectEqual(try selectionHashForInput(allocator, old_inputs[1]), old_linked.manifest.inputs[1].selection_hash);
1877 
1878     var state = try old_linked.prepareIncrementalState(allocator);
1879     defer state.deinit(allocator);
1880 
1881     var classified = try classifyTestInputs(
1882         allocator,
1883         state.manifest,
1884         &new_inputs,
1885     );
1886     defer classified.deinit(allocator);
1887     const changes = classified.changes;
1888 
1889     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, options);
1890     defer evidence.deinit(allocator);
1891     const replacements = evidence.replacements;
1892     try std.testing.expect(evidence.inputs_proven);
1893     try std.testing.expectEqual(@as(usize, 0), replacements.len);
1894 
1895     const plan = state.planChangedInputRelinkFromInputChanges(options, changes, replacements);
1896     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
1897 
1898     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
1899     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
1900     try std.testing.expectEqual(@as(usize, 0), application.contributions_written);
1901     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, options);
1902     try state.updateManifestForAcceptedChangedInputRelinkFromInputChanges(&new_inputs, changes, replacements, evidence.member_updates, 0, plan);
1903     try std.testing.expectEqual(incremental.hashBytes(new_archive), state.manifest.inputs[1].hash);
1904     try std.testing.expectEqual(try selectionHashForInput(allocator, new_inputs[1]), state.manifest.inputs[1].selection_hash);
1905 
1906     var candidate = try root.link(allocator, &new_inputs, options);
1907     defer candidate.deinit(allocator);
1908 
1909     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
1910     try std.testing.expect(state.canReuseFor(options, &new_inputs));
1911 }
1912 
1913 test "ELF direct relink skips writes for unchanged selected archive members" {
1914     const allocator = std.testing.allocator;
1915     const options = model.LinkOptions{};
1916 
1917     const caller_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xc3 };
1918     const caller_object = try relocatedTextObject(
1919         allocator,
1920         ".text.start",
1921         &caller_text,
1922         &.{
1923             ObjectSymbol.section(1),
1924             ObjectSymbol.function("_start", 1, 0, caller_text.len),
1925             ObjectSymbol.undefinedFunction("needed"),
1926         },
1927         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
1928     );
1929     defer allocator.free(caller_object);
1930 
1931     const needed_text = [_]u8{0xc3};
1932     const needed_object = try textObject(allocator, ".text.needed", &needed_text, &.{
1933         ObjectSymbol.section(1),
1934         ObjectSymbol.function("needed", 1, 0, needed_text.len),
1935     });
1936     defer allocator.free(needed_object);
1937 
1938     const old_unused_text = [_]u8{ 0x90, 0xc3 };
1939     const old_unused_object = try textObject(allocator, ".text.unused", &old_unused_text, &.{
1940         ObjectSymbol.section(1),
1941         ObjectSymbol.function("unused", 1, 0, old_unused_text.len),
1942     });
1943     defer allocator.free(old_unused_object);
1944 
1945     const new_unused_text = [_]u8{ 0xcc, 0xc3 };
1946     const new_unused_object = try textObject(allocator, ".text.unused", &new_unused_text, &.{
1947         ObjectSymbol.section(1),
1948         ObjectSymbol.function("unused", 1, 0, new_unused_text.len),
1949     });
1950     defer allocator.free(new_unused_object);
1951 
1952     const old_archive = try archive.build(allocator, &.{
1953         .{ .name = "needed.o", .bytes = needed_object, .symbols = &.{"needed"} },
1954         .{ .name = "unused.o", .bytes = old_unused_object, .symbols = &.{"unused"} },
1955     });
1956     defer allocator.free(old_archive);
1957     const new_archive = try archive.build(allocator, &.{
1958         .{ .name = "needed.o", .bytes = needed_object, .symbols = &.{"needed"} },
1959         .{ .name = "unused.o", .bytes = new_unused_object, .symbols = &.{"unused"} },
1960     });
1961     defer allocator.free(new_archive);
1962 
1963     const old_inputs = [_]model.Input{
1964         .{ .name = "caller.o", .bytes = caller_object },
1965         .{ .name = "libneeded.a", .bytes = old_archive },
1966     };
1967     const new_inputs = [_]model.Input{
1968         .{ .name = "caller.o", .bytes = caller_object },
1969         .{ .name = "libneeded.a", .bytes = new_archive },
1970     };
1971 
1972     var old_linked = try root.link(allocator, &old_inputs, options);
1973     defer old_linked.deinit(allocator);
1974     try std.testing.expectEqual(@as(usize, 2), old_linked.manifest.inputs.len);
1975     try std.testing.expectEqual(@as(usize, 2), old_linked.manifest.contributions.len);
1976 
1977     const before = try allocator.dupe(u8, old_linked.bytes);
1978     defer allocator.free(before);
1979 
1980     var state = try old_linked.prepareIncrementalState(allocator);
1981     defer state.deinit(allocator);
1982 
1983     var classified = try classifyTestInputs(
1984         allocator,
1985         state.manifest,
1986         &new_inputs,
1987     );
1988     defer classified.deinit(allocator);
1989     const changes = classified.changes;
1990     try std.testing.expectEqual(@as(usize, 1), changes.summary.changed);
1991 
1992     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, options);
1993     defer evidence.deinit(allocator);
1994     const replacements = evidence.replacements;
1995     try std.testing.expect(evidence.inputs_proven);
1996     try std.testing.expectEqual(@as(usize, 1), replacements.len);
1997     try std.testing.expectEqualStrings("needed.o", replacements[0].input_name);
1998     try std.testing.expectEqualStrings(".text.needed", replacements[0].name);
1999     try std.testing.expect(replacements[0].unchanged);
2000     try std.testing.expectEqual(@as(usize, 0), replacements[0].payload.len);
2001 
2002     const plan = state.planChangedInputRelinkFromInputChanges(options, changes, replacements);
2003     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
2004 
2005     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
2006     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
2007     try std.testing.expectEqual(@as(usize, 0), application.contributions_written);
2008     try std.testing.expectEqual(@as(usize, 0), application.bytes_written);
2009     try std.testing.expectEqual(@as(usize, 0), application.zero_fill_bytes);
2010     try std.testing.expectEqualSlices(u8, before, old_linked.bytes);
2011 
2012     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, options);
2013     try state.updateManifestForAcceptedChangedInputRelinkFromInputChanges(&new_inputs, changes, replacements, evidence.member_updates, 0, plan);
2014     try std.testing.expect(state.canReuseFor(options, &new_inputs));
2015 
2016     var candidate = try root.link(allocator, &new_inputs, options);
2017     defer candidate.deinit(allocator);
2018     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
2019 }
2020 
2021 fn twoCallArchiveMember(allocator: Allocator, symbol: []const u8, section: []const u8, text: []const u8) ![]u8 {
2022     return try textObject(allocator, section, text, &.{
2023         ObjectSymbol.section(1),
2024         ObjectSymbol.function(symbol, 1, 0, text.len),
2025     });
2026 }
2027 
2028 test "ELF direct relink refreshes changed member hashes for later relinks" {
2029     const allocator = std.testing.allocator;
2030     const options = model.LinkOptions{};
2031 
2032     const caller_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xe8, 0, 0, 0, 0, 0xc3 };
2033     const caller_object = try relocatedTextObject(
2034         allocator,
2035         ".text.start",
2036         &caller_text,
2037         &.{
2038             ObjectSymbol.section(1),
2039             ObjectSymbol.function("_start", 1, 0, caller_text.len),
2040             ObjectSymbol.undefinedFunction("alpha"),
2041             ObjectSymbol.undefinedFunction("beta"),
2042         },
2043         &.{
2044             ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4),
2045             ObjectRelocation.x86_64(1, 6, 4, .PLT32, -4),
2046         },
2047     );
2048     defer allocator.free(caller_object);
2049 
2050     const alpha_v1 = try twoCallArchiveMember(allocator, "alpha", ".text.alpha", &.{ 0x90, 0xc3 });
2051     defer allocator.free(alpha_v1);
2052     const alpha_v2 = try twoCallArchiveMember(allocator, "alpha", ".text.alpha", &.{ 0xcc, 0xc3 });
2053     defer allocator.free(alpha_v2);
2054     const beta_v1 = try twoCallArchiveMember(allocator, "beta", ".text.beta", &.{ 0x90, 0xc3 });
2055     defer allocator.free(beta_v1);
2056     const beta_v2 = try twoCallArchiveMember(allocator, "beta", ".text.beta", &.{ 0xcc, 0xc3 });
2057     defer allocator.free(beta_v2);
2058 
2059     const archive_v1 = try archive.build(allocator, &.{
2060         .{ .name = "alpha.o", .bytes = alpha_v1, .symbols = &.{"alpha"} },
2061         .{ .name = "beta.o", .bytes = beta_v1, .symbols = &.{"beta"} },
2062     });
2063     defer allocator.free(archive_v1);
2064     const archive_v2 = try archive.build(allocator, &.{
2065         .{ .name = "alpha.o", .bytes = alpha_v2, .symbols = &.{"alpha"} },
2066         .{ .name = "beta.o", .bytes = beta_v1, .symbols = &.{"beta"} },
2067     });
2068     defer allocator.free(archive_v2);
2069     const archive_v3 = try archive.build(allocator, &.{
2070         .{ .name = "alpha.o", .bytes = alpha_v2, .symbols = &.{"alpha"} },
2071         .{ .name = "beta.o", .bytes = beta_v2, .symbols = &.{"beta"} },
2072     });
2073     defer allocator.free(archive_v3);
2074 
2075     const inputs_v1 = [_]model.Input{
2076         .{ .name = "caller.o", .bytes = caller_object },
2077         .{ .name = "libcalls.a", .bytes = archive_v1 },
2078     };
2079     const inputs_v2 = [_]model.Input{
2080         .{ .name = "caller.o", .bytes = caller_object },
2081         .{ .name = "libcalls.a", .bytes = archive_v2 },
2082     };
2083     const inputs_v3 = [_]model.Input{
2084         .{ .name = "caller.o", .bytes = caller_object },
2085         .{ .name = "libcalls.a", .bytes = archive_v3 },
2086     };
2087 
2088     var old_linked = try root.link(allocator, &inputs_v1, options);
2089     defer old_linked.deinit(allocator);
2090 
2091     var state = try old_linked.prepareIncrementalState(allocator);
2092     defer state.deinit(allocator);
2093     try std.testing.expectEqual(@as(usize, 2), state.manifest.archive_members.len);
2094 
2095     const encoded_v1 = try state.manifest.formatBinaryAlloc(allocator);
2096     defer allocator.free(encoded_v1);
2097 
2098     var classified_v2 = try classifyTestInputs(
2099         allocator,
2100         state.manifest,
2101         &inputs_v2,
2102     );
2103     defer classified_v2.deinit(allocator);
2104     const changes_v2 = classified_v2.changes;
2105 
2106     var evidence_v2 = try directEvidenceAlloc(allocator, state.manifest, &inputs_v2, changes_v2, options);
2107     defer evidence_v2.deinit(allocator);
2108     try std.testing.expect(evidence_v2.inputs_proven);
2109     try std.testing.expectEqual(@as(usize, 1), evidence_v2.member_updates.len);
2110     const alpha_record_index = evidence_v2.member_updates[0].member_index;
2111     try std.testing.expectEqualStrings("alpha.o", state.manifest.string(state.manifest.archive_members[alpha_record_index].name_id));
2112     try std.testing.expectEqual(incremental.hashBytes(alpha_v2), evidence_v2.member_updates[0].hash);
2113 
2114     try state.ensureReplacementIndex(allocator, evidence_v2.replacements);
2115     const plan_v2 = state.planChangedInputRelinkFromInputChanges(options, changes_v2, evidence_v2.replacements);
2116     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan_v2.decision);
2117     const application_v2 = try state.applyAcceptedChangedInputRelink(old_linked.bytes, evidence_v2.replacements, plan_v2);
2118     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application_v2.plan.decision);
2119     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, options);
2120     try state.updateManifestForAcceptedChangedInputRelinkFromInputChanges(
2121         &inputs_v2,
2122         changes_v2,
2123         evidence_v2.replacements,
2124         evidence_v2.member_updates,
2125         0,
2126         plan_v2,
2127     );
2128     try std.testing.expectEqual(incremental.hashBytes(alpha_v2), state.manifest.archive_members[alpha_record_index].hash);
2129     try std.testing.expect(state.canReuseFor(options, &inputs_v2));
2130 
2131     var record_updates = try state.acceptedRelinkRecordUpdates(
2132         allocator,
2133         &inputs_v2,
2134         changes_v2,
2135         evidence_v2.replacements,
2136         evidence_v2.member_updates,
2137     );
2138     defer record_updates.deinit(allocator);
2139     const patches = try state.manifest.scalarPatchesAlloc(
2140         allocator,
2141         encoded_v1,
2142         record_updates.input_indexes.items,
2143         record_updates.contribution_indexes.items,
2144         record_updates.archive_member_indexes.items,
2145     );
2146     defer allocator.free(patches);
2147     const patched_encoding = try allocator.dupe(u8, encoded_v1);
2148     defer allocator.free(patched_encoding);
2149     for (patches) |field_patch| {
2150         @memcpy(patched_encoding[field_patch.offset..][0..field_patch.len], field_patch.slice());
2151     }
2152     const rewritten_encoding = try state.manifest.formatBinaryAlloc(allocator);
2153     defer allocator.free(rewritten_encoding);
2154     try std.testing.expectEqualSlices(u8, rewritten_encoding, patched_encoding);
2155 
2156     var candidate_v2 = try root.link(allocator, &inputs_v2, options);
2157     defer candidate_v2.deinit(allocator);
2158     try std.testing.expectEqualSlices(u8, candidate_v2.bytes, old_linked.bytes);
2159 
2160     var classified_v3 = try classifyTestInputs(
2161         allocator,
2162         state.manifest,
2163         &inputs_v3,
2164     );
2165     defer classified_v3.deinit(allocator);
2166     const changes_v3 = classified_v3.changes;
2167 
2168     var evidence_v3 = try directEvidenceAlloc(allocator, state.manifest, &inputs_v3, changes_v3, options);
2169     defer evidence_v3.deinit(allocator);
2170     try std.testing.expect(evidence_v3.inputs_proven);
2171     try std.testing.expectEqual(@as(usize, 1), evidence_v3.member_updates.len);
2172     const beta_record_index = evidence_v3.member_updates[0].member_index;
2173     try std.testing.expectEqualStrings("beta.o", state.manifest.string(state.manifest.archive_members[beta_record_index].name_id));
2174 
2175     try state.ensureReplacementIndex(allocator, evidence_v3.replacements);
2176     const plan_v3 = state.planChangedInputRelinkFromInputChanges(options, changes_v3, evidence_v3.replacements);
2177     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan_v3.decision);
2178     const application_v3 = try state.applyAcceptedChangedInputRelink(old_linked.bytes, evidence_v3.replacements, plan_v3);
2179     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application_v3.plan.decision);
2180     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, options);
2181     try state.updateManifestForAcceptedChangedInputRelinkFromInputChanges(
2182         &inputs_v3,
2183         changes_v3,
2184         evidence_v3.replacements,
2185         evidence_v3.member_updates,
2186         0,
2187         plan_v3,
2188     );
2189     try std.testing.expectEqual(incremental.hashBytes(beta_v2), state.manifest.archive_members[beta_record_index].hash);
2190     try std.testing.expect(state.canReuseFor(options, &inputs_v3));
2191 
2192     var candidate_v3 = try root.link(allocator, &inputs_v3, options);
2193     defer candidate_v3.deinit(allocator);
2194     try std.testing.expectEqualSlices(u8, candidate_v3.bytes, old_linked.bytes);
2195 }
2196 
2197 test "ELF direct relink keeps identical code folding on fallback" {
2198     const allocator = std.testing.allocator;
2199     const options = model.LinkOptions{ .icf = .all };
2200 
2201     const old_text = [_]u8{ 0xc3, 0x90, 0x90 };
2202     const section_index: u16 = 1;
2203     const old_object = try textObject(allocator, ".text.patch", &old_text, &.{ObjectSymbol.section(section_index)});
2204     defer allocator.free(old_object);
2205 
2206     const new_text = [_]u8{ 0xc3, 0x90, 0xcc };
2207     const new_object = try textObject(allocator, ".text.patch", &new_text, &.{ObjectSymbol.section(section_index)});
2208     defer allocator.free(new_object);
2209 
2210     var parsed_old = try parser.parseObject(allocator, .{ .name = "patch.o", .bytes = old_object });
2211     defer parsed_old.deinit(allocator);
2212 
2213     var builder = try incremental.Builder.init(allocator, options);
2214     defer builder.deinit();
2215     try builder.addInput(.{ .name = "patch.o", .bytes = old_object });
2216     builder.setInputLinkHash(0, try inputLinkHashForObject(parsed_old));
2217     try builder.addContribution("patch.o", 0, .section, ".text.patch", @intCast(section_index), ".text", 0x401000, 4, old_text.len, 16, 16);
2218 
2219     var manifest = try builder.finish();
2220     defer manifest.deinit(allocator);
2221 
2222     var state = try incremental.PreparedState.fromOwnedManifest(allocator, manifest.take());
2223     defer state.deinit(allocator);
2224 
2225     const inputs = [_]model.Input{.{ .name = "patch.o", .bytes = new_object }};
2226     var classified = try classifyTestInputs(
2227         allocator,
2228         state.manifest,
2229         &inputs,
2230     );
2231     defer classified.deinit(allocator);
2232     const changes = classified.changes;
2233 
2234     var evidence = try directEvidenceAlloc(allocator, state.manifest, &inputs, changes, options);
2235     defer evidence.deinit(allocator);
2236     const replacements = evidence.replacements;
2237 
2238     try std.testing.expectEqual(@as(usize, 0), replacements.len);
2239 
2240     const plan = state.planChangedInputRelinkFromInputChanges(options, changes, replacements);
2241     try std.testing.expectEqual(incremental.RelinkDecision.full_link, plan.decision);
2242     try std.testing.expectEqual(incremental.RelinkBlocker.replacement_missing, plan.blocker.?);
2243 }
2244 
2245 fn metadataContributionPresent(
2246     manifest: incremental.Manifest,
2247     input_name: []const u8,
2248     contribution_name: []const u8,
2249     ordinal: u16,
2250 ) bool {
2251     for (manifest.contributions) |contribution| {
2252         if (contribution.kind != .section) continue;
2253         if (!std.mem.eql(u8, manifest.string(contribution.input_name_id), input_name)) continue;
2254         if (!std.mem.eql(u8, manifest.string(contribution.name_id), contribution_name)) continue;
2255         if (contribution.ordinal != ordinal) continue;
2256         return true;
2257     }
2258     return false;
2259 }
2260 
2261 test "ELF direct relink replacements allow generated build ids" {
2262     const allocator = std.testing.allocator;
2263     const options = model.LinkOptions{ .build_id = .fast };
2264 
2265     const old_text = [_]u8{ 0xc3, 0x90, 0x90 };
2266     const section_index: u16 = 1;
2267     const old_object = try textObject(allocator, ".text.patch", &old_text, &.{ObjectSymbol.section(section_index)});
2268     defer allocator.free(old_object);
2269 
2270     const new_text = [_]u8{ 0xc3, 0x90, 0xcc };
2271     const new_object = try textObject(allocator, ".text.patch", &new_text, &.{ObjectSymbol.section(section_index)});
2272     defer allocator.free(new_object);
2273 
2274     var parsed_old = try parser.parseObject(allocator, .{ .name = "patch.o", .bytes = old_object });
2275     defer parsed_old.deinit(allocator);
2276 
2277     var builder = try incremental.Builder.init(allocator, options);
2278     defer builder.deinit();
2279     try builder.addInput(.{ .name = "patch.o", .bytes = old_object });
2280     builder.setInputLinkHash(0, try inputLinkHashForObject(parsed_old));
2281     try builder.addContribution("patch.o", 0, .section, ".text.patch", @intCast(section_index), ".text", 0x401000, 4, old_text.len, 16, 16);
2282 
2283     var manifest = try builder.finish();
2284     defer manifest.deinit(allocator);
2285 
2286     var state = try incremental.PreparedState.fromOwnedManifest(allocator, manifest.take());
2287     defer state.deinit(allocator);
2288 
2289     const inputs = [_]model.Input{.{ .name = "patch.o", .bytes = new_object }};
2290     var classified = try classifyTestInputs(
2291         allocator,
2292         state.manifest,
2293         &inputs,
2294     );
2295     defer classified.deinit(allocator);
2296     const changes = classified.changes;
2297 
2298     var evidence = try directEvidenceAlloc(allocator, state.manifest, &inputs, changes, options);
2299     defer evidence.deinit(allocator);
2300     const replacements = evidence.replacements;
2301 
2302     try std.testing.expectEqual(@as(usize, 1), replacements.len);
2303     try std.testing.expectEqualSlices(u8, &new_text, replacements[0].payload);
2304 
2305     const plan = state.planChangedInputRelinkFromInputChanges(options, changes, replacements);
2306     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
2307 }
2308 
2309 test "ELF direct relink refreshes generated build ids" {
2310     const allocator = std.testing.allocator;
2311     const options = model.LinkOptions{ .build_id = .fast };
2312 
2313     const old_text = [_]u8{ 0xc3, 0x90, 0x90 };
2314     const old_object = try textObject(allocator, ".text", &old_text, &.{
2315         ObjectSymbol.section(1),
2316         ObjectSymbol.function("_start", 1, 0, old_text.len),
2317     });
2318     defer allocator.free(old_object);
2319 
2320     const new_text = [_]u8{ 0xc3, 0x90, 0xcc };
2321     const new_object = try textObject(allocator, ".text", &new_text, &.{
2322         ObjectSymbol.section(1),
2323         ObjectSymbol.function("_start", 1, 0, new_text.len),
2324     });
2325     defer allocator.free(new_object);
2326 
2327     const old_inputs = [_]model.Input{.{ .name = "start.o", .bytes = old_object }};
2328     const new_inputs = [_]model.Input{.{ .name = "start.o", .bytes = new_object }};
2329 
2330     var old_linked = try root.link(allocator, &old_inputs, options);
2331     defer old_linked.deinit(allocator);
2332 
2333     var state = try old_linked.prepareIncrementalState(allocator);
2334     defer state.deinit(allocator);
2335 
2336     var classified = try classifyTestInputs(
2337         allocator,
2338         state.manifest,
2339         &new_inputs,
2340     );
2341     defer classified.deinit(allocator);
2342     const changes = classified.changes;
2343 
2344     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, options);
2345     defer evidence.deinit(allocator);
2346     const replacements = evidence.replacements;
2347 
2348     const plan = state.planChangedInputRelinkFromInputChanges(options, changes, replacements);
2349     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
2350 
2351     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
2352     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
2353     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, options);
2354 
2355     var candidate = try root.link(allocator, &new_inputs, options);
2356     defer candidate.deinit(allocator);
2357 
2358     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
2359 }
2360 
2361 test "ELF direct relink preserves generated eh frame headers" {
2362     const allocator = std.testing.allocator;
2363     const options = model.LinkOptions{ .eh_frame_header = true };
2364 
2365     const old_text = [_]u8{ 0xc3, 0x90, 0x90 };
2366     const old_start_object = try textObject(allocator, ".text", &old_text, &.{
2367         ObjectSymbol.section(1),
2368         ObjectSymbol.function("_start", 1, 0, old_text.len),
2369     });
2370     defer allocator.free(old_start_object);
2371 
2372     const new_text = [_]u8{ 0xc3, 0x90, 0xcc };
2373     const new_start_object = try textObject(allocator, ".text", &new_text, &.{
2374         ObjectSymbol.section(1),
2375         ObjectSymbol.function("_start", 1, 0, new_text.len),
2376     });
2377     defer allocator.free(new_start_object);
2378 
2379     var unwind = @as([0x2c]u8, @splat(0));
2380     std.mem.writeInt(u32, unwind[0x00..][0..4], 0x14, .little);
2381     std.mem.writeInt(u32, unwind[0x04..][0..4], 0, .little);
2382     std.mem.writeInt(u32, unwind[0x18..][0..4], 0x10, .little);
2383     std.mem.writeInt(u32, unwind[0x1c..][0..4], 0x1c, .little);
2384     std.mem.writeInt(u32, unwind[0x24..][0..4], old_text.len, .little);
2385     const frame_sections = [_]ObjectSection{.{
2386         .name = ".eh_frame",
2387         .section_type = std.elf.SHT_X86_64_UNWIND,
2388         .flags = std.elf.SHF_ALLOC,
2389         .alignment = 8,
2390         .bytes = &unwind,
2391     }};
2392     const frame_symbols = [_]ObjectSymbol{
2393         ObjectSymbol.section(1),
2394         ObjectSymbol.undefinedFunction("_start"),
2395     };
2396     const frame_relocations = [_]ObjectRelocation{
2397         ObjectRelocation.x86_64(1, 0x20, 2, .PC32, 0),
2398     };
2399     const frame_object = try buildObject(allocator, .{
2400         .sections = &frame_sections,
2401         .symbols = &frame_symbols,
2402         .relocations = &frame_relocations,
2403     });
2404     defer allocator.free(frame_object);
2405 
2406     const old_inputs = [_]model.Input{
2407         .{ .name = "start.o", .bytes = old_start_object },
2408         .{ .name = "frame.o", .bytes = frame_object },
2409     };
2410     const new_inputs = [_]model.Input{
2411         .{ .name = "start.o", .bytes = new_start_object },
2412         .{ .name = "frame.o", .bytes = frame_object },
2413     };
2414 
2415     var old_linked = try root.link(allocator, &old_inputs, options);
2416     defer old_linked.deinit(allocator);
2417 
2418     var state = try old_linked.prepareIncrementalState(allocator);
2419     defer state.deinit(allocator);
2420 
2421     var classified = try classifyTestInputs(
2422         allocator,
2423         state.manifest,
2424         &new_inputs,
2425     );
2426     defer classified.deinit(allocator);
2427     const changes = classified.changes;
2428 
2429     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, options);
2430     defer evidence.deinit(allocator);
2431     const replacements = evidence.replacements;
2432 
2433     const plan = state.planChangedInputRelinkFromInputChanges(options, changes, replacements);
2434     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
2435 
2436     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
2437     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
2438     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, options);
2439 
2440     var candidate = try root.link(allocator, &new_inputs, options);
2441     defer candidate.deinit(allocator);
2442 
2443     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
2444 }
2445 
2446 test "ELF direct relink synthesizes local relocated payloads" {
2447     const allocator = std.testing.allocator;
2448 
2449     const text = [_]u8{0xc3};
2450     var old_data = @as([16]u8, @splat(0));
2451     old_data[8] = 0x11;
2452     const old_sections = [_]ObjectSection{
2453         ObjectSection.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16),
2454         ObjectSection.progbits(".data.ptr", &old_data, std.elf.SHF_WRITE, 8),
2455     };
2456     const old_symbols = [_]ObjectSymbol{
2457         ObjectSymbol.section(1),
2458         ObjectSymbol.section(2),
2459         ObjectSymbol.function("_start", 1, 0, text.len),
2460     };
2461     const old_relocations = [_]ObjectRelocation{ObjectRelocation.x86_64(2, 0, 3, .@"64", 0)};
2462     const old_object = try buildObject(allocator, .{
2463         .sections = &old_sections,
2464         .symbols = &old_symbols,
2465         .relocations = &old_relocations,
2466     });
2467     defer allocator.free(old_object);
2468 
2469     var new_data = @as([16]u8, @splat(0));
2470     new_data[8] = 0x22;
2471     const new_sections = [_]ObjectSection{
2472         ObjectSection.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16),
2473         ObjectSection.progbits(".data.ptr", &new_data, std.elf.SHF_WRITE, 8),
2474     };
2475     const new_symbols = [_]ObjectSymbol{
2476         ObjectSymbol.section(1),
2477         ObjectSymbol.section(2),
2478         ObjectSymbol.function("_start", 1, 0, text.len),
2479     };
2480     const new_relocations = [_]ObjectRelocation{ObjectRelocation.x86_64(2, 0, 3, .@"64", 0)};
2481     const new_object = try buildObject(allocator, .{
2482         .sections = &new_sections,
2483         .symbols = &new_symbols,
2484         .relocations = &new_relocations,
2485     });
2486     defer allocator.free(new_object);
2487 
2488     const old_inputs = [_]model.Input{.{ .name = "local.o", .bytes = old_object }};
2489     const new_inputs = [_]model.Input{.{ .name = "local.o", .bytes = new_object }};
2490 
2491     var old_linked = try root.link(allocator, &old_inputs, .{});
2492     defer old_linked.deinit(allocator);
2493 
2494     var state = try old_linked.prepareIncrementalState(allocator);
2495     defer state.deinit(allocator);
2496 
2497     var classified = try classifyTestInputs(
2498         allocator,
2499         state.manifest,
2500         &new_inputs,
2501     );
2502     defer classified.deinit(allocator);
2503     const changes = classified.changes;
2504 
2505     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
2506     defer evidence.deinit(allocator);
2507     const replacements = evidence.replacements;
2508 
2509     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
2510     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
2511 
2512     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
2513     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
2514     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
2515 
2516     var candidate = try root.link(allocator, &new_inputs, .{});
2517     defer candidate.deinit(allocator);
2518 
2519     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
2520 }
2521 
2522 test "ELF direct relink synthesizes owned eh frame payloads" {
2523     const allocator = std.testing.allocator;
2524     const options = model.LinkOptions{ .eh_frame_header = true };
2525 
2526     const old_text = [_]u8{ 0xc3, 0x90, 0x90 };
2527     var unwind = @as([0x2c]u8, @splat(0));
2528     std.mem.writeInt(u32, unwind[0x00..][0..4], 0x14, .little);
2529     std.mem.writeInt(u32, unwind[0x04..][0..4], 0, .little);
2530     std.mem.writeInt(u32, unwind[0x18..][0..4], 0x10, .little);
2531     std.mem.writeInt(u32, unwind[0x1c..][0..4], 0x1c, .little);
2532     std.mem.writeInt(u32, unwind[0x24..][0..4], old_text.len, .little);
2533     const frame_section = ObjectSection{
2534         .name = ".eh_frame",
2535         .section_type = std.elf.SHT_X86_64_UNWIND,
2536         .flags = std.elf.SHF_ALLOC,
2537         .alignment = 8,
2538         .bytes = &unwind,
2539     };
2540     const old_sections = [_]ObjectSection{
2541         ObjectSection.progbits(".text", &old_text, std.elf.SHF_EXECINSTR, 16),
2542         frame_section,
2543     };
2544     const old_symbols = [_]ObjectSymbol{
2545         ObjectSymbol.section(1),
2546         ObjectSymbol.section(2),
2547         ObjectSymbol.function("_start", 1, 0, old_text.len),
2548     };
2549     const old_relocations = [_]ObjectRelocation{ObjectRelocation.x86_64(2, 0x20, 3, .PC32, 0)};
2550     const old_object = try buildObject(allocator, .{
2551         .sections = &old_sections,
2552         .symbols = &old_symbols,
2553         .relocations = &old_relocations,
2554     });
2555     defer allocator.free(old_object);
2556 
2557     const new_text = [_]u8{ 0xc3, 0x90, 0xcc };
2558     const new_sections = [_]ObjectSection{
2559         ObjectSection.progbits(".text", &new_text, std.elf.SHF_EXECINSTR, 16),
2560         frame_section,
2561     };
2562     const new_symbols = [_]ObjectSymbol{
2563         ObjectSymbol.section(1),
2564         ObjectSymbol.section(2),
2565         ObjectSymbol.function("_start", 1, 0, new_text.len),
2566     };
2567     const new_relocations = [_]ObjectRelocation{ObjectRelocation.x86_64(2, 0x20, 3, .PC32, 0)};
2568     const new_object = try buildObject(allocator, .{
2569         .sections = &new_sections,
2570         .symbols = &new_symbols,
2571         .relocations = &new_relocations,
2572     });
2573     defer allocator.free(new_object);
2574 
2575     const old_inputs = [_]model.Input{.{ .name = "frame.o", .bytes = old_object }};
2576     const new_inputs = [_]model.Input{.{ .name = "frame.o", .bytes = new_object }};
2577 
2578     var old_linked = try root.link(allocator, &old_inputs, options);
2579     defer old_linked.deinit(allocator);
2580 
2581     var state = try old_linked.prepareIncrementalState(allocator);
2582     defer state.deinit(allocator);
2583 
2584     var classified = try classifyTestInputs(
2585         allocator,
2586         state.manifest,
2587         &new_inputs,
2588     );
2589     defer classified.deinit(allocator);
2590     const changes = classified.changes;
2591 
2592     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, options);
2593     defer evidence.deinit(allocator);
2594     const replacements = evidence.replacements;
2595 
2596     const plan = state.planChangedInputRelinkFromInputChanges(options, changes, replacements);
2597     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
2598 
2599     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
2600     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
2601     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, options);
2602 
2603     var candidate = try root.link(allocator, &new_inputs, options);
2604     defer candidate.deinit(allocator);
2605 
2606     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
2607 }
2608 
2609 test "ELF direct relink synthesizes external relocated payloads" {
2610     const allocator = std.testing.allocator;
2611 
2612     const old_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0x90 };
2613     const old_caller_object = try relocatedTextObject(
2614         allocator,
2615         ".text",
2616         &old_text,
2617         &.{
2618             ObjectSymbol.section(1),
2619             ObjectSymbol.function("_start", 1, 0, old_text.len),
2620             ObjectSymbol.undefinedFunction("callee"),
2621         },
2622         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
2623     );
2624     defer allocator.free(old_caller_object);
2625 
2626     const new_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xcc };
2627     const new_caller_object = try relocatedTextObject(
2628         allocator,
2629         ".text",
2630         &new_text,
2631         &.{
2632             ObjectSymbol.section(1),
2633             ObjectSymbol.function("_start", 1, 0, new_text.len),
2634             ObjectSymbol.undefinedFunction("callee"),
2635         },
2636         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
2637     );
2638     defer allocator.free(new_caller_object);
2639 
2640     const callee_text = [_]u8{0xc3};
2641     const callee_object = try textObject(allocator, ".text.callee", &callee_text, &.{
2642         ObjectSymbol.section(1),
2643         ObjectSymbol.function("callee", 1, 0, callee_text.len),
2644     });
2645     defer allocator.free(callee_object);
2646 
2647     const old_inputs = [_]model.Input{
2648         .{ .name = "caller.o", .bytes = old_caller_object },
2649         .{ .name = "callee.o", .bytes = callee_object },
2650     };
2651     const new_inputs = [_]model.Input{
2652         .{ .name = "caller.o", .bytes = new_caller_object },
2653         .{ .name = "callee.o", .bytes = callee_object },
2654     };
2655 
2656     var old_linked = try root.link(allocator, &old_inputs, .{});
2657     defer old_linked.deinit(allocator);
2658 
2659     var state = try old_linked.prepareIncrementalState(allocator);
2660     defer state.deinit(allocator);
2661 
2662     var classified = try classifyTestInputs(
2663         allocator,
2664         state.manifest,
2665         &new_inputs,
2666     );
2667     defer classified.deinit(allocator);
2668     const changes = classified.changes;
2669 
2670     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
2671     defer evidence.deinit(allocator);
2672     const replacements = evidence.replacements;
2673 
2674     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
2675     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
2676 
2677     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
2678     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
2679     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
2680 
2681     var candidate = try root.link(allocator, &new_inputs, .{});
2682     defer candidate.deinit(allocator);
2683 
2684     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
2685 }
2686 
2687 test "ELF direct relink synthesizes external archive member targets" {
2688     const allocator = std.testing.allocator;
2689 
2690     const old_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0x90 };
2691     const old_caller_object = try relocatedTextObject(
2692         allocator,
2693         ".text",
2694         &old_text,
2695         &.{
2696             ObjectSymbol.section(1),
2697             ObjectSymbol.function("_start", 1, 0, old_text.len),
2698             ObjectSymbol.undefinedFunction("callee"),
2699         },
2700         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
2701     );
2702     defer allocator.free(old_caller_object);
2703 
2704     const new_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xcc };
2705     const new_caller_object = try relocatedTextObject(
2706         allocator,
2707         ".text",
2708         &new_text,
2709         &.{
2710             ObjectSymbol.section(1),
2711             ObjectSymbol.function("_start", 1, 0, new_text.len),
2712             ObjectSymbol.undefinedFunction("callee"),
2713         },
2714         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
2715     );
2716     defer allocator.free(new_caller_object);
2717 
2718     const callee_text = [_]u8{0xc3};
2719     const callee_object = try textObject(allocator, ".text.callee", &callee_text, &.{
2720         ObjectSymbol.section(1),
2721         ObjectSymbol.function("callee", 1, 0, callee_text.len),
2722     });
2723     defer allocator.free(callee_object);
2724 
2725     const callee_archive = try archive.build(allocator, &.{.{ .name = "callee.o", .bytes = callee_object }});
2726     defer allocator.free(callee_archive);
2727 
2728     const old_inputs = [_]model.Input{
2729         .{ .name = "caller.o", .bytes = old_caller_object },
2730         .{ .name = "libcallee.a", .bytes = callee_archive },
2731     };
2732     const new_inputs = [_]model.Input{
2733         .{ .name = "caller.o", .bytes = new_caller_object },
2734         .{ .name = "libcallee.a", .bytes = callee_archive },
2735     };
2736 
2737     var old_linked = try root.link(allocator, &old_inputs, .{});
2738     defer old_linked.deinit(allocator);
2739 
2740     var state = try old_linked.prepareIncrementalState(allocator);
2741     defer state.deinit(allocator);
2742 
2743     var classified = try classifyTestInputs(
2744         allocator,
2745         state.manifest,
2746         &new_inputs,
2747     );
2748     defer classified.deinit(allocator);
2749     const changes = classified.changes;
2750 
2751     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
2752     defer evidence.deinit(allocator);
2753     const replacements = evidence.replacements;
2754 
2755     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
2756     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
2757 
2758     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
2759     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
2760     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
2761 
2762     var candidate = try root.link(allocator, &new_inputs, .{});
2763     defer candidate.deinit(allocator);
2764 
2765     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
2766 }
2767 
2768 test "ELF direct relink synthesizes changed external object targets" {
2769     const allocator = std.testing.allocator;
2770 
2771     const old_caller_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0x90 };
2772     const old_caller_object = try relocatedTextObject(
2773         allocator,
2774         ".text",
2775         &old_caller_text,
2776         &.{
2777             ObjectSymbol.section(1),
2778             ObjectSymbol.function("_start", 1, 0, old_caller_text.len),
2779             ObjectSymbol.undefinedFunction("callee"),
2780         },
2781         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
2782     );
2783     defer allocator.free(old_caller_object);
2784 
2785     const new_caller_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xcc };
2786     const new_caller_object = try relocatedTextObject(
2787         allocator,
2788         ".text",
2789         &new_caller_text,
2790         &.{
2791             ObjectSymbol.section(1),
2792             ObjectSymbol.function("_start", 1, 0, new_caller_text.len),
2793             ObjectSymbol.undefinedFunction("callee"),
2794         },
2795         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
2796     );
2797     defer allocator.free(new_caller_object);
2798 
2799     const old_callee_text = [_]u8{ 0xc3, 0x90 };
2800     const old_callee_object = try textObject(allocator, ".text.callee", &old_callee_text, &.{
2801         ObjectSymbol.section(1),
2802         ObjectSymbol.function("callee", 1, 0, old_callee_text.len),
2803     });
2804     defer allocator.free(old_callee_object);
2805 
2806     const new_callee_text = [_]u8{ 0xc3, 0xcc };
2807     const new_callee_object = try textObject(allocator, ".text.callee", &new_callee_text, &.{
2808         ObjectSymbol.section(1),
2809         ObjectSymbol.function("callee", 1, 0, new_callee_text.len),
2810     });
2811     defer allocator.free(new_callee_object);
2812 
2813     const old_inputs = [_]model.Input{
2814         .{ .name = "caller.o", .bytes = old_caller_object },
2815         .{ .name = "callee.o", .bytes = old_callee_object },
2816     };
2817     const new_inputs = [_]model.Input{
2818         .{ .name = "caller.o", .bytes = new_caller_object },
2819         .{ .name = "callee.o", .bytes = new_callee_object },
2820     };
2821 
2822     var old_linked = try root.link(allocator, &old_inputs, .{});
2823     defer old_linked.deinit(allocator);
2824 
2825     var state = try old_linked.prepareIncrementalState(allocator);
2826     defer state.deinit(allocator);
2827 
2828     var classified = try classifyTestInputs(
2829         allocator,
2830         state.manifest,
2831         &new_inputs,
2832     );
2833     defer classified.deinit(allocator);
2834     const changes = classified.changes;
2835 
2836     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
2837     defer evidence.deinit(allocator);
2838     const replacements = evidence.replacements;
2839 
2840     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
2841     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
2842 
2843     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
2844     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
2845     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
2846 
2847     var candidate = try root.link(allocator, &new_inputs, .{});
2848     defer candidate.deinit(allocator);
2849 
2850     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
2851 }
2852 
2853 test "ELF direct relink synthesizes changed archive external targets" {
2854     const allocator = std.testing.allocator;
2855 
2856     const old_caller_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0x90 };
2857     const old_caller_object = try relocatedTextObject(
2858         allocator,
2859         ".text",
2860         &old_caller_text,
2861         &.{
2862             ObjectSymbol.section(1),
2863             ObjectSymbol.function("_start", 1, 0, old_caller_text.len),
2864             ObjectSymbol.undefinedFunction("callee"),
2865         },
2866         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
2867     );
2868     defer allocator.free(old_caller_object);
2869 
2870     const new_caller_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xcc };
2871     const new_caller_object = try relocatedTextObject(
2872         allocator,
2873         ".text",
2874         &new_caller_text,
2875         &.{
2876             ObjectSymbol.section(1),
2877             ObjectSymbol.function("_start", 1, 0, new_caller_text.len),
2878             ObjectSymbol.undefinedFunction("callee"),
2879         },
2880         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
2881     );
2882     defer allocator.free(new_caller_object);
2883 
2884     const old_callee_text = [_]u8{ 0xc3, 0x90 };
2885     const old_callee_object = try textObject(allocator, ".text.callee", &old_callee_text, &.{
2886         ObjectSymbol.section(1),
2887         ObjectSymbol.function("callee", 1, 0, old_callee_text.len),
2888     });
2889     defer allocator.free(old_callee_object);
2890 
2891     const new_callee_text = [_]u8{ 0xc3, 0xcc };
2892     const new_callee_object = try textObject(allocator, ".text.callee", &new_callee_text, &.{
2893         ObjectSymbol.section(1),
2894         ObjectSymbol.function("callee", 1, 0, new_callee_text.len),
2895     });
2896     defer allocator.free(new_callee_object);
2897 
2898     const old_archive = try archive.build(allocator, &.{.{ .name = "callee.o", .bytes = old_callee_object }});
2899     defer allocator.free(old_archive);
2900     const new_archive = try archive.build(allocator, &.{.{ .name = "callee.o", .bytes = new_callee_object }});
2901     defer allocator.free(new_archive);
2902 
2903     const old_inputs = [_]model.Input{
2904         .{ .name = "caller.o", .bytes = old_caller_object },
2905         .{ .name = "libcallee.a", .bytes = old_archive },
2906     };
2907     const new_inputs = [_]model.Input{
2908         .{ .name = "caller.o", .bytes = new_caller_object },
2909         .{ .name = "libcallee.a", .bytes = new_archive },
2910     };
2911 
2912     var old_linked = try root.link(allocator, &old_inputs, .{});
2913     defer old_linked.deinit(allocator);
2914 
2915     var state = try old_linked.prepareIncrementalState(allocator);
2916     defer state.deinit(allocator);
2917 
2918     var classified = try classifyTestInputs(
2919         allocator,
2920         state.manifest,
2921         &new_inputs,
2922     );
2923     defer classified.deinit(allocator);
2924     const changes = classified.changes;
2925 
2926     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
2927     defer evidence.deinit(allocator);
2928     const replacements = evidence.replacements;
2929 
2930     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
2931     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
2932 
2933     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
2934     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
2935     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
2936 
2937     var candidate = try root.link(allocator, &new_inputs, .{});
2938     defer candidate.deinit(allocator);
2939 
2940     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
2941 }
2942 
2943 test "ELF direct relink synthesizes weak external relocations" {
2944     const allocator = std.testing.allocator;
2945 
2946     const old_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0x90 };
2947     const old_caller_object = try relocatedTextObject(
2948         allocator,
2949         ".text",
2950         &old_text,
2951         &.{
2952             ObjectSymbol.section(1),
2953             ObjectSymbol.function("_start", 1, 0, old_text.len),
2954             ObjectSymbol.undefinedFunction("callee"),
2955         },
2956         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
2957     );
2958     defer allocator.free(old_caller_object);
2959 
2960     const new_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xcc };
2961     const new_caller_object = try relocatedTextObject(
2962         allocator,
2963         ".text",
2964         &new_text,
2965         &.{
2966             ObjectSymbol.section(1),
2967             ObjectSymbol.function("_start", 1, 0, new_text.len),
2968             ObjectSymbol.undefinedFunction("callee"),
2969         },
2970         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
2971     );
2972     defer allocator.free(new_caller_object);
2973 
2974     const callee_text = [_]u8{0xc3};
2975     const callee_object = try textObject(allocator, ".text.callee", &callee_text, &.{
2976         ObjectSymbol.section(1),
2977         ObjectSymbol.weakFunction("callee", 1, 0, callee_text.len),
2978     });
2979     defer allocator.free(callee_object);
2980 
2981     const old_inputs = [_]model.Input{
2982         .{ .name = "caller.o", .bytes = old_caller_object },
2983         .{ .name = "callee.o", .bytes = callee_object },
2984     };
2985     const new_inputs = [_]model.Input{
2986         .{ .name = "caller.o", .bytes = new_caller_object },
2987         .{ .name = "callee.o", .bytes = callee_object },
2988     };
2989 
2990     var old_linked = try root.link(allocator, &old_inputs, .{});
2991     defer old_linked.deinit(allocator);
2992 
2993     var state = try old_linked.prepareIncrementalState(allocator);
2994     defer state.deinit(allocator);
2995 
2996     var classified = try classifyTestInputs(
2997         allocator,
2998         state.manifest,
2999         &new_inputs,
3000     );
3001     defer classified.deinit(allocator);
3002     const changes = classified.changes;
3003 
3004     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
3005     defer evidence.deinit(allocator);
3006     const replacements = evidence.replacements;
3007 
3008     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
3009     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
3010 
3011     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
3012     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
3013     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
3014 
3015     var candidate = try root.link(allocator, &new_inputs, .{});
3016     defer candidate.deinit(allocator);
3017 
3018     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
3019 }
3020 
3021 test "ELF direct relink preserves first weak external target" {
3022     const allocator = std.testing.allocator;
3023 
3024     const old_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0x90 };
3025     const old_caller_object = try relocatedTextObject(
3026         allocator,
3027         ".text",
3028         &old_text,
3029         &.{
3030             ObjectSymbol.section(1),
3031             ObjectSymbol.function("_start", 1, 0, old_text.len),
3032             ObjectSymbol.undefinedFunction("hook"),
3033         },
3034         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
3035     );
3036     defer allocator.free(old_caller_object);
3037 
3038     const new_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xcc };
3039     const new_caller_object = try relocatedTextObject(
3040         allocator,
3041         ".text",
3042         &new_text,
3043         &.{
3044             ObjectSymbol.section(1),
3045             ObjectSymbol.function("_start", 1, 0, new_text.len),
3046             ObjectSymbol.undefinedFunction("hook"),
3047         },
3048         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
3049     );
3050     defer allocator.free(new_caller_object);
3051 
3052     const first_text = [_]u8{0xc3};
3053     const first_object = try textObject(allocator, ".text.first", &first_text, &.{
3054         ObjectSymbol.section(1),
3055         ObjectSymbol.weakFunction("hook", 1, 0, first_text.len),
3056     });
3057     defer allocator.free(first_object);
3058 
3059     const second_text = [_]u8{ 0x90, 0xc3 };
3060     const second_object = try textObject(allocator, ".text.second", &second_text, &.{
3061         ObjectSymbol.section(1),
3062         ObjectSymbol.weakFunction("hook", 1, 0, second_text.len),
3063     });
3064     defer allocator.free(second_object);
3065 
3066     const old_inputs = [_]model.Input{
3067         .{ .name = "caller.o", .bytes = old_caller_object },
3068         .{ .name = "first.o", .bytes = first_object },
3069         .{ .name = "second.o", .bytes = second_object },
3070     };
3071     const new_inputs = [_]model.Input{
3072         .{ .name = "caller.o", .bytes = new_caller_object },
3073         .{ .name = "first.o", .bytes = first_object },
3074         .{ .name = "second.o", .bytes = second_object },
3075     };
3076 
3077     var old_linked = try root.link(allocator, &old_inputs, .{});
3078     defer old_linked.deinit(allocator);
3079 
3080     var state = try old_linked.prepareIncrementalState(allocator);
3081     defer state.deinit(allocator);
3082 
3083     var classified = try classifyTestInputs(
3084         allocator,
3085         state.manifest,
3086         &new_inputs,
3087     );
3088     defer classified.deinit(allocator);
3089     const changes = classified.changes;
3090 
3091     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
3092     defer evidence.deinit(allocator);
3093     const replacements = evidence.replacements;
3094 
3095     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
3096     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
3097 
3098     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
3099     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
3100     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
3101 
3102     var candidate = try root.link(allocator, &new_inputs, .{});
3103     defer candidate.deinit(allocator);
3104 
3105     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
3106 }
3107 
3108 test "ELF direct relink lets strong external targets override weak definitions" {
3109     const allocator = std.testing.allocator;
3110 
3111     const old_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0x90 };
3112     const old_caller_object = try relocatedTextObject(
3113         allocator,
3114         ".text",
3115         &old_text,
3116         &.{
3117             ObjectSymbol.section(1),
3118             ObjectSymbol.function("_start", 1, 0, old_text.len),
3119             ObjectSymbol.undefinedFunction("hook"),
3120         },
3121         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
3122     );
3123     defer allocator.free(old_caller_object);
3124 
3125     const new_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xcc };
3126     const new_caller_object = try relocatedTextObject(
3127         allocator,
3128         ".text",
3129         &new_text,
3130         &.{
3131             ObjectSymbol.section(1),
3132             ObjectSymbol.function("_start", 1, 0, new_text.len),
3133             ObjectSymbol.undefinedFunction("hook"),
3134         },
3135         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
3136     );
3137     defer allocator.free(new_caller_object);
3138 
3139     const weak_text = [_]u8{0xc3};
3140     const weak_object = try textObject(allocator, ".text.weak", &weak_text, &.{
3141         ObjectSymbol.section(1),
3142         ObjectSymbol.weakFunction("hook", 1, 0, weak_text.len),
3143     });
3144     defer allocator.free(weak_object);
3145 
3146     const strong_text = [_]u8{ 0x90, 0xc3 };
3147     const strong_object = try textObject(allocator, ".text.strong", &strong_text, &.{
3148         ObjectSymbol.section(1),
3149         ObjectSymbol.function("hook", 1, 0, strong_text.len),
3150     });
3151     defer allocator.free(strong_object);
3152 
3153     const old_inputs = [_]model.Input{
3154         .{ .name = "caller.o", .bytes = old_caller_object },
3155         .{ .name = "weak.o", .bytes = weak_object },
3156         .{ .name = "strong.o", .bytes = strong_object },
3157     };
3158     const new_inputs = [_]model.Input{
3159         .{ .name = "caller.o", .bytes = new_caller_object },
3160         .{ .name = "weak.o", .bytes = weak_object },
3161         .{ .name = "strong.o", .bytes = strong_object },
3162     };
3163 
3164     var old_linked = try root.link(allocator, &old_inputs, .{});
3165     defer old_linked.deinit(allocator);
3166 
3167     var state = try old_linked.prepareIncrementalState(allocator);
3168     defer state.deinit(allocator);
3169 
3170     var classified = try classifyTestInputs(
3171         allocator,
3172         state.manifest,
3173         &new_inputs,
3174     );
3175     defer classified.deinit(allocator);
3176     const changes = classified.changes;
3177 
3178     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
3179     defer evidence.deinit(allocator);
3180     const replacements = evidence.replacements;
3181 
3182     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
3183     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
3184 
3185     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
3186     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
3187     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
3188 
3189     var candidate = try root.link(allocator, &new_inputs, .{});
3190     defer candidate.deinit(allocator);
3191 
3192     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
3193 }
3194 
3195 test "ELF direct relink synthesizes unresolved weak null relocations" {
3196     const allocator = std.testing.allocator;
3197 
3198     const text = [_]u8{0xc3};
3199     var old_data = @as([16]u8, @splat(0));
3200     old_data[8] = 0x11;
3201     const old_sections = [_]ObjectSection{
3202         ObjectSection.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16),
3203         ObjectSection.progbits(".data.ptr", &old_data, std.elf.SHF_WRITE, 8),
3204     };
3205     const old_symbols = [_]ObjectSymbol{
3206         ObjectSymbol.section(1),
3207         ObjectSymbol.section(2),
3208         ObjectSymbol.function("_start", 1, 0, text.len),
3209         ObjectSymbol.weakUndefinedFunction("optional_hook"),
3210     };
3211     const old_relocations = [_]ObjectRelocation{ObjectRelocation.x86_64(2, 0, 4, .@"64", 0)};
3212     const old_object = try buildObject(allocator, .{
3213         .sections = &old_sections,
3214         .symbols = &old_symbols,
3215         .relocations = &old_relocations,
3216     });
3217     defer allocator.free(old_object);
3218 
3219     var new_data = @as([16]u8, @splat(0));
3220     new_data[8] = 0x22;
3221     const new_sections = [_]ObjectSection{
3222         ObjectSection.progbits(".text", &text, std.elf.SHF_EXECINSTR, 16),
3223         ObjectSection.progbits(".data.ptr", &new_data, std.elf.SHF_WRITE, 8),
3224     };
3225     const new_symbols = [_]ObjectSymbol{
3226         ObjectSymbol.section(1),
3227         ObjectSymbol.section(2),
3228         ObjectSymbol.function("_start", 1, 0, text.len),
3229         ObjectSymbol.weakUndefinedFunction("optional_hook"),
3230     };
3231     const new_relocations = [_]ObjectRelocation{ObjectRelocation.x86_64(2, 0, 4, .@"64", 0)};
3232     const new_object = try buildObject(allocator, .{
3233         .sections = &new_sections,
3234         .symbols = &new_symbols,
3235         .relocations = &new_relocations,
3236     });
3237     defer allocator.free(new_object);
3238 
3239     const old_inputs = [_]model.Input{.{ .name = "weak-null.o", .bytes = old_object }};
3240     const new_inputs = [_]model.Input{.{ .name = "weak-null.o", .bytes = new_object }};
3241 
3242     var old_linked = try root.link(allocator, &old_inputs, .{});
3243     defer old_linked.deinit(allocator);
3244 
3245     var state = try old_linked.prepareIncrementalState(allocator);
3246     defer state.deinit(allocator);
3247 
3248     var classified = try classifyTestInputs(
3249         allocator,
3250         state.manifest,
3251         &new_inputs,
3252     );
3253     defer classified.deinit(allocator);
3254     const changes = classified.changes;
3255 
3256     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
3257     defer evidence.deinit(allocator);
3258     const replacements = evidence.replacements;
3259 
3260     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
3261     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
3262 
3263     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
3264     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
3265     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
3266 
3267     var candidate = try root.link(allocator, &new_inputs, .{});
3268     defer candidate.deinit(allocator);
3269 
3270     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
3271 }
3272 
3273 test "ELF direct relink replacements copy changed archive member payloads" {
3274     const allocator = std.testing.allocator;
3275 
3276     const old_text = [_]u8{ 0xc3, 0x90, 0x90 };
3277     const section_index: u16 = 1;
3278     const old_member_object = try textObject(allocator, ".text.patch", &old_text, &.{ObjectSymbol.section(section_index)});
3279     defer allocator.free(old_member_object);
3280 
3281     const new_text = [_]u8{ 0xc3, 0x90, 0xcc };
3282     const new_member_object = try textObject(allocator, ".text.patch", &new_text, &.{ObjectSymbol.section(section_index)});
3283     defer allocator.free(new_member_object);
3284 
3285     const old_archive = try archive.build(allocator, &.{.{ .name = "member.o", .bytes = old_member_object }});
3286     defer allocator.free(old_archive);
3287     const new_archive = try archive.build(allocator, &.{.{ .name = "member.o", .bytes = new_member_object }});
3288     defer allocator.free(new_archive);
3289 
3290     var parsed_old = try parser.parseObject(allocator, .{ .name = "member.o", .bytes = old_member_object });
3291     defer parsed_old.deinit(allocator);
3292     parsed_old.input_index = 0;
3293 
3294     var builder = try incremental.Builder.init(allocator, .{});
3295     defer builder.deinit();
3296     try builder.addInput(.{ .name = "libpatch.a", .bytes = old_archive });
3297     builder.setInputLinkHash(0, try inputLinkHashForObject(parsed_old));
3298     builder.setInputSelectionHash(0, try selectionHashForInput(allocator, .{ .name = "libpatch.a", .bytes = old_archive }));
3299     try builder.addArchiveMember(0, "member.o", incremental.hashBytes(old_member_object), try linkageHash(parsed_old), true);
3300     try builder.addContribution("member.o", 0, .section, ".text.patch", @intCast(section_index), ".text", 0x401000, 4, old_text.len, 16, 16);
3301 
3302     var manifest = try builder.finish();
3303     defer manifest.deinit(allocator);
3304 
3305     var state = try incremental.PreparedState.fromOwnedManifest(allocator, manifest.take());
3306     defer state.deinit(allocator);
3307 
3308     const inputs = [_]model.Input{.{ .name = "libpatch.a", .bytes = new_archive }};
3309     var classified = try classifyTestInputs(
3310         allocator,
3311         state.manifest,
3312         &inputs,
3313     );
3314     defer classified.deinit(allocator);
3315     const changes = classified.changes;
3316 
3317     var evidence = try directEvidenceAlloc(allocator, state.manifest, &inputs, changes, .{});
3318     defer evidence.deinit(allocator);
3319     const replacements = evidence.replacements;
3320 
3321     try std.testing.expectEqual(@as(usize, 1), replacements.len);
3322     try std.testing.expectEqualStrings("member.o", replacements[0].input_name);
3323     try std.testing.expectEqualStrings(".text.patch", replacements[0].name);
3324     try std.testing.expectEqual(@as(u64, new_text.len), replacements[0].size);
3325     try std.testing.expectEqualSlices(u8, &new_text, replacements[0].payload);
3326 
3327     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
3328     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
3329 }
3330 
3331 test "ELF direct relink resolves deduplicated comdat external targets" {
3332     const allocator = std.testing.allocator;
3333 
3334     const old_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0x90 };
3335     const old_caller_object = try relocatedTextObject(
3336         allocator,
3337         ".text",
3338         &old_text,
3339         &.{
3340             ObjectSymbol.section(1),
3341             ObjectSymbol.function("_start", 1, 0, old_text.len),
3342             ObjectSymbol.undefinedFunction("inline_fn"),
3343         },
3344         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
3345     );
3346     defer allocator.free(old_caller_object);
3347 
3348     const new_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xcc };
3349     const new_caller_object = try relocatedTextObject(
3350         allocator,
3351         ".text",
3352         &new_text,
3353         &.{
3354             ObjectSymbol.section(1),
3355             ObjectSymbol.function("_start", 1, 0, new_text.len),
3356             ObjectSymbol.undefinedFunction("inline_fn"),
3357         },
3358         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
3359     );
3360     defer allocator.free(new_caller_object);
3361 
3362     const inline_text = [_]u8{0xc3};
3363     const inline_index: u16 = 1;
3364     const inline_signature: u32 = 2;
3365     var group_storage: [2 * 4]u8 = undefined;
3366     const group_payload = object_writer.groupBytes(&group_storage, &.{inline_index});
3367     const inline_flags = std.elf.SHF_EXECINSTR | std.elf.SHF_GROUP;
3368     const comdat_sections = [_]ObjectSection{
3369         ObjectSection.progbits(".text.inline", &inline_text, inline_flags, 16),
3370         ObjectSection.group(".group", group_payload, inline_signature),
3371     };
3372     const comdat_symbols = [_]ObjectSymbol{
3373         ObjectSymbol.section(inline_index),
3374         ObjectSymbol.function("inline_fn", inline_index, 0, inline_text.len),
3375     };
3376     const retained_object = try buildObject(allocator, .{
3377         .sections = &comdat_sections,
3378         .symbols = &comdat_symbols,
3379     });
3380     defer allocator.free(retained_object);
3381 
3382     const duplicate_object = try buildObject(allocator, .{
3383         .sections = &comdat_sections,
3384         .symbols = &comdat_symbols,
3385     });
3386     defer allocator.free(duplicate_object);
3387 
3388     const old_inputs = [_]model.Input{
3389         .{ .name = "caller.o", .bytes = old_caller_object },
3390         .{ .name = "retained.o", .bytes = retained_object },
3391         .{ .name = "duplicate.o", .bytes = duplicate_object },
3392     };
3393     const new_inputs = [_]model.Input{
3394         .{ .name = "caller.o", .bytes = new_caller_object },
3395         .{ .name = "retained.o", .bytes = retained_object },
3396         .{ .name = "duplicate.o", .bytes = duplicate_object },
3397     };
3398 
3399     var old_linked = try root.link(allocator, &old_inputs, .{});
3400     defer old_linked.deinit(allocator);
3401 
3402     var state = try old_linked.prepareIncrementalState(allocator);
3403     defer state.deinit(allocator);
3404 
3405     var classified = try classifyTestInputs(
3406         allocator,
3407         state.manifest,
3408         &new_inputs,
3409     );
3410     defer classified.deinit(allocator);
3411     const changes = classified.changes;
3412 
3413     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
3414     defer evidence.deinit(allocator);
3415     const replacements = evidence.replacements;
3416     try std.testing.expect(replacements.len != 0);
3417 
3418     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
3419     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
3420 
3421     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
3422     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
3423     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
3424 
3425     var candidate = try root.link(allocator, &new_inputs, .{});
3426     defer candidate.deinit(allocator);
3427 
3428     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
3429 }
3430 
3431 test "ELF direct relink rejects changed archives with new selectable members" {
3432     const allocator = std.testing.allocator;
3433 
3434     const caller_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xc3 };
3435     const caller_object = try relocatedTextObject(
3436         allocator,
3437         ".text",
3438         &caller_text,
3439         &.{
3440             ObjectSymbol.section(1),
3441             ObjectSymbol.function("_start", 1, 0, caller_text.len),
3442             ObjectSymbol.undefinedFunction("callee"),
3443         },
3444         &.{ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4)},
3445     );
3446     defer allocator.free(caller_object);
3447 
3448     const impl_text = [_]u8{ 0xc3, 0x90 };
3449     const impl_object = try textObject(allocator, ".text.callee", &impl_text, &.{
3450         ObjectSymbol.section(1),
3451         ObjectSymbol.function("callee", 1, 0, impl_text.len),
3452     });
3453     defer allocator.free(impl_object);
3454 
3455     const front_text = [_]u8{ 0x31, 0xc0, 0xc3 };
3456     const front_object = try textObject(allocator, ".text.front", &front_text, &.{
3457         ObjectSymbol.section(1),
3458         ObjectSymbol.function("callee", 1, 0, front_text.len),
3459     });
3460     defer allocator.free(front_object);
3461 
3462     const old_archive = try archive.build(allocator, &.{
3463         .{ .name = "impl.o", .bytes = impl_object, .symbols = &.{"callee"} },
3464     });
3465     defer allocator.free(old_archive);
3466     const new_archive = try archive.build(allocator, &.{
3467         .{ .name = "front.o", .bytes = front_object, .symbols = &.{"callee"} },
3468         .{ .name = "impl.o", .bytes = impl_object, .symbols = &.{"callee"} },
3469     });
3470     defer allocator.free(new_archive);
3471 
3472     const old_inputs = [_]model.Input{
3473         .{ .name = "caller.o", .bytes = caller_object },
3474         .{ .name = "libcallee.a", .bytes = old_archive },
3475     };
3476     const new_inputs = [_]model.Input{
3477         .{ .name = "caller.o", .bytes = caller_object },
3478         .{ .name = "libcallee.a", .bytes = new_archive },
3479     };
3480 
3481     var old_linked = try root.link(allocator, &old_inputs, .{});
3482     defer old_linked.deinit(allocator);
3483 
3484     var state = try old_linked.prepareIncrementalState(allocator);
3485     defer state.deinit(allocator);
3486 
3487     var classified = try classifyTestInputs(
3488         allocator,
3489         state.manifest,
3490         &new_inputs,
3491     );
3492     defer classified.deinit(allocator);
3493     const changes = classified.changes;
3494 
3495     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
3496     defer evidence.deinit(allocator);
3497 
3498     var candidate = try root.link(allocator, &new_inputs, .{});
3499     defer candidate.deinit(allocator);
3500 
3501     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, evidence.replacements);
3502     if (plan.decision == .in_place) {
3503         const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, evidence.replacements, plan);
3504         try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
3505         try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
3506         try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
3507     }
3508 }
3509 
3510 test "ELF direct relink patches only changed archive members" {
3511     const allocator = std.testing.allocator;
3512 
3513     const caller_text = [_]u8{ 0xe8, 0, 0, 0, 0, 0xe8, 0, 0, 0, 0, 0xc3 };
3514     const caller_object = try relocatedTextObject(
3515         allocator,
3516         ".text",
3517         &caller_text,
3518         &.{
3519             ObjectSymbol.section(1),
3520             ObjectSymbol.function("_start", 1, 0, caller_text.len),
3521             ObjectSymbol.undefinedFunction("stable_fn"),
3522             ObjectSymbol.undefinedFunction("edited_fn"),
3523         },
3524         &.{
3525             ObjectRelocation.x86_64(1, 1, 3, .PLT32, -4),
3526             ObjectRelocation.x86_64(1, 6, 4, .PLT32, -4),
3527         },
3528     );
3529     defer allocator.free(caller_object);
3530 
3531     const stable_text = [_]u8{ 0xc3, 0x90 };
3532     const stable_object = try textObject(allocator, ".text.stable", &stable_text, &.{
3533         ObjectSymbol.section(1),
3534         ObjectSymbol.function("stable_fn", 1, 0, stable_text.len),
3535     });
3536     defer allocator.free(stable_object);
3537 
3538     const old_edited_text = [_]u8{ 0xc3, 0x90 };
3539     const old_edited_object = try textObject(allocator, ".text.edited", &old_edited_text, &.{
3540         ObjectSymbol.section(1),
3541         ObjectSymbol.function("edited_fn", 1, 0, old_edited_text.len),
3542     });
3543     defer allocator.free(old_edited_object);
3544 
3545     const new_edited_text = [_]u8{ 0xc3, 0xcc };
3546     const new_edited_object = try textObject(allocator, ".text.edited", &new_edited_text, &.{
3547         ObjectSymbol.section(1),
3548         ObjectSymbol.function("edited_fn", 1, 0, new_edited_text.len),
3549     });
3550     defer allocator.free(new_edited_object);
3551 
3552     const old_archive = try archive.build(allocator, &.{
3553         .{ .name = "stable.o", .bytes = stable_object, .symbols = &.{"stable_fn"} },
3554         .{ .name = "edited.o", .bytes = old_edited_object, .symbols = &.{"edited_fn"} },
3555     });
3556     defer allocator.free(old_archive);
3557     const new_archive = try archive.build(allocator, &.{
3558         .{ .name = "stable.o", .bytes = stable_object, .symbols = &.{"stable_fn"} },
3559         .{ .name = "edited.o", .bytes = new_edited_object, .symbols = &.{"edited_fn"} },
3560     });
3561     defer allocator.free(new_archive);
3562 
3563     const old_inputs = [_]model.Input{
3564         .{ .name = "caller.o", .bytes = caller_object },
3565         .{ .name = "libboth.a", .bytes = old_archive },
3566     };
3567     const new_inputs = [_]model.Input{
3568         .{ .name = "caller.o", .bytes = caller_object },
3569         .{ .name = "libboth.a", .bytes = new_archive },
3570     };
3571 
3572     var old_linked = try root.link(allocator, &old_inputs, .{});
3573     defer old_linked.deinit(allocator);
3574 
3575     var state = try old_linked.prepareIncrementalState(allocator);
3576     defer state.deinit(allocator);
3577 
3578     var classified = try classifyTestInputs(
3579         allocator,
3580         state.manifest,
3581         &new_inputs,
3582     );
3583     defer classified.deinit(allocator);
3584     const changes = classified.changes;
3585 
3586     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
3587     defer evidence.deinit(allocator);
3588     const replacements = evidence.replacements;
3589     try std.testing.expect(evidence.inputs_proven);
3590     try std.testing.expectEqual(@as(usize, 2), replacements.len);
3591 
3592     var unchanged_count: usize = 0;
3593     var payload_count: usize = 0;
3594     for (replacements) |replacement| {
3595         if (replacement.unchanged) {
3596             unchanged_count += 1;
3597             try std.testing.expectEqualStrings("stable.o", replacement.input_name);
3598         } else {
3599             payload_count += 1;
3600             try std.testing.expectEqualStrings("edited.o", replacement.input_name);
3601             try std.testing.expectEqualSlices(u8, &new_edited_text, replacement.payload);
3602         }
3603     }
3604     try std.testing.expectEqual(@as(usize, 1), unchanged_count);
3605     try std.testing.expectEqual(@as(usize, 1), payload_count);
3606 
3607     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, replacements);
3608     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
3609 
3610     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, replacements, plan);
3611     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
3612     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
3613 
3614     var candidate = try root.link(allocator, &new_inputs, .{});
3615     defer candidate.deinit(allocator);
3616 
3617     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
3618 }
3619 
3620 test "ELF direct relink resolves synthetic boundary targets" {
3621     const allocator = std.testing.allocator;
3622 
3623     const old_text = [_]u8{ 0x48, 0x8d, 0x05, 0, 0, 0, 0, 0xc3, 0x90 };
3624     const old_caller_object = try relocatedTextObject(
3625         allocator,
3626         ".text",
3627         &old_text,
3628         &.{
3629             ObjectSymbol.section(1),
3630             ObjectSymbol.function("_start", 1, 0, old_text.len),
3631             ObjectSymbol.undefinedObject("__start_metadata"),
3632         },
3633         &.{ObjectRelocation.x86_64(1, 3, 3, .PC32, -4)},
3634     );
3635     defer allocator.free(old_caller_object);
3636 
3637     const new_text = [_]u8{ 0x48, 0x8d, 0x05, 0, 0, 0, 0, 0xc3, 0xcc };
3638     const new_caller_object = try relocatedTextObject(
3639         allocator,
3640         ".text",
3641         &new_text,
3642         &.{
3643             ObjectSymbol.section(1),
3644             ObjectSymbol.function("_start", 1, 0, new_text.len),
3645             ObjectSymbol.undefinedObject("__start_metadata"),
3646         },
3647         &.{ObjectRelocation.x86_64(1, 3, 3, .PC32, -4)},
3648     );
3649     defer allocator.free(new_caller_object);
3650 
3651     const metadata = [_]u8{ 1, 2, 3, 4 };
3652     const provider_sections = [_]ObjectSection{
3653         ObjectSection.progbits("metadata", &metadata, 0, 4),
3654     };
3655     const provider_symbols = [_]ObjectSymbol{
3656         ObjectSymbol.section(1),
3657         ObjectSymbol.object("metadata_entry", 1, 0, metadata.len),
3658     };
3659     const provider_object = try buildObject(allocator, .{
3660         .sections = &provider_sections,
3661         .symbols = &provider_symbols,
3662     });
3663     defer allocator.free(provider_object);
3664 
3665     const old_inputs = [_]model.Input{
3666         .{ .name = "caller.o", .bytes = old_caller_object },
3667         .{ .name = "provider.o", .bytes = provider_object },
3668     };
3669     const new_inputs = [_]model.Input{
3670         .{ .name = "caller.o", .bytes = new_caller_object },
3671         .{ .name = "provider.o", .bytes = provider_object },
3672     };
3673 
3674     var old_linked = try root.link(allocator, &old_inputs, .{});
3675     defer old_linked.deinit(allocator);
3676 
3677     var boundary_recorded = false;
3678     for (old_linked.manifest.external_targets) |target| {
3679         if (std.mem.eql(u8, old_linked.manifest.string(target.name_id), "__start_metadata")) boundary_recorded = true;
3680     }
3681     try std.testing.expect(boundary_recorded);
3682 
3683     var state = try old_linked.prepareIncrementalState(allocator);
3684     defer state.deinit(allocator);
3685 
3686     var classified = try classifyTestInputs(
3687         allocator,
3688         state.manifest,
3689         &new_inputs,
3690     );
3691     defer classified.deinit(allocator);
3692     const changes = classified.changes;
3693 
3694     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
3695     defer evidence.deinit(allocator);
3696     try std.testing.expect(evidence.replacements.len != 0);
3697 
3698     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, evidence.replacements);
3699     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
3700 
3701     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, evidence.replacements, plan);
3702     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
3703     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
3704 
3705     var candidate = try root.link(allocator, &new_inputs, .{});
3706     defer candidate.deinit(allocator);
3707 
3708     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
3709 }
3710 
3711 test "ELF direct relink resolves merged string piece targets" {
3712     const allocator = std.testing.allocator;
3713 
3714     const strings = "aa\x00bb\x00";
3715 
3716     const old_text = [_]u8{ 0xc3, 0x90 };
3717     const old_pointer = @as([8]u8, @splat(0));
3718     const old_merge_index: u16 = 2;
3719     const old_data_index: u16 = 3;
3720     const old_sections = [_]ObjectSection{
3721         ObjectSection.progbits(".text", &old_text, std.elf.SHF_EXECINSTR, 16),
3722         ObjectSection.strings(".rodata.str1.1", strings, 1),
3723         ObjectSection.progbits(".data.ptr", &old_pointer, std.elf.SHF_WRITE, 8),
3724     };
3725     const old_symbols = [_]ObjectSymbol{
3726         ObjectSymbol.section(1),
3727         ObjectSymbol.section(old_merge_index),
3728         ObjectSymbol.section(old_data_index),
3729         ObjectSymbol.function("_start", 1, 0, old_text.len),
3730         ObjectSymbol.object("string_ptr", old_data_index, 0, old_pointer.len),
3731     };
3732     const old_merge_symbol: u32 = @intCast(old_merge_index);
3733     const old_relocations = [_]ObjectRelocation{
3734         ObjectRelocation.x86_64(old_data_index, 0, old_merge_symbol, .@"64", 3),
3735     };
3736     const old_object = try buildObject(allocator, .{
3737         .sections = &old_sections,
3738         .symbols = &old_symbols,
3739         .relocations = &old_relocations,
3740     });
3741     defer allocator.free(old_object);
3742 
3743     const new_text = [_]u8{ 0xc3, 0xcc };
3744     const new_pointer = @as([8]u8, @splat(0));
3745     const new_merge_index: u16 = 2;
3746     const new_data_index: u16 = 3;
3747     const new_sections = [_]ObjectSection{
3748         ObjectSection.progbits(".text", &new_text, std.elf.SHF_EXECINSTR, 16),
3749         ObjectSection.strings(".rodata.str1.1", strings, 1),
3750         ObjectSection.progbits(".data.ptr", &new_pointer, std.elf.SHF_WRITE, 8),
3751     };
3752     const new_symbols = [_]ObjectSymbol{
3753         ObjectSymbol.section(1),
3754         ObjectSymbol.section(new_merge_index),
3755         ObjectSymbol.section(new_data_index),
3756         ObjectSymbol.function("_start", 1, 0, new_text.len),
3757         ObjectSymbol.object("string_ptr", new_data_index, 0, new_pointer.len),
3758     };
3759     const new_merge_symbol: u32 = @intCast(new_merge_index);
3760     const new_relocations = [_]ObjectRelocation{
3761         ObjectRelocation.x86_64(new_data_index, 0, new_merge_symbol, .@"64", 3),
3762     };
3763     const new_object = try buildObject(allocator, .{
3764         .sections = &new_sections,
3765         .symbols = &new_symbols,
3766         .relocations = &new_relocations,
3767     });
3768     defer allocator.free(new_object);
3769 
3770     const old_inputs = [_]model.Input{.{ .name = "main.o", .bytes = old_object }};
3771     const new_inputs = [_]model.Input{.{ .name = "main.o", .bytes = new_object }};
3772 
3773     var old_linked = try root.link(allocator, &old_inputs, .{});
3774     defer old_linked.deinit(allocator);
3775     try std.testing.expect(old_linked.manifest.merge_pieces.len != 0);
3776 
3777     var state = try old_linked.prepareIncrementalState(allocator);
3778     defer state.deinit(allocator);
3779 
3780     var classified = try classifyTestInputs(
3781         allocator,
3782         state.manifest,
3783         &new_inputs,
3784     );
3785     defer classified.deinit(allocator);
3786     const changes = classified.changes;
3787 
3788     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
3789     defer evidence.deinit(allocator);
3790     try std.testing.expect(evidence.inputs_proven);
3791     try std.testing.expect(evidence.replacements.len != 0);
3792 
3793     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, evidence.replacements);
3794     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
3795 
3796     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, evidence.replacements, plan);
3797     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
3798     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
3799 
3800     var candidate = try root.link(allocator, &new_inputs, .{});
3801     defer candidate.deinit(allocator);
3802     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
3803 }
3804 
3805 test "ELF direct relink replicates got relocation handling" {
3806     const allocator = std.testing.allocator;
3807 
3808     const provider_data = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8 };
3809     const provider_sections = [_]ObjectSection{
3810         ObjectSection.progbits(".data.shared", &provider_data, std.elf.SHF_WRITE, 8),
3811     };
3812     const provider_symbols = [_]ObjectSymbol{
3813         ObjectSymbol.section(1),
3814         ObjectSymbol.object("shared_value", 1, 0, provider_data.len),
3815     };
3816     const provider_object = try buildObject(allocator, .{
3817         .sections = &provider_sections,
3818         .symbols = &provider_symbols,
3819     });
3820     defer allocator.free(provider_object);
3821 
3822     const old_text = [_]u8{
3823         0x48, 0x8b, 0x05, 0, 0, 0, 0,
3824         0x0f, 0x18, 0x05, 0, 0, 0, 0,
3825         0xc3, 0x90,
3826     };
3827     const old_caller = try relocatedTextObject(
3828         allocator,
3829         ".text",
3830         &old_text,
3831         &.{
3832             ObjectSymbol.section(1),
3833             ObjectSymbol.function("_start", 1, 0, old_text.len),
3834             ObjectSymbol.undefinedObject("shared_value"),
3835         },
3836         &.{
3837             ObjectRelocation.x86_64(1, 3, 3, .REX_GOTPCRELX, -4),
3838             ObjectRelocation.x86_64(1, 10, 3, .GOTPCREL, -4),
3839         },
3840     );
3841     defer allocator.free(old_caller);
3842 
3843     const new_text = [_]u8{
3844         0x48, 0x8b, 0x05, 0, 0, 0, 0,
3845         0x0f, 0x18, 0x05, 0, 0, 0, 0,
3846         0xc3, 0xcc,
3847     };
3848     const new_caller = try relocatedTextObject(
3849         allocator,
3850         ".text",
3851         &new_text,
3852         &.{
3853             ObjectSymbol.section(1),
3854             ObjectSymbol.function("_start", 1, 0, new_text.len),
3855             ObjectSymbol.undefinedObject("shared_value"),
3856         },
3857         &.{
3858             ObjectRelocation.x86_64(1, 3, 3, .REX_GOTPCRELX, -4),
3859             ObjectRelocation.x86_64(1, 10, 3, .GOTPCREL, -4),
3860         },
3861     );
3862     defer allocator.free(new_caller);
3863 
3864     const old_inputs = [_]model.Input{
3865         .{ .name = "caller.o", .bytes = old_caller },
3866         .{ .name = "provider.o", .bytes = provider_object },
3867     };
3868     const new_inputs = [_]model.Input{
3869         .{ .name = "caller.o", .bytes = new_caller },
3870         .{ .name = "provider.o", .bytes = provider_object },
3871     };
3872 
3873     var old_linked = try root.link(allocator, &old_inputs, .{});
3874     defer old_linked.deinit(allocator);
3875     try std.testing.expect(old_linked.manifest.got_entries.len != 0);
3876 
3877     var state = try old_linked.prepareIncrementalState(allocator);
3878     defer state.deinit(allocator);
3879 
3880     var classified = try classifyTestInputs(
3881         allocator,
3882         state.manifest,
3883         &new_inputs,
3884     );
3885     defer classified.deinit(allocator);
3886     const changes = classified.changes;
3887 
3888     var evidence = try directEvidenceAlloc(allocator, state.manifest, &new_inputs, changes, .{});
3889     defer evidence.deinit(allocator);
3890     try std.testing.expect(evidence.inputs_proven);
3891     try std.testing.expect(evidence.replacements.len != 0);
3892 
3893     const plan = state.planChangedInputRelinkFromInputChanges(.{}, changes, evidence.replacements);
3894     try std.testing.expectEqual(incremental.RelinkDecision.in_place, plan.decision);
3895 
3896     const application = try state.applyAcceptedChangedInputRelink(old_linked.bytes, evidence.replacements, plan);
3897     try std.testing.expectEqual(incremental.RelinkDecision.in_place, application.plan.decision);
3898     try root.finishDirectIncrementalMetadataPatch(old_linked.bytes, .{});
3899 
3900     var candidate = try root.link(allocator, &new_inputs, .{});
3901     defer candidate.deinit(allocator);
3902     try std.testing.expectEqualSlices(u8, candidate.bytes, old_linked.bytes);
3903 }