• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

systemd / systemd / 23722534442

29 Mar 2026 09:58PM UTC coverage: 72.208% (-0.2%) from 72.41%
23722534442

push

github

daandemeyer
repart: allow --el-torito= with any --empty= value

The restriction requiring --empty= to be require, force, or create
when using --el-torito= is unnecessary.
context_verify_eltorito_overlap() already validates that the ISO 9660
blocks don't collide with GPT partition entries or the first usable
LBA, which is sufficient to guarantee safety regardless of the empty
mode.

This is needed for two-stage image builds where the first stage creates
the usr and verity partitions, and the second stage adds --el-torito=
to produce a bootable ISO with a UKI containing usrhash= derived from
the verity hash of the first stage. In the second stage, repart runs
with --empty=allow since the image already exists.

Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>

317578 of 439810 relevant lines covered (72.21%)

1212802.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.71
/src/basic/uid-range.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <sched.h>
4
#include <string.h>
5

6
#include "alloc-util.h"
7
#include "errno-util.h"
8
#include "fd-util.h"
9
#include "format-util.h"
10
#include "namespace-util.h"
11
#include "path-util.h"
12
#include "pidref.h"
13
#include "process-util.h"
14
#include "sort-util.h"
15
#include "stat-util.h"
16
#include "uid-range.h"
17
#include "user-util.h"
18

19
UIDRange *uid_range_free(UIDRange *range) {
3,968✔
20
        if (!range)
3,968✔
21
                return NULL;
22

23
        free(range->entries);
1,183✔
24
        return mfree(range);
1,183✔
25
}
26

27
static bool uid_range_entry_intersect(const UIDRangeEntry *a, const UIDRangeEntry *b) {
108✔
28
        assert(a);
108✔
29
        assert(b);
108✔
30

31
        return a->start <= b->start + b->nr && a->start + a->nr >= b->start;
108✔
32
}
33

34
static int uid_range_entry_compare(const UIDRangeEntry *a, const UIDRangeEntry *b) {
297✔
35
        int r;
297✔
36

37
        assert(a);
297✔
38
        assert(b);
297✔
39

40
        r = CMP(a->start, b->start);
297✔
41
        if (r != 0)
138✔
42
                return r;
277✔
43

44
        return CMP(a->nr, b->nr);
20✔
45
}
46

47
static void uid_range_coalesce(UIDRange *range) {
748✔
48
        assert(range);
748✔
49

50
        if (range->n_entries <= 0)
748✔
51
                return;
52

53
        typesafe_qsort(range->entries, range->n_entries, uid_range_entry_compare);
402✔
54

55
        for (size_t i = 0; i < range->n_entries; i++) {
824✔
56
                UIDRangeEntry *x = range->entries + i;
422✔
57

58
                for (size_t j = i + 1; j < range->n_entries; j++) {
510✔
59
                        UIDRangeEntry *y = range->entries + j;
108✔
60
                        uid_t begin, end;
108✔
61

62
                        if (!uid_range_entry_intersect(x, y))
108✔
63
                                break;
64

65
                        begin = MIN(x->start, y->start);
88✔
66

67
                        /* Silence static analyzers, overflow is prevented by uid_range_add_internal() */
68
                        assert(x->start <= UINT32_MAX - x->nr);
88✔
69
                        assert(y->start <= UINT32_MAX - y->nr);
88✔
70
                        end = MAX(x->start + x->nr, y->start + y->nr);
88✔
71

72
                        x->start = begin;
88✔
73
                        x->nr = end - begin;
88✔
74

75
                        if (range->n_entries > j + 1)
88✔
76
                                memmove(y, y + 1, sizeof(UIDRangeEntry) * (range->n_entries - j - 1));
81✔
77

78
                        range->n_entries--;
88✔
79
                        j--;
88✔
80
                }
81
        }
82
}
83

