all messages for Guix-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
blob 6f4eb8f72bc6ecf376a0b981a6b85692fd8ac462 19717 bytes (raw)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
 
Rework the growing algorithm in patchelf to support ARM systems.
See <https://github.com/NixOS/patchelf/issues/8>.
This patch copied from:
<https://github.com/sriemer/patchelf/commit/0a96239cea6b97b9a0fff80da576e58ca2dfb2a2>

From 0a96239cea6b97b9a0fff80da576e58ca2dfb2a2 Mon Sep 17 00:00:00 2001
From: Sebastian Parschauer <s.parschauer@gmx.de>
Date: Sat, 28 Jun 2014 01:24:57 +0200
Subject: [PATCH] Rework the growing algorithm

On ARM systems there is no space in virtual memory for another LOAD
area in front of the code LOAD area. So insert data to its end
instead. At this location there should be enough space in virtual
memory due to alignment. We can extend it until the end of the
alignment but the file shift may be greater as it must be aligned
to the page size. Do the same for the data LOAD area.
---
 src/patchelf.cc | 357 ++++++++++++++++++++++----------------------------------
 1 file changed, 142 insertions(+), 215 deletions(-)

diff --git a/src/patchelf.cc b/src/patchelf.cc
index dcbfd38..4fce9e6 100644
--- a/src/patchelf.cc
+++ b/src/patchelf.cc
@@ -116,7 +116,11 @@ private:
 
     void sortShdrs();
 
-    void shiftFile(unsigned int extraPages, Elf_Addr startPage);
+    void shiftFileSingle(size_t fileShift, Elf_Off insertOff);
+
+    void shiftFile(size_t neededCode, size_t neededData,
+                   Elf_Off codeOff[], Elf_Off dataOff[],
+                   Elf_Addr *codePage, Elf_Addr *dataPage);
 
     string getSectionName(const Elf_Shdr & shdr);
 
@@ -130,13 +134,11 @@ private:
         unsigned int size);
 
     void writeReplacedSections(Elf_Off & curOff,
-        Elf_Addr startAddr, Elf_Off startOffset);
+        Elf_Addr startAddr, Elf_Off startOffset, bool isData);
 
     void rewriteHeaders(Elf_Addr phdrAddress);
 
-    void rewriteSectionsLibrary();
-
-    void rewriteSectionsExecutable();
+    void rewriteSectionsBinary();
 
 public:
 
@@ -391,46 +393,119 @@ static unsigned int roundUp(unsigned int n, unsigned int m)
 
 
 template<ElfFileParams>
