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
FIXTURE(tiocsti)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
FIXTURE_VARIANT(tiocsti)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 */
FIXTURE_VARIANT_ADD(tiocsti,basic_pty_permissive_withcap)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
FIXTURE_VARIANT_ADD(tiocsti,basic_pty_permissive_nocap)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
FIXTURE_VARIANT_ADD(tiocsti,basic_pty_restricted_withcap)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
FIXTURE_VARIANT_ADD(tiocsti,basic_pty_restricted_nocap)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 */
FIXTURE_VARIANT_ADD(tiocsti,fdpass_pty_permissive_withcap)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
FIXTURE_VARIANT_ADD(tiocsti,fdpass_pty_permissive_nocap)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
FIXTURE_VARIANT_ADD(tiocsti,fdpass_pty_restricted_withcap)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
FIXTURE_VARIANT_ADD(tiocsti,fdpass_pty_restricted_nocap)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 */
FIXTURE_VARIANT_ADD(tiocsti,basic_nopty_permissive_withcap)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
FIXTURE_VARIANT_ADD(tiocsti,basic_nopty_permissive_nocap)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
FIXTURE_VARIANT_ADD(tiocsti,basic_nopty_restricted_withcap)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
FIXTURE_VARIANT_ADD(tiocsti,basic_nopty_restricted_nocap)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
FIXTURE_VARIANT_ADD(tiocsti,fdpass_nopty_permissive_withcap)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
FIXTURE_VARIANT_ADD(tiocsti,fdpass_nopty_permissive_nocap)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
FIXTURE_VARIANT_ADD(tiocsti,fdpass_nopty_restricted_withcap)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
FIXTURE_VARIANT_ADD(tiocsti,fdpass_nopty_restricted_nocap)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 */
send_fd_via_socket(int socket_fd,int fd_to_send)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 */
recv_fd_via_socket(int socket_fd)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
has_cap_sys_admin(void)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 */
drop_all_privs(struct __test_metadata * _metadata)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
get_legacy_tiocsti_setting(struct __test_metadata * _metadata)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
set_legacy_tiocsti_setting(struct __test_metadata * _metadata,int value)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 */
test_tiocsti_injection(struct __test_metadata * _metadata,int tty_fd)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 */
run_basic_tiocsti_test(struct __test_metadata * _metadata,FIXTURE_DATA (tiocsti)* self,const FIXTURE_VARIANT (tiocsti)* variant)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 */
run_fdpass_tiocsti_test(struct __test_metadata * _metadata,const FIXTURE_VARIANT (tiocsti)* variant,int sockfd)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
FIXTURE_SETUP(tiocsti)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
FIXTURE_TEARDOWN(tiocsti)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
TEST_F(tiocsti,test)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