Skip to content

Commit 44ba11a

Browse files
authored
0.7.1 --- a bounded transfer waited upon the descriptor below the one it moved (#19)
`kal_timeout_read` and `kal_timeout_write` decoded their `kal_stream` argument with `okl::unpack`, which is the decoder for an OWNED handle. A stream handle is not one: `stream.cpp` states that these are this environment's own descriptors and `kal_stream_read` and `kal_stream_write` take them as such. THE DECODE DID NOT FAIL, WHICH IS WHY THE CALL SITE READ AS CORRECT. `handle.h` packs an owned handle as `(generation << 32) | (fd + 1)`, so a bare descriptor N has exactly the shape of a packed handle naming N-1, and `unpack` accepts it whenever the generation recorded for N-1 is still zero. That is every index at which no owned handle has yet been released. The wait was therefore performed upon descriptor N-1 while the transfer that followed was performed upon N. Measured with the packing arithmetic alone: `unpack(1) = 0`, `unpack(2) = 1`, `unpack(5) = 4`. Of the three standard streams only `kal_stdin`, whose handle is zero, fails to decode; `kal_stdout` decoded to descriptor 0 and `kal_stderr` to 1. The former reading tested `unpack` and fell back when it failed, on the stated ground that the standard streams are not packed. The ground is correct and the conclusion drawn from it was not: no stream handle is packed here, so the fallback was the path that should always have been taken. WHAT A CALLER OBSERVED. Waiting upon a descriptor that never becomes ready is an expiry, so a bounded read of a stream with bytes waiting reported `kal_err_again`; waiting upon one that is ready lets the transfer proceed and block, so an operation that states a bound could wait without one. Which of the two occurred is a function of what happened to occupy the descriptor below, so the same program answered differently run alone and run after something else, and the index healed permanently once an owned handle at N-1 had been released. Reported through openkal-musl in #13 as `std::filesystem::copy_file` reporting `Resource temporarily unavailable` for two ordinary files. libc++ opens the source with `O_NONBLOCK`, which is meaningless for a regular file on a kernel and which openkal-musl routes through this interface, so every byte of a file copy passed through the wait above. `kal_timeout_accept` and `kal_timeout_recv_from` keep the decode and are correct: a listener and a datagram are owned. The four call sites divide exactly along the borrowed/owned line and the two that were wrong were the two holding a borrowed handle. --- the criterion ----------------------------------------------------------- `tests/conformance_v08.cpp` gains `timeout_stream_section`, and it is the first section `main` runs. Nothing in this file reached the two operations before: the existing observations are of an accept, whose handle is owned and was decoded correctly, and of a zero-length write, which returns before it waits. Sixteen channels rather than one, because under the defect the wait was upon descriptor N-1 and whether that expires depends on what occupies N-1. First rather than last, because the mistaken decode corrected itself for every index at which an owned handle had already been released, so the same observation placed after the listener and datagram sections would have held upon the defect. Confirmed by putting the old implementation back: 0 of 16 bounded reads transferred FAIL: a bounded read of a stream that has bytes waiting transfers them conformance_v08 ... FAIL (exit 1, 0.04s) and with this change, 7 suites passed, 0 failed. `src/handle.h` records beside `unpack` that a word which was never packed is accepted silently, and that a stream handle must not reach it.
1 parent 844c2c5 commit 44ba11a

5 files changed

Lines changed: 125 additions & 13 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ for Linux, written on the kernel's own system-call interface.
88
openkal = "0.9.0"
99

1010
[target.'cfg(os = "linux")'.dependencies]
11-
openkal-linux = "0.7.0"
11+
openkal-linux = "0.7.1"
1212
```
1313

1414
## Why it does not use a C library

‎mcpp.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
namespace = "mcpplibs"
33
name = "openkal-linux"
4-
version = "0.7.0"
4+
version = "0.7.1"
55
description = "The reference implementation of openkal for Linux, written on the kernel's own system-call interface so that it can be placed beneath a C library as well as above one."
66
license = "Apache-2.0"
77

‎src/handle.h‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ inline okl_uptr pack(int fd) {
2424
}
2525

2626
// Returns the descriptor, or -1 if the word does not name a live one.
27+
//
28+
// THIS ACCEPTS A WORD THAT WAS NEVER PACKED, AND SILENTLY. A bare descriptor N
29+
// has the shape of a packed handle naming N-1 whose generation is still zero,
30+
// so this returns N-1 for it rather than -1. Nothing here can tell the two
31+
// apart: the word is one machine word and carries no tag.
32+
//
33+
// The consequence is that a handle of the OTHER discipline must never reach
34+
// this function. openkal.stream's handles are bare descriptors (stream.cpp
35+
// states why), and src/timeout.cpp used to pass one here and wait upon the
36+
// descriptor below the one it then transferred upon. Owned handles --- kal_file,
37+
// kal_dir, kal_net_listener, kal_net_conn, kal_datagram --- are the whole of
38+
// this function's domain.
2739
inline int unpack(okl_uptr h) {
2840
const int fd = static_cast<int>(h & 0xffffffffu) - 1;
2941
if (fd < 0 || fd >= kMaxDescriptor) return -1;

‎src/timeout.cpp‎

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,33 @@ int await(int fd, short events, kal_u64 ns) {
5252
}
5353
}
5454

55+
// A STREAM HANDLE IS A DESCRIPTOR AND IS NOT DECODED. stream.cpp states it in
56+
// terms, kal_stream_read and kal_stream_write take it as one, and so must the
57+
// wait that precedes them: the wait and the transfer that follows it have to
58+
// name the same object or the wait answers about something else.
59+
//
60+
// THIS FILE USED TO DECODE IT AS AN OWNED HANDLE, AND THE DECODE SUCCEEDED.
61+
// handle.h packs an owned handle as (generation << 32) | (fd + 1), so a bare
62+
// descriptor N has exactly the shape of a packed handle naming N-1, and
63+
// okl::unpack accepts it whenever generation N-1 is still zero -- which is
64+
// every index at which no owned handle has yet been released. The wait was
65+
// therefore performed upon descriptor N-1 and the transfer upon N.
66+
//
67+
// The former reading tested `unpack` and fell back when it failed, on the
68+
// stated ground that the standard streams are not packed. That ground is
69+
// correct and the conclusion drawn from it was not: NO stream handle is packed
70+
// here, and of the three standard ones only kal_stdin, whose handle is zero,
71+
// fails to decode. kal_stdout decoded to descriptor 0 and kal_stderr to 1.
72+
//
73+
// What a caller observed was an expiry for a stream that had bytes waiting, or
74+
// a wait without bound inside an operation that states one, according to what
75+
// happened to occupy the descriptor below. Both are functions of the process's
76+
// descriptor history, so the same program answered differently when run alone
77+
// and when run after something else -- and the index healed for good once an
78+
// owned handle at N-1 had been released, because that advances the generation
79+
// and makes the decode fail correctly.
80+
int stream_fd(kal_stream s) { return static_cast<int>(s.h); }
81+
5582
} // namespace
5683

5784
extern "C" {
@@ -61,26 +88,24 @@ kal_intptr kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 ns
6188
// would turn a call that always succeeds into one that can expire.
6289
if (len == 0) return 0;
6390

64-
const int fd = okl::unpack(s.h);
65-
// THE STANDARD STREAMS ARE NOT PACKED HANDLES. openkal.stream reports them
66-
// as the descriptors themselves, so a word that does not unpack is taken to
67-
// be one of those rather than being refused.
68-
const int use = (fd >= 0) ? fd : static_cast<int>(s.h);
69-
70-
if (const int rc = await(use, okl::poll_in, ns); rc != kal_ok) return -rc;
91+
if (const int rc = await(stream_fd(s), okl::poll_in, ns); rc != kal_ok) return -rc;
7192
return kal_stream_read(s, buf, len);
7293
}
7394

7495
kal_intptr kal_timeout_write(kal_stream s, const void* buf, kal_uintptr len, kal_u64 ns) {
7596
if (len == 0) return 0;
7697

77-
const int fd = okl::unpack(s.h);
78-
const int use = (fd >= 0) ? fd : static_cast<int>(s.h);
79-
80-
if (const int rc = await(use, okl::poll_out, ns); rc != kal_ok) return -rc;
98+
if (const int rc = await(stream_fd(s), okl::poll_out, ns); rc != kal_ok) return -rc;
8199
return kal_stream_write(s, buf, len);
82100
}
83101

102+
// THE TWO OPERATIONS BELOW DO DECODE, AND THAT IS NOT AN INCONSISTENCY WITH THE
103+
// TWO ABOVE. A listener and a datagram are owned: their handles are made by
104+
// okl::pack and released by okl::retire, so the decode is the operation that
105+
// recovers the descriptor and its failure is how a released handle is refused.
106+
// A stream is borrowed and carries no generation. The four call sites divide
107+
// exactly along that line, and the two that were wrong were the two that had a
108+
// borrowed handle in hand.
84109
int kal_timeout_accept(kal_net_listener l, kal_u64 ns, kal_net_conn* out) {
85110
if (out == nullptr) return kal_err_invalid;
86111
const int fd = okl::unpack(l.h);

‎tests/conformance_v08.cpp‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,75 @@ void space_section() {
190190
kal_process_close(p.p);
191191
}
192192

193+
// A BOUNDED TRANSFER WAITS UPON THE STREAM IT THEN TRANSFERS UPON.
194+
//
195+
// This is the observation that was missing while the two operations it examines
196+
// waited upon the descriptor below the one they transferred upon. Nothing else
197+
// in this file reached them: the two observations in timeout_section below are
198+
// of an accept, whose handle is owned and was decoded correctly, and of a
199+
// zero-length write, which returns before it waits.
200+
//
201+
// WHY IT MAKES SIXTEEN CHANNELS AND NOT ONE. Under the defect the wait was
202+
// performed upon descriptor N-1, and whether that expires depends on what
203+
// occupies N-1. For a channel made after another, N-1 is the previous channel's
204+
// writing end, upon which input is never reported, so the read expires while
205+
// bytes sit in the stream that was asked for. One channel would be answering
206+
// about whichever descriptor happened to precede it; sixteen make the answer a
207+
// property of the implementation rather than of the process that ran it.
208+
//
209+
// AND WHY IT RUNS FIRST. The mistaken decode succeeded only while the
210+
// generation recorded for N-1 was still zero, so it corrected itself for every
211+
// index at which an owned handle had already been released. Placed after the
212+
// sections that create and release listeners and datagrams, this would have
213+
// examined reused descriptors and held upon the defect.
214+
//
215+
// The count is reported rather than the first failure. Sixteen distinguishes a
216+
// correct implementation from one that answers about a neighbouring descriptor;
217+
// the first failure alone would not say which.
218+
void timeout_stream_section() {
219+
constexpr int n = 16;
220+
kal_stream mine[n]{}, theirs[n]{};
221+
int made = 0;
222+
223+
for (; made < n; ++made) {
224+
if (kal_process_channel(&mine[made], &theirs[made]) != kal_ok) break;
225+
const char byte = 'x';
226+
if (kal_stream_write(theirs[made], &byte, 1) != 1) break;
227+
}
228+
check(made == n, "sixteen channels are created and each is written to");
229+
230+
int transferred = 0;
231+
for (int i = 0; i < made; ++i) {
232+
char buf[1] = {};
233+
// One millisecond. Every one of these streams has a byte waiting, so a
234+
// correct implementation does not reach the bound at all; the bound is
235+
// present so that a wait upon the wrong descriptor ends the run rather
236+
// than hanging it.
237+
if (kal::timeout::read(mine[i], buf, 1, 1000000) == 1 && buf[0] == 'x')
238+
++transferred;
239+
}
240+
if (transferred != made)
241+
std::printf(" %d of %d bounded reads transferred\n", transferred, made);
242+
check(transferred == made,
243+
"a bounded read of a stream that has bytes waiting transfers them");
244+
245+
// The mirror. A stream with nothing in it must expire, and must expire
246+
// rather than transfer a byte belonging to another stream.
247+
kal_stream empty_mine{}, empty_theirs{};
248+
if (kal_process_channel(&empty_mine, &empty_theirs) == kal_ok) {
249+
char buf[1] = {};
250+
check(kal::timeout::read(empty_mine, buf, 1, 1000000) == -kal_err_again,
251+
"a bounded read of a stream with nothing waiting expires");
252+
kal_process_channel_close(empty_theirs);
253+
kal_process_channel_close(empty_mine);
254+
}
255+
256+
for (int i = 0; i < made; ++i) {
257+
kal_process_channel_close(theirs[i]);
258+
kal_process_channel_close(mine[i]);
259+
}
260+
}
261+
193262
// openkal.timeout
194263
void timeout_section() {
195264
check(kal_timeout_granularity() > 0,
@@ -257,6 +326,12 @@ void process_additions_section() {
257326
} // namespace
258327

259328
int main() {
329+
// FIRST, AND THE ORDER IS LOAD-BEARING. The comment above this section
330+
// records why: it examines a defect that corrects itself for any descriptor
331+
// index at which an owned handle has already been released, so running it
332+
// after the sections that make and release listeners would hold upon the
333+
// defect it exists to detect.
334+
timeout_stream_section();
260335
process_additions_section();
261336
terminal_section();
262337
net_section();

0 commit comments

Comments
 (0)