The Pedigree Project 0.1
accounting-contract-test/main.c
1/* Copyright (c) 2026, Pedigree Developers. */
2#define _GNU_SOURCE
3#include <errno.h>
4#include <fcntl.h>
5#include <pthread.h>
6#include <signal.h>
7#include <stdio.h>
8#include <stdlib.h>
9#include <string.h>
10#include <time.h>
11#include <unistd.h>
12
13#include <sys/acct.h>
14#include <sys/stat.h>
15#include <sys/wait.h>
16
17#define CHECK(expression) \
18 do { \
19 if (!(expression)) { \
20 fprintf(stderr, "ACCOUNTING-CONTRACT: FAIL line=%d errno=%d\n", __LINE__, errno); \
21 failed = 1; \
22 goto out; \
23 } \
24 } while (0)
25
26static int wait_for(pid_t child, int expected) {
27 int status = 0;
28 while (waitpid(child, &status, 0) == -1) {
29 if (errno != EINTR)
30 return 0;
31 }
32 return status == expected;
33}
34
35static int record_for(int fd, pid_t pid, struct acct_v3* result) {
36 struct stat metadata;
37 if (fstat(fd, &metadata) || metadata.st_size < 0 || metadata.st_size % sizeof(*result))
38 return -1;
39 int found = 0;
40 for (off_t offset = 0; offset < metadata.st_size; offset += sizeof(*result)) {
41 struct acct_v3 record;
42 if (pread(fd, &record, sizeof(record), offset) != sizeof(record) ||
43 record.ac_version != (3 | ACCT_BYTEORDER))
44 return -1;
45 if (record.ac_pid == (unsigned)pid) {
46 *result = record;
47 ++found;
48 }
49 }
50 return found;
51}
52
53static pid_t child_exit(int code) {
54 pid_t child = fork();
55 if (!child)
56 _exit(code);
57 return child;
58}
59
60static void* thread_exit(void* argument) {
61 return argument;
62}
63
64int main(int argc, char** argv) {
65 if (argc > 1 && !strcmp(argv[1], "--exec-child"))
66 return 42;
67 int failed = 0, first = -1, second = -1;
68 char directory[256], first_path[300], second_path[300], missing[300];
69 const char* base = argc > 1 ? argv[1] : "/tests";
70 snprintf(directory, sizeof(directory), "%s/accounting-contract.XXXXXX", base);
71 first_path[0] = second_path[0] = missing[0] = 0;
72 alarm(40);
73 CHECK(geteuid() == 0 && acct(NULL) == 0);
74 CHECK(mkdtemp(directory));
75 snprintf(first_path, sizeof(first_path), "%s/first", directory);
76 snprintf(second_path, sizeof(second_path), "%s/second", directory);
77 snprintf(missing, sizeof(missing), "%s/missing", directory);
78 first = open(first_path, O_CREAT | O_EXCL | O_RDWR, 0600);
79 second = open(second_path, O_CREAT | O_EXCL | O_RDWR, 0600);
80 CHECK(first >= 0 && second >= 0);
81 errno = 0;
82 CHECK(acct("") == -1 && errno == ENOENT);
83 errno = 0;
84 CHECK(acct(directory) == -1 && errno == EACCES);
85 errno = 0;
86 CHECK(acct(missing) == -1 && errno == ENOENT);
87 CHECK(acct(first_path) == 0);
88 errno = 0;
89 CHECK(acct(missing) == -1 && errno == ENOENT);
90
91 time_t before = time(NULL);
92 pid_t child = fork();
93 CHECK(child >= 0);
94 if (!child) {
95 struct timespec pause = {.tv_nsec = 80000000};
96 nanosleep(&pause, NULL);
97 volatile unsigned sum = 0;
98 for (unsigned i = 0; i < 5000000; ++i)
99 sum += i;
100 _exit(23);
101 }
102 CHECK(wait_for(child, 23 << 8));
103 struct acct_v3 record;
104 CHECK(record_for(first, child, &record) == 1);
105 CHECK(record.ac_ppid == (unsigned)getpid() && record.ac_uid == (unsigned)getuid() &&
106 record.ac_gid == (unsigned)getgid() && record.ac_exitcode == (23 << 8));
107 CHECK((record.ac_flag & AFORK) && !(record.ac_flag & AXSIG) && record.ac_etime >= 5 &&
108 record.ac_btime >= (unsigned)before && record.ac_btime <= (unsigned)time(NULL));
109 puts("ACCOUNTING-CONTRACT: PASS record-before-wait");
110
111 child = fork();
112 CHECK(child >= 0);
113 if (!child) {
114 struct timespec pause = {.tv_nsec = 80000000};
115 nanosleep(&pause, NULL);
116 execl("/usr/bin/accounting-contract-test", "accounting-contract-test", "--exec-child", NULL);
117 _exit(99);
118 }
119 CHECK(wait_for(child, 42 << 8));
120 CHECK(record_for(first, child, &record) == 1 && !(record.ac_flag & AFORK) &&
121 record.ac_exitcode == (42 << 8) && record.ac_etime >= 5 &&
122 !strncmp(record.ac_comm, "accounting-cont", 15));
123 puts("ACCOUNTING-CONTRACT: PASS exec-lifetime");
124
125 child = fork();
126 CHECK(child >= 0);
127 if (!child) {
128 pthread_t threads[3];
129 for (unsigned i = 0; i < 3; ++i)
130 if (pthread_create(&threads[i], NULL, thread_exit, NULL))
131 _exit(97);
132 for (unsigned i = 0; i < 3; ++i)
133 if (pthread_join(threads[i], NULL))
134 _exit(98);
135 if (record_for(first, getpid(), &record) != 0)
136 _exit(99);
137 _exit(24);
138 }
139 CHECK(wait_for(child, 24 << 8));
140 CHECK(record_for(first, child, &record) == 1 && record.ac_exitcode == (24 << 8));
141 puts("ACCOUNTING-CONTRACT: PASS final-process-exit");
142
143 child = fork();
144 CHECK(child >= 0);
145 if (!child) {
146 if (setgid(65534) || setuid(65534))
147 _exit(1);
148 errno = 0;
149 if (acct(NULL) != -1 || errno != EPERM)
150 _exit(2);
151 errno = 0;
152 if (acct(first_path) != -1 || errno != EPERM)
153 _exit(3);
154 _exit(0);
155 }
156 CHECK(wait_for(child, 0));
157 CHECK(record_for(first, child, &record) == 1 && record.ac_uid == 65534 && record.ac_gid == 65534);
158 puts("ACCOUNTING-CONTRACT: PASS credentials");
159
160 child = fork();
161 CHECK(child >= 0);
162 if (!child) {
163 kill(getpid(), SIGTERM);
164 _exit(99);
165 }
166 CHECK(wait_for(child, SIGTERM));
167 CHECK(record_for(first, child, &record) == 1 && (record.ac_flag & AXSIG) &&
168 record.ac_exitcode == SIGTERM);
169
170 pid_t children[8];
171 for (unsigned i = 0; i < 8; ++i) {
172 children[i] = child_exit(i);
173 CHECK(children[i] > 0);
174 }
175 for (unsigned i = 0; i < 8; ++i) {
176 CHECK(wait_for(children[i], i << 8));
177 CHECK(record_for(first, children[i], &record) == 1 && record.ac_exitcode == (i << 8));
178 }
179 CHECK(unlink(first_path) == 0);
180 child = child_exit(17);
181 CHECK(child > 0 && wait_for(child, 17 << 8));
182 CHECK(record_for(first, child, &record) == 1);
183 puts("ACCOUNTING-CONTRACT: PASS concurrent-and-unlinked");
184
185 struct stat old_first, old_second, current;
186 CHECK(acct(second_path) == 0 && fstat(first, &old_first) == 0);
187 child = child_exit(19);
188 CHECK(child > 0 && wait_for(child, 19 << 8));
189 CHECK(record_for(second, child, &record) == 1 && record_for(first, child, &record) == 0);
190 CHECK(fstat(first, &current) == 0 && current.st_size == old_first.st_size);
191 CHECK(acct(NULL) == 0 && fstat(second, &old_second) == 0);
192 child = child_exit(21);
193 CHECK(child > 0 && wait_for(child, 21 << 8));
194 CHECK(fstat(second, &current) == 0 && current.st_size == old_second.st_size);
195 CHECK(fsync(second) == 0);
196 puts("ACCOUNTING-CONTRACT: PASS switch-and-disable");
197
198out:
199 acct(NULL);
200 if (first >= 0)
201 close(first);
202 if (second >= 0)
203 close(second);
204 if (first_path[0])
205 unlink(first_path);
206 if (second_path[0])
207 unlink(second_path);
208 rmdir(directory);
209 puts(failed ? "ACCOUNTING-CONTRACT: END FAIL" : "ACCOUNTING-CONTRACT: END PASS");
210 return failed;
211}