-void ElfFile<ElfFileParamNames>::shiftFile(unsigned int extraPages, Elf_Addr startPage)
+void ElfFile<ElfFileParamNames>::shiftFileSingle(size_t fileShift,
+        Elf_Off insertOff)
 {
-    /* Move the entire contents of the file `extraPages' pages
-       further. */
     unsigned int oldSize = fileSize;
-    unsigned int shift = extraPages * pageSize;
-    growFile(fileSize + extraPages * pageSize);
-    memmove(contents + extraPages * pageSize, contents, oldSize);
-    memset(contents + sizeof(Elf_Ehdr), 0, shift - sizeof(Elf_Ehdr));
+
+    /* Grow at the end */
+    growFile(fileSize + fileShift);
+
+    /* move the data from the insertion point
+       to the end and zero inserted space */
+    memmove(contents + insertOff + fileShift,
+            contents + insertOff, oldSize - insertOff);
+    memset(contents + insertOff, 0, fileShift);
 
     /* Adjust the ELF header. */
     wri(hdr->e_phoff, sizeof(Elf_Ehdr));
-    wri(hdr->e_shoff, rdi(hdr->e_shoff) + shift);
+    if (rdi(hdr->e_shoff) >= insertOff)
+        wri(hdr->e_shoff, rdi(hdr->e_shoff) + fileShift);
 
     /* Update the offsets in the section headers. */
-    for (int i = 1; i < rdi(hdr->e_shnum); ++i)
-        wri(shdrs[i].sh_offset, rdi(shdrs[i].sh_offset) + shift);
+    for (int i = 1; i < rdi(hdr->e_shnum); ++i) {
+        if (rdi(shdrs[i].sh_offset) >= insertOff)
+            wri(shdrs[i].sh_offset, rdi(shdrs[i].sh_offset) + fileShift);
+    }
 
     /* Update the offsets in the program headers. */
     for (int i = 0; i < rdi(hdr->e_phnum); ++i) {
-        wri(phdrs[i].p_offset, rdi(phdrs[i].p_offset) + shift);
-        if (rdi(phdrs[i].p_align) != 0 &&
-            (rdi(phdrs[i].p_vaddr) - rdi(phdrs[i].p_offset)) % rdi(phdrs[i].p_align) != 0) {
-            debug("changing alignment of program header %d from %d to %d\n", i,
-                rdi(phdrs[i].p_align), pageSize);
-            wri(phdrs[i].p_align, pageSize);
+        if (rdi(phdrs[i].p_offset) >= insertOff)
+            wri(phdrs[i].p_offset, rdi(phdrs[i].p_offset) + fileShift);
+        /* Check for ELF load command alignment issue the same
+           way as glibc/elf/dl-load.c does. This gives us the
+           chance to run an interpreter explicitly. */
+        if (rdi(phdrs[i].p_type) == PT_LOAD && ((rdi(phdrs[i].p_vaddr) -
+          rdi(phdrs[i].p_offset)) & (rdi(phdrs[i].p_align) - 1)) != 0) {
+             debug("changing alignment of program header %d from %d to %d\n",
+                   i, rdi(phdrs[i].p_align), pageSize);
+             wri(phdrs[i].p_align, pageSize);
         }
     }
+}
+
+template<ElfFileParams>
+void ElfFile<ElfFileParamNames>::shiftFile(size_t neededCode,
+        size_t neededData, Elf_Off codeOff[], Elf_Off dataOff[],
+        Elf_Addr *codePage, Elf_Addr *dataPage)
+{
+    /* Move some contents of the file further. The binary has one LOAD area
+     * for code and one for data. There is virtual memory space between
+     * these which we can use due to alignment.
+     */
+    unsigned int memShift = neededCode;
+    unsigned int fileShift = roundUp(neededCode, pageSize);
+    unsigned int maxMemShift = 0;
+
+    if (neededCode > 0) {
+        /* find the LOAD program header for code and extend it */
+        for (int i = 0; i < rdi(hdr->e_phnum); ++i) {
+            if (rdi(phdrs[i].p_type) == PT_LOAD &&
+              rdi(phdrs[i].p_flags) & PF_X) {
+                codeOff[1] = rdi(phdrs[i].p_filesz);
+                codeOff[0] = codeOff[1] + rdi(phdrs[i].p_offset);
+                maxMemShift = rdi(phdrs[i].p_memsz) % rdi(phdrs[i].p_align);
+                if (maxMemShift == 0)
+                    continue;
+                maxMemShift = rdi(phdrs[i].p_align) - maxMemShift;
+                if (maxMemShift == 0 || memShift > maxMemShift)
+                    continue;
+                *codePage = rdi(phdrs[i].p_vaddr);
+                wri(phdrs[i].p_filesz, rdi(phdrs[i].p_filesz) + memShift);
+                wri(phdrs[i].p_memsz, rdi(phdrs[i].p_memsz) + memShift);
+                break;
+            }
+        }
+        debug("codeOff: %#lx, memShift: %d, maxMemShift: %d, fileShift: %d\n",
+              codeOff[1], memShift, maxMemShift, fileShift);
+        if (codeOff[1] == 0 || maxMemShift == 0)
+            goto out;
+
+        shiftFileSingle(fileShift, codeOff[0]);
+    }
+
+    /* +++ Do the same for the data LOAD area  +++ */
+    memShift = neededData;
+    fileShift = roundUp(neededData, pageSize);
+    maxMemShift = 0;
+    if (neededData > 0) {
+        /* find the LOAD program header for data and extend it */
+        for (int i = 0; i < rdi(hdr->e_phnum); ++i) {
+            if (rdi(phdrs[i].p_type) == PT_LOAD &&
+              rdi(phdrs[i].p_flags) & PF_W) {
+                dataOff[1] = rdi(phdrs[i].p_filesz);
+                dataOff[0] = dataOff[1] + rdi(phdrs[i].p_offset);
+                maxMemShift = rdi(phdrs[i].p_memsz) % rdi(phdrs[i].p_align);
+                if (maxMemShift == 0)
+                    continue;
+                maxMemShift = rdi(phdrs[i].p_align) - maxMemShift;
+                if (maxMemShift == 0 || memShift > maxMemShift)
+                    continue;
+                *dataPage = rdi(phdrs[i].p_vaddr);
+                wri(phdrs[i].p_filesz, rdi(phdrs[i].p_filesz) + memShift);
+                wri(phdrs[i].p_memsz, rdi(phdrs[i].p_memsz) + memShift);
+                break;
+            }
+        }
+        debug("dataOff: %#lx, memShift: %d, maxMemShift: %d, fileShift: %d\n",
+              dataOff[1], memShift, maxMemShift, fileShift);
+        if (dataOff[1] == 0 || maxMemShift == 0)
+            goto out;
 
-    /* Add a segment that maps the new program/section headers and
-       PT_INTERP segment into memory.  Otherwise glibc will choke. */
-    phdrs.resize(rdi(hdr->e_phnum) + 1);
-    wri(hdr->e_phnum, rdi(hdr->e_phnum) + 1);
-    Elf_Phdr & phdr = phdrs[rdi(hdr->e_phnum) - 1];
-    wri(phdr.p_type, PT_LOAD);
-    wri(phdr.p_offset, 0);
-    wri(phdr.p_vaddr, wri(phdr.p_paddr, startPage));
-    wri(phdr.p_filesz, wri(phdr.p_memsz, shift));
-    wri(phdr.p_flags, PF_R | PF_W);
-    wri(phdr.p_align, pageSize);
+        shiftFileSingle(fileShift, dataOff[0]);
+    }
+out:
+    return;
 }
 
 
@@ -491,7 +566,7 @@ string & ElfFile<ElfFileParamNames>::replaceSection(const SectionName & sectionN
 
 template<ElfFileParams>
 void ElfFile<ElfFileParamNames>::writeReplacedSections(Elf_Off & curOff,
-    Elf_Addr startAddr, Elf_Off startOffset)
+    Elf_Addr startAddr, Elf_Off startOffset, bool isData = false)
 {
     /* Overwrite the old section contents with 'X's.  Do this
        *before* writing the new section contents (below) to prevent
@@ -501,6 +576,9 @@ void ElfFile<ElfFileParamNames>::writeReplacedSections(Elf_Off & curOff,
     {
         string sectionName = i->first;
         Elf_Shdr & shdr = findSection(sectionName);
+        if ((!isData && rdi(shdr.sh_flags) & SHF_WRITE) ||
+         (isData && ~(rdi(shdr.sh_flags)) & SHF_WRITE))
+            continue;
         memset(contents + rdi(shdr.sh_offset), 'X', rdi(shdr.sh_size));
     }
 
@@ -509,6 +587,9 @@ void ElfFile<ElfFileParamNames>::writeReplacedSections(Elf_Off & curOff,
     {
         string sectionName = i->first;
         Elf_Shdr & shdr = findSection(sectionName);
+        if ((!isData && rdi(shdr.sh_flags) & SHF_WRITE) ||
+         (isData && ~(rdi(shdr.sh_flags)) & SHF_WRITE))
+            continue;
         debug("rewriting section `%s' from offset 0x%x (size %d) to offset 0x%x (size %d)\n",
             sectionName.c_str(), rdi(shdr.sh_offset), rdi(shdr.sh_size), curOff, i->second.size());
 
@@ -546,201 +627,47 @@ void ElfFile<ElfFileParamNames>::writeReplacedSections(Elf_Off & curOff,
         curOff += roundUp(i->second.size(), sectionAlignment);
     }
 
-    replacedSections.clear();
+    if (isData)
+        replacedSections.clear();
 }
 
 
 template<ElfFileParams>
-void ElfFile<ElfFileParamNames>::rewriteSectionsLibrary()
+void ElfFile<ElfFileParamNames>::rewriteSectionsBinary()
 {
-    /* For dynamic libraries, we just place the replacement sections
-       at the end of the file.  They're mapped into memory by a
-       PT_LOAD segment located directly after the last virtual address
-       page of other segments. */
-    Elf_Addr startPage = 0;
-    for (unsigned int i = 0; i < phdrs.size(); ++i) {
-        Elf_Addr thisPage = roundUp(rdi(phdrs[i].p_vaddr) + rdi(phdrs[i].p_memsz), pageSize);
-        if (thisPage > startPage) startPage = thisPage;
-    }
-
-    debug("last page is 0x%llx\n", (unsigned long long) startPage);
+    Elf_Off codeOff[2] = {0}, dataOff[2] = {0};
+    Elf_Addr codePage = 0, dataPage = 0;
+    size_t neededCode = 0, neededData = 0, oldCode = 0, oldData = 0;
+    Elf_Shdr shdr = findSection(".text");
+    Elf_Addr firstPage = rdi(shdr.sh_addr) - rdi(shdr.sh_offset);
 
+    debug("first page is 0x%llx\n", (unsigned long long) firstPage);
 
-    /* Compute the total space needed for the replaced sections and
-       the program headers. */
-    off_t neededSpace = (phdrs.size() + 1) * sizeof(Elf_Phdr);
+    /* Compute the total space needed for the replaced sections */
     for (ReplacedSections::iterator i = replacedSections.begin();
-         i != replacedSections.end(); ++i)
-        neededSpace += roundUp(i->second.size(), sectionAlignment);
-    debug("needed space is %d\n", neededSpace);
-
-
-    size_t startOffset = roundUp(fileSize, pageSize);
-
-    growFile(startOffset + neededSpace);
-
-
-    /* Even though this file is of type ET_DYN, it could actually be
-       an executable.  For instance, Gold produces executables marked
-       ET_DYN.  In that case we can still hit the kernel bug that
-       necessitated rewriteSectionsExecutable().  However, such
-       executables also tend to start at virtual address 0, so
-       rewriteSectionsExecutable() won't work because it doesn't have
-       any virtual address space to grow downwards into.  As a
-       workaround, make sure that the virtual address of our new
-       PT_LOAD segment relative to the first PT_LOAD segment is equal
-       to its offset; otherwise we hit the kernel bug.  This may
-       require creating a hole in the executable.  The bigger the size
-       of the uninitialised data segment, the bigger the hole. */
-    if (isExecutable) {
-        if (startOffset >= startPage) {
-            debug("shifting new PT_LOAD segment by %d bytes to work around a Linux kernel bug\n", startOffset - startPage);
-        } else {
-            size_t hole = startPage - startOffset;
-            /* Print a warning, because the hole could be very big. */
-            fprintf(stderr, "warning: working around a Linux kernel bug by creating a hole of %zu bytes in ‘%s’\n", hole, fileName.c_str());
-            assert(hole % pageSize == 0);
-            /* !!! We could create an actual hole in the file here,
-               but it's probably not worth the effort. */
-            growFile(fileSize + hole);
-            startOffset += hole;
-        }
-        startPage = startOffset;
-    }
-
-
-    /* Add a segment that maps the replaced sections and program
-       headers into memory. */
-    phdrs.resize(rdi(hdr->e_phnum) + 1);
-    wri(hdr->e_phnum, rdi(hdr->e_phnum) + 1);
-    Elf_Phdr & phdr = phdrs[rdi(hdr->e_phnum) - 1];
-    wri(phdr.p_type, PT_LOAD);
-    wri(phdr.p_offset, startOffset);
-    wri(phdr.p_vaddr, wri(phdr.p_paddr, startPage));
-    wri(phdr.p_filesz, wri(phdr.p_memsz, neededSpace));
-    wri(phdr.p_flags, PF_R | PF_W);
-    wri(phdr.p_align, pageSize);
-
-
-    /* Write out the replaced sections. */
-    Elf_Off curOff = startOffset + phdrs.size() * sizeof(Elf_Phdr);
-    writeReplacedSections(curOff, startPage, startOffset);
-    assert((off_t) curOff == startOffset + neededSpace);
-
-
-    /* Move the program header to the start of the new area. */
-    wri(hdr->e_phoff, startOffset);
-
-    rewriteHeaders(startPage);
-}
-
-
-template<ElfFileParams>
-void ElfFile<ElfFileParamNames>::rewriteSectionsExecutable()
-{
-    /* Sort the sections by offset, otherwise we won't correctly find
-       all the sections before the last replaced section. */
-    sortShdrs();
-
-
-    /* What is the index of the last replaced section? */
-    unsigned int lastReplaced = 0;
-    for (unsigned int i = 1; i < rdi(hdr->e_shnum); ++i) {
-        string sectionName = getSectionName(shdrs[i]);
-        if (replacedSections.find(sectionName) != replacedSections.end()) {
-            debug("using replaced section `%s'\n", sectionName.c_str());
-            lastReplaced = i;
-        }
-    }
-
-    assert(lastReplaced != 0);
-
-    debug("last replaced is %d\n", lastReplaced);
-
-    /* Try to replace all sections before that, as far as possible.
-       Stop when we reach an irreplacable section (such as one of type
-       SHT_PROGBITS).  These cannot be moved in virtual address space
-       since that would invalidate absolute references to them. */
-    assert(lastReplaced + 1 < shdrs.size()); /* !!! I'm lazy. */
-    size_t startOffset = rdi(shdrs[lastReplaced + 1].sh_offset);
-    Elf_Addr startAddr = rdi(shdrs[lastReplaced + 1].sh_addr);
-    string prevSection;
-    for (unsigned int i = 1; i <= lastReplaced; ++i) {
-        Elf_Shdr & shdr(shdrs[i]);
-        string sectionName = getSectionName(shdr);
-        debug("looking at section `%s'\n", sectionName.c_str());
-        /* !!! Why do we stop after a .dynstr section? I can't
-           remember! */
-        if ((rdi(shdr.sh_type) == SHT_PROGBITS && sectionName != ".interp")
-            || prevSection == ".dynstr")
-        {
-            startOffset = rdi(shdr.sh_offset);
-            startAddr = rdi(shdr.sh_addr);
-            lastReplaced = i - 1;
-            break;
+         i != replacedSections.end(); ++i) {
+        shdr = findSection(i->first);
+        if (rdi(shdr.sh_flags) & SHF_WRITE) {
+            oldData += rdi(shdr.sh_size);
+            neededData += roundUp(i->second.size(), sectionAlignment);
         } else {
-            if (replacedSections.find(sectionName) == replacedSections.end()) {
-                debug("replacing section `%s' which is in the way\n", sectionName.c_str());
-                replaceSection(sectionName, rdi(shdr.sh_size));
-            }
+            oldCode += rdi(shdr.sh_size);
+            neededCode += roundUp(i->second.size(), sectionAlignment);
         }
-        prevSection = sectionName;
     }
 
-    debug("first reserved offset/addr is 0x%x/0x%llx\n",
-        startOffset, (unsigned long long) startAddr);
-
-    assert(startAddr % pageSize == startOffset % pageSize);
-    Elf_Addr firstPage = startAddr - startOffset;
-    debug("first page is 0x%llx\n", (unsigned long long) firstPage);
-
-    /* Right now we assume that the section headers are somewhere near
-       the end, which appears to be the case most of the time.
-       Therefore they're not accidentally overwritten by the replaced
-       sections. !!!  Fix this. */
-    assert((off_t) rdi(hdr->e_shoff) >= startOffset);
-
-
-    /* Compute the total space needed for the replaced sections, the
-       ELF header, and the program headers. */
-    size_t neededSpace = sizeof(Elf_Ehdr) + phdrs.size() * sizeof(Elf_Phdr);
-    for (ReplacedSections::iterator i = replacedSections.begin();
-         i != replacedSections.end(); ++i)
-        neededSpace += roundUp(i->second.size(), sectionAlignment);
-
-    debug("needed space is %d\n", neededSpace);
-
-    /* If we need more space at the start of the file, then grow the
-       file by the minimum number of pages and adjust internal
-       offsets. */
-    if (neededSpace > startOffset) {
-
-        /* We also need an additional program header, so adjust for that. */
-        neededSpace += sizeof(Elf_Phdr);
-        debug("needed space is %d\n", neededSpace);
-
-        unsigned int neededPages = roundUp(neededSpace - startOffset, pageSize) / pageSize;
-        debug("needed pages is %d\n", neededPages);
-        if (neededPages * pageSize > firstPage)
-            error("virtual address space underrun!");
-
-        firstPage -= neededPages * pageSize;
-        startOffset += neededPages * pageSize;
-
-        shiftFile(neededPages, firstPage);
-    }
-
-
-    /* Clear out the free space. */
-    Elf_Off curOff = sizeof(Elf_Ehdr) + phdrs.size() * sizeof(Elf_Phdr);
-    debug("clearing first %d bytes\n", startOffset - curOff);
-    memset(contents + curOff, 0, startOffset - curOff);
+    debug("needed space is C: %d, D: %d\n", neededCode, neededData);
 
+    /* If we need more space within the file, then grow the
+       file and adjust internal offsets. */
+    shiftFile(neededCode, neededData, codeOff, dataOff, &codePage,
+              &dataPage);
+    assert(codeOff[0] > 0);
 
     /* Write out the replaced sections. */
-    writeReplacedSections(curOff, firstPage, 0);
-    assert((off_t) curOff == neededSpace);
-
+    debug("codePage: %#lx, dataPage: %#lx\n", codePage, dataPage);
+    writeReplacedSections(codeOff[0], codePage + codeOff[1], codeOff[0]);
+    writeReplacedSections(dataOff[0], dataPage + dataOff[1], dataOff[0], true);
 
     rewriteHeaders(firstPage + rdi(hdr->e_phoff));
 }
@@ -758,10 +685,10 @@ void ElfFile<ElfFileParamNames>::rewriteSections()
 
     if (rdi(hdr->e_type) == ET_DYN) {
         debug("this is a dynamic library\n");
-        rewriteSectionsLibrary();
+        rewriteSectionsBinary();
     } else if (rdi(hdr->e_type) == ET_EXEC) {
         debug("this is an executable\n");
-        rewriteSectionsExecutable();
+        rewriteSectionsBinary();
     } else error("unknown ELF type");
 }
 
-- 
2.1.2


debug log:

solving 6f4eb8f72b ...
found 6f4eb8f72b in https://git.savannah.gnu.org/cgit/guix.git

Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/guix.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.