Merge pull request #4619 from MiniRML/fix/nid-version-requirement-chains

fix(nid): bound version requirement chains to their section
This commit is contained in:
Adrià 2026-10-13 17:31:25 +02:00 committed by GitHub
commit 6629c09392
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 134 additions and 65 deletions

View File

@ -214,6 +214,10 @@ if(BUILD_TESTING)
target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc)
configure_windows_unwind(guest_kernel_sys_tests)
add_test(NAME guest_kernel_sys COMMAND guest_kernel_sys_tests)
if(Python3_Interpreter_FOUND)
add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $<TARGET_FILE:nid_patcher>)
endif()
if(WIN32)
set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")

View File

@ -216,49 +216,65 @@ void RemapDynamicNeededOffsets(
}
}
struct VersionNameField {
std::size_t Offset;
std::uint32_t Name;
};
std::vector<VersionNameField> ReadVersionNameFields(const std::vector<std::uint8_t>& elf, const Elf64_Ehdr& ehdr) {
using namespace Internal;
std::vector<VersionNameField> fields;
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
const auto section = Read<Elf64_Shdr>(elf, static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr));
if (section.sh_type != kShtGnuVerneed || section.sh_size == 0u) continue;
if (section.sh_offset > elf.size() || section.sh_size > elf.size() - section.sh_offset)
throw std::runtime_error("invalid .gnu.version_r section range");
const auto location = [&](std::uint64_t relative, std::size_t size) {
if (relative > section.sh_size || size > section.sh_size - relative)
throw std::runtime_error(".gnu.version_r record outside section");
return static_cast<std::size_t>(section.sh_offset + relative);
};
const auto advance = [&](std::uint64_t current, std::uint32_t step, std::size_t size) {
if (step < size || step > section.sh_size - current)
throw std::runtime_error("invalid .gnu.version_r record link");
return current + step;
};
std::uint64_t current = 0u;
while (true) {
const auto offset = location(current, sizeof(Elf64_Verneed));
const auto need = Read<Elf64_Verneed>(elf, offset);
if (need.vn_version != 1u) throw std::runtime_error("unsupported .gnu.version_r structure version");
fields.push_back({offset + offsetof(Elf64_Verneed, vn_file), need.vn_file});
if (need.vn_cnt != 0u) {
auto auxiliary = advance(current, need.vn_aux, sizeof(Elf64_Verneed));
for (std::uint16_t index = 0u; index < need.vn_cnt; ++index) {
const auto auxOffset = location(auxiliary, sizeof(Elf64_Vernaux));
const auto aux = Read<Elf64_Vernaux>(elf, auxOffset);
fields.push_back({auxOffset + offsetof(Elf64_Vernaux, vna_name), aux.vna_name});
if (index + 1u == need.vn_cnt) {
if (aux.vna_next != 0u) throw std::runtime_error(".gnu.version_r auxiliary count mismatch");
} else {
auxiliary = advance(auxiliary, aux.vna_next, sizeof(Elf64_Vernaux));
}
}
}
if (need.vn_next == 0u) break;
current = advance(current, need.vn_next, sizeof(Elf64_Verneed));
}
}
return fields;
}
void RemapVerneedOffsets(
std::vector<std::uint8_t>& elf,
const Elf64_Ehdr& ehdr,
const std::vector<std::pair<std::uint32_t, std::uint32_t>>& oldToNewOffset
) {
using namespace Internal;
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
if (shdr.sh_type != kShtGnuVerneed) continue;
const std::size_t verneedOffset = static_cast<std::size_t>(shdr.sh_offset);
std::size_t entryOffset = verneedOffset;
while (true) {
const auto verneed = Read<Elf64_Verneed>(elf, entryOffset);
const auto mappedFile = std::find_if(
oldToNewOffset.begin(), oldToNewOffset.end(),
[&](const auto& entry) { return entry.first == verneed.vn_file; }
);
if (mappedFile != oldToNewOffset.end())
Write(elf, entryOffset + offsetof(Elf64_Verneed, vn_file), mappedFile->second);
std::size_t auxOffset = entryOffset + verneed.vn_aux;
for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) {
const auto vernaux = Read<Elf64_Vernaux>(elf, auxOffset);
const auto mappedName = std::find_if(
oldToNewOffset.begin(), oldToNewOffset.end(),
[&](const auto& entry) { return entry.first == vernaux.vna_name; }
);
if (mappedName != oldToNewOffset.end())
Write(elf, auxOffset + offsetof(Elf64_Vernaux, vna_name), mappedName->second);
if (vernaux.vna_next == 0u) break;
auxOffset += vernaux.vna_next;
}
if (verneed.vn_next == 0u) break;
entryOffset += verneed.vn_next;
}
for (const auto& field : ReadVersionNameFields(elf, ehdr)) {
const auto mapped = std::find_if(oldToNewOffset.begin(), oldToNewOffset.end(),
[&](const auto& entry) { return entry.first == field.Name; });
if (mapped != oldToNewOffset.end()) Write(elf, field.Offset, mapped->second);
}
}
@ -287,35 +303,8 @@ std::vector<std::uint32_t> CollectDynamicNameOffsets(const std::vector<std::uint
}
std::vector<std::uint32_t> CollectVerneedNameOffsets(const std::vector<std::uint8_t>& elf, const Elf64_Ehdr& ehdr) {
using namespace Internal;
std::vector<std::uint32_t> offsets;
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
if (shdr.sh_type != kShtGnuVerneed) continue;
std::size_t entryOffset = static_cast<std::size_t>(shdr.sh_offset);
while (true) {
const auto verneed = Read<Elf64_Verneed>(elf, entryOffset);
offsets.push_back(verneed.vn_file);
std::size_t auxOffset = entryOffset + verneed.vn_aux;
for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) {
const auto vernaux = Read<Elf64_Vernaux>(elf, auxOffset);
offsets.push_back(vernaux.vna_name);
if (vernaux.vna_next == 0u) break;
auxOffset += vernaux.vna_next;
}
if (verneed.vn_next == 0u) break;
entryOffset += verneed.vn_next;
}
}
for (const auto& field : ReadVersionNameFields(elf, ehdr)) offsets.push_back(field.Name);
return offsets;
}

