Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
objcnamemangler.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include <llvm/Object/MachO.h>
5#include <llvm/Object/MachOUniversal.h>
6#include <llvm/Support/MemoryBuffer.h>
7#include <llvm/Support/raw_ostream.h>
8
9#include <filesystem>
10#include <optional>
11#include <random>
12#include <set>
13#include <string>
14#include <string_view>
15
16using namespace llvm;
17using namespace object;
18
19namespace fs = std::filesystem;
20
21namespace {
22
23struct CommandLineArgs
24{
25 fs::path binaryPath;
26 std::set<std::string> excludedClasses;
27 bool quietMode {false};
28 bool dryRun {false};
29 std::string pattern;
30 std::string replacement;
31};
32
33void printUsage(const char *progName)
34{
35 errs() << "Usage: " << progName << " [OPTIONS] <binary_to_patch>\n\n"
36 << "A tool to patch Objective-C metadata in Mach-O binaries.\n\n"
37 << "Positional arguments:\n"
38 << " binary_to_patch The binary file to patch\n\n"
39 << "Options:\n"
40 << " -h, --help Show this help message and exit\n"
41 << " --quiet Suppress output messages\n"
42 << " --dry-run Perform a dry run without modifying the file\n"
43 << " --exclude CLASS Exclude class name from patching (can be repeated)\n"
44 << " --replace PATTERN REPLACEMENT\n"
45 << " Replace pattern with replacement string\n"
46 << " (must be same length for binary safety)\n\n"
47 << "Examples:\n"
48 << " " << progName << " myapp.app/Contents/MacOS/myapp\n"
49 << " " << progName << " --quiet --exclude MyClass myapp\n"
50 << " " << progName << " --replace QtCore QTCore myapp\n";
51}
52
53std::optional<CommandLineArgs> parseCommandLine(int argc, char **argv)
54{
55 CommandLineArgs args;
56 fs::path binaryPath;
57
58 if (argc < 2) {
59 errs() << "Error: No binary file specified.\n\n";
60 printUsage(argv[0]);
61 return std::nullopt;
62 }
63
64 int i = 1;
65 while (i < argc) {
66 std::string arg = argv[i];
67
68 if (arg == "-h" || arg == "--help") {
69 printUsage(argv[0]);
70 return std::nullopt;
71 } else if (arg == "--quiet") {
72 args.quietMode = true;
73 i++;
74 } else if (arg == "--dry-run") {
75 args.dryRun = true;
76 i++;
77 } else if (arg == "--exclude") {
78 if (i + 1 >= argc) {
79 errs() << "Error: --exclude requires a CLASS argument.\n";
80 return std::nullopt;
81 }
82 args.excludedClasses.insert(argv[i + 1]);
83 i += 2;
84 } else if (arg == "--replace") {
85 if (i + 2 >= argc) {
86 errs() << "Error: --replace requires PATTERN and REPLACEMENT arguments.\n";
87 return std::nullopt;
88 }
89 args.pattern = argv[i + 1];
90 args.replacement = argv[i + 2];
91
92 if (args.pattern.empty()) {
93 errs() << "Error: replacement pattern cannot be empty.\n";
94 return std::nullopt;
95 }
96 if (args.pattern.length() != args.replacement.length()) {
97 errs() << "Error: for binary safety, the replacement pattern and "
98 << "the replacement string must be the same length.\n";
99 return std::nullopt;
100 }
101 i += 3;
102 } else if (arg[0] == '-') {
103 errs() << "Error: unknown option '" << arg << "'.\n\n";
104 printUsage(argv[0]);
105 return std::nullopt;
106 } else {
107 if (!binaryPath.empty()) {
108 errs() << "Error: multiple binary files specified.\n";
109 return std::nullopt;
110 }
111 binaryPath = argv[i];
112 i++;
113 }
114 }
115
116 if (binaryPath.empty()) {
117 errs() << "Error: No binary file specified.\n\n";
118 printUsage(argv[0]);
119 return std::nullopt;
120 }
121
122 std::error_code ec;
123 if (!fs::exists(binaryPath, ec) || ec) {
124 errs() << "Error: file '" << binaryPath.string() << "' does not exist.\n";
125 return std::nullopt;
126 }
127
128 args.binaryPath = binaryPath;
129 return args;
130}
131
132std::string generateRandomString(size_t length)
133{
134 constexpr std::string_view charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234"
135 "56789";
136 static std::mt19937 generator(std::random_device {}());
137 std::uniform_int_distribution<int> distribution(0, charset.length() - 1);
138 std::string result;
139 for (size_t i = 0; i < length; ++i)
140 result += charset[distribution(generator)];
141 return result;
142}
143
144// Converts a virtual address to a file offset relative to the start of the Mach-O slice.
145std::optional<uint64_t> virtualAddressToFileOffset(const MachOObjectFile *obj, uint64_t va)
146{
147 for (const auto &lci : obj->load_commands()) {
148 if (lci.C.cmd == MachO::LC_SEGMENT_64) {
149 const MachO::segment_command_64 seg = obj->getSegment64LoadCommand(lci);
150 if (va >= seg.vmaddr && va < (seg.vmaddr + seg.vmsize))
151 return (va - seg.vmaddr) + seg.fileoff;
152 } else if (lci.C.cmd == MachO::LC_SEGMENT) {
153 const MachO::segment_command seg = obj->getSegmentLoadCommand(lci);
154 if (va >= seg.vmaddr && va < (seg.vmaddr + seg.vmsize))
155 return (va - seg.vmaddr) + seg.fileoff;
156 }
157 }
158 return std::nullopt;
159}
160
161void patchClassNameSection(const SectionRef &section,
162 const MachOObjectFile *machOObj,
163 WritableMemoryBuffer &writableMB,
164 uint64_t sliceOffset,
165 const CommandLineArgs &args)
166{
167 uint64_t sectionFileOffset = 0;
168 if (machOObj->is64Bit()) {
169 const MachO::section_64 sec = machOObj->getSection64(section.getRawDataRefImpl());
170 sectionFileOffset = sec.offset;
171 } else {
172 const MachO::section sec = machOObj->getSection(section.getRawDataRefImpl());
173 sectionFileOffset = sec.offset;
174 }
175
176 Expected<StringRef> contentsOrErr = section.getContents();
177 if (auto e = contentsOrErr.takeError()) {
178 consumeError(std::move(e));
179 return;
180 }
181 StringRef contents = *contentsOrErr;
182 const char *current = contents.begin();
183 while (current < contents.end()) {
184 StringRef name(current);
185 if (name.empty()) {
186 current++;
187 continue;
188 }
189
190 if (args.excludedClasses.count(name.str())) {
191 if (!args.quietMode)
192 outs() << "[CLASS] Skipping excluded class: " << name.str() << "\n";
193 current += name.size() + 1;
194 continue;
195 }
196
197 uint64_t realFileOffset = sliceOffset + sectionFileOffset + (current - contents.begin());
198
199 if (!args.pattern.empty()) {
200 std::string originalName = name.str();
201 std::string newName = originalName;
202 size_t pos = 0;
203 bool replaced = false;
204 while ((pos = newName.find(args.pattern, pos)) != std::string::npos) {
205 newName.replace(pos, args.pattern.length(), args.replacement);
206 pos += args.replacement.length();
207 replaced = true;
208 }
209
210 if (replaced) {
211 if (!args.quietMode) {
212 outs() << "[CLASS] Found: " << originalName << " at file offset "
213 << realFileOffset << "\n"
214 << " -> Replaced with: " << newName << "\n";
215 }
216 char *patchLocation = writableMB.getBufferStart() + realFileOffset;
217 memcpy(patchLocation, newName.c_str(), originalName.length());
218 }
219 } else {
220 if (!args.quietMode)
221 outs() << "[CLASS] Found: " << name.str() << " at file offset "
222 << realFileOffset << "\n";
223
224 std::string randomString = generateRandomString(name.size());
225 char *patchLocation = writableMB.getBufferStart() + realFileOffset;
226 memcpy(patchLocation, randomString.c_str(), randomString.length());
227 if (!args.quietMode)
228 outs() << " -> Replaced with: " << randomString << "\n";
229 }
230
231 current += name.size() + 1;
232 }
233}
234
235void patchCategoryListSection(const SectionRef &section,
236 const MachOObjectFile *machOObj,
237 const MemoryBuffer &originalMB,
238 WritableMemoryBuffer &writableMB,
239 uint64_t sliceOffset,
240 const CommandLineArgs &args)
241{
242 Expected<StringRef> contentsOrErr = section.getContents();
243 if (auto e = contentsOrErr.takeError()) {
244 consumeError(std::move(e));
245 return;
246 }
247 StringRef contents = *contentsOrErr;
248 const char *data = contents.data();
249 unsigned ptrSize = machOObj->is64Bit() ? 8 : 4;
250
251 for (unsigned i = 0; i + ptrSize <= contents.size(); i += ptrSize) {
252 uint64_t categoryVa = (ptrSize == 8) ? *(const uint64_t *)(data + i)
253 : *(const uint32_t *)(data + i);
254 auto categoryOffsetOpt = virtualAddressToFileOffset(machOObj, categoryVa);
255 if (!categoryOffsetOpt)
256 continue;
257
258 const char *categoryStructPtr
259 = originalMB.getBufferStart() + sliceOffset + *categoryOffsetOpt;
260 uint64_t categoryNameVa = (ptrSize == 8) ? *(const uint64_t *)categoryStructPtr
261 : *(const uint32_t *)categoryStructPtr;
262
263 auto nameOffsetOpt = virtualAddressToFileOffset(machOObj, categoryNameVa);
264 if (!nameOffsetOpt)
265 continue;
266
267 uint64_t realNameOffset = sliceOffset + *nameOffsetOpt;
268 StringRef categoryName(originalMB.getBufferStart() + realNameOffset);
269 if (categoryName.empty())
270 continue;
271
272 if (!args.pattern.empty()) {
273 std::string originalName = categoryName.str();
274 std::string newName = originalName;
275 size_t pos = 0;
276 bool replaced = false;
277 while ((pos = newName.find(args.pattern, pos)) != std::string::npos) {
278 newName.replace(pos, args.pattern.length(), args.replacement);
279 pos += args.replacement.length();
280 replaced = true;
281 }
282
283 if (replaced) {
284 if (!args.quietMode) {
285 outs() << "[CATEGORY] Found: " << originalName << " at file offset "
286 << realNameOffset << "\n"
287 << " -> Replaced with: " << newName << "\n";
288 }
289 char *patchLocation = writableMB.getBufferStart() + realNameOffset;
290 memcpy(patchLocation, newName.c_str(), originalName.length());
291 }
292 } else {
293 if (!args.quietMode)
294 outs() << "[CATEGORY] Found: " << categoryName.str() << " at file offset "
295 << realNameOffset << "\n";
296
297 std::string randomString = generateRandomString(categoryName.size());
298 char *patchLocation = writableMB.getBufferStart() + realNameOffset;
299 memcpy(patchLocation, randomString.c_str(), randomString.length());
300
301 if (!args.quietMode)
302 outs() << " -> Replaced with: " << randomString << "\n";
303 }
304 }
305}
306
307Error patchMachOSlice(MachOObjectFile *machOObj,
308 const MemoryBuffer &originalMB,
309 WritableMemoryBuffer &writableMB,
310 uint64_t sliceOffset,
311 const CommandLineArgs &args)
312{
313 if (!args.quietMode) {
314 outs() << "--- Patching architecture: " << machOObj->getArchTriple().getArchName()
315 << " (slice offset: " << sliceOffset << ") ---\n";
316 }
317 for (const SectionRef &section : machOObj->sections()) {
318 Expected<StringRef> sectionNameOrErr = section.getName();
319 if (auto e = sectionNameOrErr.takeError())
320 return e;
321 StringRef sectionName = *sectionNameOrErr;
322
323 if (sectionName == "__objc_classname")
324 patchClassNameSection(section, machOObj, writableMB, sliceOffset, args);
325 else if (sectionName == "__objc_catlist")
326 patchCategoryListSection(section, machOObj, originalMB, writableMB, sliceOffset, args);
327 }
328 return Error::success();
329}
330
331} // namespace
332
333int main(int argc, char **argv)
334{
335 auto argsOpt = parseCommandLine(argc, argv);
336 if (!argsOpt)
337 return 1;
338 const auto &args = *argsOpt;
339
340 Expected<OwningBinary<Binary>> binOrErr = createBinary(args.binaryPath.string());
341 if (auto e = binOrErr.takeError()) {
342 errs() << "Error opening binary: " << toString(std::move(e)) << "\n";
343 return 1;
344 }
345 OwningBinary<Binary> &bin = binOrErr.get();
346
347 ErrorOr<std::unique_ptr<MemoryBuffer>> mbOrErr = MemoryBuffer::getFile(args.binaryPath.string());
348 if (std::error_code ec = mbOrErr.getError()) {
349 errs() << "Error reading file into buffer: " << ec.message() << "\n";
350 return 1;
351 }
352 std::unique_ptr<MemoryBuffer> originalMB {std::move(mbOrErr.get())};
353 std::unique_ptr<WritableMemoryBuffer> writableMB
354 = WritableMemoryBuffer::getNewMemBuffer(originalMB->getBufferSize());
355 memcpy(writableMB->getBufferStart(), originalMB->getBufferStart(), originalMB->getBufferSize());
356
357 if (auto *machOUni = dyn_cast<MachOUniversalBinary>(bin.getBinary())) {
358 for (const auto &objForArch : machOUni->objects()) {
359 Expected<std::unique_ptr<MachOObjectFile>> machOObjOrErr = objForArch.getAsObjectFile();
360 if (auto e = machOObjOrErr.takeError()) {
361 errs() << "Failed to get object for architecture: " << toString(std::move(e))
362 << "\n";
363 continue;
364 }
365 if (auto e = patchMachOSlice(
366 machOObjOrErr->get(), *originalMB, *writableMB, objForArch.getOffset(), args)) {
367 errs() << "Failed to patch Mach-O slice: " << toString(std::move(e)) << "\n";
368 }
369 }
370 } else if (auto *machOObj = dyn_cast<MachOObjectFile>(bin.getBinary())) {
371 if (auto e = patchMachOSlice(machOObj, *originalMB, *writableMB, 0, args)) {
372 errs() << "Failed to patch Mach-O file: " << toString(std::move(e)) << "\n";
373 return 1;
374 }
375 } else {
376 errs() << "The provided file is not a valid Mach-O binary.\n";
377 return 1;
378 }
379
380 if (args.dryRun) {
381 if (!args.quietMode)
382 outs() << "\nDry run complete. Binary was not modified.\n";
383 return 0;
384 }
385
386 std::error_code ec;
387 raw_fd_ostream outFile(args.binaryPath.string(), ec);
388 if (ec) {
389 errs() << "Error opening file for writing: " << ec.message() << "\n";
390 return 1;
391 }
392 outFile.write(writableMB->getBufferStart(), writableMB->getBufferSize());
393 outFile.close();
394
395 if (!args.quietMode)
396 outs() << "\nSuccessfully patched binary in-place: " << args.binaryPath.string() << "\n";
397
398 return 0;
399}
int main(int argc, char *argv[])
[ctor_close]