84
int uid_range_add_internal(UIDRange **range, uid_t start, uid_t nr, bool coalesce) {
856✔
85
        _cleanup_(uid_range_freep) UIDRange *range_new = NULL;
856✔
86
        UIDRange *p;
856✔
87

88
        assert(range);
856✔
89

90
        if (nr <= 0)
856✔
91
                return 0;
92

93
        if (start > UINT32_MAX - nr) /* overflow check */
856✔
94
                return -ERANGE;
95

96
        if (*range)
856✔
97
                p = *range;
98
        else {
99
                range_new = new0(UIDRange, 1);
140✔
100
                if (!range_new)
140✔
101
                        return -ENOMEM;
102

103
                p = range_new;
104
        }
105

106
        if (!GREEDY_REALLOC(p->entries, p->n_entries + 1))
856✔
107
                return -ENOMEM;
108

109
        p->entries[p->n_entries++] = (UIDRangeEntry) {
856✔
110
                .start = start,
111
                .nr = nr,
112
        };
113

114
        if (coalesce)
856✔
115
                uid_range_coalesce(p);
152✔
116

117
        TAKE_PTR(range_new);
856✔
118
        *range = p;
856✔
119

120
        return 0;
856✔
121
}
122

123
int uid_range_add_str_full(UIDRange **range, const char *s, bool coalesce) {
40✔
124
        uid_t start, end;
40✔
125
        int r;
40✔
126

127
        assert(range);
40✔
128
        assert(s);
40✔
129

130
        r = parse_uid_range(s, &start, &end);
40✔
131
        if (r < 0)
40✔
132
                return r;
40✔
133

134
        return uid_range_add_internal(range, start, end - start + 1, coalesce);
40✔
135
}
136

137
int uid_range_next_lower(const UIDRange *range, uid_t *uid) {
272✔
138
        uid_t closest = UID_INVALID, candidate;
272✔
139

140
        assert(range);
272✔
141
        assert(uid);
272✔
142

143
        if (*uid == 0)
272✔
144
                return -EBUSY;
145

146
        candidate = *uid - 1;
272✔
147

148
        for (size_t i = 0; i < range->n_entries; i++) {
320✔
149
                uid_t begin, end;
272✔
150

151
                begin = range->entries[i].start;
272✔
152
                end = range->entries[i].start + range->entries[i].nr - 1;
272✔
153

154
                if (candidate >= begin && candidate <= end) {
272✔
155
                        *uid = candidate;
224✔
156
                        return 1;
224✔
157
                }
158

159
                if (end < candidate)
48✔
160
                        closest = end;
47✔
161
        }
162

163
        if (closest == UID_INVALID)
48✔
164
                return -EBUSY;
165

166
        *uid = closest;
47✔
167
        return 1;
47✔
168
}
169

170
bool uid_range_covers(const UIDRange *range, uid_t start, uid_t nr) {
239✔
171
        if (nr == 0) /* empty range? always covered... */
239✔
172
                return true;
173

174
        if (start > UINT32_MAX - nr) /* range overflows? definitely not covered... */
238✔
175
                return false;
176

177
        if (!range)
235✔
178
                return false;
179

180
        FOREACH_ARRAY(i, range->entries, range->n_entries)
251✔
181
                if (start >= i->start &&
239✔
182
                    start + nr <= i->start + i->nr)
233✔
183
                        return true;
184

185
        return false;
186
}
187

188
int uid_map_read_one(FILE *f, uid_t *ret_base, uid_t *ret_shift, uid_t *ret_range) {
1,816✔
189
        uid_t uid_base, uid_shift, uid_range;
1,816✔
190
        int r;
1,816✔
191

192
        assert(f);
1,816✔
193

194
        errno = 0;
1,816✔
195
        r = fscanf(f, UID_FMT " " UID_FMT " " UID_FMT "\n", &uid_base, &uid_shift, &uid_range);
1,816✔
196
        if (r == EOF)
1,816✔
197
                return errno_or_else(ENOMSG);
1,019✔
198
        assert(r >= 0);
797✔
199
        if (r != 3)
797✔
200
                return -EBADMSG;
201
        if (uid_range <= 0)
797✔
202
                return -EBADMSG;
203

204
        if (ret_base)
797✔
205
                *ret_base = uid_base;
797✔
206
        if (ret_shift)
797✔
207
                *ret_shift = uid_shift;
797✔
208
        if (ret_range)
797✔
209
                *ret_range = uid_range;
735✔
210

211
        return 0;
212
}
213