View File

@ -0,0 +1,76 @@
import struct
import sys
import unittest
from pathlib import Path
from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSTR_OFFSET, sections
VERNEED = 0x580
def image(*, auxiliary=16, count=1, size=32, version=1, auxnext=0, nextrecord=0):
data = build_image(symbols(False), False)
strings = sections(data)[2]
filename = strings[5]
names = b"libfixture.so\0FIXTURE_1\0"
data[DYNSTR_OFFSET + filename:DYNSTR_OFFSET + filename + len(names)] = names
struct.pack_into("<Q", data, SHOFF + 2 * 64 + 32, filename + len(names))
struct.pack_into("<H", data, 0x3c, 5)
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 0x6ffffffe, 2, 0, VERNEED, size, 2, 1, 8, 0)
struct.pack_into("<HHIII", data, VERNEED, version, count, filename, auxiliary, nextrecord)
struct.pack_into("<IHHII", data, VERNEED + auxiliary, 0, 0, 2, filename + len(b"libfixture.so\0"), auxnext)
return data
class VersionRequirementsTests(unittest.TestCase):
def test_auxiliary_record_cannot_leave_declared_section(self):
self.reject(image(auxiliary=32))
def test_auxiliary_count_must_match_chain(self):
self.reject(image(count=2))
def test_unsupported_structure_version_is_rejected(self):
self.reject(image(version=2))
def test_invalid_forward_links_and_section_ranges_are_rejected(self):
for fields in ({"auxnext": 16}, {"nextrecord": 64}, {"size": 15},
{"size": 0x10000}, {"auxiliary": 8}, {"count": 2, "auxnext": 8}):
with self.subTest(fields=fields):
self.reject(image(**fields))
def test_valid_multiple_auxiliaries_and_requirements(self):
data = image(count=2, size=80, auxnext=16, nextrecord=48)
original_name = struct.unpack_from("<I", data, VERNEED + 24)[0]
original_file = struct.unpack_from("<I", data, VERNEED + 4)[0]
struct.pack_into("<IHHII", data, VERNEED + 32, 0, 0, 3, original_name, 0)
struct.pack_into("<HHIII", data, VERNEED + 48, 1, 1, original_file, 16, 0)
struct.pack_into("<IHHII", data, VERNEED + 64, 0, 0, 4, original_name, 0)
result, after = patch(PATCHER, bytes(data), "multiple-versions.so")
self.assertEqual(result.returncode, 0, result.stderr)
strings = sections(after)[2]
for position, expected in ((4, b"libfixture.so"), (24, b"FIXTURE_1"),
(40, b"FIXTURE_1"), (52, b"libfixture.so"), (72, b"FIXTURE_1")):
offset = struct.unpack_from("<I", after, VERNEED + position)[0]
start = strings[4] + offset
self.assertEqual(after[start:after.index(0, start)], expected)
def reject(self, data):
result, after = patch(PATCHER, bytes(data), "versions.so")
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
self.assertIn(b"version_r", result.stderr)
self.assertEqual(after, data)
def test_valid_names_are_preserved_when_strings_are_rebuilt(self):
data = image()
result, after = patch(PATCHER, bytes(data), "valid-versions.so")
self.assertEqual(result.returncode, 0, result.stderr)
strings = sections(after)[2]
for position, expected in ((VERNEED + 4, b"libfixture.so"), (VERNEED + 16 + 8, b"FIXTURE_1")):
offset = struct.unpack_from("<I", after, position)[0]
start = strings[4] + offset
self.assertEqual(after[start:after.index(0, start)], expected)
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()