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

stefanberger / swtpm / #2757

20 Jan 2025 04:22PM UTC coverage: 73.325% (+0.1%) from 73.203%
#2757

push

travis-ci

web-flow
Merge a51e38e2b into cfe93d90b

8032 of 10954 relevant lines covered (73.32%)

13619.67 hits per line

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

60.48
/src/swtpm/utils.c
1
/*
2
 * utils.s -- utilities
3
 *
4
 * (c) Copyright IBM Corporation 2014, 2015, 2019.
5
 *
6
 * Author: Stefan Berger <stefanb@us.ibm.com>
7
 *
8
 * All rights reserved.
9
 *
10
 * Redistribution and use in source and binary forms, with or without
11
 * modification, are permitted provided that the following conditions are
12
 * met:
13
 *
14
 * Redistributions of source code must retain the above copyright notice,
15
 * this list of conditions and the following disclaimer.
16
 *
17
 * Redistributions in binary form must reproduce the above copyright
18
 * notice, this list of conditions and the following disclaimer in the
19
 * documentation and/or other materials provided with the distribution.
20
 *
21
 * Neither the names of the IBM Corporation nor the names of its
22
 * contributors may be used to endorse or promote products derived from
23
 * this software without specific prior written permission.
24
 *
25
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
26
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
27
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
28
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
29
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
30
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
31
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
32
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
33
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
34
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
35
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
 */
37

38
#include "config.h"
39

40
#include <grp.h>
41
#include <pwd.h>
42
#include <fcntl.h>
43
#include <unistd.h>
44
#include <limits.h>
45
#include <stdlib.h>
46
#include <string.h>
47
#include <errno.h>
48

49
#if defined __APPLE__
50
#include <fcntl.h>
51
#include <sys/param.h>
52
#endif
53

54
#include <json-glib/json-glib.h>
55

56
#include <openssl/rand.h>
57

58
#include "utils.h"
59
#include "logging.h"
60
#include "tpmlib.h"
61
#include "swtpm_debug.h"
62

63
void uninstall_sighandlers()
409✔
64
{
65
    if (signal(SIGTERM, SIG_DFL) == SIG_ERR)
409✔
66
        logprintf(STDERR_FILENO, "Could not uninstall signal handler for SIGTERM.\n");
×
67

68
    if (signal(SIGPIPE, SIG_DFL) == SIG_ERR)
409✔
69
        logprintf(STDERR_FILENO, "Could not uninstall signal handler for SIGPIPE.\n");
×
70
}
409✔
71

72
int install_sighandlers(int pipefd[2], sighandler_t handler)
409✔
73
{
74
    if (pipe(pipefd) < 0) {
409✔
75
        logprintf(STDERR_FILENO, "Error: Could not open pipe.\n");
×
76
        goto err_exit;
×
77
    }
78

79
    if (signal(SIGTERM, handler) == SIG_ERR) {
409✔
80
        logprintf(STDERR_FILENO, "Could not install signal handler for SIGTERM.\n");
×
81
        goto err_close_pipe;
×
82
    }
83

84
    if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
409✔
85
        logprintf(STDERR_FILENO, "Could not install signal handler for SIGPIPE.\n");
×
86
        goto err_close_pipe;
×
87
    }
88

89
    return 0;
90

91
err_close_pipe:
×
92
    close(pipefd[0]);
×
93
    pipefd[0] = -1;
×
94
    close(pipefd[1]);
×
95
    pipefd[1] = -1;
×
96

97
err_exit:
98
    return -1;
99
}
100

