Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
345 changes: 345 additions & 0 deletions .agents/docs/2026-10-01-what-a-started-program-receives-design.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ compile_commands.json
# What a system leaves behind.
.DS_Store
Thumbs.db

# Reviews of other people's issues and pull requests. They quote work that is not
# ours and judge it before its authors have answered, so they stay on the machine
# that wrote them.
.agents/docs/reviews/
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,16 @@ conditional on the target.

```toml
[dependencies]
openkal = "0.14.0"
openkal = "0.14.1"

[target.'cfg(os = "linux")'.dependencies]
openkal-linux = "0.14.0"
openkal-linux = "0.15.1"

[target.'cfg(os = "macos")'.dependencies]
openkal-macos = "0.11.0"
openkal-macos = "0.12.1"

[target.'cfg(windows)'.dependencies]
openkal-windows = "0.9.0"
openkal-windows = "0.10.1"
```

The program imports the interface and names no implementation.
Expand Down
35 changes: 33 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# openkal Specification, version 0.14
# openkal Specification, version 0.14.1

## 1. Scope

Expand Down Expand Up @@ -925,6 +925,36 @@ handle.
`".."` remains invalid. The asymmetry is the point: the first names the thing
the program already holds, and the second names something it does not.

### 7.13 What a started program receives

A program started by `kal_process_spawn` shall receive the three streams and the
directories its caller granted, and no other handle. The requirement includes a
handle the calling program itself received from its own environment: such a
handle was not granted by the caller, and a caller that starts programs inside a
confinement relies on nothing reaching them that it did not name.

Where the environment's mechanism for starting a program that needs an
interpreter requires a handle to survive the start, an implementation may leave
one. It shall name no more than the program being started, and shall be left
only for a program that needs it.

A granted directory is received as a preopen: the started program enumerates,
through `kal_fs_preopen`, exactly the directories granted, in the order given and
under the names given, and a grant of none leaves it none. The first grant is
the directory the started program regards as the one it was started in, as the
first preopen is for every program. How an implementation conveys a grant is its
own concern; a grant confines a program that uses only its preopens, and the
confinement of one that does not is the environment's responsibility (clause 11,
entry 6).

The requirement was descriptive in version 0.13, in clause 11, entry 18, and two
implementations departed from it without any observation reporting the
departure. One conveyed every inheritable handle of the caller; another conveyed
a handle for the directory a program was found in, which reached the whole file
system from inside a confinement. Each implementation observes the first
paragraph in its own tests, since positions and handle tables are not portable;
the conformance suite observes the third.

## 8. Evolution

Each interface is versioned independently. A revision may add declarations and
Expand Down Expand Up @@ -1275,7 +1305,8 @@ The following are recorded so that they are not mistaken for oversights.
waiting.
18. **A stream at an arbitrary position of a started program.** Considered in
0.13 and not defined. A started program receives three streams, and a
descriptor at any other position is not conveyed. A position is the shape of
descriptor at any other position is not conveyed; version 0.14.1 states this
as a requirement in clause 7.13. A position is the shape of
one environment's descriptor table (clause 7.1): another environment conveys
handles to a started program as a list of values and has no positions, and a
started program would still need to be told where to look. Were the need
Expand Down
51 changes: 51 additions & 0 deletions conformance/src/sections/child.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const char* argument_for(errand e) {
case errand::exit_after_writing: return "--child-exit-after-writing";
case errand::abort_with_message: return "--child-abort";
case errand::wait_to_be_terminated: return "--child-wait";
case errand::report_grants: return "--child-grants";
case errand::report_no_preopens: return "--child-no-preopens";
case errand::none: break;
}
return "";
Expand All @@ -50,6 +52,8 @@ errand child_errand() {
if (same(a, len, argument_for(errand::exit_after_writing))) return errand::exit_after_writing;
if (same(a, len, argument_for(errand::abort_with_message))) return errand::abort_with_message;
if (same(a, len, argument_for(errand::wait_to_be_terminated))) return errand::wait_to_be_terminated;
if (same(a, len, argument_for(errand::report_grants))) return errand::report_grants;
if (same(a, len, argument_for(errand::report_no_preopens))) return errand::report_no_preopens;
}
#endif
return errand::none;
Expand Down Expand Up @@ -134,6 +138,32 @@ after g_after;
#else
for (;;) { }
#endif
case errand::report_grants:
// The copy reports through its status which part disagreed: the
// number of preopens, a name, or the directory behind a name.
#ifdef MCPP_FEATURE_FS
{
if (kal_fs_preopen_count() != 2) kal_exit(40);
for (kal_uintptr i = 0; i < 2; ++i) {
kal_dir d{}; char name[64]; kal_uintptr nlen = 0;
if (kal_fs_preopen(i, &d, name, sizeof name, &nlen) != kal_ok) kal_exit(41);
if (!same(name, nlen, grant_names[i])) kal_exit(42);
kal_file f{}; char buf[8];
if (kal_fs_open(d, grant_marker_file, sizeof grant_marker_file - 1,
kal::fs::open::read.bits, &f) != kal_ok) kal_exit(43);
const kal_intptr r = kal_stream_read(kal_fs_stream(f), buf, sizeof buf);
kal_fs_close_file(f);
if (r < 0 || !same(buf, static_cast<kal_uintptr>(r), grant_markers[i])) kal_exit(44);
}
kal_exit(0);
}
#endif
kal_exit(45);
case errand::report_no_preopens:
#ifdef MCPP_FEATURE_FS
kal_exit(kal_fs_preopen_count() == 0 ? 0 : 46);
#endif
kal_exit(45);
case errand::none:
break;
}
Expand Down Expand Up @@ -232,10 +262,31 @@ bool start_copy(const char* first_element, const char* errand_argument,
return e == kal_ok;
}