214
unsigned uid_range_size(const UIDRange *range) {
7✔
215
        if (!range)
7✔
216
                return 0;
217

218
        unsigned n = 0;
6✔
219

220
        FOREACH_ARRAY(e, range->entries, range->n_entries)
16✔
221
                n += e->nr;
10✔
222

223
        return n;
224
}
225

226
bool uid_range_is_empty(const UIDRange *range) {
548✔
227

228
        if (!range)
548✔
229
                return true;
230

231
        FOREACH_ARRAY(e, range->entries, range->n_entries)
546✔
232
                if (e->nr > 0)
195✔
233
                        return false;
234

235
        return true;
236
}
237

238
int uid_range_load_userns_full(const char *path, UIDRangeUsernsMode mode, bool coalesce, UIDRange **ret) {
948✔
239
        _cleanup_(uid_range_freep) UIDRange *range = NULL;
×
240
        _cleanup_fclose_ FILE *f = NULL;
948✔
241
        int r;
948✔
242

243
        /* If 'path' is NULL loads the UID range of the userns namespace we run. Otherwise load the data from
244
         * the specified file (which can be either uid_map or gid_map, in case caller needs to deal with GID
245
         * maps).
246
         *
247
         * To simplify things this will modify the passed array in case of later failure. */
248

249
        assert(mode >= 0);
948✔
250
        assert(mode < _UID_RANGE_USERNS_MODE_MAX);
948✔
251
        assert(ret);
948✔
252

253
        if (!path)
948✔
254
                path = IN_SET(mode, UID_RANGE_USERNS_INSIDE, UID_RANGE_USERNS_OUTSIDE) ? "/proc/self/uid_map" : "/proc/self/gid_map";
588✔
255

256
        f = fopen(path, "re");
948✔
257
        if (!f) {
948✔
258
                r = -errno;
×
259

260
                if (r == -ENOENT && path_startswith(path, "/proc/"))
×
261
                        return proc_mounted() > 0 ? -EOPNOTSUPP : -ENOSYS;
×
262

263
                return r;
264
        }
265

266
        range = new0(UIDRange, 1);
948✔
267
        if (!range)
948✔
268
                return -ENOMEM;
269

270
        for (;;) {
618✔
271
                uid_t uid_base, uid_shift, uid_range;
1,566✔
272

273
                r = uid_map_read_one(f, &uid_base, &uid_shift, &uid_range);
1,566✔
274
                if (r == -ENOMSG)
1,566✔
275
                        break;
276
                if (r < 0)
618✔
277
                        return r;
×
278

279
                r = uid_range_add_internal(
618✔
280
                                &range,
281
                                IN_SET(mode, UID_RANGE_USERNS_INSIDE, GID_RANGE_USERNS_INSIDE) ? uid_base : uid_shift,
618✔
282
                                uid_range,
283
                                /* coalesce= */ false);
284
                if (r < 0)
618✔
285
                        return r;
286
        }
287

288
        if (coalesce)
948✔
289
                uid_range_coalesce(range);
596✔
290

291
        *ret = TAKE_PTR(range);
948✔
292
        return 0;
948✔
293
}
294

295
int uid_range_load_userns_by_fd_full(int userns_fd, UIDRangeUsernsMode mode, bool coalesce, UIDRange **ret) {
793✔
296
        _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
793✔
297
        int r;
793✔
298

299
        assert(userns_fd >= 0);
793✔
300
        assert(mode >= 0);
793✔
301
        assert(mode < _UID_RANGE_USERNS_MODE_MAX);
793✔
302
        assert(ret);
793✔
303

304
        r = is_our_namespace(userns_fd, NAMESPACE_USER);
793✔
305
        if (r < 0)
793✔
306
                return r;
307
        if (r > 0)
793✔
308
                return uid_range_load_userns_full(/* path= */ NULL, mode, coalesce, ret);
434✔
309

310
        r = userns_enter_and_pin(userns_fd, &pidref);
359✔
311
        if (r < 0)
359✔
312
                return r;
313

314
        const char *p = procfs_file_alloca(
359✔
315
                        pidref.pid,
316
                        IN_SET(mode, UID_RANGE_USERNS_INSIDE, UID_RANGE_USERNS_OUTSIDE) ? "uid_map" : "gid_map");
317

318
        return uid_range_load_userns_full(p, mode, coalesce, ret);
359✔
319
}
320

