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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ for Linux, written on the kernel's own system-call interface.
openkal = "0.9.0"

[target.'cfg(os = "linux")'.dependencies]
openkal-linux = "0.8.0"
openkal-linux = "0.9.0"
```

## Why it does not use a C library
Expand Down
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "openkal-linux"
version = "0.8.0"
version = "0.9.0"
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."
license = "Apache-2.0"

Expand All @@ -18,7 +18,7 @@ authors = ["mcpplibs"]
repo = "https://github.com/mcpplibs/openkal-linux"

[dependencies]
openkal = "0.9.0"
openkal = "0.10.0"

# The package contributes definitions and no modules. The interface it
# implements is declared by the specification package, which this package
Expand Down
132 changes: 129 additions & 3 deletions src/fs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,33 @@ int kal_fs_set_modified(kal_file f, kal_u64 modified_ns) {
return okl::failed(r) ? okl::translate(r) : kal_ok;
}

// The same, upon a NAME. Version 0.10.
//
// ⚠️⚠️ ADDED BECAUSE THE FORM ABOVE CANNOT REACH A DIRECTORY, AND A CONSUMER
// PAID FOR THAT. `kal_fs_set_modified' takes a `kal_file'; a directory is opened
// as a `kal_dir'; there was no third thing. openkal-musl reached a lock
// directory's timestamp by opening the directory for READING and setting the
// time on that, which worked here and was outside anything the interface said.
// This is the stated route.
int kal_fs_set_modified_at(kal_dir base, const char* name, kal_uintptr len,
kal_u64 modified_ns) {
const int b = okl::unpack(base.h);
if (b < 0 || !okl::acceptable(name, len)) return kal_err_invalid;
okl::terminated t(name, len); if (!t.ok) return kal_err_invalid;

constexpr okl_i64 utime_omit = 0x3ffffffe;
okl::ktimespec times[2];
times[0].sec = 0; times[0].nsec = utime_omit;
times[1].sec = static_cast<okl_i64>(modified_ns / 1000000000u);
times[1].nsec = static_cast<okl_i64>(modified_ns % 1000000000u);

// Resolves, because opening resolves and this is stated to agree with it.
const okl_long r = okl::sys(okl::nr_utimensat, b,
reinterpret_cast<okl_long>(t.buf),
reinterpret_cast<okl_long>(times), 0);
return okl::failed(r) ? okl::translate(r) : kal_ok;
}