bool start_copy_granting(const char* errand_argument, const kal_preopen* grants,
kal_uintptr count, int& status, int& terminated) {
kal_dir base{}; const char* rel = nullptr; kal_uintptr rel_len = 0;
if (!locate_self(base, rel, rel_len)) return false;

const char* argv[2] = { "openkal-conformance-child", errand_argument };
kal_uintptr lens[2];
for (int i = 0; i < 2; ++i) { kal_uintptr n = 0; while (argv[i][n]) ++n; lens[i] = n; }

const kal_spawn_streams streams{ 0, 0, 0 };
const kal_spawn how{ base, base, nullptr, grants, count, 0 };
kal_process child{};
if (kal_process_spawn(&how, rel, rel_len, argv, lens, 2, nullptr, nullptr, 0,
&streams, &child) != kal_ok)
return false;
const int e = kal_process_wait(child, &status, &terminated);
kal_process_close(child);
return e == kal_ok;
}

#else

bool start_copy(const char*, const char*, int&, int&) { return false; }
bool start_copy_running(const char*, const char*, kal_process&) { return false; }
bool start_copy_granting(const char*, const kal_preopen*, kal_uintptr, int&, int&) { return false; }

#endif

Expand Down
14 changes: 14 additions & 0 deletions conformance/src/sections/child.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,18 @@ enum class errand {
exit_after_writing, // ... and shall not run what a C library would run after
abort_with_message, // kal_abort shall report the message and not return
wait_to_be_terminated, // so that a request to terminate has something to reach
report_grants, // the directories a starter granted are its preopens
report_no_preopens, // ... and a count of zero leaves it none (clause 7.13)
};

// The names the starter grants and the copy expects, in order, and the marker
// each directory holds. The second name carries both separators an
// implementation might use to convey names, so that one that does not quote
// them is reported.
inline constexpr const char* grant_names[2] = { "/okc-grant-a", "okc-grant-b;," };
inline constexpr const char* grant_markers[2] = { "A", "B" };
inline constexpr const char grant_marker_file[] = "okc-grant-marker";

// What this program was started to do, or `none' if it was started by a person.
errand child_errand();

Expand Down Expand Up @@ -56,4 +66,8 @@ bool start_copy(const char* first_element, const char* errand_argument,
bool start_copy_running(const char* first_element, const char* errand_argument,
kal_process& out);

// The same, granting the copy the given directories.
bool start_copy_granting(const char* errand_argument, const kal_preopen* grants,
kal_uintptr count, int& status, int& terminated);

} // namespace okc
66 changes: 66 additions & 0 deletions conformance/src/sections/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,72 @@ void run() {
"a unit this implementation cannot form is refused, not ignored",
"the implementation claims prop_job");
}

// A GRANT IS OBSERVED BY ITS EFFECT, WHICH IS VISIBLE TO AN openkal
// PROGRAM: the directories a started program receives are the ones it
// reads back through `kal_fs_preopen'. The copy is granted two
// directories, each holding a marker of its own, and reports whether it
// enumerates exactly those, in order, under the names given. A count of
// zero is a request for none, which is different from not asking.
//
// An implementation that does not claim the position refuses a grant
// rather than starting a program without it (clause 6.2). Version 0.14.1
// added both observations, because two implementations claimed the
// position for three releases while the program they started read back
// the directories it would have had anyway.
if (kal::process::has(kal::process::grant_dir)) {
const char* dirs[2] = { "okc-grant-a.tmp", "okc-grant-b.tmp" };
kal_preopen grants[2]{};
bool made = true;
for (int i = 0; i < 2; ++i) {
kal_uintptr n = 0; while (dirs[i][n]) ++n;
kal_fs_mkdir(kal::fs::working(), dirs[i], n);
kal_dir d{};
kal_file f{};
made = made && kal_fs_open_dir(kal::fs::working(), dirs[i], n, &d) == kal_ok
&& kal::fs::open_file(d, grant_marker_file, sizeof grant_marker_file - 1,
kal::fs::open::write | kal::fs::open::create
| kal::fs::open::truncate, &f) == kal_ok
&& kal_stream_write(kal_fs_stream(f), grant_markers[i], 1) == 1;
if (f.h) kal_fs_close_file(f);
kal_uintptr nl = 0; while (grant_names[i][nl]) ++nl;
grants[i] = kal_preopen{ d, grant_names[i], nl };
}
// A start that a claimed grant prevents is itself the failure: the
// copy is started from the directory that holds it, and placing a
// grant over that directory is one of the ways this went wrong.
int status = -1, terminated = -1;
if (made) {
const bool started = start_copy_granting(argument_for(errand::report_grants),
grants, 2, status, terminated);
observe(kind::behaviour, started && terminated == 0 && status == 0,
"granted directories are the started program's preopens, in order and by name");
} else {
unobserved(kind::behaviour, "granted directories are the started program's preopens",
"the directories to grant could not be made");
}
const bool started = start_copy_granting(argument_for(errand::report_no_preopens),
grants, 0, status, terminated);
observe(kind::behaviour, started && terminated == 0 && status == 0,
"a count of zero starts a program with no preopens");
for (int i = 0; i < 2; ++i) {
kal_fs_remove(grants[i].dir, grant_marker_file, sizeof grant_marker_file - 1);
if (grants[i].dir.h) kal_fs_close_dir(grants[i].dir);
kal_uintptr n = 0; while (dirs[i][n]) ++n;
kal_fs_remove(kal::fs::working(), dirs[i], n);
}
} else {
kal_process p{};
const char* argv[1] = { "x" };
const kal_uintptr lens[1] = { 1 };
const kal_preopen grant{ kal::fs::working(), "x", 1 };
const kal_spawn how{ kal::fs::working(), kal::fs::working(), nullptr,
&grant, 1, 0 };
const int e = kal_process_spawn(&how, "x", 1, argv, lens, 1,
nullptr, nullptr, 0, nullptr, &p);
observe(kind::behaviour, e == kal_err_not_supported,
"a grant this implementation cannot convey is refused, not ignored");
}
}

if (performs(kind::stability)) {
Expand Down
15 changes: 13 additions & 2 deletions include/openkal/process.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,19 @@ struct kal_spawn {
struct kal_job* job;

/* The directories the started program receives, read back through
* `kal_fs_preopen'. A count of zero starts a program with no preopens at
* all, which is a different thing from not asking. */
* `kal_fs_preopen' in the order given and under the names given. A count
* of zero starts a program with no preopens at all, which is a different
* thing from not asking (a null `grants'), which leaves the directories an
* implementation supplies by default.
*
* The first grant is the directory the started program regards as the one
* it was started in, as the first preopen is for every program; a C
* library above openkal resolves a relative name against it and an absolute
* name against the grant whose name is the longest prefix, so names that
* are absolute paths serve such a program best. A grant confines a program
* that uses only its preopens; one that reaches beyond them through its
* environment is confined by the environment or not at all. How a grant is
* conveyed is the implementation's concern. Clause 7.13. */
const struct kal_preopen* grants;
kal_uintptr grant_count;

Expand Down
2 changes: 1 addition & 1 deletion include/openkal/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
* consumer actually asks for was checked by nobody. */
#define KAL_VERSION_MAJOR 0u
#define KAL_VERSION_MINOR 14u
#define KAL_VERSION_PATCH 0u
#define KAL_VERSION_PATCH 1u

#define KAL_VERSION_MAKE(major, minor, patch) \
(((kal_u64)(major) << 32) | ((kal_u64)(minor) << 16) | \
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "openkal"
version = "0.14.0"
version = "0.14.1"
description = "openkal: a portable kernel ABI specification. This package carries the normative declarations; implementations are separate packages."
license = "Apache-2.0"
authors = ["mcpplibs"]
Expand Down
4 changes: 2 additions & 2 deletions src/fs.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ inline int open_file(dir base, const char* name, kal_uintptr len,

inline kal_uintptr preopen_count() { return kal_fs_preopen_count(); }

// The first entry, which every implementation supplies and which denotes the
// directory the program was started in.
// The first entry, which denotes the directory the program was started in: the
// one an implementation supplies, or the first a starter granted (clause 7.13).
inline dir working() {
dir d{}; kal_uintptr l = 0;
kal_fs_preopen(0, &d, nullptr, 0, &l);
Expand Down
Loading