321
bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr) {
×
322

323
        if (!range)
×
324
                return false;
325

326
        /* Avoid overflow */
327
        if (start > UINT32_MAX - nr)
×
328
                nr = UINT32_MAX - start;
×
329

330
        if (nr == 0)
×
331
                return false;
332

333
        FOREACH_ARRAY(entry, range->entries, range->n_entries)
×
334
                if (start < entry->start + entry->nr &&
×
335
                    start + nr >= entry->start)
×
336
                        return true;
337

338
        return false;
339
}
340

341
int uid_range_clip(UIDRange *range, uid_t min, uid_t max) {
99✔
342
        assert(range);
99✔
343

344
        if (min > max)
99✔
345
                return -EINVAL;
346

347
        size_t t = 0;
98✔
348
        FOREACH_ARRAY(e, range->entries, range->n_entries) {
213✔
349
                uid_t entry_end = e->start + e->nr; /* one past the last UID in entry */
115✔
350

351
                /* Skip entries completely outside [min, max] */
352
                if (entry_end <= min || e->start > max)
115✔
353
                        continue;
6✔
354

355
                /* Trim the entry to fit within [min, max] */
356
                uid_t new_start = MAX(e->start, min);
109✔
357
                /* entry_end is exclusive, avoid overflow when max == UINT32_MAX */
358
                uid_t new_end = entry_end <= max ? entry_end : max + 1;
109✔
359
                assert(new_end > new_start);
109✔
360

361
                range->entries[t++] = (UIDRangeEntry) {
109✔
362
                        .start = new_start,
363
                        .nr = new_end - new_start,
109✔
364
                };
365
        }
366

367
        range->n_entries = t;
98✔
368

369
        return 0;
98✔
370
}
371

372
int uid_range_partition(UIDRange *range, uid_t size) {
97✔
373
        assert(range);
97✔
374
        assert(size > 0);
97✔
375

376
        /* Partitions the UID range entries into buckets of the given size. Any entry larger than the given
377
         * size will be partitioned into multiple entries, each of the given size. Any leftover UIDs in the
378
         * entry are dropped. Any entries smaller than the given size are also dropped. */
379

380
        /* Count how many entries we'll need after partitioning */
381
        size_t n_new_entries = 0;
97✔
382
        FOREACH_ARRAY(e, range->entries, range->n_entries)
204✔
383
                n_new_entries += e->nr / size;
107✔
384

385
        if (n_new_entries == 0) {
97✔
386
                range->n_entries = 0;
1✔
387
                return 0;
1✔
388
        }
389

390
        if (n_new_entries > range->n_entries && !GREEDY_REALLOC(range->entries, n_new_entries))
96✔
391
                return -ENOMEM;
392

393
        /* Work backwards to avoid overwriting entries we still need to read */
394
        size_t t = n_new_entries;
96✔
395
        for (size_t i = range->n_entries; i > 0; i--) {
202✔
396
                UIDRangeEntry *e = range->entries + i - 1;
106✔
397
                unsigned n_parts = e->nr / size;
106✔
398

399
                for (unsigned j = n_parts; j > 0; j--)
2,551,126✔
400
                        range->entries[--t] = (UIDRangeEntry) {
2,551,020✔
401
                                .start = e->start + (j - 1) * size,
2,551,020✔
402
                                .nr = size,
403
                        };
404
        }
405

406
        range->n_entries = n_new_entries;
96✔
407

408
        return 0;
96✔
409
}
410

411
int uid_range_copy(const UIDRange *range, UIDRange **ret) {
95✔
412
        assert(ret);
95✔
413

414
        if (!range) {
95✔
415
                *ret = NULL;
1✔
416
                return 0;
95✔
417
        }
418

419
        _cleanup_(uid_range_freep) UIDRange *copy = new0(UIDRange, 1);
94✔
420
        if (!copy)
94✔
421
                return -ENOMEM;
422

423
        if (range->n_entries > 0) {
94✔
424
                copy->entries = newdup(UIDRangeEntry, range->entries, range->n_entries);
93✔
425
                if (!copy->entries)
93✔
426
                        return -ENOMEM;
427

428
                copy->n_entries = range->n_entries;
93✔
429
        }
430

431
        *ret = TAKE_PTR(copy);
94✔
432
        return 0;
94✔
433
}
434

