[PATCH v3 19/20] selftests/landlock: Add landlock_enforce_domain trace tests

Mickaël Salaün <[email protected]>
Newsgroups org.kernel.vger.linux-trace-kernel,org.kernel.vger.linux-security-module
Message-ID <[email protected]>
Add trace tests for the landlock_enforce_domain event in trace_test.c,
asserting field counts after the syscall returns rather than line
ordering across per-CPU buffers.  They cover single-threaded and TSYNC
enforcement (complete and process_wide set), a multi-threaded non-TSYNC
process (process_wide clear), the single-threaded non-leader edge case,
the flags-only path that creates no domain, and a thread-sync abort that
emits create_domain and free_domain but no enforce_domain.

landlock_enforce_domain is added to the fixture enable path and every
disable list so its zero-events assertions cannot be tripped by a stray
enforcement event.

Test coverage for security/landlock is 91.6% of 2552 lines according to
LLVM 22.

Cc: Günther Noack <[email protected]>
Cc: Tingmao Wang <[email protected]>
Signed-off-by: Mickaël Salaün <[email protected]>
---

Changes since v2:
- New patch.
---
 tools/testing/selftests/landlock/trace.h      |   9 +
 tools/testing/selftests/landlock/trace_test.c | 522 ++++++++++++++++++
 2 files changed, 531 insertions(+)

diff --git a/tools/testing/selftests/landlock/trace.h b/tools/testing/selftests/landlock/trace.h
index e03c689ef398..9e901823ddaf 100644
--- a/tools/testing/selftests/landlock/trace.h
+++ b/tools/testing/selftests/landlock/trace.h
@@ -25,6 +25,8 @@
 	TRACEFS_LANDLOCK_DIR "/landlock_create_ruleset/enable"
 #define TRACEFS_CREATE_DOMAIN_ENABLE \
 	TRACEFS_LANDLOCK_DIR "/landlock_create_domain/enable"
+#define TRACEFS_ENFORCE_DOMAIN_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_enforce_domain/enable"
 #define TRACEFS_ADD_RULE_FS_ENABLE \
 	TRACEFS_LANDLOCK_DIR "/landlock_add_rule_fs/enable"
 #define TRACEFS_ADD_RULE_NET_ENABLE \
@@ -108,6 +110,13 @@
 	"parent=[0-9a-f]\\+ "      \
 	"ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
 
+#define REGEX_ENFORCE_DOMAIN(task)  \
+	TRACE_PREFIX(task)          \
+	"landlock_enforce_domain: " \
+	"domain=[0-9a-f]\\+ "       \
+	"complete=[01] "            \
+	"process_wide=[01]$"
+
 #define REGEX_CHECK_RULE_FS(task)  \
 	TRACE_PREFIX(task)         \
 	"landlock_check_rule_fs: " \
diff --git a/tools/testing/selftests/landlock/trace_test.c b/tools/testing/selftests/landlock/trace_test.c
index a141f22ad98f..e2415b98868d 100644
--- a/tools/testing/selftests/landlock/trace_test.c
+++ b/tools/testing/selftests/landlock/trace_test.c
@@ -9,6 +9,7 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <linux/landlock.h>
+#include <pthread.h>
 #include <sched.h>
 #include <stdio.h>
 #include <string.h>
@@ -47,6 +48,7 @@ FIXTURE_SETUP(trace)
 
 	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, true));
 	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, true));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, true));
 	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, true));
 	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, true));
 	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
@@ -69,6 +71,7 @@ FIXTURE_TEARDOWN(trace)
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false);
 	tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false);
+	tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, false);
 	tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false);
 	tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, false);
 	tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
@@ -98,6 +101,8 @@ TEST_F(trace, no_trace_when_disabled)
 	ASSERT_EQ(0,
 		  tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false));
 	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false));
+	ASSERT_EQ(0,
+		  tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, false));
 	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false));
 	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, false));
 	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false));
@@ -1069,6 +1074,523 @@ TEST_F(trace, free_ruleset_on_close)
 	free(buf);
 }
 
+/*
+ * Counts landlock_enforce_domain lines, filtered by @domain (NULL matches any),
+ * @complete and @process_wide (a negative value matches any).  Builds the
+ * anchored regex dynamically so a single helper covers every field assertion.
+ */
+static int count_enforce_matches(const char *buf, const char *domain,
+				 int complete, int process_wide)
+{
+	char pattern[512], dom[80], comp[8], pw[8];
+
+	if (domain)
+		snprintf(dom, sizeof(dom), "%s", domain);
+	else
+		snprintf(dom, sizeof(dom), "[0-9a-f]\\+");
+	if (complete < 0)
+		snprintf(comp, sizeof(comp), "[01]");
+	else
+		snprintf(comp, sizeof(comp), "%d", complete);
+	if (process_wide < 0)
+		snprintf(pw, sizeof(pw), "[01]");
+	else
+		snprintf(pw, sizeof(pw), "%d", process_wide);
+
+	snprintf(pattern, sizeof(pattern),
+		 TRACE_PREFIX(TRACE_TASK) "landlock_enforce_domain: "
+					  "domain=%s "
+					  "complete=%s process_wide=%s$",
+		 dom, comp, pw);
+	return tracefs_count_matches(buf, pattern);
+}
+
+/* Idle sibling: waits on the barrier so it is a live thread, then sleeps. */
+static void *enforce_idle(void *arg)
+{
+	pthread_barrier_t *barrier = arg;
+
+	pthread_barrier_wait(barrier);
+	while (true)
+		sleep(1);
+	return NULL;
+}
+
+/*
+ * Child body: spawns @nthreads idle siblings (barrier-synchronized so they are
+ * live when the syscall runs), then enforces a domain with @flags.  Returns 0
+ * on success; the process exits afterwards, reaping the siblings.
+ */
+static int child_enforce(int nthreads, __u32 flags)
+{
+	pthread_t threads[8];
+	pthread_barrier_t barrier;
+	int ruleset_fd, i;
+
+	if (nthreads > 0) {
+		if (pthread_barrier_init(&barrier, NULL, nthreads + 1))
+			return 1;
+		for (i = 0; i < nthreads; i++)
+			if (pthread_create(&threads[i], NULL, enforce_idle,
+					   &barrier))
+				return 1;
+		pthread_barrier_wait(&barrier);
+	}
+
+	ruleset_fd = build_enforce_ruleset();
+	if (ruleset_fd < 0)
+		return 1;
+
+	prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	if (landlock_restrict_self(ruleset_fd, flags))
+		return 1;
+	close(ruleset_fd);
+	return 0;
+}
+
+/* Reads the group's live thread count from /proc/self/status (get_nr_threads). */
+static int read_thread_count(void)
+{
+	char line[128];
+	FILE *f;
+	int n = -1;
+
+	f = fopen("/proc/self/status", "re");
+	if (!f)
+		return -1;
+	while (fgets(line, sizeof(line), f))
+		if (sscanf(line, "Threads: %d", &n) == 1)
+			break;
+	fclose(f);
+	return n;
+}
+
+/* Exit code the non-leader child uses to signal "could not go single-threaded". */
+#define ENFORCE_SKIP_EXIT 2
+
+/*
+ * Runs in a spawned thread after the group leader called pthread_exit().  Waits
+ * until it is the only live thread (get_nr_threads() == 1 despite not being the
+ * leader), then enforces a domain.
+ */
+static void *enforce_nonleader(void *arg)
+{
+	int ruleset_fd, i;
+
+	for (i = 0; i < 200; i++) {
+		if (read_thread_count() == 1)
+			break;
+		usleep(10000);
+	}
+	if (read_thread_count() != 1)
+		_exit(ENFORCE_SKIP_EXIT);
+
+	ruleset_fd = build_enforce_ruleset();
+	if (ruleset_fd < 0)
+		_exit(1);
+	prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	if (landlock_restrict_self(ruleset_fd, 0))
+		_exit(1);
+	_exit(0);
+}
+
+/*
+ * Verifies the single-threaded non-TSYNC case: one create_domain and exactly
+ * one enforce_domain, with complete=1 (the operation concludes) and
+ * process_wide=1 (get_nr_threads() == 1), sharing the created domain ID.
+ */
+TEST_F(trace, enforce_single)
+{
+	pid_t pid;
+	int status;
+	char *buf;
+	char domain[64];
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+	if (pid == 0)
+		_exit(child_enforce(0, 0));
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_EQ(1,
+		  tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)));
+	EXPECT_EQ(1, count_enforce_matches(buf, NULL, -1, -1))
+	{
+		TH_LOG("Expected 1 enforce_domain event\n%s", buf);
+	}
+	EXPECT_EQ(1, count_enforce_matches(buf, NULL, 1, 1));
+
+	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+					   "domain", domain, sizeof(domain)));
+	EXPECT_EQ(1, count_enforce_matches(buf, domain, 1, 1));
+
+	free(buf);
+}
+
+/*
+ * Verifies that TSYNC on a single-threaded process still concludes: one
+ * enforce_domain with complete=1 and process_wide=1.
+ */
+TEST_F(trace, enforce_tsync_single)
+{
+	pid_t pid;
+	int status;
+	char *buf;
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+	if (pid == 0)
+		_exit(child_enforce(0, LANDLOCK_RESTRICT_SELF_TSYNC));
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_EQ(1,
+		  tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)));
+	EXPECT_EQ(1, count_enforce_matches(buf, NULL, -1, -1));
+	EXPECT_EQ(1, count_enforce_matches(buf, NULL, 1, 1));
+
+	free(buf);
+}
+
+/*
+ * Verifies TSYNC across a multi-threaded process: one create_domain and
+ * exactly N+1 enforce_domain events (caller plus N siblings), of which exactly
+ * one is complete=1, all are process_wide=1, and all carry the same domain ID.
+ * These are order-independent field counts, evaluated after the syscall
+ * returns (the caller has waited on all siblings by then).
+ */
+TEST_F(trace, enforce_tsync_multithread)
+{
+	const int nsiblings = 3;
+	pid_t pid;
+	int status;
+	char *buf;
+	char domain[64];
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+	if (pid == 0)
+		_exit(child_enforce(nsiblings, LANDLOCK_RESTRICT_SELF_TSYNC));
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_EQ(1,
+		  tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)));
+	EXPECT_EQ(nsiblings + 1, count_enforce_matches(buf, NULL, -1, -1))
+	{
+		TH_LOG("Expected %d enforce_domain events\n%s", nsiblings + 1,
+		       buf);
+	}
+	EXPECT_EQ(1, count_enforce_matches(buf, NULL, 1, -1));
+	EXPECT_EQ(nsiblings, count_enforce_matches(buf, NULL, 0, -1));
+	EXPECT_EQ(nsiblings + 1, count_enforce_matches(buf, NULL, -1, 1));
+
+	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+					   "domain", domain, sizeof(domain)));
+	EXPECT_EQ(nsiblings + 1, count_enforce_matches(buf, domain, -1, -1));
+
+	free(buf);
+}
+
+/*
+ * Verifies the disambiguation case: a non-TSYNC restrict_self on a
+ * multi-threaded process enforces only the caller, so the single enforce_domain
+ * has process_wide=0 (the siblings stay unconfined).
+ */
+TEST_F(trace, enforce_multithread_non_tsync)
+{
+	const int nsiblings = 3;
+	pid_t pid;
+	int status;
+	char *buf;
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+	if (pid == 0)
+		_exit(child_enforce(nsiblings, 0));
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_EQ(1,
+		  tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)));
+	EXPECT_EQ(1, count_enforce_matches(buf, NULL, -1, -1));
+	EXPECT_EQ(1, count_enforce_matches(buf, NULL, 1, 0))
+	{
+		TH_LOG("Expected process_wide=0 enforce_domain event\n%s", buf);
+	}
+
+	free(buf);
+}
+
+/*
+ * Verifies that a single-threaded caller that is not the group leader (the
+ * leader called pthread_exit()) still reports process_wide=1, the case
+ * get_nr_threads() handles and the leader-relative thread_group_empty() would
+ * not.  SKIPs if the group cannot be reduced to one live thread.
+ */
+TEST_F(trace, enforce_single_non_leader)
+{
+	pid_t pid;
+	int status;
+	char *buf;
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+	if (pid == 0) {
+		pthread_t worker;
+
+		if (pthread_create(&worker, NULL, enforce_nonleader, NULL))
+			_exit(1);
+		/* Leader leaves; the worker becomes the only live thread. */
+		pthread_exit(NULL);
+	}
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	if (WEXITSTATUS(status) == ENFORCE_SKIP_EXIT)
+		SKIP(return, "could not reduce group to a single thread");
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_EQ(1,
+		  tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)));
+	EXPECT_EQ(1, count_enforce_matches(buf, NULL, 1, 1))
+	{
+		TH_LOG("Expected process_wide=1 for non-leader caller\n%s",
+		       buf);
+	}
+
+	free(buf);
+}
+
+/*
+ * Verifies the flags-only path (ruleset_fd == -1) creates no domain and emits
+ * neither create_domain nor enforce_domain, with and without TSYNC.
+ */
+TEST_F(trace, enforce_flags_only)
+{
+	pid_t pid;
+	int status;
+	char *buf;
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+	if (pid == 0) {
+		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+		if (landlock_restrict_self(
+			    -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF))
+			_exit(1);
+		if (landlock_restrict_self(
+			    -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+					LANDLOCK_RESTRICT_SELF_TSYNC))
+			_exit(1);
+		_exit(0);
+	}
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_EQ(0,
+		  tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)));
+	EXPECT_EQ(0, count_enforce_matches(buf, NULL, -1, -1))
+	{
+		TH_LOG("No enforce_domain expected on flags-only path\n%s",
+		       buf);
+	}
+
+	free(buf);
+}
+
+static void enforce_nop_handler(int sig)
+{
+}
+
+struct abort_signaler_data {
+	pthread_t target;
+	volatile bool stop;
+};
+
+/* Hammers the target thread with SIGUSR1 to interrupt the TSYNC prepare wait. */
+static void *abort_signaler(void *arg)
+{
+	struct abort_signaler_data *data = arg;
+
+	while (!data->stop)
+		pthread_kill(data->target, SIGUSR1);
+	return NULL;
+}
+
+/*
+ * Child body for the abort test: with idle siblings and a signaler interrupting
+ * it, repeatedly enforces under TSYNC.  An interrupted attempt aborts its
+ * just-created domain (create_domain + free_domain, zero enforce_domain) while
+ * -ERESTARTNOINTR transparently restarts the syscall, so a successful retry may
+ * add its own full lifecycle.
+ */
+static int child_abort(int nsiblings, int attempts)
+{
+	pthread_t threads[16];
+	pthread_t signaler;
+	pthread_barrier_t barrier;
+	struct abort_signaler_data data = {};
+	struct sigaction sa = {};
+	int i;
+
+	sa.sa_handler = enforce_nop_handler;
+	if (sigaction(SIGUSR1, &sa, NULL))
+		return 1;
+
+	if (pthread_barrier_init(&barrier, NULL, nsiblings + 1))
+		return 1;
+	for (i = 0; i < nsiblings; i++)
+		if (pthread_create(&threads[i], NULL, enforce_idle, &barrier))
+			return 1;
+	pthread_barrier_wait(&barrier);
+
+	data.target = pthread_self();
+	if (pthread_create(&signaler, NULL, abort_signaler, &data))
+		return 1;
+
+	prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	for (i = 0; i < attempts; i++) {
+		int ruleset_fd = build_enforce_ruleset();
+
+		if (ruleset_fd < 0)
+			break;
+		/* Ignore the result: an abort returns an error, that is fine. */
+		landlock_restrict_self(ruleset_fd,
+				       LANDLOCK_RESTRICT_SELF_TSYNC);
+		close(ruleset_fd);
+	}
+
+	data.stop = true;
+	pthread_join(signaler, NULL);
+	return 0;
+}
+
+/*
+ * Verifies the abort contract: a domain aborted by a thread-sync failure emits
+ * create_domain and free_domain but zero enforce_domain.  The signal race is
+ * probabilistic and -ERESTARTNOINTR may add a successful retry's lifecycle, so
+ * events are grouped by domain ID and the test SKIPs if no abort occurred.
+ */
+TEST_F(trace, enforce_abort)
+{
+	pid_t pid;
+	int status, retry;
+	char *buf = NULL;
+	const char *cursor;
+	char domain[64];
+	bool abort_found = false;
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	/* free_domain fires from a kworker, so widen the filter first. */
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	tracefs_clear_pid_filter();
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+	if (pid == 0)
+		_exit(child_abort(8, 8));
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	/* Poll for the asynchronous free_domain events. */
+	for (retry = 0; retry < 10; retry++) {
+		usleep(100000);
+		set_cap(_metadata, CAP_SYS_ADMIN);
+		free(buf);
+		buf = tracefs_read_trace();
+		clear_cap(_metadata, CAP_SYS_ADMIN);
+		ASSERT_NE(NULL, buf);
+	}
+
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	/*
+	 * Walk every create_domain and look for one whose domain ID has zero
+	 * enforce_domain events but a matching free_domain: that is an aborted
+	 * domain (created, never enforced, freed).
+	 */
+	cursor = buf;
+	while (tracefs_extract_field(cursor, REGEX_CREATE_DOMAIN(TRACE_TASK),
+				     "domain", domain, sizeof(domain)) == 0) {
+		const char *cd, *nl;
+		char free_pattern[256];
+
+		if (count_enforce_matches(buf, domain, -1, -1) == 0) {
+			snprintf(
+				free_pattern, sizeof(free_pattern),
+				TRACE_PREFIX(
+					KWORKER_TASK) "landlock_free_domain: "
+						      "domain=%s denials=[0-9]\\+$",
+				domain);
+			if (tracefs_count_matches(buf, free_pattern) >= 1)
+				abort_found = true;
+		}
+
+		cd = strstr(cursor, "landlock_create_domain:");
+		if (!cd)
+			break;
+		nl = strchr(cd, '\n');
+		if (!nl)
+			break;
+		cursor = nl + 1;
+	}
+
+	if (!abort_found) {
+		free(buf);
+		SKIP(return, "signal race did not produce a thread-sync abort");
+	}
+
+	free(buf);
+}
+
 /*
  * The following tests are intentionally elided because the underlying kernel
  * mechanisms are already validated by audit tests:
-- 
2.54.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.