101
int
102
change_process_owner(const char *user)
×
103
{
104
    struct passwd *passwd;
×
105
    long int uid, gid;
×
106
    char *endptr = NULL;
×
107

108
    uid = strtoul(user, &endptr, 10);
×
109
    if (*endptr != '\0') {
×
110
        /* a string */
111
        passwd = getpwnam(user);
×
112
        if (!passwd) {
×
113
            logprintf(STDERR_FILENO,
×
114
                      "Error: User '%s' does not exist.\n",
115
                      user);
116
            return -14;
×
117
        }
118

119
        if (initgroups(passwd->pw_name, passwd->pw_gid) < 0) {
×
120
            logprintf(STDERR_FILENO,
×
121
                      "Error: initgroups(%s, %d) failed.\n",
122
                      passwd->pw_name, passwd->pw_gid);
123
           return -10;
×
124
        }
125
        gid = passwd->pw_gid;
×
126
        uid = passwd->pw_uid;
×
127
    } else {
128
        /* an integer */
129
        if ((unsigned long int)uid > UINT_MAX) {
×
130
            logprintf(STDERR_FILENO,
×
131
                      "Error: uid %s outside valid range.\n",
132
                      user);
133
            return -13;
×
134
        }
135
        gid = uid;
136
    }
137

138
    if (setgid(gid) < 0) {
×
139
        logprintf(STDERR_FILENO,
×
140
                  "Error: setgid(%d) failed.\n",
141
                  gid);
142
        return -11;
×
143
    }
144
    if (setuid(uid) < 0) {
×
145
        logprintf(STDERR_FILENO,
×
146
                  "Error: setuid(%d) failed.\n",
147
                  uid);
148
        return -12;
×
149
    }
150
    return 0;
151
}
152

153
int
154
do_chroot(const char *path)
×
155
{
156
    if (chroot(path) < 0) {
×
157
        logprintf(STDERR_FILENO, "chroot failed: %s\n",
×
158
                  strerror(errno));
×
159
        return -1;
×
160
    }
161

162
    if (chdir("/") < 0) {
×
163
        logprintf(STDERR_FILENO, "chdir failed: %s\n",
×
164
                  strerror(errno));
×
165
        return -1;
×
166
    }
167

168
    if (!RAND_status()) {
×
169
        logprintf(STDERR_FILENO,
×
170
                  "Error: no good entropy source in chroot environment\n");
171
        return -1;
×
172
    }
173

174
    return 0;
175
}
176

177
void tpmlib_debug_libtpms_parameters(TPMLIB_TPMVersion tpmversion)
442✔
178
{
179
    switch (tpmversion) {
442✔
180
    case TPMLIB_TPM_VERSION_1_2:
181
        TPM_DEBUG("TPM 1.2: Compiled for %u auth, %u transport, "
182
                  "and %u DAA session slots\n",
183
            tpmlib_get_tpm_property(TPMPROP_TPM_MIN_AUTH_SESSIONS),
184
            tpmlib_get_tpm_property(TPMPROP_TPM_MIN_TRANS_SESSIONS),
185
            tpmlib_get_tpm_property(TPMPROP_TPM_MIN_DAA_SESSIONS));
186
        TPM_DEBUG("TPM 1.2: Compiled for %u key slots, %u owner evict slots\n",
187
            tpmlib_get_tpm_property(TPMPROP_TPM_KEY_HANDLES),
188
            tpmlib_get_tpm_property(TPMPROP_TPM_OWNER_EVICT_KEY_HANDLES));
189
        TPM_DEBUG("TPM 1.2: Compiled for %u counters, %u saved sessions\n",
190
            tpmlib_get_tpm_property(TPMPROP_TPM_MIN_COUNTERS),
191
            tpmlib_get_tpm_property(TPMPROP_TPM_MIN_SESSION_LIST));
192
        TPM_DEBUG("TPM 1.2: Compiled for %u family, "
193
                  "%u delegate table entries\n",
194
            tpmlib_get_tpm_property(TPMPROP_TPM_NUM_FAMILY_TABLE_ENTRY_MIN),
195
            tpmlib_get_tpm_property(TPMPROP_TPM_NUM_DELEGATE_TABLE_ENTRY_MIN));
196
        TPM_DEBUG("TPM 1.2: Compiled for %u total NV, %u savestate, "
197
                  "%u volatile space\n",
198
            tpmlib_get_tpm_property(TPMPROP_TPM_MAX_NV_SPACE),
199
            tpmlib_get_tpm_property(TPMPROP_TPM_MAX_SAVESTATE_SPACE),
200
            tpmlib_get_tpm_property(TPMPROP_TPM_MAX_VOLATILESTATE_SPACE));
201
#if 0
202
        TPM_DEBUG("TPM1.2: Compiled for %u NV defined space\n",
203
            tpmlib_get_tpm_property(TPMPROP_TPM_MAX_NV_DEFINED_SIZE));
204
#endif
205
    break;
206
    case TPMLIB_TPM_VERSION_2:
207
    break;
208
    }
209
}
442✔
210

211
char *fd_to_filename(int fd)
×
212
{
213
#if defined __linux__
214

215
    char buffer[64];
×
216
    char *path;
×
217

218
    snprintf(buffer, sizeof(buffer), "/proc/self/fd/%d", fd);
×
219

220
    path = realpath(buffer, NULL);
×
221
    if (!path) {
×
222
        logprintf(STDERR_FILENO, "Could not read %s: %s\n",
×
223
                  buffer, strerror(errno));
×
224
        return NULL;
×
225
    }
226

227
    return path;
228

229
#elif defined __APPLE__
230

231
    char *path = malloc(MAXPATHLEN);
232
    if (!path) {
233
        logprintf(STDERR_FILENO, "Out of memory.\n");
234
        return NULL;
235
    }
236
    if (fcntl(fd, F_GETPATH, path) < 0) {
237
        logprintf(STDERR_FILENO, "fcntl for F_GETPATH failed: %\n",
238
                  strerror(errno));
239
        free(path);
240
        return NULL;
241
    }
242
    return path;
243

244
#else
245
    (void)fd;
246
    logprintf(STDERR_FILENO,
247
              "Cannot convert file descriptor to filename on this platform.\n");
248
    return NULL;
249

250
#endif
251
}
252

253
/*
254
 * write_full: Write all bytes of a buffer into the file descriptor
255
 *             and handle partial writes on the way.
256
 *
257
 * @fd: file descriptor to write to
258
 * @buffer: buffer
259
 * @buflen: length of buffer
260
 *
261
 * Returns -1 in case not all bytes could be transferred, number of
262
 * bytes written otherwise (must be equal to buflen).
263
 */
264
ssize_t write_full(int fd, const void *buffer, size_t buflen)
93,278✔
265
{
266
    size_t written = 0;
93,278✔
267
    ssize_t n;
93,278✔
268

269
    while (written < buflen) {
186,556✔
270
        n = write(fd, buffer, buflen - written);
93,278✔
271
        if (n == 0)
93,278✔
272
            return -1;
273
        if (n < 0) {
93,278✔
274
            if (errno == EINTR)
×
275
                continue;
×
276
            return -1;
277
        }
278
        written += n;
93,278✔
279
        buffer = (const char *)buffer + n;
93,278✔
280
    }
281
    return written;
93,278✔
282
}
283

284
/*
285
 * writev_full: Write all bytes of an iovec into the file descriptor
286
 *              and handle partial writes on the way.
287
 * @fd: file descriptor to write to
288
 * @iov: pointer to iov
289
 * @iovcnt: length of iov array
290
 *
291
 * Returns -1 in case not all bytes could be transferred, number of
292
 * bytes written otherwise (must be equal to buflen).
293
 */
294
ssize_t writev_full(int fd, const struct iovec *iov, int iovcnt)
22,125✔
295
{
296
    int i;
22,125✔
297
    size_t off;
22,125✔
298
    unsigned char *buf;
22,125✔
299
    ssize_t n;
22,125✔
300
    size_t bytecount = 0;
22,125✔
301
    size_t numbufs = 0;
22,125✔
302
    size_t lastidx = -1;
22,125✔
303

304
    for (i = 0; i < iovcnt; i++) {
88,441✔
305
        if (iov[i].iov_len) {
66,316✔
306
            bytecount += iov[i].iov_len;
35,066✔
307
            numbufs++;
35,066✔
308
            lastidx = i;
35,066✔
309
        }
310
    }
311

312
    if (numbufs == 1)
22,125✔
313
        return write_full(fd, iov[lastidx].iov_base, iov[lastidx].iov_len);
15,628✔
314

315
    buf = malloc(bytecount);
6,497✔
316
    if (!buf) {
6,497✔
317
        errno = ENOMEM;
×
318
        return -1;
×
319
    }
320

321
    off = 0;
322
    for (i = 0; i < iovcnt; i++) {
25,935✔
323
        if (!iov[i].iov_len)
19,438✔
324
            continue;
×
325
        memcpy(&buf[off], iov[i].iov_base, iov[i].iov_len);
19,438✔
326
        off += iov[i].iov_len;
19,438✔
327
    }
328

329
    n = write_full(fd, buf, off);
6,497✔
330

331
    free(buf);
6,497✔
332

333
    return n;
6,497✔
334
}
335

336
/*
337
 * read_einter: Read bytes from a file descriptor into a buffer
338
 *              and handle EINTR. Perform one read().
339
 *
340
 * @fd: file descriptor to read from
341
 * @buffer: buffer
342
 * @buflen: length of buffer
343
 *
344
 * Returns -1 in case an error occurred, number of bytes read otherwise.
345
 */
346
ssize_t read_eintr(int fd, void *buffer, size_t buflen)
270✔
347
{
348
    ssize_t n;
270✔
349

350
    while (true) {
270✔
351
        n = read(fd, buffer, buflen);
270✔
352
        if (n < 0) {
270✔
353
            if (errno == EINTR)
×
354
                continue;
×
355
            return -1;
356
        }
357
        return n;
358
    }
359
}
360

361
/*
362
 * Get the value of a map's key.
363
 *
364
 * Returns:
365
 * 0 : success
366
 * -1 : failure to parse the JSON input
367
 * -2 : could not find the key
368
 */
369
int json_get_map_key_value(const char *json_input,
11✔
370
                           const char *key, char **value)
371
{
372
    g_autoptr(GError) error = NULL;
22✔
373
    g_autoptr(JsonParser) jp = NULL;
11✔
374
    g_autoptr(JsonReader) jr = NULL;
11✔
375
    JsonNode *root;
11✔
376

377
    jp = json_parser_new();
11✔
378
    if (!json_parser_load_from_data(jp, json_input, -1, &error)) {
11✔
379
        logprintf(STDERR_FILENO,
×
380
                  "Could not parse JSON '%s': %s\n", json_input, error->message);
×
381
        return -1;
×
382
    }
383

384
    root = json_parser_get_root(jp);
11✔
385
    if (!root) {
11✔
386
        logprintf(STDERR_FILENO,
×
387
                  "Could not get root of JSON '%s'\n", json_input);
388
        return -1;
×
389
    }
390
    jr = json_reader_new(root);
11✔
391

392
    if (!json_reader_read_member(jr, key))
11✔
393
        return -2;
394

395
    *value = g_strdup(json_reader_get_string_value(jr));
7✔
396
    if (*value == NULL) {
7✔
397
        /* value not a string */
398
        logprintf(STDERR_FILENO,
×
399
                  "'%s' in JSON map is not a string\n", key);
400
        return -1;
×
401
    }
402

403
    return 0;
404
}
405

406
/*
407
 * Set the value of a map's key and return the new string
408
 *
409
 * Returns:
410
 * 0 : success
411
 * -1 : fatal failure
412
 */
413
int json_set_map_key_value(char **json_string,
6✔
414
                           const char *key, const char *value)
415
{
416
    g_autoptr(JsonParser) jp = NULL;
12✔
417
    g_autoptr(GError) error = NULL;
6✔
418
    g_autoptr(JsonGenerator) jg = NULL;
6✔
419
    JsonObject *jo;
6✔
420
    JsonNode *root;
6✔
421

422
    jg = json_generator_new();
6✔
423
    if (!jg)
6✔
424
        return -1;
425

426
    jp = json_parser_new();
6✔
427
    if (!json_parser_load_from_data(jp, *json_string, -1, &error)) {
6✔
428
        logprintf(STDERR_FILENO,
×
429
                  "Could not parse JSON '%s': %s\n", *json_string, error->message);
×
430
        return -1;
×
431
    }
432

433
    root = json_parser_get_root(jp);
6✔
434
    if (!root) {
6✔
435
        logprintf(STDERR_FILENO,
×
436
                  "Could not get root of JSON '%s'\n", *json_string);
437
        return -1;
×
438
    }
439
    json_generator_set_root(jg, root);
6✔
440

441
    jo = json_node_get_object(root);
6✔
442
    json_object_set_string_member(jo, key, value);
6✔
443

444
    g_free(*json_string);
6✔
445
    *json_string = json_generator_to_data(jg, NULL);
6✔
446

447
    return 0;
6✔
448
}
449

