File Index
Files and directories
Sign in
FILE

tty_tiocsti_test.c

File preview with syntax highlighting.

linux-6.19.14/tools/testing/selftests/tty/tty_tiocsti_test.c C
  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
 28enum 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
 65FIXTURE(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
 75FIXTURE_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 */
 98FIXTURE_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
106FIXTURE_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
114FIXTURE_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
122FIXTURE_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 */
138FIXTURE_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
146FIXTURE_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
154FIXTURE_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
162FIXTURE_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 */
184FIXTURE_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
192FIXTURE_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
200FIXTURE_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
208FIXTURE_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
216FIXTURE_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
224FIXTURE_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
232FIXTURE_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
240FIXTURE_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 */
249static 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 */
273static 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
301static 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 */
320static 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
344static 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
365static 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 */
397static 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 */
412static 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 */
450static 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
493FIXTURE_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
539FIXTURE_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
566TEST_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
650TEST_HARNESS_MAIN
~/ Read-only file preview.