summaryrefslogtreecommitdiff
path: root/lockbox.c
blob: f6cc234f33da6ca9471898bc6f3aa53306ee0142 (plain)
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
/*
 * lockbox.c - Landlock LSM build isolation for mkpkg
 * https://rawnix.org
 *
 * Copyright (c) 2025 zorz@gmx.com
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Restricts the build (in a child process, see lockbox_enter) to:
 *   READ:  files owned by lockbox_base + declared deps/makedepends
 *   WRITE: work_dir, source_dir, package_dir, tmp_dir, port dir, /tmp
 *
 * Undeclared library probes are blocked at the kernel level — builds
 * either skip the feature or fail, never silently link an unlisted dep.
 */

#include "mkpkg.h"
#include "lockbox_common.h"

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <unistd.h>

/*
 * Runs in the child forked by lockbox_enter(): build the ruleset,
 * restrict this process, build.  Returns the build's exit code.
 */
static int lockbox_child(pkgfile_t *pkgfile, config_t *config,
                         const char *target)
{
    int     abi, ruleset_fd;
    uint64_t all_rw;
    int     base_count, declared_count, implied_count, total, n, i;
    const char **names;
    const char **declared;
    strarray_t  *implied;
    struct landlock_ruleset_attr rs_attr = {0};
    const char *build_paths[8];
    int ret;

    /* probe Landlock ABI version */
    abi = (int)syscall(SYS_landlock_create_ruleset,
                       (void *)NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
    if (abi < 1) {
        fprintf(stderr,
                "mkpkg: error: lockbox: Landlock not available (abi=%d): %s\n"
                "mkpkg: error: lockbox: kernel >= 5.13 with "
                "CONFIG_SECURITY_LANDLOCK=y required\n",
                abi, strerror(errno));
        return E_BUILD;
    }
    fprintf(stderr, "mkpkg: lockbox: Landlock ABI v%d\n", abi);

    /* mask TRUNCATE on kernels < 6.2 (ABI v3) */
    all_rw = ACCESS_RW;
    if (abi < 3)
        all_rw &= ~(uint64_t)LANDLOCK_ACCESS_FS_TRUNCATE;

    /* create ruleset.  Handling the net rights with no NET_PORT rule
     * added denies both outright: the download phase has already run
     * in the parent, so a build has no business opening a socket.
     * Landlock covers TCP only -- UDP (and so DNS) is untouched; the
     * cargo profile's CLONE_NEWNET is what closes that. */
    rs_attr.handled_access_fs = ACCESS_RO | all_rw;
#ifdef LANDLOCK_ACCESS_NET_CONNECT_TCP
    if (abi >= 4)
        rs_attr.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
                                     LANDLOCK_ACCESS_NET_CONNECT_TCP;
#endif
    ruleset_fd = ll_create_ruleset(&rs_attr, sizeof(rs_attr), 0);
    if (ruleset_fd < 0) {
        fprintf(stderr, "mkpkg: error: lockbox: create_ruleset: %s\n",
                strerror(errno));
        return E_BUILD;
    }

    /* declared = makedepends + depends, as written in the MAKEPKG */
    declared_count = pkgfile->makedepend_count + pkgfile->depend_count;
    declared = malloc((size_t)(declared_count ? declared_count : 1)
                      * sizeof(char *));
    if (!declared) {
        fprintf(stderr, "mkpkg: error: lockbox: malloc: %s\n", strerror(errno));
        close(ruleset_fd);
        return E_BUILD;
    }
    n = 0;
    for (i = 0; i < pkgfile->makedepend_count; i++)
        declared[n++] = pkgfile->makedepends[i];
    for (i = 0; i < pkgfile->depend_count; i++)
        declared[n++] = pkgfile->depends[i];

    /* implied = transitive closure of the declared names over deps.db.
     * lockbox_base is deliberately NOT expanded: it is the hand-audited
     * floor that has to hold on a box with mkpkg and no pkg installed. */
    implied = lb_expand_deps(declared, declared_count);
    implied_count = implied ? (int)implied->count : 0;

    /* name list: base + declared + implied */
    base_count = 0;
    while (lockbox_base[base_count]) base_count++;

    total = base_count + declared_count + implied_count;
    names = malloc((size_t)total * sizeof(char *));
    if (!names) {
        fprintf(stderr, "mkpkg: error: lockbox: malloc: %s\n", strerror(errno));
        free(declared);
        strarray_free(implied);
        close(ruleset_fd);
        return E_BUILD;
    }

    n = 0;
    for (i = 0; i < base_count; i++)
        names[n++] = lockbox_base[i];
    for (i = 0; i < declared_count; i++)
        names[n++] = declared[i];
    for (i = 0; i < implied_count; i++)
        names[n++] = implied->items[i];

    /* one pass over db: a name reached twice still emits its files once */
    if (lb_add_rules_ex(ruleset_fd, names, total, base_count,
                         base_count + declared_count) != 0) {
        free(names);
        free(declared);
        strarray_free(implied);
        close(ruleset_fd);
        return E_BUILD;
    }
    free(names);
    free(declared);

    if (implied_count > 0) {
        size_t k;
        fprintf(stderr, "mkpkg: lockbox: %d implied via deps.db:",
                implied_count);
        for (k = 0; k < implied->count; k++)
            fprintf(stderr, " %s", implied->items[k]);
        fputc('\n', stderr);
    }
    strarray_free(implied);

    /* add RW rules for build paths */
    build_paths[0] = config->work_dir;
    build_paths[1] = config->source_dir;
    build_paths[2] = config->package_dir;
    build_paths[3] = config->tmp_dir;
    build_paths[4] = config->root_dir;   /* port dir: MAKEPKG, patches, sources */
    build_paths[5] = "/tmp";
    build_paths[6] = NULL;

    for (i = 0; build_paths[i]; i++) {
        if (!build_paths[i][0]) continue;
        if (ll_add_path(ruleset_fd, build_paths[i], all_rw) != 0) {
            fprintf(stderr,
                    "mkpkg: error: lockbox: ll_add_path '%s': %s\n",
                    build_paths[i], strerror(errno));
            close(ruleset_fd);
            return E_BUILD;
        }
    }

    /* essential pseudo-filesystems */
    if (ll_add_path(ruleset_fd, "/dev",  all_rw)    != 0 ||
        ll_add_path(ruleset_fd, "/proc", ACCESS_RO) != 0 ||
        ll_add_path(ruleset_fd, "/etc",  ACCESS_RO) != 0) {
        fprintf(stderr,
                "mkpkg: error: lockbox: ll_add_path pseudo-fs: %s\n",
                strerror(errno));
        close(ruleset_fd);
        return E_BUILD;
    }

    /* the hooks source the configuration; with -g it may live outside
     * /etc.  Best effort: a missing file is simply not sourced. */
    if (config->config_file[0])
        (void)ll_add_path(ruleset_fd, config->config_file,
                          LANDLOCK_ACCESS_FS_READ_FILE);

    /* engage — no new privileges */
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
        fprintf(stderr,
                "mkpkg: error: lockbox: PR_SET_NO_NEW_PRIVS: %s\n",
                strerror(errno));
        close(ruleset_fd);
        return E_BUILD;
    }

    if (ll_restrict_self(ruleset_fd) != 0) {
        fprintf(stderr,
                "mkpkg: error: lockbox: restrict_self: %s\n",
                strerror(errno));
        close(ruleset_fd);
        return E_BUILD;
    }

    close(ruleset_fd);
    fprintf(stderr,
            "mkpkg: lockbox: filesystem restricted to declared deps + build paths\n");
#ifdef LANDLOCK_ACCESS_NET_CONNECT_TCP
    if (abi >= 4)
        fprintf(stderr, "mkpkg: lockbox: network denied (TCP bind + connect)\n");
#endif

    /* run the build inside the sandbox -- the caller already made (or,
     * with -ww, kept) the work directory and removes it afterwards */
    ret = build_package(pkgfile, config, target);
    return ret;
}

/*
 * Landlock cannot be lifted once applied, and it is inherited by every
 * later child.  Restricting mkpkg itself therefore also sandboxed what
 * runs after the build -- footprint, signing, shell() [post] and the
 * addpkg of -r -- which then failed on paths outside the ruleset.  The
 * sandbox now lives in a child that only builds; mkpkg continues
 * unrestricted once it exits.
 */
int lockbox_enter(pkgfile_t *pkgfile, config_t *config, const char *target)
{
    pid_t pid;
    int   status;

    fflush(stdout);
    fflush(stderr);

    pid = fork();
    if (pid < 0) {
        fprintf(stderr, "mkpkg: error: lockbox: fork: %s\n", strerror(errno));
        return E_BUILD;
    }
    if (pid == 0) {
        int r = lockbox_child(pkgfile, config, target);
        fflush(stdout);
        fflush(stderr);
        _exit(r);
    }

    while (waitpid(pid, &status, 0) < 0) {
        if (errno != EINTR) {
            fprintf(stderr, "mkpkg: error: lockbox: waitpid: %s\n",
                    strerror(errno));
            return E_BUILD;
        }
    }
    if (WIFEXITED(status))
        return WEXITSTATUS(status);
    return E_BUILD;
}