450
/*
451
 * In the given JSON map find a map with name @field_name and then
452
 * access the field @field_name2 in this map and return its value.
453
 *
454
 * @json_input: JSON object as string
455
 * @field_name: Name of map
456
 * @field_name2: Name of entry in map
457
 * @value: Results is returned here
458
 *
459
 * Returns 0 in case of success, -1 otherwise.
460
 */
461
int json_get_submap_value(const char *json_input, const char *field_name,
27,570✔
462
                          const char *field_name2, char **value)
463
{
464
    g_autoptr(JsonParser) jp = NULL;
55,140✔
465
    g_autoptr(JsonReader) jr = NULL;
27,570✔
466
    g_autoptr(GError) error = NULL;
27,570✔
467
    JsonNode *root;
27,570✔
468

469
    jp = json_parser_new();
27,570✔
470
    if (!json_parser_load_from_data(jp, json_input, -1, &error)) {
27,570✔
471
        logprintf(STDERR_FILENO,
×
472
                  "Could not parse JSON '%s': %s\n", json_input, error->message);
×
473
        return -1;
×
474
    }
475

476
    root = json_parser_get_root(jp);
27,570✔
477
    if (!root) {
27,570✔
478
        logprintf(STDERR_FILENO,
×
479
                  "Could not get root of JSON '%s'\n", json_input);
480
        return -1;
×
481
    }
482
    jr = json_reader_new(root);
27,570✔
483

484
    if (!json_reader_read_member(jr, field_name)) {
27,570✔
485
        logprintf(STDERR_FILENO, "Missing '%s' field in '%s'\n",
×
486
                  field_name, json_input);
487
        return -1;
×
488
    }
489

490
    if (!json_reader_read_member(jr, field_name2)) {
27,570✔
491
        logprintf(STDERR_FILENO, "Missing '%s/%s' field in '%s'\n",
×
492
                  field_name, field_name2, json_input);
493
        return -1;
×
494
    }
495
    *value = g_strdup(json_reader_get_string_value(jr));
27,570✔
496
    if (*value == NULL) {
27,570✔
497
        /* value not a string */
498
        logprintf(STDERR_FILENO,
×
499
                  "'%s/%s' field in '%s' is not a string\n",
500
                  field_name, field_name2, json_input);
501
        return -1;
×
502
    }
503

504
    return 0;
505
}
506

507
ssize_t strv_strncmp(const gchar *const*str_array, const gchar *s, size_t n)
6✔
508
{
509
    size_t i;
6✔
510

511
    for (i = 0; str_array[i]; i++) {
90✔
512
        if (strncmp(str_array[i], s, n) == 0)
90✔
513
            return (ssize_t)i;
6✔
514
    }
515
    return -1;
516
}
517

518
/* Try to find exactly the needle in the given haystack */
519
static ssize_t strv_strcmp(const gchar *const*haystack, const gchar *needle)
155,963✔
520
{
521
    size_t i;
155,963✔
522

523
    for (i = 0; haystack[i]; i++) {
1,667,051✔
524
        if (strcmp(haystack[i], needle) == 0)
1,648,372✔
525
            return (ssize_t)i;
137,284✔
526
    }
527
    return -1;
528
}
529

530
/*
531
 * Try to find all the needles in the haystack; both arrays of strings
532
 * must be NULL-terminated.
533
 */
534
gboolean strv_contains_all(const gchar *const*haystack, const gchar *const*needles)
82,692✔
535
{
536
    size_t i;
82,692✔
537

538
    for (i = 0; needles[i]; i++) {
219,976✔
539
        if (strv_strcmp(haystack, needles[i]) < 0)
155,963✔
540
            return false;
541
    }
542
    return true;
543
}
544