int kal_fs_mkdir(kal_dir base, const char* name, kal_uintptr len) {
const int b = okl::unpack(base.h);
if (b < 0 || !okl::acceptable(name, len)) return kal_err_invalid;
Expand Down Expand Up @@ -379,8 +406,15 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter,
// them.
kal_uintptr kal_fs_props(kal_dir d) {
const int fd = okl::unpack(d.h);
// ⭐ LOCKS AND CAPACITY ARE IN THE CONSERVATIVE SET, and that is a claim
// about this kernel rather than about the volume: an open-file lock and
// `fstatfs' are answered by the VFS for every format beneath it, including
// the read-only ones --- a lock excludes writers a read-only volume does not
// have, which is a true answer and not a useful one. A format that could not
// would have to be excluded by name here, and this kernel has none.
const kal_uintptr conservative =
KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME;
KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME
| KAL_FS_PROP_LOCKS | KAL_FS_PROP_CAPACITY;
if (fd < 0) return 0;

okl::kstatfs sf{};
Expand All @@ -399,9 +433,10 @@ kal_uintptr kal_fs_props(kal_dir d) {
// rename cannot be atomic because there is no rename.
case okl::fs_squashfs: case okl::fs_erofs:
return KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_CASE_SENSITIVE
| KAL_FS_PROP_LINKS;
| KAL_FS_PROP_LINKS | KAL_FS_PROP_LOCKS | KAL_FS_PROP_CAPACITY;
case okl::fs_iso9660:
return KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_CASE_SENSITIVE;
return KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_CASE_SENSITIVE
| KAL_FS_PROP_LOCKS | KAL_FS_PROP_CAPACITY;

// The FAT family stores neither a case distinction nor a node that
// names another. `symlink' on such a volume reports EPERM, and this is
Expand All @@ -418,6 +453,97 @@ kal_uintptr kal_fs_props(kal_dir d) {
}
}

// --- exclusion upon a range of a file --------------------------------------
//
// ⭐⭐ THE OPEN-FILE FORM, AND THE DIFFERENCE IS THE WHOLE REASON THIS IS
// WORTH SPECIFYING.
//
// This kernel's oldest record lock is held by the PROCESS and is released as
// soon as that process closes ANY descriptor for the node --- so a library that
// opened one file twice destroyed its own lock, and two parts of one program
// could not exclude each other at all. openkal states that the holder is the
// `kal_file', which is exactly what `F_OFD_*' describes: the lock belongs to the
// open file description and ends when the last descriptor for it closes, and
// when the program ends however it ends.
//
// ⚠️ Releasing on death is the half a caller cannot build for itself. Exclusion
// it can: `KAL_OPEN_EXCLUSIVE' and a name beside the file. What nothing above
// this line can do is release that name when its holder dies, so a program that
// ended abnormally while holding one would be locked out of its own file for
// ever.
static int lock_range(kal_file f, kal_u64 start, kal_u64 len,
short type, bool wait) {
const int fd = okl::unpack(f.h);
if (fd < 0) return kal_err_invalid;

okl::kflock fl{};
fl.l_type = type;
fl.l_whence = okl::seek_set;
fl.l_start = static_cast<okl_i64>(start);
// openkal spells "to the end, however far that comes to be" as zero, and so
// does this kernel. The two agree, so nothing is translated.
fl.l_len = static_cast<okl_i64>(len);

const okl_long cmd = wait ? okl::f_ofd_setlkw : okl::f_ofd_setlk;
okl_long r;
do {
r = okl::sys(okl::nr_fcntl, fd, cmd, reinterpret_cast<okl_long>(&fl));
} while (okl::interrupted(r));
if (!okl::failed(r)) return kal_ok;

// ⚠️⚠️ TWO VALUES MEAN ONE THING HERE, AND openkal NAMES ONE OF THEM.
//
// The standard this call comes from says a range another holder has is
// reported as EITHER of two values, and leaves the choice to the system ---
// so a caller must accept both and an implementation of openkal must not
// pass that choice on. openkal says `kal_err_again', which is the answer a
// caller polls upon; the other value translates to `permission', which a
// caller reads as "asking again will not help" and acts upon by stopping.
//
// ⭐ Narrowed to the attempt that does NOT wait, because that is the only
// one for which the two values carry this meaning. A permission failure on
// any other path keeps its own answer.
if (!wait && (-r == okl::e_acces || -r == okl::e_again)) return kal_err_again;
return okl::translate(r);
}

int kal_fs_lock(kal_file f, kal_u64 start, kal_u64 len, kal_uintptr mode) {
const bool shared = (mode & KAL_LOCK_SHARED) != 0;
const bool exclusive = (mode & KAL_LOCK_EXCLUSIVE) != 0;
// One of the two, and not both and not neither: a caller that asked for
// both asked for something no environment has, and one that asked for
// neither did not say what it wanted.
if (shared == exclusive) return kal_err_invalid;
return lock_range(f, start, len,
shared ? okl::lock_read : okl::lock_write,
(mode & KAL_LOCK_WAIT) != 0);
}

int kal_fs_unlock(kal_file f, kal_u64 start, kal_u64 len) {
// Never waits: releasing is not a request another holder can block.
return lock_range(f, start, len, okl::lock_unlock, false);
}

// --- how much the volume holds ----------------------------------------------
int kal_fs_capacity(kal_dir d, kal_u64* total, kal_u64* available) {
const int fd = okl::unpack(d.h);
if (fd < 0) return kal_err_invalid;

okl::kstatfs sf{};
const okl_long r = okl::sys(okl::nr_fstatfs, fd, reinterpret_cast<okl_long>(&sf));
if (okl::failed(r)) return okl::translate(r);

// In bytes, because that is what the interface says and what a caller of it
// wants; this kernel reports blocks and the size of one.
const kal_u64 unit = static_cast<kal_u64>(sf.f_bsize);
// ⚠️ `f_bavail' AND NOT `f_bfree'. The second counts blocks the volume has,
// including those only a privileged writer may reach; the first counts the
// ones THIS program could actually use, which is the question asked.
if (total) *total = static_cast<kal_u64>(sf.f_blocks) * unit;
if (available) *available = static_cast<kal_u64>(sf.f_bavail) * unit;
return kal_ok;
}

// Nodes whose content is another name.
int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len,
const char* target, kal_uintptr target_len,
Expand Down
120 changes: 114 additions & 6 deletions src/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,34 @@ int kal_process_spawn(kal_dir base,
if (in != 0) okl::sys(okl::nr_dup3, in, 0, 0);
if (ou != 0) okl::sys(okl::nr_dup3, ou, 1, 0);
if (er != 0) okl::sys(okl::nr_dup3, er, 2, 0);
// The started program's working directory is the directory supplied
// here, expressed by naming the program relative to it. There is no
// operation that changes a working directory afterwards, because a
// working directory that can be changed is shared mutable state
// between execution contexts.
// ⚠️⚠️ THIS COMMENT USED TO CLAIM A PROPERTY THIS CODE DOES NOT HAVE.
// It said the started program's working directory is the directory
// supplied here. It is not. `b' is the directory the NAME resolves
// against and nothing more --- `execveat' takes a dirfd to resolve
// `p.buf', and resolving a name is not entering a directory. The
// started program's working directory is this implementation's own,
// whatever that happens to be, inherited across the clone above.
//
// ⭐ Found by a consumer's test rather than by reading, which is the
// point: `chdir' then start a program, ask it for its working
// directory, and it answers the directory the caller left --- against
// a host as control, which answers the one the caller entered.
//
// ⇒ AND IT IS NOT FIXABLE HERE. An `fchdir(b)' before the replacement
// would make the sentence true and the behaviour no better: `b' is
// whichever preopen the name resolved under --- for a program named
// `/usr/bin/sh' that is the root --- so the started program would get
// an arbitrary directory instead of a different arbitrary directory.
// Naming the program and naming where it runs are two directories, and
// openkal has an argument for one of them. openkal-musl's `chdir'
// therefore rebinds its own table and cannot do better; the interface
// has no operation that carries the second directory across a spawn.
//
// There is deliberately no operation that changes a working directory
// afterwards, because a working directory that can be changed is
// shared mutable state between execution contexts. That refusal is
// sound and is NOT what is missing --- what is missing is a way to say,
// at the moment of starting, which directory the program starts in.
const okl_long why =
okl::sys(okl::nr_execveat, b, reinterpret_cast<okl_long>(p.buf),
reinterpret_cast<okl_long>(args.slots),
Expand All @@ -225,6 +248,90 @@ int kal_process_spawn(kal_dir base,
return kal_ok;
}

// The same, with the started program's lifetime bound to this one's. 0.10.
//
// ⭐⭐ WHY THIS EXISTS, AND IT IS NOT A CONVENIENCE.
//
// A C library asked for `execve' composes it out of what this interface has:
// start the program, wait for it, end with its status. That composition leaves
// THREE images where a system with the operation has two --- the caller, a copy
// that waits, and the program --- and `kal_process_terminate' upon the identifier
// the caller holds reaches the WAITER. Measured with a host as control: identical
// status words, opposite outcomes; the caller is told the program died on the
// signal it sent, while the program runs to completion, unsupervised.
// openkal-linux#13.
//
// ⚠️ THE BINDING IS SET IN THE STARTED IMAGE AND NOT FROM HERE, which is why it
// is a second spawn rather than an operation applied to a handle. This kernel's
// facility answers "end this context when the one that started it ends", and only
// that context can ask for it.
//
// ⚠️ AND IT IS ASKED FOR BEFORE THE REPLACEMENT AND CHECKED AFTER: the setting
// survives the replacement, but the parent could have ended in between --- in
// which case the signal has already been delivered and there is nothing to
// notice. Reading the parent's identity after arming closes that window: if it
// is no longer the one that armed, this image ends now rather than becoming the
// orphan the caller asked not to have.
int kal_process_spawn_bound(kal_dir base,
const char* path, kal_uintptr path_len,
const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc,
const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc,
const kal_spawn_streams* streams,
kal_process* out) {
const int b = okl::unpack(base.h);
if (b < 0 || out == nullptr) return kal_err_invalid;
if (!okl::acceptable(path, path_len)) return kal_err_invalid;
okl::terminated p(path, path_len);
if (!p.ok) return kal_err_invalid;

vector args, envs;
if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory;
if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory;

const okl_long in = streams ? static_cast<okl_long>(streams->in.h) : 0;
const okl_long ou = streams ? static_cast<okl_long>(streams->out.h) : 0;
const okl_long er = streams ? static_cast<okl_long>(streams->err.h) : 0;

const okl_long mine = okl::sys(okl::nr_getpid);

exec_report report;
report.open(0);

const okl_long child = okl::sys(okl::nr_clone, 17 /* SIGCHLD */, 0, 0, 0, 0);
if (okl::failed(child)) { report.close_both(); return okl::translate(child); }

if (child == 0) {
if (in != 0) okl::sys(okl::nr_dup3, in, 0, 0);
if (ou != 0) okl::sys(okl::nr_dup3, ou, 1, 0);
if (er != 0) okl::sys(okl::nr_dup3, er, 2, 0);

// 9 is SIGKILL: the binding must not be something the started program
// can decline, because the caller asked for a program that does not
// outlive it and not for one that is invited not to.
okl::sys(okl::nr_prctl, okl::pr_set_pdeathsig, 9, 0, 0, 0);
// The window: if the caller ended between the clone and the line above,
// the signal is already spent and this image would survive it.
if (okl::sys(okl::nr_getppid) != mine)
okl::sys(okl::nr_exit_group, 127);

const okl_long why =
okl::sys(okl::nr_execveat, b, reinterpret_cast<okl_long>(p.buf),
reinterpret_cast<okl_long>(args.slots),
reinterpret_cast<okl_long>(envs.slots), 0);
report.say(why);
okl::sys(okl::nr_exit_group, 127);
for (;;) { }
}

if (const okl_long why = report.heard()) {
reap(child);
return okl::translate(why);
}

*out = kal_process{ static_cast<kal_uintptr>(child) };
return kal_ok;
}

// A channel: a pair of streams of which one end is meant to cross a spawn.
//
// WHY THIS IS A KERNEL FACILITY AND kal::kit's CHANNEL IS NOT. A started program
Expand Down Expand Up @@ -389,7 +496,8 @@ void kal_process_close(kal_process) { }
kal_uintptr kal_process_props(void) {
return KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING
| KAL_PROCESS_PROP_EXIT_STATUS
| KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR;
| KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR
| KAL_PROCESS_PROP_BOUND_LIFETIME;
}

}
36 changes: 35 additions & 1 deletion src/sys.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ enum : okl_long {
nr_readv = 19, nr_writev = 20, nr_sched_yield = 24, nr_nanosleep = 35,
nr_getpid = 39, nr_clone = 56, nr_execve = 59, nr_exit = 60, nr_wait4 = 61,
nr_kill = 62, nr_ftruncate = 77, nr_getcwd = 79, nr_fsync = 74,
nr_fcntl = 72,
nr_fcntl = 72, nr_prctl = 157, nr_sched_getaffinity = 204, nr_getppid = 110,
nr_arch_prctl = 158, nr_gettid = 186, nr_futex = 202,
nr_getdents64 = 217, nr_set_tid_address = 218, nr_clock_gettime = 228,
nr_clock_getres = 229, nr_exit_group = 231, nr_tgkill = 234,
Expand Down Expand Up @@ -180,6 +180,7 @@ enum : okl_long {
nr_getpid = 172, nr_mmap = 222, nr_munmap = 215, nr_mprotect = 226,
nr_clone = 220, nr_execve = 221, nr_wait4 = 260, nr_renameat = 38,
nr_dup3 = 24, nr_execveat = 281, nr_dup2 = -1, nr_fcntl = 25,
nr_prctl = 167, nr_sched_getaffinity = 123, nr_getppid = 173,
nr_arch_prctl = -1, nr_utimensat = 88, nr_symlinkat = 36, nr_fstatfs = 44,
nr_getrandom = 278,
// openkal.net and openkal.datagram
Expand Down Expand Up @@ -252,6 +253,26 @@ enum : okl_long {
// already had there. `dup3' cannot do this: it is told the number, and it
// closes what is on it.
f_dupfd_cloexec = 1030,

// ⭐⭐ THE OPEN-FILE FORM AND NOT THE PROCESS FORM, WHICH IS THE WHOLE
// DIFFERENCE.
//
// This kernel's oldest record lock is held by the PROCESS, and it is
// released as soon as that process closes ANY descriptor for the node ---
// so a library that opened one file twice destroyed its own lock, and two
// parts of one program could not exclude each other at all. openkal states
// the holder as the `kal_file', which is exactly what these describe: the
// lock belongs to the open file description and ends when the last
// descriptor for it closes.
f_ofd_getlk = 36, f_ofd_setlk = 37, f_ofd_setlkw = 38,

// A lock's kind, and where a range begins.
lock_read = 0, lock_write = 1, lock_unlock = 2,
seek_set = 0,

// "End this context when the one that started it ends", which is what
// binding a started program's lifetime is expressed as here.
pr_set_pdeathsig = 1,
#if defined(__x86_64__)
o_directory = 0200000, o_nofollow = 0400000,
#else
Expand Down Expand Up @@ -304,6 +325,19 @@ inline bool interrupted(okl_long r) { return r == -e_intr; }
// What the kernel reports about the volume a descriptor is on. The layout is
// the kernel's own `struct statfs', which is one layout on every architecture
// this implementation supports because both are LP64.
// The kernel's own `struct flock'. One layout on both architectures this
// implementation builds for: two shorts, padded to eight, then two 64-bit
// positions and the identifier of a holder this implementation never asks about.
struct kflock {
short l_type;
short l_whence;
int l_pad;
okl_i64 l_start;
okl_i64 l_len;
int l_pid;
int l_pad2;
};

struct kstatfs {
okl_long f_type;
okl_long f_bsize;
Expand Down
Loading
Loading