| 1 | |
| 2 | |
| 3 | * Landlock - Ptrace and scope hooks
|
| 4 | *
|
| 5 | * Copyright © 2017-2020 Mickaël Salaün <[email protected]>
|
| 6 | * Copyright © 2019-2020 ANSSI
|
| 7 | * Copyright © 2024-2025 Microsoft Corporation
|
| 8 |
|
| 9 |
|
| 10 | #include <asm/current.h>
|
| 11 | #include <linux/cleanup.h>
|
| 12 | #include <linux/cred.h>
|
| 13 | #include <linux/errno.h>
|
| 14 | #include <linux/kernel.h>
|
| 15 | #include <linux/lsm_audit.h>
|
| 16 | #include <linux/lsm_hooks.h>
|
| 17 | #include <linux/rcupdate.h>
|
| 18 | #include <linux/sched.h>
|
| 19 | #include <linux/sched/signal.h>
|
| 20 | #include <net/af_unix.h>
|
| 21 | #include <net/sock.h>
|
| 22 |
|
| 23 | #include "common.h"
|
| 24 | #include "cred.h"
|
| 25 | #include "domain.h"
|
| 26 | #include "fs.h"
|
| 27 | #include "log.h"
|
| 28 | #include "ruleset.h"
|
| 29 | #include "setup.h"
|
| 30 | #include "task.h"
|
| 31 |
|
| 32 | |
| 33 | * domain_scope_le - Checks domain ordering for scoped ptrace
|
| 34 | *
|
| 35 | * @parent: Parent domain.
|
| 36 | * @child: Potential child of @parent.
|
| 37 | *
|
| 38 | * Checks if the @parent domain is less or equal to (i.e. an ancestor, which
|
| 39 | * means a subset of) the @child domain.
|
| 40 | *
|
| 41 | * Return: True if @parent is an ancestor of or equal to @child, false
|
| 42 | * otherwise.
|
| 43 |
|
| 44 | static bool domain_scope_le(const struct landlock_domain *const parent,
|
| 45 | const struct landlock_domain *const child)
|
| 46 | {
|
| 47 | const struct landlock_hierarchy *walker;
|
| 48 |
|
| 49 |
|
| 50 | if (!parent)
|
| 51 | return true;
|
| 52 |
|
| 53 | if (!child)
|
| 54 | return false;
|
| 55 |
|
| 56 | for (walker = child->hierarchy; walker; walker = walker->parent) {
|
| 57 | if (walker == parent->hierarchy)
|
| 58 |
|
| 59 | return true;
|
| 60 | }
|
| 61 |
|
| 62 |
|
| 63 | return false;
|
| 64 | }
|
| 65 |
|
| 66 | static int domain_ptrace(const struct landlock_domain *const parent,
|
| 67 | const struct landlock_domain *const child)
|
| 68 | {
|
| 69 | if (domain_scope_le(parent, child))
|
| 70 | return 0;
|
| 71 |
|
| 72 | return -EPERM;
|
| 73 | }
|
| 74 |
|
| 75 | |
| 76 | * hook_ptrace_access_check - Determines whether the current process may access
|
| 77 | * another
|
| 78 | *
|
| 79 | * @child: Process to be accessed.
|
| 80 | * @mode: Mode of attachment.
|
| 81 | *
|
| 82 | * If the current task has Landlock rules, then the child must have at least
|
| 83 | * the same rules. Else denied.
|
| 84 | *
|
| 85 | * Return: 0 if permission is granted, -errno if denied.
|
| 86 |
|
| 87 | static int hook_ptrace_access_check(struct task_struct *const child,
|
| 88 | const unsigned int mode)
|
| 89 | {
|
| 90 | const struct landlock_cred_security *parent_subject;
|
| 91 | #ifdef CONFIG_TRACEPOINTS
|
| 92 | u64 tracee_domain_id = 0;
|
| 93 | #endif
|
| 94 | int err;
|
| 95 |
|
| 96 |
|
| 97 | parent_subject = landlock_cred(current_cred());
|
| 98 | if (!parent_subject)
|
| 99 | return 0;
|
| 100 |
|
| 101 | scoped_guard(rcu) {
|
| 102 | const struct landlock_domain *const child_dom =
|
| 103 | landlock_get_task_domain(child);
|
| 104 | err = domain_ptrace(parent_subject->domain, child_dom);
|
| 105 | #ifdef CONFIG_TRACEPOINTS
|
| 106 | if (child_dom)
|
| 107 | tracee_domain_id = child_dom->hierarchy->id;
|
| 108 | #endif
|
| 109 | }
|
| 110 |
|
| 111 | if (!err)
|
| 112 | return 0;
|
| 113 |
|
| 114 | |
| 115 | * For the ptrace_access_check case, we log the current/parent domain
|
| 116 | * and the child task.
|
| 117 |
|
| 118 | if (!(mode & PTRACE_MODE_NOAUDIT))
|
| 119 | landlock_log_denial(parent_subject, &(struct landlock_request) {
|
| 120 | .type = LANDLOCK_REQUEST_PTRACE,
|
| 121 | .audit = {
|
| 122 | .type = LSM_AUDIT_DATA_TASK,
|
| 123 | .u.tsk = child,
|
| 124 | },
|
| 125 | .layer_plus_one = parent_subject->domain->num_layers,
|
| 126 | #ifdef CONFIG_TRACEPOINTS
|
| 127 | .trace_ptrace = &(struct landlock_ptrace_trace) {
|
| 128 | .tracee_domain_id = tracee_domain_id,
|
| 129 | .tracer = current,
|
| 130 | },
|
| 131 | #endif
|
| 132 | });
|
| 133 |
|
| 134 | return err;
|
| 135 | }
|
| 136 |
|
| 137 | |
| 138 | * hook_ptrace_traceme - Determines whether another process may trace the
|
| 139 | * current one
|
| 140 | *
|
| 141 | * @parent: Task proposed to be the tracer.
|
| 142 | *
|
| 143 | * If the parent has Landlock rules, then the current task must have the same
|
| 144 | * or more rules. Else denied.
|
| 145 | *
|
| 146 | * Return: 0 if permission is granted, -errno if denied.
|
| 147 |
|
| 148 | static int hook_ptrace_traceme(struct task_struct *const parent)
|
| 149 | {
|
| 150 | const struct landlock_cred_security *parent_subject;
|
| 151 | const struct landlock_domain *child_dom;
|
| 152 | int err;
|
| 153 |
|
| 154 | child_dom = landlock_get_current_domain();
|
| 155 |
|
| 156 | guard(rcu)();
|
| 157 | parent_subject = landlock_cred(__task_cred(parent));
|
| 158 | err = domain_ptrace(parent_subject->domain, child_dom);
|
| 159 |
|
| 160 | if (!err)
|
| 161 | return 0;
|
| 162 |
|
| 163 | |
| 164 | * For the ptrace_traceme case, we log the domain which is the cause of
|
| 165 | * the denial, which means the parent domain instead of the current
|
| 166 | * domain. This may look unusual because the ptrace_traceme action is a
|
| 167 | * request to be traced, but the semantic is consistent with
|
| 168 | * hook_ptrace_access_check().
|
| 169 |
|
| 170 | landlock_log_denial(parent_subject, &(struct landlock_request) {
|
| 171 | .type = LANDLOCK_REQUEST_PTRACE,
|
| 172 | .audit = {
|
| 173 | .type = LSM_AUDIT_DATA_TASK,
|
| 174 | .u.tsk = current,
|
| 175 | },
|
| 176 | .layer_plus_one = parent_subject->domain->num_layers,
|
| 177 | #ifdef CONFIG_TRACEPOINTS
|
| 178 | .trace_ptrace = &(struct landlock_ptrace_trace) {
|
| 179 |
|
| 180 | .tracee_domain_id = child_dom ? child_dom->hierarchy->id : 0,
|
| 181 | .tracer = parent,
|
| 182 | },
|
| 183 | #endif
|
| 184 | });
|
| 185 | return err;
|
| 186 | }
|
| 187 |
|
| 188 | |
| 189 | * domain_is_scoped - Check if an interaction from a client/sender to a
|
| 190 | * server/receiver should be restricted based on scope controls.
|
| 191 | *
|
| 192 | * @client: IPC sender domain.
|
| 193 | * @server: IPC receiver domain.
|
| 194 | * @scope: The scope restriction criteria.
|
| 195 | *
|
| 196 | * Return: True if @server is in a different domain from @client and @client
|
| 197 | * is scoped to access @server (i.e. access should be denied), false otherwise.
|
| 198 |
|
| 199 | static bool domain_is_scoped(const struct landlock_domain *const client,
|
| 200 | const struct landlock_domain *const server,
|
| 201 | access_mask_t scope)
|
| 202 | {
|
| 203 | int client_layer, server_layer;
|
| 204 | const struct landlock_hierarchy *client_walker, *server_walker;
|
| 205 |
|
| 206 |
|
| 207 | if (WARN_ON_ONCE(!client))
|
| 208 | return false;
|
| 209 |
|
| 210 | client_layer = client->num_layers - 1;
|
| 211 | client_walker = client->hierarchy;
|
| 212 | |
| 213 | * client_layer must be able to represent all numbers from
|
| 214 | * LANDLOCK_MAX_NUM_LAYERS - 1 to -1 for the loop below to terminate.
|
| 215 | * (It must be large enough, and it must be signed.)
|
| 216 |
|
| 217 | BUILD_BUG_ON(!is_signed_type(typeof(client_layer)));
|
| 218 | BUILD_BUG_ON(LANDLOCK_MAX_NUM_LAYERS - 1 >
|
| 219 | type_max(typeof(client_layer)));
|
| 220 |
|
| 221 | server_layer = server ? (server->num_layers - 1) : -1;
|
| 222 | server_walker = server ? server->hierarchy : NULL;
|
| 223 |
|
| 224 | |
| 225 | * Walks client's parent domains down to the same hierarchy level
|
| 226 | * as the server's domain, and checks that none of these client's
|
| 227 | * parent domains are scoped.
|
| 228 |
|
| 229 | for (; client_layer > server_layer; client_layer--) {
|
| 230 | if (landlock_get_scope_mask(client, client_layer) & scope)
|
| 231 | return true;
|
| 232 |
|
| 233 | client_walker = client_walker->parent;
|
| 234 | }
|
| 235 | |
| 236 | * Walks server's parent domains down to the same hierarchy level as
|
| 237 | * the client's domain.
|
| 238 |
|
| 239 | for (; server_layer > client_layer; server_layer--)
|
| 240 | server_walker = server_walker->parent;
|
| 241 |
|
| 242 | for (; client_layer >= 0; client_layer--) {
|
| 243 | if (landlock_get_scope_mask(client, client_layer) & scope) {
|
| 244 | |
| 245 | * Client and server are at the same level in the
|
| 246 | * hierarchy. If the client is scoped, the request is
|
| 247 | * only allowed if this domain is also a server's
|
| 248 | * ancestor.
|
| 249 |
|
| 250 | return server_walker != client_walker;
|
| 251 | }
|
| 252 | client_walker = client_walker->parent;
|
| 253 | server_walker = server_walker->parent;
|
| 254 | }
|
| 255 | return false;
|
| 256 | }
|
| 257 |
|
| 258 | static bool sock_is_scoped(struct sock *const other,
|
| 259 | const struct landlock_domain *const domain)
|
| 260 | {
|
| 261 | const struct landlock_domain *dom_other;
|
| 262 |
|
| 263 |
|
| 264 | lockdep_assert_held(&unix_sk(other)->lock);
|
| 265 |
|
| 266 | |
| 267 | * A live kernel socket (e.g. from sock_create_kern()) has no backing
|
| 268 | * file, hence no Landlock domain, so treat it as unscoped. The
|
| 269 | * sk_socket check only guards that dereference; sk_socket is NULL
|
| 270 | * solely for a dead peer, which the caller already excludes under the
|
| 271 | * held lock, so no separate SOCK_DEAD check is needed.
|
| 272 |
|
| 273 | if (unlikely(!other->sk_socket || !other->sk_socket->file))
|
| 274 | return false;
|
| 275 |
|
| 276 | dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;
|
| 277 | return domain_is_scoped(domain, dom_other,
|
| 278 | LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
|
| 279 | }
|
| 280 |
|
| 281 | #ifdef CONFIG_TRACEPOINTS
|
| 282 |
|
| 283 | static u64 get_socket_domain_id(const struct sock *const other)
|
| 284 | {
|
| 285 | const struct landlock_domain *domain;
|
| 286 |
|
| 287 | lockdep_assert_held(&unix_sk(other)->lock);
|
| 288 | domain = landlock_cred(other->sk_socket->file->f_cred)->domain;
|
| 289 | return domain ? domain->hierarchy->id : 0;
|
| 290 | }
|
| 291 |
|
| 292 | #endif
|
| 293 |
|
| 294 | static bool is_abstract_socket(struct sock *const sock)
|
| 295 | {
|
| 296 | struct unix_address *addr = unix_sk(sock)->addr;
|
| 297 |
|
| 298 | if (!addr)
|
| 299 | return false;
|
| 300 |
|
| 301 | if (addr->len >= offsetof(struct sockaddr_un, sun_path) + 1 &&
|
| 302 | addr->name->sun_path[0] == '\0')
|
| 303 | return true;
|
| 304 |
|
| 305 | return false;
|
| 306 | }
|
| 307 |
|
| 308 | static const struct access_masks unix_scope = {
|
| 309 | .scope = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
|
| 310 | };
|
| 311 |
|
| 312 | static int hook_unix_stream_connect(struct sock *const sock,
|
| 313 | struct sock *const other,
|
| 314 | struct sock *const newsk)
|
| 315 | {
|
| 316 | size_t handle_layer;
|
| 317 | const struct landlock_cred_security *const subject =
|
| 318 | landlock_get_applicable_subject(current_cred(), unix_scope,
|
| 319 | &handle_layer);
|
| 320 |
|
| 321 |
|
| 322 | if (!subject)
|
| 323 | return 0;
|
| 324 |
|
| 325 | if (!is_abstract_socket(other))
|
| 326 | return 0;
|
| 327 |
|
| 328 | if (!sock_is_scoped(other, subject->domain))
|
| 329 | return 0;
|
| 330 |
|
| 331 | landlock_log_denial(subject, &(struct landlock_request) {
|
| 332 | .type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
|
| 333 | .audit = {
|
| 334 | .type = LSM_AUDIT_DATA_NET,
|
| 335 | .u.net = &(struct lsm_network_audit) {
|
| 336 | .sk = other,
|
| 337 | },
|
| 338 | },
|
| 339 | .layer_plus_one = handle_layer + 1,
|
| 340 | #ifdef CONFIG_TRACEPOINTS
|
| 341 | .other_domain_id = get_socket_domain_id(other),
|
| 342 | #endif
|
| 343 | });
|
| 344 | return -EPERM;
|
| 345 | }
|
| 346 |
|
| 347 | static int hook_unix_may_send(struct socket *const sock,
|
| 348 | struct socket *const other)
|
| 349 | {
|
| 350 | size_t handle_layer;
|
| 351 | const struct landlock_cred_security *const subject =
|
| 352 | landlock_get_applicable_subject(current_cred(), unix_scope,
|
| 353 | &handle_layer);
|
| 354 |
|
| 355 | if (!subject)
|
| 356 | return 0;
|
| 357 |
|
| 358 | |
| 359 | * Checks if this datagram socket was already allowed to be connected
|
| 360 | * to other.
|
| 361 |
|
| 362 | if (unix_peer(sock->sk) == other->sk)
|
| 363 | return 0;
|
| 364 |
|
| 365 | if (!is_abstract_socket(other->sk))
|
| 366 | return 0;
|
| 367 |
|
| 368 | if (!sock_is_scoped(other->sk, subject->domain))
|
| 369 | return 0;
|
| 370 |
|
| 371 | landlock_log_denial(subject, &(struct landlock_request) {
|
| 372 | .type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
|
| 373 | .audit = {
|
| 374 | .type = LSM_AUDIT_DATA_NET,
|
| 375 | .u.net = &(struct lsm_network_audit) {
|
| 376 | .sk = other->sk,
|
| 377 | },
|
| 378 | },
|
| 379 | .layer_plus_one = handle_layer + 1,
|
| 380 | #ifdef CONFIG_TRACEPOINTS
|
| 381 | .other_domain_id = get_socket_domain_id(other->sk),
|
| 382 | #endif
|
| 383 | });
|
| 384 | return -EPERM;
|
| 385 | }
|
| 386 |
|
| 387 | static const struct access_masks signal_scope = {
|
| 388 | .scope = LANDLOCK_SCOPE_SIGNAL,
|
| 389 | };
|
| 390 |
|
| 391 | static int hook_task_kill(struct task_struct *const p,
|
| 392 | struct kernel_siginfo *const info, const int sig,
|
| 393 | const struct cred *cred)
|
| 394 | {
|
| 395 | bool is_scoped;
|
| 396 | size_t handle_layer;
|
| 397 | #ifdef CONFIG_TRACEPOINTS
|
| 398 | u64 target_domain_id = 0;
|
| 399 | #endif
|
| 400 | const struct landlock_cred_security *subject;
|
| 401 |
|
| 402 | if (!cred) {
|
| 403 | |
| 404 | * Always allow sending signals between threads of the same process.
|
| 405 | * This is required for process credential changes by the Native POSIX
|
| 406 | * Threads Library and implemented by the set*id(2) wrappers and
|
| 407 | * libcap(3) with tgkill(2). See nptl(7) and libpsx(3).
|
| 408 | *
|
| 409 | * This exception is similar to the __ptrace_may_access() one.
|
| 410 |
|
| 411 | if (same_thread_group(p, current))
|
| 412 | return 0;
|
| 413 |
|
| 414 |
|
| 415 | cred = current_cred();
|
| 416 | }
|
| 417 |
|
| 418 | subject = landlock_get_applicable_subject(cred, signal_scope,
|
| 419 | &handle_layer);
|
| 420 |
|
| 421 |
|
| 422 | if (!subject)
|
| 423 | return 0;
|
| 424 |
|
| 425 | scoped_guard(rcu) {
|
| 426 | const struct landlock_domain *const other =
|
| 427 | landlock_get_task_domain(p);
|
| 428 |
|
| 429 | is_scoped = domain_is_scoped(subject->domain, other,
|
| 430 | signal_scope.scope);
|
| 431 | #ifdef CONFIG_TRACEPOINTS
|
| 432 | if (other)
|
| 433 | target_domain_id = other->hierarchy->id;
|
| 434 | #endif
|
| 435 | }
|
| 436 |
|
| 437 | if (!is_scoped)
|
| 438 | return 0;
|
| 439 |
|
| 440 | landlock_log_denial(subject, &(struct landlock_request) {
|
| 441 | .type = LANDLOCK_REQUEST_SCOPE_SIGNAL,
|
| 442 | .audit = {
|
| 443 | .type = LSM_AUDIT_DATA_TASK,
|
| 444 | .u.tsk = p,
|
| 445 | },
|
| 446 | .layer_plus_one = handle_layer + 1,
|
| 447 | #ifdef CONFIG_TRACEPOINTS
|
| 448 | .trace_signal = &(struct landlock_signal_trace) {
|
| 449 | .target_domain_id = target_domain_id,
|
| 450 | .signal = sig,
|
| 451 | },
|
| 452 | #endif
|
| 453 | });
|
| 454 | return -EPERM;
|
| 455 | }
|
| 456 |
|
| 457 | static int hook_file_send_sigiotask(struct task_struct *tsk,
|
| 458 | struct fown_struct *fown, int signum)
|
| 459 | {
|
| 460 | const struct landlock_cred_security *subject;
|
| 461 | bool is_scoped = false;
|
| 462 | #ifdef CONFIG_TRACEPOINTS
|
| 463 | u64 target_domain_id = 0;
|
| 464 | #endif
|
| 465 |
|
| 466 |
|
| 467 | lockdep_assert_held(&fown->lock);
|
| 468 | subject = &landlock_file(fown->file)->fown_subject;
|
| 469 |
|
| 470 | |
| 471 | * Quick return for unowned socket.
|
| 472 | *
|
| 473 | * subject->domain has already been filtered when saved by
|
| 474 | * hook_file_set_fowner(), so there is no need to call
|
| 475 | * landlock_get_applicable_subject() here.
|
| 476 |
|
| 477 | if (!subject->domain)
|
| 478 | return 0;
|
| 479 |
|
| 480 | |
| 481 | * Always allow delivery to the file owner's own process, including a
|
| 482 | * thread-group leader reached through a process-group owner. This
|
| 483 | * mirrors hook_task_kill()'s same-process exemption and preserves the
|
| 484 | * guarantee of commit 18eb75f3af40 ("landlock: Always allow signals
|
| 485 | * between threads of the same process"), which the registration-time
|
| 486 | * check cannot honor for a process-group target.
|
| 487 |
|
| 488 | if (task_tgid(tsk) == landlock_file(fown->file)->fown_tg)
|
| 489 | return 0;
|
| 490 |
|
| 491 | scoped_guard(rcu) {
|
| 492 | const struct landlock_domain *const other =
|
| 493 | landlock_get_task_domain(tsk);
|
| 494 |
|
| 495 | is_scoped = domain_is_scoped(subject->domain, other,
|
| 496 | signal_scope.scope);
|
| 497 | #ifdef CONFIG_TRACEPOINTS
|
| 498 | if (other)
|
| 499 | target_domain_id = other->hierarchy->id;
|
| 500 | #endif
|
| 501 | }
|
| 502 |
|
| 503 | if (!is_scoped)
|
| 504 | return 0;
|
| 505 |
|
| 506 | landlock_log_denial(subject, &(struct landlock_request) {
|
| 507 | .type = LANDLOCK_REQUEST_SCOPE_SIGNAL,
|
| 508 | .audit = {
|
| 509 | .type = LSM_AUDIT_DATA_TASK,
|
| 510 | .u.tsk = tsk,
|
| 511 | },
|
| 512 | #ifdef CONFIG_SECURITY_LANDLOCK_LOG
|
| 513 | .layer_plus_one = landlock_file(fown->file)->fown_layer + 1,
|
| 514 | #endif
|
| 515 | #ifdef CONFIG_TRACEPOINTS
|
| 516 | .trace_signal = &(struct landlock_signal_trace) {
|
| 517 | .target_domain_id = target_domain_id,
|
| 518 | .signal = signum ? signum : SIGIO,
|
| 519 | },
|
| 520 | #endif
|
| 521 | });
|
| 522 | return -EPERM;
|
| 523 | }
|
| 524 |
|
| 525 | static struct security_hook_list landlock_hooks[] __ro_after_init = {
|
| 526 | LSM_HOOK_INIT(ptrace_access_check, hook_ptrace_access_check),
|
| 527 | LSM_HOOK_INIT(ptrace_traceme, hook_ptrace_traceme),
|
| 528 |
|
| 529 | LSM_HOOK_INIT(unix_stream_connect, hook_unix_stream_connect),
|
| 530 | LSM_HOOK_INIT(unix_may_send, hook_unix_may_send),
|
| 531 |
|
| 532 | LSM_HOOK_INIT(task_kill, hook_task_kill),
|
| 533 | LSM_HOOK_INIT(file_send_sigiotask, hook_file_send_sigiotask),
|
| 534 | };
|
| 535 |
|
| 536 | __init void landlock_add_task_hooks(void)
|
| 537 | {
|
| 538 | security_add_hooks(landlock_hooks, ARRAY_SIZE(landlock_hooks),
|
| 539 | &landlock_lsmid);
|
| 540 | }
|