545
/*
546
 * Remove all entries in the @array that either fully match @toremove
547
 * (@len = -1) or where @toremove is a prefix of.
548
 * This function returns the number of entries that were removed.
549
 */
550
size_t strv_remove(gchar **array, const gchar *toremove, ssize_t len,
18✔
551
                   gboolean freethem)
552
{
553
    size_t i = 0, j, num = 0;
18✔
554

555
    while (array[i]) {
873✔
556
        if ((len < 0 && strcmp(array[i], toremove) == 0) ||
855✔
557
            (len > 0 && strncmp(array[i], toremove, len) == 0)) {
288✔
558
            if (freethem)
18✔
559
                g_free(array[i]);
18✔
560

561
            j = i;
562
            do {
456✔
563
                j++;
456✔
564
                array[j - 1] = array[j];
456✔
565
            } while(array[j]);
456✔
566

567
            num++;
18✔
568
        } else {
569
            i++;
837✔
570
        }
571
    }
572
    return num;
18✔
573
}
574

575
/*
576
 * Deduplicate items in a NULL-terminated array of strings.
577
 * When a duplicate item is found then the first item is removed and all later
578
 * ones are kept -- this is to deduplicate items in the same way as libtpms
579
 * deduplicates comma separated items in a string. The string to use for
580
 * finding duplicates is expected to be returned from an optional gencmpstr_t
581
 * function that in the simplest case can return the passed string and adjust
582
 * string comparison to be done on full string (len = -1) or prefix comparison.
583
 * If not function is given then full string matching is done.
584
 *
585
 * This function returns the number of entries removed from the array.
586
 */
587
size_t strv_dedup(gchar **array, gencmpstr_t gencmpstr, gboolean freethem)
3✔
588
{
589
    gboolean free_cmp = false;
3✔
590
    size_t num = 0, i = 0, j;
3✔
591
    ssize_t len = 0;
3✔
592
    gchar *cmp;
3✔
593

594
    while (array[i]) {
135✔
595
        if (gencmpstr) {
132✔
596
            cmp = gencmpstr(array[i], &len);
132✔
597
            free_cmp = array[i] != cmp;
132✔
598
        } else {
599
            cmp = array[i];
×
600
            len = strlen(cmp);
×
601
        }
602

603
        j = i + 1;
132✔
604
        while (array[j]) {
2,970✔
605
            if ((len < 0 && strcmp(array[j], cmp) == 0) ||
2,838✔
606
                (len > 0 && strncmp(array[j], cmp, len) == 0)) {
402✔
607

608
                num++;
×
609
                if (freethem)
×
610
                    g_free(array[i]);
×
611

612
                /*
613
                 * Keep the later ones in the array since libtpms also keeps
614
                 * later items ones in string when deduplicating.
615
                 */
616
                j = i;
617
                do {
×
618
                    array[j] = array[j + 1];
×
619
                    j++;
×
620
                } while (array[j]);
×
621
                break;
622
            }
623
            j++;
2,838✔
624
        }
625

626
        if (free_cmp)
132✔
627
            g_free(cmp);
×
628
        i++;
629
    }
630
    return num;
3✔
631
}
632

633
/*
634
 * Append entries from a 2nd string array to the first one. Make copies of
635
 * each entry.
636
 */
637
gchar **strv_extend(gchar **array, const gchar *const*append)
2✔
638
{
639
    size_t len1 = 0, len2 = 0, i;
2✔
640

641
    if (array)
2✔
642
        len1 = g_strv_length(array);
1✔
643

644
    while (append[len2])
8✔
645
        len2++;
6✔
646

647
    array = g_realloc(array, sizeof(char *) * (len1 + len2 + 1));
2✔
648

649
    for (i = 0; i < len2; i++)
10✔
650
        array[len1 + i] = g_strdup(append[i]);
6✔
651
    array[len1 + len2] = NULL;
2✔
652

653
    return array;
2✔
654
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc