Skip to content

Commit e51fc5f

Browse files
committed
Provide the three operations openkal 0.8 adds to openkal.process
ADDING TO AN EXISTING INTERFACE OBLIGES EVERY IMPLEMENTATION OF IT, and adding a new interface obliges none. Clause 6.1 makes an interface an implementation does not provide absent at the link and not a deviation; it makes one provided IN PART a deviation. The five interfaces version 0.8 adds are therefore free for a backend to decline, and the three names added to openkal.process are not. The specification's own surface checker is what said so, before anything else noticed: openkal.process is provided in part: 3 of 8 names are not exported -- kal_process_channel kal_process_channel_close kal_process_spawn_with kal_process_channel is pipe2 with O_CLOEXEC on both ends. The far end is placed deliberately by the spawn that receives it; an end that leaked into every other started program would keep the channel open after the intended reader had closed it, and the writer would never see the end of input. The streams are bare descriptors rather than packed handles, because openkal.stream's transfer operations take what the environment takes. kal_fs_stream reports a file's stream the same way and for the same reason. kal_process_channel_close refuses descriptors below three. They are the standard streams, which are borrowed; closing one through this operation would take a stream away from the whole program. kal_process_spawn_with places the grants as descriptors three and upward, which is where kal_fs_preopen reads them back from. The inverse relationship clause 7.11 describes is between those two operations, which is why they must agree about the numbering rather than each choosing one. ⚠️ dup3 refuses a duplication onto itself, and the ordinary case reaches that whenever a granted directory already occupies the number it is destined for. Refusing is correct of dup3 --- the flags could not be applied --- and here it means the descriptor is already in place, so it is left alone rather than treated as a failure. The grants are resolved before the fork. A failure after it would leave a child to be reaped and a caller holding an error it cannot act upon. surface exported surface is complete and conforms: 90 names mcpp test 7 passed, 0 failed observed a channel carries bytes, and closing the far end is observed as end of input on the near one
1 parent 3fad9e3 commit e51fc5f

3 files changed

Lines changed: 164 additions & 3 deletions

File tree

src/process.cpp

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,122 @@ int kal_process_spawn(kal_dir base,
101101
return kal_ok;
102102
}
103103

104+
// A channel: a pair of streams of which one end is meant to cross a spawn.
105+
//
106+
// WHY THIS IS A KERNEL FACILITY AND kal::kit's CHANNEL IS NOT. A started program
107+
// is another address space, so a pointer into this one is not something it can
108+
// be handed. The pair must therefore be made of whatever the environment carries
109+
// across a spawn, which here is a descriptor.
110+
//
111+
// BOTH ENDS ARE OWNED AND BOTH ARE RELEASED THROUGH kal_process_channel_close.
112+
// A parent that does not release the far end after the spawn never observes the
113+
// end of input on its own --- the classic deadlock of this arrangement, and the
114+
// reason the release is declared beside the operation rather than left to
115+
// openkal.stream, which has no release at all.
116+
int kal_process_channel(kal_stream* mine, kal_stream* theirs) {
117+
if (mine == nullptr || theirs == nullptr) return kal_err_invalid;
118+
119+
int fds[2] = { -1, -1 };
120+
// O_CLOEXEC on both. The far end is placed deliberately, by the spawn that
121+
// receives it; an end that leaked into every other started program would
122+
// keep the channel open after the intended reader had closed it, and the
123+
// writer would then never see the end of input.
124+
const okl_long r = okl::sys(okl::nr_pipe2, reinterpret_cast<okl_long>(fds),
125+
okl::o_cloexec);
126+
if (okl::failed(r)) return okl::translate(r);
127+
128+
// THE STREAMS ARE BARE DESCRIPTORS AND NOT PACKED HANDLES, because
129+
// openkal.stream's transfer operations take what the environment takes.
130+
// kal_fs_stream reports a file's stream the same way and for the same
131+
// reason.
132+
*mine = kal_stream{ static_cast<kal_uintptr>(fds[0]) }; // the reading end
133+
*theirs = kal_stream{ static_cast<kal_uintptr>(fds[1]) }; // the writing end
134+
return kal_ok;
135+
}
136+
137+
void kal_process_channel_close(kal_stream s) {
138+
// A bare descriptor, so there is no generation to retire. The standard
139+
// streams are borrowed and are numbered 0, 1 and 2; closing one of those
140+
// through this operation would take a stream away from the whole program,
141+
// so they are refused rather than closed.
142+
const okl_long fd = static_cast<okl_long>(s.h);
143+
if (fd < 3) return;
144+
okl::sys(okl::nr_close, fd);
145+
}
146+
147+
// Starting a program that receives exactly the directories named.
148+
//
149+
// THE GRANTS ARE PLACED AS DESCRIPTORS THREE AND UPWARD, which is the
150+
// arrangement kal_fs_preopen reads them back from. The inverse relationship
151+
// clause 7.11 describes is therefore between this operation and that one, and
152+
// it is why the two must agree about the numbering rather than each choosing.
153+
//
154+
// A COUNT OF ZERO IS NOT THE SAME AS kal_process_spawn. It starts a program with
155+
// no preopens at all, which is the whole reason a caller reaches for this
156+
// operation, so the loop below is not skipped when there is nothing to place ---
157+
// what matters is that nothing else is inherited either.
158+
int kal_process_spawn_with(kal_dir base,
159+
const char* path, kal_uintptr path_len,
160+
const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc,
161+
const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc,
162+
const kal_spawn_streams* streams,
163+
const kal_preopen* grants, kal_uintptr grant_count,
164+
kal_process* out) {
165+
const int b = okl::unpack(base.h);
166+
if (b < 0 || out == nullptr) return kal_err_invalid;
167+
if (!okl::acceptable(path, path_len)) return kal_err_invalid;
168+
if (grant_count > 0 && grants == nullptr) return kal_err_invalid;
169+
okl::terminated p(path, path_len);
170+
if (!p.ok) return kal_err_invalid;
171+
172+
vector args, envs;
173+
if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory;
174+
if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory;
175+
176+
// Resolved before the fork, because a failure after it would leave a child
177+
// to be reaped and a caller with an error it cannot act upon.
178+
constexpr kal_uintptr max_grants = 16;
179+
if (grant_count > max_grants) return kal_err_invalid;
180+
int granted[max_grants];
181+
for (kal_uintptr i = 0; i < grant_count; ++i) {
182+
granted[i] = okl::unpack(grants[i].dir.h);
183+
if (granted[i] < 0) return kal_err_invalid;
184+
}
185+
186+
const okl_long in = streams ? static_cast<okl_long>(streams->in) : 0;
187+
const okl_long ou = streams ? static_cast<okl_long>(streams->out) : 0;
188+
const okl_long er = streams ? static_cast<okl_long>(streams->err) : 0;
189+
190+
const okl_long child = okl::sys(okl::nr_clone, 17 /* SIGCHLD */, 0, 0, 0, 0);
191+
if (okl::failed(child)) return okl::translate(child);
192+
193+
if (child == 0) {
194+
if (in != 0) okl::sys(okl::nr_dup3, in, 0, 0);
195+
if (ou != 0) okl::sys(okl::nr_dup3, ou, 1, 0);
196+
if (er != 0) okl::sys(okl::nr_dup3, er, 2, 0);
197+
198+
// ⚠️ dup3 REFUSES A DUPLICATION ONTO ITSELF, which the ordinary case
199+
// reaches whenever a granted directory already occupies the number it
200+
// is destined for. Refusing there is correct of dup3 --- the flags could
201+
// not be applied --- and here it means the descriptor is already in
202+
// place, so it is left alone rather than treated as a failure.
203+
for (kal_uintptr i = 0; i < grant_count; ++i) {
204+
const okl_long want = static_cast<okl_long>(3 + i);
205+
if (granted[i] != want)
206+
okl::sys(okl::nr_dup3, granted[i], want, 0);
207+
}
208+
209+
okl::sys(okl::nr_execveat, b, reinterpret_cast<okl_long>(p.buf),
210+
reinterpret_cast<okl_long>(args.slots),
211+
reinterpret_cast<okl_long>(envs.slots), 0);
212+
okl::sys(okl::nr_exit_group, 127);
213+
for (;;) { }
214+
}
215+
216+
*out = kal_process{ static_cast<kal_uintptr>(child) };
217+
return kal_ok;
218+
}
219+
104220
int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) {
105221
if (h.h == 0) return kal_err_invalid;
106222
int st = 0;
@@ -137,6 +253,7 @@ void kal_process_close(kal_process) { }
137253

138254
const kal_uintptr kal_process_props =
139255
KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING
140-
| KAL_PROCESS_PROP_EXIT_STATUS;
256+
| KAL_PROCESS_PROP_EXIT_STATUS
257+
| KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR;
141258

142259
}

