1 // SPDX-License-Identifier: GPL-2.0 2 /* 3 * TTY Tests - TIOCSTI 4 * 5 * Copyright © 2025 Abhinav Saxena <xandfury@gmail.com> 6 */ 7 8 #include <stdio.h> 9 #include <stdlib.h> 10 #include <unistd.h> 11 #include <fcntl.h> 12 #include <sys/ioctl.h> 13 #include <errno.h> 14 #include <stdbool.h> 15 #include <string.h> 16 #include <sys/socket.h> 17 #include <sys/wait.h> 18 #include <pwd.h> 19 #include <termios.h> 20 #include <grp.h> 21 #include <sys/capability.h> 22 #include <sys/prctl.h> 23 #include <pty.h> 24 #include <utmp.h> 25 26 #include "../kselftest_harness.h" 27 28 enum test_type { 29 TEST_PTY_TIOCSTI_BASIC, 30 TEST_PTY_TIOCSTI_FD_PASSING, 31 /* other tests cases such as serial may be added. */ 32 }; 33 34 /* 35 * Test Strategy: 36 * - Basic tests: Use PTY with/without TIOCSCTTY (controlling terminal for 37 * current process) 38 * - FD passing tests: Child creates PTY, parent receives FD (demonstrates 39 * security issue) 40 * 41 * SECURITY VULNERABILITY DEMONSTRATION: 42 * FD passing tests show that TIOCSTI uses CURRENT process credentials, not 43 * opener credentials. This means privileged processes can be given FDs from 44 * unprivileged processes and successfully perform TIOCSTI operations that the 45 * unprivileged process couldn't do directly. 46 * 47 * Attack scenario: 48 * 1. Unprivileged process opens TTY (direct TIOCSTI fails due to lack of 49 * privileges) 50 * 2. Unprivileged process passes FD to privileged process via SCM_RIGHTS 51 * 3. Privileged process can use TIOCSTI on the FD (succeeds due to its 52 * privileges) 53 * 4. Result: Effective privilege escalation via file descriptor passing 54 * 55 * This matches the kernel logic in tiocsti(): 56 * 1. if (!tty_legacy_tiocsti && !capable(CAP_SYS_ADMIN)) return -EIO; 57 * 2. if ((current->signal->tty != tty) && !capable(CAP_SYS_ADMIN)) 58 * return -EPERM; 59 * Note: Both checks use capable() on CURRENT process, not FD opener! 60 * 61 * If the file credentials were also checked along with the capable() checks 62 * then the results for FD pass tests would be consistent with the basic tests. 63 */ 64 65 FIXTURE(tiocsti) 66 { 67 int pty_master_fd; /* PTY - for basic tests */ 68 int pty_slave_fd; 69 bool has_pty; 70 bool initial_cap_sys_admin; 71 int original_legacy_tiocsti_setting; 72 bool can_modify_sysctl; 73 }; 74 75 FIXTURE_VARIANT(tiocsti) 76 { 77 const enum test_type test_type; 78 const bool controlling_tty; /* true=current->signal->tty == tty */ 79 const int legacy_tiocsti; /* 0=restricted, 1=permissive */ 80 const bool requires_cap; /* true=with CAP_SYS_ADMIN, false=without */ 81 const int expected_success; /* 0=success, -EIO/-EPERM=specific error */ 82 }; 83 84 /* 85 * Tests Controlling Terminal Variants (current->signal->tty == tty) 86 * 87 * TIOCSTI Test Matrix: 88 * 89 * | legacy_tiocsti | CAP_SYS_ADMIN | Expected Result | Error | 90 * |----------------|---------------|-----------------|-------| 91 * | 1 (permissive) | true | SUCCESS | - | 92 * | 1 (permissive) | false | SUCCESS | - | 93 * | 0 (restricted) | true | SUCCESS | - | 94 * | 0 (restricted) | false | FAILURE | -EIO | 95 */ 96 97 /* clang-format off */ 98 FIXTURE_VARIANT_ADD(tiocsti, basic_pty_permissive_withcap) { 99 .test_type = TEST_PTY_TIOCSTI_BASIC, 100 .controlling_tty = true, 101 .legacy_tiocsti = 1, 102 .requires_cap = true, 103 .expected_success = 0, 104 }; 105 106 FIXTURE_VARIANT_ADD(tiocsti, basic_pty_permissive_nocap) { 107 .test_type = TEST_PTY_TIOCSTI_BASIC, 108 .controlling_tty = true, 109 .legacy_tiocsti = 1, 110 .requires_cap = false, 111 .expected_success = 0, 112 }; 113 114 FIXTURE_VARIANT_ADD(tiocsti, basic_pty_restricted_withcap) { 115 .test_type = TEST_PTY_TIOCSTI_BASIC, 116 .controlling_tty = true, 117 .legacy_tiocsti = 0, 118 .requires_cap = true, 119 .expected_success = 0, 120 }; 121 122 FIXTURE_VARIANT_ADD(tiocsti, basic_pty_restricted_nocap) { 123 .test_type = TEST_PTY_TIOCSTI_BASIC, 124 .controlling_tty = true, 125 .legacy_tiocsti = 0, 126 .requires_cap = false, 127 .expected_success = -EIO, /* FAILURE: legacy restriction */ 128 }; /* clang-format on */ 129 130 /* 131 * Note for FD Passing Test Variants 132 * Since we're testing the scenario where an unprivileged process pass an FD 133 * to a privileged one, .requires_cap here means the caps of the child process. 134 * Not the parent; parent would always be privileged. 135 */ 136 137 /* clang-format off */ 138 FIXTURE_VARIANT_ADD(tiocsti, fdpass_pty_permissive_withcap) { 139 .test_type = TEST_PTY_TIOCSTI_FD_PASSING, 140 .controlling_tty = true, 141 .legacy_tiocsti = 1, 142 .requires_cap = true, 143 .expected_success = 0, 144 }; 145 146 FIXTURE_VARIANT_ADD(tiocsti, fdpass_pty_permissive_nocap) { 147 .test_type = TEST_PTY_TIOCSTI_FD_PASSING, 148 .controlling_tty = true, 149 .legacy_tiocsti = 1, 150 .requires_cap = false, 151 .expected_success = 0, 152 }; 153 154 FIXTURE_VARIANT_ADD(tiocsti, fdpass_pty_restricted_withcap) { 155 .test_type = TEST_PTY_TIOCSTI_FD_PASSING, 156 .controlling_tty = true, 157 .legacy_tiocsti = 0, 158 .requires_cap = true, 159 .expected_success = 0, 160 }; 161 162 FIXTURE_VARIANT_ADD(tiocsti, fdpass_pty_restricted_nocap) { 163 .test_type = TEST_PTY_TIOCSTI_FD_PASSING, 164 .controlling_tty = true, 165 .legacy_tiocsti = 0, 166 .requires_cap = false, 167 .expected_success = -EIO, 168 }; /* clang-format on */ 169 170 /* 171 * Non-Controlling Terminal Variants (current->signal->tty != tty) 172 * 173 * TIOCSTI Test Matrix: 174 * 175 * | legacy_tiocsti | CAP_SYS_ADMIN | Expected Result | Error | 176 * |----------------|---------------|-----------------|-------| 177 * | 1 (permissive) | true | SUCCESS | - | 178 * | 1 (permissive) | false | FAILURE | -EPERM| 179 * | 0 (restricted) | true | SUCCESS | - | 180 * | 0 (restricted) | false | FAILURE | -EIO | 181 */ 182 183 /* clang-format off */ 184 FIXTURE_VARIANT_ADD(tiocsti, basic_nopty_permissive_withcap) { 185 .test_type = TEST_PTY_TIOCSTI_BASIC, 186 .controlling_tty = false, 187 .legacy_tiocsti = 1, 188 .requires_cap = true, 189 .expected_success = 0, 190 }; 191 192 FIXTURE_VARIANT_ADD(tiocsti, basic_nopty_permissive_nocap) { 193 .test_type = TEST_PTY_TIOCSTI_BASIC, 194 .controlling_tty = false, 195 .legacy_tiocsti = 1, 196 .requires_cap = false, 197 .expected_success = -EPERM, 198 }; 199 200 FIXTURE_VARIANT_ADD(tiocsti, basic_nopty_restricted_withcap) { 201 .test_type = TEST_PTY_TIOCSTI_BASIC, 202 .controlling_tty = false, 203 .legacy_tiocsti = 0, 204 .requires_cap = true, 205 .expected_success = 0, 206 }; 207 208 FIXTURE_VARIANT_ADD(tiocsti, basic_nopty_restricted_nocap) { 209 .test_type = TEST_PTY_TIOCSTI_BASIC, 210 .controlling_tty = false, 211 .legacy_tiocsti = 0, 212 .requires_cap = false, 213 .expected_success = -EIO, 214 }; 215 216 FIXTURE_VARIANT_ADD(tiocsti, fdpass_nopty_permissive_withcap) { 217 .test_type = TEST_PTY_TIOCSTI_FD_PASSING, 218 .controlling_tty = false, 219 .legacy_tiocsti = 1, 220 .requires_cap = true, 221 .expected_success = 0, 222 }; 223 224 FIXTURE_VARIANT_ADD(tiocsti, fdpass_nopty_permissive_nocap) { 225 .test_type = TEST_PTY_TIOCSTI_FD_PASSING, 226 .controlling_tty = false, 227 .legacy_tiocsti = 1, 228 .requires_cap = false, 229 .expected_success = -EPERM, 230 }; 231 232 FIXTURE_VARIANT_ADD(tiocsti, fdpass_nopty_restricted_withcap) { 233 .test_type = TEST_PTY_TIOCSTI_FD_PASSING, 234 .controlling_tty = false, 235 .legacy_tiocsti = 0, 236 .requires_cap = true, 237 .expected_success = 0, 238 }; 239 240 FIXTURE_VARIANT_ADD(tiocsti, fdpass_nopty_restricted_nocap) { 241 .test_type = TEST_PTY_TIOCSTI_FD_PASSING, 242 .controlling_tty = false, 243 .legacy_tiocsti = 0, 244 .requires_cap = false, 245 .expected_success = -EIO, 246 }; /* clang-format on */ 247 248 /* Helper function to send FD via SCM_RIGHTS */ 249 static int send_fd_via_socket(int socket_fd, int fd_to_send) 250 { 251 struct msghdr msg = { 0 }; 252 struct cmsghdr *cmsg; 253 char cmsg_buf[CMSG_SPACE(sizeof(int))]; 254 char dummy_data = 'F'; 255 struct iovec iov = { .iov_base = &dummy_data, .iov_len = 1 }; 256 257 msg.msg_iov = &iov; 258 msg.msg_iovlen = 1; 259 msg.msg_control = cmsg_buf; 260 msg.msg_controllen = sizeof(cmsg_buf); 261 262 cmsg = CMSG_FIRSTHDR(&msg); 263 cmsg->cmsg_level = SOL_SOCKET; 264 cmsg->cmsg_type = SCM_RIGHTS; 265 cmsg->cmsg_len = CMSG_LEN(sizeof(int)); 266 267 memcpy(CMSG_DATA(cmsg), &fd_to_send, sizeof(int)); 268 269 return sendmsg(socket_fd, &msg, 0) < 0 ? -1 : 0; 270 } 271 272 /* Helper function to receive FD via SCM_RIGHTS */ 273 static int recv_fd_via_socket(int socket_fd) 274 { 275 struct msghdr msg = { 0 }; 276 struct cmsghdr *cmsg; 277 char cmsg_buf[CMSG_SPACE(sizeof(int))]; 278 char dummy_data; 279 struct iovec iov = { .iov_base = &dummy_data, .iov_len = 1 }; 280 int received_fd = -1; 281 282 msg.msg_iov = &iov; 283 msg.msg_iovlen = 1; 284 msg.msg_control = cmsg_buf; 285 msg.msg_controllen = sizeof(cmsg_buf); 286 287 if (recvmsg(socket_fd, &msg, 0) < 0) 288 return -1; 289 290 for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { 291 if (cmsg->cmsg_level == SOL_SOCKET && 292 cmsg->cmsg_type == SCM_RIGHTS) { 293 memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int)); 294 break; 295 } 296 } 297 298 return received_fd; 299 } 300 301 static inline bool has_cap_sys_admin(void) 302 { 303 cap_t caps = cap_get_proc(); 304 305 if (!caps) 306 return false; 307 308 cap_flag_value_t cap_val; 309 bool has_cap = (cap_get_flag(caps, CAP_SYS_ADMIN, CAP_EFFECTIVE, 310 &cap_val) == 0) && 311 (cap_val == CAP_SET); 312 313 cap_free(caps); 314 return has_cap; 315 } 316 317 /* 318 * Switch to non-root user and clear all capabilities 319 */ 320 static inline bool drop_all_privs(struct __test_metadata *_metadata) 321 { 322 /* Drop supplementary groups */ 323 ASSERT_EQ(setgroups(0, NULL), 0); 324 325 /* Switch to non-root user */ 326 ASSERT_EQ(setgid(1000), 0); 327 ASSERT_EQ(setuid(1000), 0); 328 329 /* Clear all capabilities */ 330 cap_t empty = cap_init(); 331 332 ASSERT_NE(empty, NULL); 333 ASSERT_EQ(cap_set_proc(empty), 0); 334 cap_free(empty); 335 336 /* Prevent privilege regain */ 337 ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0); 338 339 /* Verify privilege drop */ 340 ASSERT_FALSE(has_cap_sys_admin()); 341 return true; 342 } 343 344 static inline int get_legacy_tiocsti_setting(struct __test_metadata *_metadata) 345 { 346 FILE *fp; 347 int value = -1; 348 349 fp = fopen("/proc/sys/dev/tty/legacy_tiocsti", "r"); 350 if (!fp) { 351 /* legacy_tiocsti sysctl not available (kernel < 6.2) */ 352 return -1; 353 } 354 355 if (fscanf(fp, "%d", &value) == 1 && fclose(fp) == 0) { 356 if (value < 0 || value > 1) 357 value = -1; /* Invalid value */ 358 } else { 359 value = -1; /* Failed to parse */ 360 } 361 362 return value; 363 } 364 365 static inline bool set_legacy_tiocsti_setting(struct __test_metadata *_metadata, 366 int value) 367 { 368 FILE *fp; 369 bool success = false; 370 371 /* Sanity-check the value */ 372 ASSERT_GE(value, 0); 373 ASSERT_LE(value, 1); 374 375 /* 376 * Try to open for writing; if we lack permission, return false so 377 * the test harness will skip variants that need to change it 378 */ 379 fp = fopen("/proc/sys/dev/tty/legacy_tiocsti", "w"); 380 if (!fp) 381 return false; 382 383 /* Write the new setting */ 384 if (fprintf(fp, "%d\n", value) > 0 && fclose(fp) == 0) 385 success = true; 386 else 387 TH_LOG("Failed to write legacy_tiocsti: %s", strerror(errno)); 388 389 return success; 390 } 391 392 /* 393 * TIOCSTI injection test function 394 * @tty_fd: TTY slave file descriptor to test TIOCSTI on 395 * Returns: 0 on success, -errno on failure 396 */ 397 static inline int test_tiocsti_injection(struct __test_metadata *_metadata, 398 int tty_fd) 399 { 400 int ret; 401 char inject_char = 'V'; 402 403 errno = 0; 404 ret = ioctl(tty_fd, TIOCSTI, &inject_char); 405 return ret == 0 ? 0 : -errno; 406 } 407 408 /* 409 * Child process: test TIOCSTI directly with capability/controlling 410 * terminal setup 411 */ 412 static void run_basic_tiocsti_test(struct __test_metadata *_metadata, 413 FIXTURE_DATA(tiocsti) * self, 414 const FIXTURE_VARIANT(tiocsti) * variant) 415 { 416 /* Handle capability requirements */ 417 if (self->initial_cap_sys_admin && !variant->requires_cap) 418 ASSERT_TRUE(drop_all_privs(_metadata)); 419 420 if (variant->controlling_tty) { 421 /* 422 * Create new session and set PTY as 423 * controlling terminal 424 */ 425 pid_t sid = setsid(); 426 427 ASSERT_GE(sid, 0); 428 ASSERT_EQ(ioctl(self->pty_slave_fd, TIOCSCTTY, 0), 0); 429 } 430 431 /* 432 * Validate test environment setup and verify final 433 * capability state matches expectation 434 * after potential drop. 435 */ 436 ASSERT_TRUE(self->has_pty); 437 ASSERT_EQ(has_cap_sys_admin(), variant->requires_cap); 438 439 /* Test TIOCSTI and validate result */ 440 int result = test_tiocsti_injection(_metadata, self->pty_slave_fd); 441 442 /* Check against expected result from variant */ 443 EXPECT_EQ(result, variant->expected_success); 444 _exit(0); 445 } 446 447 /* 448 * Child process: create PTY and then pass FD to parent via SCM_RIGHTS 449 */ 450 static void run_fdpass_tiocsti_test(struct __test_metadata *_metadata, 451 const FIXTURE_VARIANT(tiocsti) * variant, 452 int sockfd) 453 { 454 signal(SIGHUP, SIG_IGN); 455 456 /* Handle privilege dropping */ 457 if (!variant->requires_cap && has_cap_sys_admin()) 458 ASSERT_TRUE(drop_all_privs(_metadata)); 459 460 /* Create child's PTY */ 461 int child_master_fd, child_slave_fd; 462 463 ASSERT_EQ(openpty(&child_master_fd, &child_slave_fd, NULL, NULL, NULL), 464 0); 465 466 if (variant->controlling_tty) { 467 pid_t sid = setsid(); 468 469 ASSERT_GE(sid, 0); 470 ASSERT_EQ(ioctl(child_slave_fd, TIOCSCTTY, 0), 0); 471 } 472 473 /* Test child's direct TIOCSTI for reference */ 474 int direct_result = test_tiocsti_injection(_metadata, child_slave_fd); 475 476 EXPECT_EQ(direct_result, variant->expected_success); 477 478 /* Send FD to parent */ 479 ASSERT_EQ(send_fd_via_socket(sockfd, child_slave_fd), 0); 480 481 /* Wait for parent completion signal */ 482 char sync_byte; 483 ssize_t bytes_read = read(sockfd, &sync_byte, 1); 484 485 ASSERT_EQ(bytes_read, 1); 486 487 close(child_master_fd); 488 close(child_slave_fd); 489 close(sockfd); 490 _exit(0); 491 } 492 493 FIXTURE_SETUP(tiocsti) 494 { 495 /* Create PTY pair for basic tests */ 496 self->has_pty = (openpty(&self->pty_master_fd, &self->pty_slave_fd, 497 NULL, NULL, NULL) == 0); 498 if (!self->has_pty) { 499 self->pty_master_fd = -1; 500 self->pty_slave_fd = -1; 501 } 502 503 self->initial_cap_sys_admin = has_cap_sys_admin(); 504 self->original_legacy_tiocsti_setting = 505 get_legacy_tiocsti_setting(_metadata); 506 507 if (self->original_legacy_tiocsti_setting < 0) 508 SKIP(return, 509 "legacy_tiocsti sysctl not available (kernel < 6.2)"); 510 511 /* Common skip conditions */ 512 if (variant->test_type == TEST_PTY_TIOCSTI_BASIC && !self->has_pty) 513 SKIP(return, "PTY not available for controlling terminal test"); 514 515 if (variant->test_type == TEST_PTY_TIOCSTI_FD_PASSING && 516 !self->initial_cap_sys_admin) 517 SKIP(return, "FD Pass tests require CAP_SYS_ADMIN"); 518 519 if (variant->requires_cap && !self->initial_cap_sys_admin) 520 SKIP(return, "Test requires initial CAP_SYS_ADMIN"); 521 522 /* Test if we can modify the sysctl (requires appropriate privileges) */ 523 self->can_modify_sysctl = set_legacy_tiocsti_setting( 524 _metadata, self->original_legacy_tiocsti_setting); 525 526 /* Sysctl setup based on variant */ 527 if (self->can_modify_sysctl && 528 self->original_legacy_tiocsti_setting != variant->legacy_tiocsti) { 529 if (!set_legacy_tiocsti_setting(_metadata, 530 variant->legacy_tiocsti)) 531 SKIP(return, "Failed to set legacy_tiocsti sysctl"); 532 533 } else if (!self->can_modify_sysctl && 534 self->original_legacy_tiocsti_setting != 535 variant->legacy_tiocsti) 536 SKIP(return, "legacy_tiocsti setting mismatch"); 537 } 538 539 FIXTURE_TEARDOWN(tiocsti) 540 { 541 /* 542 * Backup restoration - 543 * each test should restore its own sysctl changes 544 */ 545 if (self->can_modify_sysctl) { 546 int current_value = get_legacy_tiocsti_setting(_metadata); 547 548 if (current_value != self->original_legacy_tiocsti_setting) { 549 TH_LOG("Backup: Restoring legacy_tiocsti from %d to %d", 550 current_value, 551 self->original_legacy_tiocsti_setting); 552 set_legacy_tiocsti_setting( 553 _metadata, 554 self->original_legacy_tiocsti_setting); 555 } 556 } 557 558 if (self->has_pty) { 559 if (self->pty_master_fd >= 0) 560 close(self->pty_master_fd); 561 if (self->pty_slave_fd >= 0) 562 close(self->pty_slave_fd); 563 } 564 } 565 566 TEST_F(tiocsti, test) 567 { 568 int status; 569 pid_t child_pid; 570 571 if (variant->test_type == TEST_PTY_TIOCSTI_BASIC) { 572 /* ===== BASIC TIOCSTI TEST ===== */ 573 child_pid = fork(); 574 ASSERT_GE(child_pid, 0); 575 576 /* Perform the actual test in the child process */ 577 if (child_pid == 0) 578 run_basic_tiocsti_test(_metadata, self, variant); 579 580 } else { 581 /* ===== FD PASSING SECURITY TEST ===== */ 582 int sockpair[2]; 583 584 ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair), 0); 585 586 child_pid = fork(); 587 ASSERT_GE(child_pid, 0); 588 589 if (child_pid == 0) { 590 /* Child process - create PTY and send FD */ 591 close(sockpair[0]); 592 run_fdpass_tiocsti_test(_metadata, variant, 593 sockpair[1]); 594 } 595 596 /* Parent process - receive FD and test TIOCSTI */ 597 close(sockpair[1]); 598 599 int received_fd = recv_fd_via_socket(sockpair[0]); 600 601 ASSERT_GE(received_fd, 0); 602 603 bool parent_has_cap = self->initial_cap_sys_admin; 604 605 TH_LOG("=== TIOCSTI FD Passing Test Context ==="); 606 TH_LOG("legacy_tiocsti: %d, Parent CAP_SYS_ADMIN: %s, Child: %s", 607 variant->legacy_tiocsti, parent_has_cap ? "yes" : "no", 608 variant->requires_cap ? "kept" : "dropped"); 609 610 /* SECURITY TEST: Try TIOCSTI with FD opened by child */ 611 int result = test_tiocsti_injection(_metadata, received_fd); 612 613 /* Log security concern if demonstrated */ 614 if (result == 0 && !variant->requires_cap) { 615 TH_LOG("*** SECURITY CONCERN DEMONSTRATED ***"); 616 TH_LOG("Privileged parent can use TIOCSTI on FD from unprivileged child"); 617 TH_LOG("This shows current process credentials are used, not opener credentials"); 618 } 619 620 EXPECT_EQ(result, variant->expected_success) 621 { 622 TH_LOG("FD passing: expected error %d, got %d", 623 variant->expected_success, result); 624 } 625 626 /* Signal child completion */ 627 char sync_byte = 'D'; 628 ssize_t bytes_written = write(sockpair[0], &sync_byte, 1); 629 630 ASSERT_EQ(bytes_written, 1); 631 632 close(received_fd); 633 close(sockpair[0]); 634 } 635 636 /* Common child process cleanup for both test types */ 637 ASSERT_EQ(waitpid(child_pid, &status, 0), child_pid); 638 639 if (WIFSIGNALED(status)) { 640 TH_LOG("Child terminated by signal %d", WTERMSIG(status)); 641 ASSERT_FALSE(WIFSIGNALED(status)) 642 { 643 TH_LOG("Child process failed assertion"); 644 } 645 } else { 646 EXPECT_EQ(WEXITSTATUS(status), 0); 647 } 648 } 649 650 TEST_HARNESS_MAIN 651