Skip to content

Commit 7d4b3af

Browse files
authored
0.6.1 --- the same bounded-transfer defect, and the tests that had never run (#15)
* 0.6.1 --- the same bounded-transfer defect as openkal-linux, and the tests that would have caught it `kal_timeout_read` and `kal_timeout_write` decoded their `kal_stream` argument with `okm::unpack`, which is the decoder for an OWNED handle. A stream handle is not one: 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. The wait was therefore performed upon descriptor N-1 while the transfer that followed was performed upon N. Of the three standard streams only `kal_stdin`, whose handle is zero, failed to decode; `kal_stdout` decoded to descriptor 0 and `kal_stderr` to 1. This is openkal-linux's defect, in this file, unchanged. It was found there first, through openkal-musl, and the two files carry the same four call sites with the same two wrong. `kal_timeout_accept` and `kal_timeout_recv_from` keep the decode and are correct: a listener and a datagram are owned. The four sites divide exactly along the borrowed/owned line. --- and the reason it was invisible here ------------------------------------ THIS PACKAGE'S OWN TESTS HAD NEVER RUN. `tests/` has held five suites since the implementation was written and no workflow invoked them; what ran was the specification's suite, which is a different instrument. The evidence that they had never run is in the suites themselves: all five announced their success as `openkal-linux: ...`, and `conformance_env_time.cpp` did not compile at all, naming `kal::time::prop_wall_available`, which openkal 0.9 spells `kal::time::wall_available`. `mcpp test` is now a step, taken from openkal-linux unchanged, including its assertion that every suite ran: a suite that discovered nothing reports success. The five suites are corrected to name this implementation, and the stale property name is fixed. `tests/conformance_timeout.cpp` is new and is the observation that was missing. There was no test of openkal.timeout in this package at all, and the specification's suite examines it through observations that a wait upon the wrong descriptor satisfies: a bounded read of the standard input is permitted to expire, so an implementation that expires for the wrong reason is indistinguishable there from one that is right. 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 in `main`, because the mistaken decode corrected itself for every index at which an owned handle had already been released. --- measured ----------------------------------------------------------------- This system has no runner here, so the six suites were compiled and run against openkal-linux, which provides the same interface: 6 passed, 0 failed. The new suite was then run against openkal-linux with the defect put back: 0 of 16 bounded reads transferred FAIL: a bounded read of a stream that has bytes waiting transfers them Continuous integration on this branch is the criterion for this system. `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. * The suite that had never run finds a second thing: a retry that could not retry Continuous integration on the previous commit ran `tests/` here for the first time. Five of the six suites held, including the new one; `conformance_process_task` did not: conformance_process_task ... FAIL (exit 1, 0.04s) FAIL: the status it finished with is reported The observation is correct and the test was wrong. It starts a program that succeeds and reads its status, and it chose which program by spawning `bin/true` and RETRYING with `usr/bin/true` if the spawn failed. That retry can never run: `kal_process_spawn` reports whether the DUPLICATE was made, and the image is replaced afterwards, inside a copy the caller no longer is. A path that does not exist therefore produces `kal_ok` and a duplicate that finishes with 127. The first candidate was always taken and the second was unreachable. This system has `/usr/bin/true` and no `/bin/true`, so the status observed was 127. openkal-linux carries the same test and the same dead retry, and holds only because the first candidate exists there. THE OBSERVATION AFTER IT HELD THROUGHOUT, UPON A PROGRAM THAT NEVER RAN. It read `status != 0` for the program that fails, and 127 satisfies that. So of the two observations, one failed for the right reason and the other passed for the wrong one, from a single cause. What changes: * the candidate is chosen by asking `kal_fs_info` whether the name refers to anything, which is an enquiry that can distinguish the two cases; * `status != 0` becomes `status == 1`, the status `false` actually returns, so a program that was never replaced no longer satisfies it; * the failing observation prints the status it saw. `127' names an image that was not replaced and `0' names one that ran, and without the number the two arrive as the same line; * the shell candidate is located the same way, for the same reason. Measured against openkal-linux, which provides the same interface: the suite holds. Continuous integration on this system is the criterion.
1 parent 7b65854 commit 7d4b3af

11 files changed

Lines changed: 280 additions & 26 deletions

‎.github/workflows/ci.yml‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,32 @@ jobs:
215215
}
216216
subst mcpp.toml .spec
217217
218+
# THIS PACKAGE'S OWN TESTS HAD NEVER RUN.
219+
#
220+
# tests/ has held five suites since this implementation was written and no
221+
# workflow invoked them. What ran was the specification's suite, which is a
222+
# different instrument: it examines what every implementation must do, and
223+
# these examine what this one does with the system beneath it. A defect
224+
# visible only to the second kind was therefore invisible here, which is
225+
# what happened to openkal.timeout -- there was no observation of it in
226+
# either place that a wait upon the wrong descriptor did not satisfy.
227+
#
228+
# The step is openkal-linux's, unchanged, including the assertion that
229+
# every suite ran: a suite that discovered nothing reports success.
230+
- name: This package's own tests
231+
run: |
232+
set -euo pipefail
233+
mcpp test 2>&1 | tee tests.log
234+
# The list is derived from the files present rather than written out
235+
# here: a hand-written list names the suites that existed when it was
236+
# written, and one added afterwards escapes the assertion silently.
237+
missing=0
238+
for f in tests/*.cpp; do
239+
name="$(basename "$f" .cpp)"
240+
grep -q "^$name \.\.\. ok" tests.log || { echo "did not run or did not pass: $name" >&2; missing=1; }
241+
done
242+
test "$missing" -eq 0
243+
218244
# The other architecture, as far as this system allows it to be reached.
219245
#
220246
# The system-call numbers agree between the two --- measured, in the

‎README.md‎

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

1010
[target.'cfg(os = "macos")'.dependencies]
11-
openkal-macos = "0.6.0"
11+
openkal-macos = "0.6.1"
1212
```
1313

1414
Its purpose is as much to test the specification as to be used. A specification

‎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-macos"
4-
version = "0.6.0"
4+
version = "0.6.1"
55
description = "An implementation of openkal for macOS, written on the kernel's own calls. Its purpose is as much to test the specification as to be used."
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 okm_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(okm_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: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,36 @@ int await(int fd, short events, kal_u64 ns) {
5959
return kal_ok;
6060
}
6161

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

6494
extern "C" {
@@ -68,28 +98,28 @@ kal_intptr kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 ns
6898
// would turn a call that always succeeds into one that can expire.
6999
if (len == 0) return 0;
70100

71-
const int fd = okm::unpack(s.h);
72-
// THE STANDARD STREAMS ARE NOT PACKED HANDLES. openkal.stream reports them
73-
// as the descriptors themselves, so a word that does not unpack is taken to
74-
// be one of those rather than being refused.
75-
const int use = (fd >= 0) ? fd : static_cast<int>(s.h);
76-
77-
if (const int rc = await(use, static_cast<short>(okm::poll_in), ns); rc != kal_ok)
101+
if (const int rc = await(stream_fd(s), static_cast<short>(okm::poll_in), ns);
102+
rc != kal_ok)
78103
return -rc;
79104
return kal_stream_read(s, buf, len);
80105
}
81106

82107
kal_intptr kal_timeout_write(kal_stream s, const void* buf, kal_uintptr len, kal_u64 ns) {
83108
if (len == 0) return 0;
84109

85-
const int fd = okm::unpack(s.h);
86-
const int use = (fd >= 0) ? fd : static_cast<int>(s.h);
87-
88-
if (const int rc = await(use, static_cast<short>(okm::poll_out), ns); rc != kal_ok)
110+
if (const int rc = await(stream_fd(s), static_cast<short>(okm::poll_out), ns);
111+
rc != kal_ok)
89112
return -rc;
90113
return kal_stream_write(s, buf, len);
91114
}
92115

116+
// THE TWO OPERATIONS BELOW DO DECODE, AND THAT IS NOT AN INCONSISTENCY WITH THE
117+
// TWO ABOVE. A listener and a datagram are owned: their handles are made by
118+
// okm::pack and released by okm::retire, so the decode is the operation that
119+
// recovers the descriptor and its failure is how a released handle is refused.
120+
// A stream is borrowed and carries no generation. The four call sites divide
121+
// exactly along that line, and the two that were wrong were the two that had a
122+
// borrowed handle in hand.
93123
int kal_timeout_accept(kal_net_listener l, kal_u64 ns, kal_net_conn* out) {
94124
if (out == nullptr) return kal_err_invalid;
95125
const int fd = okm::unpack(l.h);

‎tests/conformance_env_time.cpp‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ int main() {
4646

4747
// The wall source is claimed by this implementation, so it must report a
4848
// time after the specification was written rather than zero.
49-
check(kal::time::has(kal::time::prop_wall_available), "the wall source is claimed");
49+
check(kal::time::has(kal::time::wall_available), "the wall source is claimed");
5050
check(kal::time::wall() > 1700000000ull * 1000000000ull,
5151
"the wall source reports a plausible time");
5252

53-
const char ok[] = "openkal-linux: env and time conformance\n";
53+
const char ok[] = "openkal-macos: env and time conformance\n";
5454
kal::write(kal::out(), ok, sizeof(ok) - 1);
5555
return failures == 0 ? 0 : 1;
5656
}

‎tests/conformance_fs.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ int main() {
100100
check(kal_fs_remove(root, "okl_dir", 7) == kal_ok, "the directory is removed");
101101
check(kal_fs_remove(root, "okl_probe.txt", 13) == kal_ok, "the file is removed");
102102

103-
const char ok[] = "openkal-linux: file system conformance\n";
103+
const char ok[] = "openkal-macos: file system conformance\n";
104104
kal::write(kal::out(), ok, sizeof(ok) - 1);
105105
return failures == 0 ? 0 : 1;
106106
}

‎tests/conformance_memory.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ int main() {
1616
// cannot be used.
1717
if (kal::alloc(0, 8) != nullptr) return 1;
1818

19-
const char ok[] = "openkal-linux: memory conformance\n";
19+
const char ok[] = "openkal-macos: memory conformance\n";
2020
kal::write(kal::out(), ok, sizeof(ok) - 1);
2121
return 0;
2222
}

‎tests/conformance_process_task.cpp‎

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ import openkal.stream;
66

77
namespace {
88
int failures = 0;
9+
10+
void say(const char* s) {
11+
kal_uintptr n = 0; while (s[n]) ++n;
12+
kal::write(kal::err(), s, n);
13+
}
14+
15+
void say_num(int v) {
16+
if (v < 0) { kal::write(kal::err(), "-", 1); v = -v; }
17+
char b[12]; int i = 12;
18+
if (v == 0) b[--i] = '0';
19+
while (v > 0) { b[--i] = static_cast<char>('0' + v % 10); v /= 10; }
20+
kal::write(kal::err(), b + i, static_cast<kal_uintptr>(12 - i));
21+
}
22+
923
void check(bool ok, const char* what) {
1024
if (ok) return;
1125
++failures;
@@ -57,17 +71,52 @@ int main() {
5771
const char* false_paths[] = { "bin/false", "usr/bin/false" };
5872
const kal_uintptr false_lens[] = { 9, 13 };
5973

74+
// THE CANDIDATE IS CHOSEN BY ASKING, AND IT USED TO BE CHOSEN BY
75+
// SPAWNING AND RETRYING. That retry could not work and never ran:
76+
// kal_process_spawn reports whether the DUPLICATE was made, and the
77+
// program is replaced afterwards, inside a copy the caller no longer
78+
// is. A path that does not exist therefore produces kal_ok and a
79+
// duplicate that finishes with 127, so the first candidate was always
80+
// taken and the second was unreachable code.
81+
//
82+
// On a system holding /usr/bin/true and no /bin/true the consequence
83+
// was `status == 127' at the observation below, which is what this
84+
// system reported the first time these suites were ever run. The
85+
// observation after it -- that a non-zero status is reported as such --
86+
// held throughout, upon a program that was never started.
87+
auto locate = [&](const char* const* paths, const kal_uintptr* lens_) -> int {
88+
for (int i = 0; i < 2; ++i) {
89+
kal_node_info info{}; info.self_size = sizeof info;
90+
if (kal_fs_info(slash, paths[i], lens_[i], 0, kal::fs::field::kind, &info) != kal_ok)
91+
continue;
92+
if (info.kind != kal_node_absent) return i;
93+
}
94+
return -1;
95+
};
96+
97+
const int t = locate(true_paths, true_lens);
98+
const int fpath = locate(false_paths, false_lens);
99+
check(t >= 0, "a program that succeeds is found");
100+
check(fpath >= 0, "a program that fails is found");
101+
60102
kal_process p{};
61103
const char* argv[] = { "openkal" };
62104
const kal_uintptr lens[] = { 7 };
63105
int rc = kal_err_invalid;
64-
for (int i = 0; i < 2 && rc != kal_ok; ++i)
65-
rc = kal_process_spawn(slash, true_paths[i], true_lens[i], argv, lens, 1,
106+
if (t >= 0)
107+
rc = kal_process_spawn(slash, true_paths[t], true_lens[t], argv, lens, 1,
66108
nullptr, nullptr, 0, nullptr, &p);
67109
check(rc == kal_ok, "a program is started");
68110
if (rc == kal_ok) {
69111
int status = -1, terminated = -1;
70112
check(kal_process_wait(p, &status, &terminated) == kal_ok, "the program is waited for");
113+
// The observed status is reported when it is wrong. `127' names an
114+
// image that was not replaced and `0' names one that ran; without
115+
// the number the two arrive as the same line.
116+
if (!(status == 0 && terminated == 0)) {
117+
say(" status="); say_num(status);
118+
say(" terminated="); say_num(terminated); say("\n");
119+
}
71120
check(status == 0 && terminated == 0, "the status it finished with is reported");
72121
kal_process_close(p);
73122
}
@@ -78,13 +127,19 @@ int main() {
78127
kal_process q{};
79128
const char* qargv[] = { "openkal" };
80129
int qrc = kal_err_invalid;
81-
for (int i = 0; i < 2 && qrc != kal_ok; ++i)
82-
qrc = kal_process_spawn(slash, false_paths[i], false_lens[i], qargv, lens, 1,
130+
if (fpath >= 0)
131+
qrc = kal_process_spawn(slash, false_paths[fpath], false_lens[fpath], qargv, lens, 1,
83132
nullptr, nullptr, 0, nullptr, &q);
133+
check(qrc == kal_ok, "the program that fails is started");
84134
if (qrc == kal_ok) {
85135
int status = -1, terminated = -1;
86136
kal_process_wait(q, &status, &terminated);
87-
check(status != 0, "a non-zero status is reported as such");
137+
// NOT MERELY NON-ZERO. 127 is what a duplicate reports when the
138+
// image was never replaced, so `!= 0' is satisfied by a program
139+
// that did not run -- which is precisely how this observation held
140+
// while the one above did not.
141+
check(status == 1 && terminated == 0,
142+
"the status of a program that fails is its own and not 127");
88143
kal_process_close(q);
89144
}
90145

@@ -105,12 +160,18 @@ int main() {
105160
const char* script = "test \"$0\" = openkal-observed-argv0";
106161
kal_uintptr script_len = 0; while (script[script_len]) ++script_len;
107162

163+
// Located rather than retried, for the reason recorded above: the retry
164+
// this replaces could not distinguish a path that does not exist from
165+
// one that does, so it always took the first.
166+
const int sh = locate(sh_paths, sh_lens);
167+
check(sh >= 0, "a shell is found");
168+
108169
kal_process r{};
109170
const char* rargv[] = { "openkal-observed-argv0", "-c", script };
110171
const kal_uintptr rlens[] = { 22, 2, script_len };
111172
int rrc = kal_err_invalid;
112-
for (int i = 0; i < 2 && rrc != kal_ok; ++i)
113-
rrc = kal_process_spawn(slash, sh_paths[i], sh_lens[i], rargv, rlens, 3,
173+
if (sh >= 0)
174+
rrc = kal_process_spawn(slash, sh_paths[sh], sh_lens[sh], rargv, rlens, 3,
114175
nullptr, nullptr, 0, nullptr, &r);
115176
check(rrc == kal_ok, "a shell is started");
116177
if (rrc == kal_ok) {
@@ -153,7 +214,7 @@ int main() {
153214
kal_task_yield();
154215
check(kal_task_current() != 0, "the calling context has an identity");
155216

156-
const char ok[] = "openkal-linux: process and task conformance\n";
217+
const char ok[] = "openkal-macos: process and task conformance\n";
157218
kal::write(kal::out(), ok, sizeof(ok) - 1);
158219
return failures == 0 ? 0 : 1;
159220
}

‎tests/conformance_stream.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ int main() {
3232
// A write transfers the whole buffer or reports why it could not. The
3333
// specification excludes a successful partial transfer, so a conforming
3434
// result reports either the full count or a non-zero error.
35-
const char msg[] = "openkal-linux: conformance\n";
35+
const char msg[] = "openkal-macos: conformance\n";
3636
// ⭐ ONE SIGNED WORD: the count, or the negated condition when no byte
3737
// moved. A caller never inspects two things to learn one thing.
3838
const kal_intptr r = kal::write(kal::out(), msg, sizeof(msg) - 1);

0 commit comments

Comments
 (0)