435
int uid_range_remove(UIDRange *range, uid_t start, uid_t size) {
101✔
436
        assert(range);
101✔
437

438
        if (size == 0)
101✔
439
                return 0;
440

441
        uid_t end = start + size; /* one past the last UID to remove */
100✔
442

443
        for (size_t i = 0; i < range->n_entries; i++) {
214✔
444
                UIDRangeEntry *e = range->entries + i;
114✔
445
                uid_t entry_end = e->start + e->nr;
114✔
446

447
                /* No overlap */
448
                if (entry_end <= start || e->start >= end)
114✔
449
                        continue;
14✔
450

451
                /* Check if this removal splits the entry into two parts */
452
                if (e->start < start && entry_end > end) {
100✔
453
                        /* Need to split: grow the array first */
454
                        if (!GREEDY_REALLOC(range->entries, range->n_entries + 1))
90✔
455
                                return -ENOMEM;
456

457
                        /* Re-fetch pointer after potential realloc */
458
                        e = range->entries + i;
90✔
459
                        entry_end = e->start + e->nr;
90✔
460

461
                        /* Shift everything after this entry to make room */
462
                        memmove(range->entries + i + 2, range->entries + i + 1,
90✔
463
                                (range->n_entries - i - 1) * sizeof(UIDRangeEntry));
90✔
464
                        range->n_entries++;
90✔
465

466
                        /* First part: before the removed range */
467
                        range->entries[i] = (UIDRangeEntry) {
90✔
468
                                .start = e->start,
90✔
469
                                .nr = start - e->start,
90✔
470
                        };
471

472
                        /* Second part: after the removed range */
473
                        range->entries[i + 1] = (UIDRangeEntry) {
90✔
474
                                .start = end,
475
                                .nr = entry_end - end,
90✔
476
                        };
477

478
                        /* Skip the newly inserted entry */
479
                        i++;
90✔
480
                        continue;
90✔
481
                }
482

483
                /* Removal covers the entire entry */
484
                if (start <= e->start && end >= entry_end) {
10✔
485
                        memmove(e, e + 1, (range->n_entries - i - 1) * sizeof(UIDRangeEntry));
6✔
486
                        range->n_entries--;
6✔
487
                        i--;
6✔
488
                        continue;
6✔
489
                }
490

491
                /* Removal trims the start of the entry */
492
                if (start <= e->start && end > e->start) {
4✔
493
                        e->nr = entry_end - end;
2✔
494
                        e->start = end;
2✔
495
                        continue;
2✔
496
                }
497

498
                /* Removal trims the end of the entry */
499
                if (start < entry_end && end >= entry_end) {
2✔
500
                        e->nr = start - e->start;
2✔
501
                        continue;
2✔
502
                }
503
        }
504

505
        return 0;
506
}
507

508
int uid_range_translate(const UIDRange *outside, const UIDRange *inside, uid_t uid, uid_t *ret) {
218✔
509
        assert(uid_range_entries(outside) == uid_range_entries(inside));
654✔
510
        assert(ret);
218✔
511

512
        /* Given two UID ranges that represent the outside UID range of a user namespace (the 2nd and 3rd
513
         * columns in /proc/xxx/uid_map) and the inside UID range of a user namespace (the 1st and 3rd
514
         * columns in /proc/xxx/uid_map), translates the given UID from the outside range to the inside
515
         * range. For example, given the following UID range:
516
         *
517
         * 0 1000 1
518
         *
519
         * calling uid_range_translate(outside, inside, 1000) will return 0 as the output UID. Alternatively,
520
         * calling uid_range_translate(inside, outside, 0) will return 1000 as the output UID.
521
         */
522

523
        for (size_t i = 0; i < uid_range_entries(outside); i++)
472✔
524
                assert(outside->entries[i].nr == inside->entries[i].nr);
254✔
525

526
        for (size_t i = 0; i < uid_range_entries(outside); i++) {
254✔
527
                const UIDRangeEntry *e = outside->entries + i;
244✔
528

529
                if (uid < e->start || uid >= e->start + e->nr)
244✔
530
                        continue;
36✔
531

532
                *ret = inside->entries[i].start + uid - e->start;
208✔
533
                return 0;
208✔
534
        }
535

536
        return -ESRCH;
537
}
538