src/sys.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ enum : okl_long {
102102
nr_socket = 41, nr_connect = 42, nr_accept = 43, nr_sendto = 44,
103103
nr_recvfrom = 45, nr_shutdown = 48, nr_bind = 49, nr_listen = 50,
104104
nr_getsockname = 51, nr_getpeername = 52, nr_accept4 = 288,
105-
nr_setsockopt = 54,
105+
nr_setsockopt = 54, nr_pipe2 = 293,
106106
// openkal.timeout. ppoll and not poll: the bound is stated in nanoseconds
107107
// and poll takes milliseconds, so poll could not express a bound finer than
108108
// the granularity this implementation reports.
@@ -185,7 +185,7 @@ enum : okl_long {
185185
nr_socket = 198, nr_connect = 203, nr_accept = 202, nr_sendto = 206,
186186
nr_recvfrom = 207, nr_shutdown = 210, nr_bind = 200, nr_listen = 201,
187187
nr_getsockname = 204, nr_getpeername = 205, nr_accept4 = 242,
188-
nr_setsockopt = 208,
188+
nr_setsockopt = 208, nr_pipe2 = 59,
189189
// openkal.timeout. This architecture has no `poll' at all, only `ppoll',
190190
// which is a second reason the bound is expressed through the latter.
191191
nr_ppoll = 73,

tests/conformance_v08.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,53 @@ void timeout_section() {
212212
check(w.e == kal_ok, "a bounded transfer of zero bytes succeeds");
213213
}
214214

215+
// The three operations openkal 0.8 adds to openkal.process.
216+
//
217+
// ADDING TO AN EXISTING INTERFACE OBLIGES EVERY IMPLEMENTATION OF IT, which is
218+
// not true of adding a new interface: clause 6.1 makes a new one optional and
219+
// clause 6.1 makes an incomplete one a deviation. The surface checker caught the
220+
// omission here before anything else did, and these observations are what says
221+
// the names do something rather than merely existing.
222+
void process_additions_section() {
223+
// A channel carries bytes from one end to the other. Both ends are owned and
224+
// both are released through kal_process_channel_close.
225+
kal_stream mine{}, theirs{};
226+
const int rc = kal_process_channel(&mine, &theirs);
227+
check(rc == kal_ok, "a channel is created");
228+
if (rc != kal_ok) return;
229+
230+
const char msg[] = "through the channel";
231+
const auto w = kal_stream_write(theirs, msg, sizeof msg - 1);
232+
check(w.e == kal_ok && w.n == sizeof msg - 1,
233+
"the far end of a channel accepts bytes");
234+
235+
char buf[64] = {};
236+
const auto r = kal_stream_read(mine, buf, sizeof buf);
237+
check(r.e == kal_ok && r.n == sizeof msg - 1 &&
238+
std::memcmp(buf, msg, sizeof msg - 1) == 0,
239+
"the near end reads what the far end wrote");
240+
241+
// THE END OF INPUT IS WHAT THE RELEASE IS FOR. A parent that does not close
242+
// the far end after a spawn never observes it, which is the deadlock this
243+
// pair invites and the reason the release is declared beside the operation.
244+
kal_process_channel_close(theirs);
245+
const auto eof = kal_stream_read(mine, buf, sizeof buf);
246+
check(eof.e == kal_ok && eof.n == 0,
247+
"closing the far end is observed as end of input on the near one");
248+
kal_process_channel_close(mine);
249+
250+
// The property word claims both additions, so both must be answered. Named
251+
// through the module, because a macro does not cross a module boundary.
252+
check(kal::process::has(kal::process::channel),
253+
"the property word claims the channel it just provided");
254+
check(kal::process::has(kal::process::grant_dir),
255+
"the property word claims the directory grant");
256+
}
257+
215258
} // namespace
216259

217260
int main() {
261+
process_additions_section();
218262
terminal_section();
219263
net_section();
220264
datagram_section();

0 commit comments

Comments
 (0)