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 = §ions,
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 }