539
int uid_range_translate_userns_fd(int userns_fd, UIDRangeUsernsMode mode, uid_t uid, uid_t *ret) {
2✔
540
        int r;
2✔
541

542
        assert(userns_fd >= 0);
2✔
543
        assert(IN_SET(mode, UID_RANGE_USERNS_OUTSIDE, GID_RANGE_USERNS_OUTSIDE));
2✔
544

545
        _cleanup_(uid_range_freep) UIDRange *outside_range = NULL;
2✔
546
        r = uid_range_load_userns_by_fd_full(userns_fd, mode, /* coalesce= */ false, &outside_range);
2✔
547
        if (r < 0)
2✔
548
                return r;
549

550
        mode = mode == UID_RANGE_USERNS_OUTSIDE ? UID_RANGE_USERNS_INSIDE : GID_RANGE_USERNS_INSIDE;
2✔
551

552
        _cleanup_(uid_range_freep) UIDRange *inside_range = NULL;
2✔
553
        r = uid_range_load_userns_by_fd_full(userns_fd, mode, /* coalesce= */ false, &inside_range);
2✔
554
        if (r < 0)
2✔
555
                return r;
556

557
        return uid_range_translate(outside_range, inside_range, uid, ret);
2✔
558
}
559

560
bool uid_range_equal(const UIDRange *a, const UIDRange *b) {
6✔
561
        if (a == b)
6✔
562
                return true;
563

564
        if (!a || !b)
6✔
565
                return false;
566

567
        if (a->n_entries != b->n_entries)
5✔
568
                return false;
569

570
        for (size_t i = 0; i < a->n_entries; i++) {
8✔
571
                if (a->entries[i].start != b->entries[i].start)
5✔
572
                        return false;
573
                if (a->entries[i].nr != b->entries[i].nr)
5✔
574
                        return false;
575
        }
576

577
        return true;
578
}
579

580
int uid_map_search_root(pid_t pid, UIDRangeUsernsMode mode, uid_t *ret) {
63✔
581
        int r;
63✔
582

583
        assert(pid_is_valid(pid));
63✔
584
        assert(IN_SET(mode, UID_RANGE_USERNS_OUTSIDE, GID_RANGE_USERNS_OUTSIDE));
63✔
585

586
        const char *p = procfs_file_alloca(pid, mode == UID_RANGE_USERNS_OUTSIDE ? "uid_map" : "gid_map");
63✔
587
        _cleanup_fclose_ FILE *f = fopen(p, "re");
126✔
588
        if (!f) {
63✔
589
                if (errno != ENOENT)
×
590
                        return -errno;
×
591

592
                r = proc_mounted();
×
593
                if (r < 0)
×
594
                        return -ENOENT; /* original error, if we can't determine /proc/ state */
595

596
                return r ? -ENOPKG : -ENOSYS;
×
597
        }
598

599
        for (;;) {
×
600
                uid_t uid_base = UID_INVALID, uid_shift = UID_INVALID;
63✔
601

602
                r = uid_map_read_one(f, &uid_base, &uid_shift, /* ret_range= */ NULL);
63✔
603
                if (r < 0)
63✔
604
                        return r;
63✔
605

606
                if (uid_base == 0) {
62✔
607
                        if (ret)
62✔
608
                                *ret = uid_shift;
62✔
609
                        return 0;
62✔
610
                }
611
        }
612
}
613

614
uid_t uid_range_base(const UIDRange *range) {
8✔
615

616
        /* Returns the lowest UID in the range (notw that elements are sorted, hence we just need to look at
617
         * the first one that is populated. */
618

619
        if (uid_range_is_empty(range))
8✔
620
                return UID_INVALID;
621

622
        FOREACH_ARRAY(e, range->entries, range->n_entries)
8✔
623
                if (e->nr > 0)
8✔
624
                        return e->start;
8✔
625

626
        return UID_INVALID;
627
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc