diff --git a/.cargo/cc b/.cargo/cc deleted file mode 120000 index 35f1592..0000000 --- a/.cargo/cc +++ /dev/null @@ -1 +0,0 @@ -../external/libgotcha/cc \ No newline at end of file diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 120000 index 5f5b023..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1 +0,0 @@ -../external/libgotcha/libgotcha.cargo \ No newline at end of file diff --git a/.cargo/linker b/.cargo/linker deleted file mode 120000 index eb80dd6..0000000 --- a/.cargo/linker +++ /dev/null @@ -1 +0,0 @@ -../ld \ No newline at end of file diff --git a/.cargo/linker.so b/.cargo/linker.so deleted file mode 120000 index d93ecbb..0000000 --- a/.cargo/linker.so +++ /dev/null @@ -1 +0,0 @@ -../ld.so \ No newline at end of file diff --git a/.cargo/rustc b/.cargo/rustc deleted file mode 100755 index 0c76c41..0000000 --- a/.cargo/rustc +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -LIBGOTCHA_SYMBOLS="--globalize-symbol libgotcha_arch_prctl --globalize-symbol libgotcha_pthread_kill" exec rustc "$@" diff --git a/.gitignore b/.gitignore index d56d499..82cb277 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,5 @@ -/.cargo/config -/target/ - +src/main.rs +target/ *.a *.lock -*.log -*.rs.bk -*.so - -!/.cargo/cc.so +*.o diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e055c01..0000000 --- a/.gitmodules +++ /dev/null @@ -1,8 +0,0 @@ -[submodule "libgotcha"] - path = external/libgotcha - url = ../libinger - branch = gotcha -[submodule "libtimetravel"] - path = external/libtimetravel - url = ../libinger - branch = timetravel diff --git a/Cargo.toml b/Cargo.toml index ccaa556..15f735b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,39 +1,6 @@ [package] -name = "inger" +name = "ucontext" version = "0.0.0" -edition = "2018" - -[lib] -crate-type = ["dylib"] -harness = false - -[features] -notls = [] [dependencies] libc = "*" - -[dependencies.gotcha] -path = "external/libgotcha" - -[dependencies.signal] -path = "internal/libsignal" -features = ["libgotcha"] - -[dependencies.timetravel] -path = "external/libtimetravel" -features = ["libgotcha"] - -[[bench]] -name = "inger" -harness = false - -[[bench]] -name = "baseline" -harness = false - -[dev-dependencies] -bencher = "*" - -[workspace] -exclude = ["external/libgotcha/examples/cargo"] diff --git a/README.md b/README.md deleted file mode 100644 index c3c1721..0000000 --- a/README.md +++ /dev/null @@ -1,394 +0,0 @@ -_libinger_ -========== -This is the source code of the _libinger_ library for making function calls with timeouts from Rust -and C, described in our ATC '20 paper, "Lightweight Preemptible Functions." _As the proverb goes, -"If you don't want your function calls to linger, link with `-linger`."_ - -Also present are a few supporting libraries: - * `external/libgotcha`: runtime providing the libset abstraction that makes all of this possible - * `external/libtimetravel`: makes it easier to safely perform unstructured jumps from Rust code - * `internal/libsignal`: unsafe wrappers around C library functions (don't use at home!) - - -License -------- -The entire contents and history of this repository are distributed under the following license: -``` -Copyright 2020 Carnegie Mellon University - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -``` - - -System requirements -------------------- -Presently, x86-64 GNU/Linux is the only supported platform. The build requirements are as follows: - * `rustc` ≥1.36.0 (versions starting with 1.37.0 include a breaking change to dylib symbol exports— - https://github.com/rust-lang/rust/issues/64340 —but the build system now implements a workaround) - * `cargo` ≤1.39.0/"0.40.0" or ≥1.55.0/"0.56.0" (a version between these introduced a breaking - but since fixed regression in path resolution: https://github.com/rust-lang/cargo/issues/8202) - * `bindgen` ≤0.52 (or a newer version with a wrapper script that passes the `--size_t-is-usize` - flag, as per https://github.com/rust-lang/rust-bindgen/issues/1901) - * `gcc` (tested with 9.2.1) - * GNU `make` - * GNU binutils (tested with 2.34) - * GNU libc ≤2.33 (versions starting with 2.30 include a breaking change to `dlopen()` behavior on - executables—https://sourceware.org/bugzilla/show_bug.cgi?id=24323 —but libgotcha now implements a - workaround; versions starting with 2.34 combine libpthread into libc, which is unlikely to work - out of the box) - * `libebl` from elfutils 0.176 (newer versions eliminate the libebl shared library, which we - currently depend on directly) - * `git` -On Debian-based systems, bindgen appears to require the libclang-dev package; if it is missing, you -will get errors about missing headers. If you don't want to install this package, you may be able -to work around it by symlinking /usr/local/include/asm to /usr/include/asm. - - -A note on terminology ---------------------- -We use the term lightweight preemptible function (LPF) to refer to the timed _version_ of a -function, as invoked via the `launch()` wrapper function in this library. It's not quite right to -say that _libinger_ "provides preemptible functions"; rather, it provides a transformation from -an ordinary function into a preemptible one. - -To provide the memory isolation necessary to introduce preemption and asynchronous cancellation at -sub-thread granularity without breaking existing program dependencies, the _libgotcha_ runtime -allocates a separate copy of all the program's loaded dynamic libraries for each preemptible -function. While the paper refers to this isolation unit as a libset, that term was unfortunately -coined late in development; as such, the source code and configuration variables refer to it as a -"group" instead. - - -A note on design ----------------- -The paper describes the _libinger_ API, and the generated HTML documentation gives a few more usage -details. Here are some of the guiding principles that inspired our interface choices: - * **We do not assume users need asynchrony.** Hence, preemptible functions _run on the same kernel - thread as their caller_. This is good for performance (and especially invocation latency), but - it is also important to be aware of; for instance, it means that a preemptible function will - deadlock if it attempts to acquire a lock held by its caller, or vice versa. If asynchrony is - something you require, you can build it atop _libinger_, as we have demonstrated with our - _libturquoise_ preemptive userland thread library. - * **We assume that simply calling a function with a timeout is the common use case.** As such, the - `launch()` wrapper both constructs and begins executing the preemptible function rather than - asking the user to first employ a separate constructor. The latter behavior can be achieved by - passing the sentinel `0` as the timeout, then later using `resume()` to invoke the preemptible - function. - * **We endeavor to keep argument and return value passing simple yet extensible.** Because Rust - supports closures, the Rust version of `launch()` accepts only nullary functions: those seeking - to pass arguments should just capture them from the environment. Because C supports neither - closures nor generics, the C version of `launch()` accepts a single `void *` argument that can - serve as an inout parameter; it occupies the last position in the parameter list to permit - (possible) eventual support for variable argument lists. - * **We choose defaults to favor flexibility and performance.** When a preemptible function times - out, _libinger_ assumes the caller might later want to resume it from where it left off. As - such, both `launch()` and `resume()` pause in this situation; this incurs some memory and time - overhead to provide a separate execution stack and package the continuation object, but has much - lower overall cost than asynchronous cancellation. If the program does require cancellation, it - can request it explicitly by calling `cancel()` (C) or dropping the continuation object (Rust). - * **We provide preemption out of the box, but the flexibility to cooperatively yield.** The - `pause()` primitive allows a preemptible function to "yield" back to its caller by immediately - "timing out." One can imagine building higher-level synchronization constructs atop this; for - example, a custom mutex that paused instead of blocking would allow two or more preemptible - functions to share state, even when some of them executed from the same kernel thread. - * **We favor a simple, language-agnostic interface.** Because the interface is based on the - foundational function call abstraction, it looks very similar in both C and Rust. Someday, it - may look _equally_ similar in other languages as well, and in the meantime, it ought to enjoy - compatibility with languages' C foreign-function interfaces. It's relatively simple to integrate - higher-level abstractions on top, such as the Rust futures wrapper available in _libinger_'s - `future` module. - - -A note on implementation ------------------------- -The timer signal handler in _libinger_ refuses to pause while the next libset is set to 0 (the -starting libset). Because _libinger_ is statically linked with _libgotcha_, the latter enforces a -transparent switch to this libset whenever a dynamic function call transfers control into the module -in the process image that corresponds to `libinger.so`. This means that preemption is deferred on a -given kernel thread while _libinger_'s own code is executing on that thread. - -Of course, things are not quite that simple. There are noteworthy exceptions to the rule: - * The public _libinger_ Rust interface includes a number of generic functions. Because the Rust - compiler monomorphizes such functions for the client code that uses them, _their implementations - are_ not _in `libinger.so`!_ Rather, there is, roughly speaking, one or more copies of them - (specialized for various type arguments) in each program module that calls them. The generic - functions are therefore implemented such that they package everything that differs by type, then - call into non-specialized functions such as `setup_stack()` and `switch_stack()` to do the scary - stuff. - * The `resume_preemption()` function is installed as a _libgotcha_ callback hook, and is implicitly - invoked at the end of each deferred-preemption library call made by a preemptible function. - _This happens in the preemptible function's libset rather than the starting one_; this is - essential because the callback's main task is to force the timer signal handler to run - _immediately_ and check for a timeout, and we don't want the libset to inhibit preemption! - - -Building glibc from source --------------------------- -_Note: If you seek only to test_ libinger _with a very small program that doesn't require many -preemptible functions and has few dynamic library dependencies, and you are willing to build -without debug symbols, you may be able to skip this section provided you restrict the number of -libsets at runtime by exporting the `$LIBGOTCHA_NUMGROUPS` variable before invoking your program -(e.g., `$ LIBGOTCHA_NUMGROUPS=1 ./i_only_use_one_lpf_at_a_time`)._ - -Although _libinger_ is compatible with an unmodified glibc in principle, in practice the build -configuration used by most distributions is insufficient for two reasons: - * By loading numerous copies of the application's libraries, we tend to exhaust the static storage - pool provided by the dynamic linker. If your program hits this limit, it will crash at load time - with an error like: `yourprogram: libgotcha error: Unable to load ancillary copies of library: - somelibrary.so: cannot allocate memory in static TLS block`. - * Stock glibc builds are limited to 16 linker namespaces, enough to support only 15 preemptible - functions at any given time. If your program hits this limit, it will crash at runtime with an - error like: `launch(): too many active timed functions`. - -Unfortunately, these configuration parameters are baked into the dynamic linker at build time. -What's more, changing (at least) the latter alters the size of internal structures that are shared -between `ld-linux.so`, `libc.so`, `libpthread.so`, and others, so making changes requires rebuilding -all of glibc. Fortunately, provided you set the prefix properly when building glibc, the dynamic -linker will know where to search (by absolute path) for the other libraries; as such, most -applications that depend on `libinger.so` need only define a custom interpreter path pointing to the -`ld-linux-x86-64.so.2` file in your build directory. - -Note that glibc 2.32 eliminated the below `TLS_STATIC_SURPLUS` compile-time constant and replaced it -with a runtime tunable read from the environment. Debian backported this change to their glibc 2.31 -distribution. If you are on an affected version, disregard the related steps below; instead, export -something like `GLIBC_TUNABLES=glibc.rtld.optional_static_tls=0x10000` when running your program. - -Follow these steps to build your own glibc, where all paths are relative to the root of this -repository: - 1. Clone the glibc source code: `$ git clone git://sourceware.org/git/glibc`. I'll assume you put - it in `../glibc`, and have checked out whatever version you want to use. - 1. Edit `TLS_STATIC_SURPLUS` in `../glibc/elf/dl-tls.c` to raise the multiplier on `DL_NNS` by at - least two orders of magnitude*. Our current recommendation is a multiplier of 2000. - 1. Change `DL_NNS` in `../glibc/sysdeps/generic/ldsodefs.h` to exceed by at least one the maximum - number of simultaneous preemptible functions your program will need*. The default value of 16 - should be fine for use cases with low parallelism and where preemptible functions are usually - allowed to run to completion before launching others; we have not tested values above 512. - 1. Make a new empty directory to use for the build and `cd` into it. Let's say this is located at - `../rtld`. - 1. Decide where you want to place your new glibc "installation." I'll be using `../rtld/usr`. - 1. Still working in `../rtld`, configure the build: - `$ ../glibc/configure --prefix=$PWD/usr --disable-werror`. - 1. Start the build: `$ make -j8`, assuming you have 8 cores. - 1. When the build finishes successfully, run `$ make install` to populate the "installation" - directory you chose earlier. Note that the `install` target appears to be finicky about running - with multiple cores, so combining this step with the previous one can cause issues (at least) - the first time you build glibc. - 1. Add to the `../rtld/usr/lib` directory a symlink to _each_ of the following libraries on your - system: `libasm.so.1` (from elfutils), `libbz2.so.1.0`, `libdw.so.1` (also from elfutils), - `libgcc_s.so.1`, `liblzma.so.5`, `libstd-*.so` (from Rust), and `libz.so.1`. - 1. Move back into the root of this repository and tell the build system where to find your custom - dynamic linker build: `ln -s ../rtld/usr/lib/ld-linux-x86-64.so.2 ld.so`. - -\* The `inger-atc20-cfp` tag records the revision used to run the _libinger_ microbenchmarks for the - ATC paper, and is annotated with the build customizations applied to glibc and _libgotcha_ for - that use case. Similarly, the repository containing our full-system benchmarks contains - annotated tags recording the configuration used to evaluate _hyper_ and _libpng_. To see the - annotations, use `git show`. To run the benchmarks, use `cargo bench`. - - -Building _libinger_ -------------------- -The _libinger_ library is built with Cargo, although Make is also required because of _libgotcha_. - 1. Follow the steps in the preceding section to build your own glibc root, or skip them at your own - peril (in which case you will have to ignore the error in the following step). - 1. Working in the root of this repository, configure the build: `$ ./configure`. - 1. Build the library: `$ cargo build --release`. - 1. If you intend to use preemptible functions from C, generate the header: - `$ cargo run --release >libinger.h` - -The `libinger.so` library will be located in `target/release`; this and the header are the only -files required to build C programs against _libinger_. For Rust programs, the `rlib` files under -`target/release/deps` must also be present during the build phase. - -To build and view the HTML documentation for the Rust interface, simply do: `$ cargo doc --open`. -There is no documentation for the C interface, but the header should be "self documenting," _as they -say_. - - -Building programs against _libinger_ ------------------------------------- -_Note: This section describes how to build simple C and Rust programs against_ libinger _by invoking -the compiler directly. If you want to use Cargo to build a Rust crate that depends on_ libinger _, -skip to the next one._ - -Let's assume you want to write a small program that uses _libinger_, and (for simplicity) that you -want to store the source file and executable in the root of this repository. - -For a C program, you'll want to begin your file with `#include "libinger.h"` and build with some -variation of this command: -``` -$ cc -fpic -Ltarget/release -Wl,-R\$ORIGIN/target/release -Wl,-I./ld.so -o prog prog.c -linger -``` - -For a Rust program, you should start your file with `extern crate inger;` and build like: -``` -$ rustc -Ltarget/release/deps -Clink-arg=-Wl,-R\$ORIGIN/target/release -Clink-arg=-Wl,-I./ld.so prog.rs -``` - -Of course, you probably want to add other flags to your compiler invocation to request things like -language edition, optimization, debugging, and warnings. If you are trying to use the system -dynamic linker instead of one that you built from source, omit the [`-Clink-arg=`]`-Wl,-I./ld.so` -switch, cross your fingers, and eat a bowl of lucky charms to minimize the probability that your -program runs out of static storage during initialization. - -In the case of both languages, the resulting executable is distributable, and will continue to work -as long as it is run from a directory containing both `ld.so` and `target/release/libinger.so`. -Note however, that the dynamic linker pointed to by the former symlink is hard to distribute without -an installer because its default library search path is based on the (absolute) prefix path used -when configuring the build. - - -Building crates against _libinger_ with Cargo ---------------------------------------------- -Cargo can build crates that depend on _libinger_, but this requires some extra configuration, not -least because it very aggressively prefers to build dependencies as static libraries rather than -shared ones. To set things up, after configuring the build system in this repository, do the -following from the root of your other Cargo tree: -``` -$ ln -s ../path/to/libinger/.cargo -``` - -If you have a nested tree of projects that depend on _libinger_, it suffices to do this once at the -outermost level of the directory structure (but still within a Cargo project directory!). - -Now you'll just need to add an entry like this to your project's `Cargo.toml`: -``` -[dependencies.inger] -path = "../path/to/libinger" -``` - - -Debugging tips --------------- -If adding _libinger_ to your program causes it to segfault or otherwise crash, it's possible the -culprit is _libgotcha_'s support for intercepting dynamic accesses to global variables, which makes -use of heuristics. Ordinarily we notify the application of accesses we were unable to resolve by -forwarding it a segmentation fault; this approach is intended to support applications and runtimes -that respond to segfaults, but it can sometimes obscure the problem. See whether running your -program with forwarding disabled gives a more informative _libgotcha_ error: -``` -$ LIBGOTCHA_ABORTSEGV= ./yourprogram -``` - -If you have produced a release build and later need to switch to a debugging one, please note that -**the build system does not support both types of builds simultaneously**: you must perform a full -clean in between by running `$ ./configure`. If you are using the C interface, also note that -**you must regenerate the header without the `--release` switch**; otherwise, the type declaration's -size will not match the implementation! Unless _your_ application uses Cargo as a build system, -you'll need to replace all instances of `release` with `debug` in the command you use to build it. -If substituting a debug build of _libinger_ causes your program to crash on initialization with an -error like `Unable to load ancillary copies of library`, either rebuild glibc with a higher -`TLS_STATIC_SURPLUS` or launch with a reduced number of libsets, e.g.: -``` -$ LIBGOTCHA_NUMGROUPS=2 ./yourprogram -``` - -The `valgrind` suite (at least Memcheck and Callgrind from version 3.16.1) is known to work, but -currently conflicts with _libgotcha_'s global variable access interception. You can work around -this by switching it off, at the risk of altering the semantics of your program: -``` -$ LIBGOTCHA_NOGLOBALS= valgrind ./yourprogram #check it for memory errors -$ LIBGOTCHA_NOGLOBALS= valgrind --tool=callgrind ./yourprogram #profile it -``` - -Sadly, LLVM's sanitizers rely heavily on dynamic linking tricks that are incompatible with -_libgotcha_, so they are not available at this time. - -The `gdb` debugger works both with and without global variable access interception, although we -recommend first trying without it for a much less confusing experience out of the box: -``` -$ LIBGOTCHA_NOGLOBALS= gdb ./yourprogram -``` - -If you do need to preserve global variable semantics while debugging, you probably don't want to -step into _libgotcha_'s segfault handler unless you're trying to debug the feature itself. You can -instruct `gdb` to ignore segmentation faults like so: -``` -$ gdb -ex handle\ SIGSEGV\ noprint ./yourprogram -``` - -The `rr` reverse-debugging backend (at least version 5.4.0) also works both with and without global -variable access interception, but conflicts with _libgotcha_'s interception of certain calls into -the runtime dynamic loader. Invoke it like so (omitting `LIBGOTCHA_NOGLOBALS=` if desired): -``` -$ LIBGOTCHA_NODYNAMIC= LIBGOTCHA_NOGLOBALS= rr ./yourprogram #record execution for reverse debugging -``` - -If you step into code located outside the main libset, GDB will be missing symbol information, and -therefore unable to display backtraces or source code, or apply your symbol breakpoints. Luckily, -_libgotcha_ includes a debugger script to fix this problem. You must have the glibc sources -corresponding to the `ld.so` build you are using. Add the following to your GDB arguments however -you are invoking it (via `gdb`, `rr replay`, etc.): -``` --x .../path/to/libinger/external/libgotcha/libgotcha.gdb -ex dir\ .../path/to/glibc/dlfcn:.../path/to/glibc/elf -``` - -When debugging the program directly with GDB rather than from a recorded rr execution capture, the -debugger and/or program may become overwhelmed by the extremely frequent preemption signals. If the -debugger freezes or the program doesn't make progress when single stepping, try recompiling -_libinger_ with a higher (say, by an order of magnitude or so) `QUANTUM_MICROSECONDS` value. If you -still have trouble single stepping or the time spent stopped is causing _libinger_ to preempt the -task you are trying to debug, you can disable preemption altogether by issuing a variation of the -following GDB command to cover the preemption signal(s) affecting your task's execution: -``` -(gdb) handle SIGALRM nopass -``` - - -Troubleshooting ---------------- -**My distribution ships a version of glibc that is newer than 2.33. What should I do?** - -Such versions are untested. If you do not encounter any obvious issues, great! Otherwise, no need -to modify your installation; instead, just follow the steps under _Building glibc from source_ above -to build version 2.33. When linking your executable, you will want to use GCC's `-Wl,-I` switch -(`-Clink-arg=-Wl,-I` in the case of rustc) to specify the newly-symlinked dynamic linker as the -program's interpreter. If you are building the program using Cargo and its source is located in -tree or you have symlinked its `.cargo` to this repository's folder of the same name, passing the -flag should happen automatically. Note that, if you get any version errors when running the -resulting binary, you may need to add a further `-L` and the path to the `lib` directory generated -by the glibc `install` target to your linker flags and rebuild. - -**My distribution ships a version of elfutils that is newer than 0.176. What should I do?** - -It probably does! The easiest thing to do is to downgrade the package. For instance, if using -Debian, download (at least) the following binary packages from https://snapshot.debian.org: -`libasm1`, `libasm-dev`, `libdw1`, `libelf1`, and `libelf-dev`. Put the packages into a new -directory, `cd` into it, and run `apt -s install ./*.deb`. If it shows any dependency errors, -download additional packages to address them and add them to the directory; you will probably need -`gdb` version 9.2-1, `libpython3.8`, `libpython3.8-minimal`, and `libpython3.8-stdlib`. Then -perform the downgrade by running `sudo apt install ./*.deb`. - -**I get the assertion: `ELF64_ST_TYPE(st->st_info) != STT_GNU_IFUNC)`!** - -This appears to occur on some Ubuntu systems because of the way Canonical packages zlib (`libz.so`). -The easiest workaround is to grab a closely matching package version from -https://packages.debian.org and extract the shared library file. Then either export the -`$LD_LIBRARY_PATH` environment variable to the path to its containing folder when executing your -application, or just place the shared library in the `lib` directory generated by glibc's `install` -target if/when you built it from source. - -**I get some other error at some point.** - -Start by rerunning `./configure` to clean the build. Make sure it finds all the dependencies. Look -back at _System requirements_ and verify that you are not on a `cargo` version affected by the -regression, and that you have the described wrapper script in your `$PATH` if you have a -sufficiently new version of `bindgen` (a shell alias will not suffice). - -Next, try building _libgotcha_ in isolation. Just `cd` into `external/libgotcha` and type `make` -(without passing `-j`). If you get errors here, go back to the previous paragraph, pinch yourself, -and check all that one more time. - -If you get this far, try building _libinger_ without per--preemptive function thread-local variables -by passing `--features notls` to your Cargo command. If you are still stuck, see whether you can -trace the source of the error using any of the techniques under _Debugging tips_ above. diff --git a/benches/baseline.rs b/benches/baseline.rs deleted file mode 100644 index 4597252..0000000 --- a/benches/baseline.rs +++ /dev/null @@ -1,85 +0,0 @@ -use bencher::Bencher; -use bencher::benchmark_group; -use bencher::benchmark_main; -use libc::exit; -use libc::waitpid; -use std::ffi::c_void; -use std::ptr::null; -use std::ptr::null_mut; - -benchmark_group![bench, fork, vfork, pthread_create, pthread_join]; - -fn fork(lo: &mut Bencher) { - use libc::fork; - - lo.iter(|| { - let pid = unsafe { - fork() - }; - if pid == 0 { - unsafe { - exit(0); - } - } - unsafe { - waitpid(pid, null_mut(), 0); - } - }) -} - -fn vfork(lo: &mut Bencher) { - use libc::vfork; - - lo.iter(|| { - let pid = unsafe { - vfork() - }; - if pid == 0 { - unsafe { - exit(0); - } - } - unsafe { - waitpid(pid, null_mut(), 0); - } - }) -} - -fn pthread_create(lo: &mut Bencher) { - use libc::pthread_create; - use libc::pthread_join; - - lo.iter(|| { - let mut tid = 0; - unsafe { - pthread_create(&mut tid, null(), identity, null_mut()); - pthread_join(tid, null_mut()); - } - }) -} - -fn pthread_join(lo: &mut Bencher) { - use libc::pthread_create; - use libc::pthread_join; - use std::collections::VecDeque; - - let mut tids: VecDeque<_> = (0..100_000).map(|_| { - let mut tid = 0; - unsafe { - pthread_create(&mut tid, null(), identity, null_mut()) - }; - tid - }).collect(); - lo.iter(|| { - let tid = tids.pop_front().unwrap(); - unsafe { - pthread_join(tid, null_mut()); - } - }); -} - -extern fn identity(val: *mut c_void) -> *mut c_void { val } - -benchmark_main! { - bench -} diff --git a/benches/bench.c b/benches/bench.c deleted file mode 100644 index ba1dca0..0000000 --- a/benches/bench.c +++ /dev/null @@ -1,101 +0,0 @@ -#include "../libinger.h" - -#ifdef BREAKDOWN -# include -#endif -#include -#include -#include -#include -#include - -#ifdef BREAKDOWN -# define ITERS 75 -#else -# define ITERS 1000000 -#endif - -#ifdef BREAKDOWN -# pragma weak ProfilerFlush -# pragma weak ProfilerStart -# pragma weak ProfilerStop -#endif - -struct bothtimes { - unsigned long usr; - unsigned long sys; - long pfs; -}; - -static unsigned long nsnow(void) { - struct timespec tv; - clock_gettime(CLOCK_REALTIME, &tv); - return tv.tv_sec * 1000000000 + tv.tv_nsec; -} - -static struct bothtimes usboth(void) { - struct rusage ru; - getrusage(RUSAGE_SELF, &ru); - return (struct bothtimes) { - .usr = ru.ru_utime.tv_sec * 1000000 + ru.ru_utime.tv_usec, - .sys = ru.ru_stime.tv_sec * 1000000 + ru.ru_stime.tv_usec, - .pfs = ru.ru_majflt + ru.ru_minflt, - }; -} - -static void nop(void *ign) { - (void) ign; -} - -int main(int argc, char **argv) { - unsigned long each[ITERS + 1]; -#ifdef BREAKDOWN - struct bothtimes breakdown[ITERS + 1]; -#endif - - char fname[strlen(*argv) + sizeof ".prof." + 2]; - unsigned long nsthen = nsnow(); - for(int iter = 0; iter < ITERS; ++iter) { -#ifdef BREAKDOWN - if(ProfilerFlush && ProfilerStart && ProfilerStop) - switch(iter) { - case 51: - case 65: - ProfilerFlush(); - ProfilerStop(); - case 5: - sprintf(fname, "%s.prof.%02d", *argv, iter); - ProfilerStart(fname); - } - putc('.', stderr); - each[iter] = nsnow(); - breakdown[iter] = usboth(); -#endif - launch(nop, UINT64_MAX, NULL); - } - each[ITERS] = nsnow(); -#ifdef BREAKDOWN - breakdown[ITERS] = usboth(); - if(ProfilerFlush && ProfilerStop) { - ProfilerFlush(); - ProfilerStop(); - } - putc('\n', stderr); -#endif - - unsigned long nswhen = (nsnow() - nsthen) / ITERS; -#ifdef BREAKDOWN - for(int iter = 0; iter < ITERS; ++iter) { - unsigned long time = each[iter + 1] - each[iter]; - printf("%2d %lu.%03lu μs\n", iter, time / 1000, time % 1000); - - unsigned long usr = breakdown[iter + 1].usr - breakdown[iter].usr; - unsigned long sys = breakdown[iter + 1].sys - breakdown[iter].sys; - long pfs = breakdown[iter + 1].pfs - breakdown[iter].pfs; - printf("(%lu μs usr, %lu μs sys, %ld pfs)\n", usr, sys, pfs); - } -#endif - printf("%ld.%03ld μs\n", nswhen / 1000, nswhen % 1000); - - return 0; -} diff --git a/benches/inger.rs b/benches/inger.rs deleted file mode 100644 index ee4163e..0000000 --- a/benches/inger.rs +++ /dev/null @@ -1,119 +0,0 @@ -use bencher::Bencher; -use bencher::benchmark_group; -use bencher::benchmark_main; -use inger::concurrency_limit; -use inger::nsnow; -use inger::pause; -use std::fs::File; -use std::io::Write; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::AtomicU64; -use std::sync::atomic::Ordering; - -benchmark_group![bench, launch, resume, renew]; - -fn launch(lo: &mut Bencher) { - use inger::STACK_N_PREALLOC; - use inger::abort; - use inger::launch; - use inger::resume; - use std::mem::MaybeUninit; - let mut lingers: [MaybeUninit<_>; STACK_N_PREALLOC] = unsafe { - MaybeUninit::uninit().assume_init() - }; - let during = AtomicU64::default(); - - let mut into = 0; - let mut outof = 0; - let mut index = 0; - let paused = AtomicBool::from(false); - lo.iter(|| { - assert!(index < concurrency_limit(), "LIBSETS tunable set too low!"); - - let before = nsnow(); - lingers[index] = MaybeUninit::new(launch(|| { - during.store(nsnow(), Ordering::Relaxed); - pause(); - if ! paused.load(Ordering::Relaxed) { - abort("pause() did not context switch!"); - } - }, u64::max_value()).unwrap()); - - let after = nsnow(); - let during = during.load(Ordering::Relaxed); - into += during - before; - outof += after - during; - - index += 1; - }); - - if let Ok(mut file) = File::create("bench_launch.log") { - let index: u64 = index as _; - writeln!(file, "entry launch ... {} ns/iter", into / index).unwrap(); - writeln!(file, "exit launch ... {} ns/iter", outof / index).unwrap(); - writeln!(file, "(ran for {} iterations)", index).unwrap(); - } - - paused.store(true, Ordering::Relaxed); - for linger in &mut lingers[..index] { - let linger = linger.as_mut_ptr(); - let linger = unsafe { - &mut *linger - }; - resume(linger, u64::max_value()).unwrap(); - } -} - -fn resume(lo: &mut Bencher) { - use inger::launch; - use inger::resume; - - let run = AtomicBool::from(true); - let during = AtomicU64::default(); - let mut lingers: Vec<_> = (0..concurrency_limit()).map(|_| launch(|| while run.load(Ordering::Relaxed) { - pause(); - during.store(nsnow(), Ordering::Relaxed); - }, u64::max_value()).unwrap()).collect(); - let nlingers = lingers.len(); - - let mut into = 0; - let mut outof = 0; - let mut index = 0; - lo.iter(|| { - let before = nsnow(); - resume(&mut lingers[index % nlingers], u64::max_value()).unwrap(); - - let after = nsnow(); - let during = during.load(Ordering::Relaxed); - into += during - before; - outof += after - during; - - index += 1; - }); - - if let Ok(mut file) = File::create("bench_resume.log") { - let index: u64 = index as _; - writeln!(file, "entry resume ... {} ns/iter", into / index).unwrap(); - writeln!(file, "exit resume ... {} ns/iter", outof / index).unwrap(); - writeln!(file, "(ran for {} iterations)", index).unwrap(); - } - - run.store(false, Ordering::Relaxed); - for linger in &mut lingers { - resume(linger, u64::max_value()).unwrap(); - } -} - -fn renew(lo: &mut Bencher) { - use inger::launch; - - let lingers: Vec<_> = (0..concurrency_limit()).map(|_| launch(pause, u64::max_value()).unwrap()).collect(); - let mut lingers = lingers.into_iter(); - lo.iter(|| - drop(lingers.next().expect("LIBSETS tunable set too low!")) - ); -} - -benchmark_main! { - bench -} diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..f929e18 --- /dev/null +++ b/build.rs @@ -0,0 +1,60 @@ +use std::env::VarError; +use std::io::Error; +use std::process::ExitStatus; + +const LIBS: [&str; 1] = [ + "libsp.a", +]; + +const SRCDIR: &str = "native"; + +pub fn main() -> Result<(), Failure> { + use std::env::var; + use std::process::Command; + + let destdir = format!("target/{}/deps", var("PROFILE")?); + let srcdir = format!("arch/{}", var("CARGO_CFG_TARGET_ARCH")?); + for lib in &LIBS { + let filename = format!("{}/{}", srcdir, lib); + Command::new("make").arg("-C").arg(SRCDIR).arg(&filename).status().success()?; + let filename = format!("{}/{}", SRCDIR, filename); + Command::new("mv").arg(&filename).arg(&destdir).status().success()?; + } + + Ok(()) +} + +#[derive(Debug)] +pub enum Failure { + VarError(VarError), + Error(Error), + ExitStatus(ExitStatus), +} + +impl From for Failure { + fn from(ve: VarError) -> Self { + Failure::VarError(ve) + } +} + +trait FailureResult { + type FailRes; + + fn success(self) -> Self::FailRes; +} + +impl FailureResult for Result { + type FailRes = Result<(), Failure>; + + fn success(self) -> Self::FailRes { + match self { + Ok(exit_status) => + if exit_status.success() { + Ok(()) + } else { + Err(Failure::ExitStatus(exit_status)) + }, + Err(error) => Err(Failure::Error(error)), + } + } +} diff --git a/compiler/.gitignore b/compiler/.gitignore deleted file mode 100644 index 59de51e..0000000 --- a/compiler/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/X86InstrInfo.h -/X86RegisterInfo.h - -*.o diff --git a/compiler/Makefile b/compiler/Makefile deleted file mode 100644 index 2ac9587..0000000 --- a/compiler/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -CXX := c++ -LD := $(CXX) -SED := sed -TBLGEN := llvm-tblgen - -override CPPFLAGS := -DGET_INSTRINFO_ENUM -DGET_REGINFO_ENUM $(CPPFLAGS) -override CXXFLAGS := -std=c++17 -O2 -fpic -fno-rtti -Wall -Wextra -Wpedantic $(CXXFLAGS) -override LDLIBS := -lLLVM-13 -ldl $(LDLIBS) -override TBLGENFLAGS := -I$(LLVM_SOURCE_DIR)/include $(TBLGENFLAGS) - -libingerc.so: X86InstrInfo.h X86RegisterInfo.h - -X86InstrInfo.h: $(LLVM_SOURCE_DIR)/lib/Target/X86/X86.td - $(TBLGEN) -gen-instr-info $(TBLGENFLAGS) -I$(dir $<) -o $@ $< - -X86RegisterInfo.h: $(LLVM_SOURCE_DIR)/lib/Target/X86/X86.td - $(TBLGEN) -gen-register-info $(TBLGENFLAGS) -I$(dir $<) -o $@ $< - $(SED) -i 's/ : \S\+\( {\)/\1/' $@ - -.PHONY: clean -clean: - $(RM) libingerc.*o - -lib%.so: %.o - $(LD) $(LDFLAGS) -shared -zdefs -ztext -o $@ $^ $(LDLIBS) diff --git a/compiler/ingerc b/compiler/ingerc deleted file mode 100755 index bbb1c3d..0000000 --- a/compiler/ingerc +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/sh - -# Skip the following LLVM optimization passes, which cause problems for us. -readonly BADPASSES="function-attrs inline prune-eh" - -epiloguefun="" -epiloguevar="" -if [ "$1" = "-e" -a "$#" -ge "2" ] -then - epiloguefun="$2" - epiloguevar="INGERC_EPILOGUE=$epiloguefun" - shift 2 -fi - -# Temporarily drop any --emit or -emit because it would prevent us from getting LLVM debug output. -args="`printf "'%s' " "$@" | sed "s/'-\?-emit\(' '\|=\)[^']\+'//"`" - -finalstage="`echo "$*" | sed -n 's/.*-\?-emit[ =]\(\S\+\).*/\1/p'`" -crate="`eval rustc $args --print crate-name 2>/dev/null`" -if [ "$?" -ne "0" ] -then - echo "USAGE: $0 [-e ] [args]... " - echo - echo "If the -e switch is provided, it must come first. These other switches are supported:" - echo "\t--emit " - echo "\trustc's usual options..." - echo - exec rustc --help -fi - -set -e - -# These -Ccodegen-units=1 switches prevent unreadable output due to interleaving. -allpasses="`eval rustc $args -Cllvm-args=-debug-pass=Arguments -Ccodegen-units=1 2>&1`" -autopasses="`eval rustc $args -Cllvm-args=-debug-pass=Arguments -Cno-prepopulate-passes -Ccodegen-units=1 2>&1`" -rm -f "$crate" - -# Each line is a grep-compatible command-line switch. -autoswitches="`echo "$autopasses" | sed -e's/^/-e"/' -e's/: \+/: \\\\+/g' -e's/$/"/'`" - -manualpasses="`echo "$allpasses" | eval grep -vx $autoswitches`" - -# Each line is a pass name, without the leading hyphen. -manualnames="`echo "$manualpasses" | sed -e's/[^-]\+-//' -e's/ -/\n/g'`" -badnames="`echo "$BADPASSES" | tr " " "\n"`" - -# Each line is a one-word grep-compatible command-line switch. -manualswitches="`echo "$badnames" | sed -e's/^/-e/'`" - -passnames="`echo "$manualnames" | grep -vxF $manualswitches`" - -dir="`dirname "$0"`" -std="`LD_TRACE_LOADED_OBJECTS= rustc | grep -o 'std-[^.]\+' | uniq`" -eval rustc $args --emit llvm-ir -Cno-prepopulate-passes -Cpasses="'$passnames'" -if [ "$finalstage" = "llvm-ir" ] -then - exit 0 -fi - -"$dir/ingerc.ts" "$crate.ll" $epiloguefun -if [ "$finalstage" = "ir" ] -then - exit 0 -fi - -eval LD_PRELOAD="'$dir/libingerc.so'" "$epiloguevar" llc "'$crate.ll'" >/dev/null -if [ "$finalstage" = "asm" ] -then - exit 0 -fi - -linkargs="`printf "'%s' " "$@" | sed -e"s/\('-C\)' '\(link-arg=\)/\1\2/g" -e"s/' '/'\n'/g" | sed -n "s/^'-Clink-arg=\(.\+\)'$/\1/p"`" -eval cc -no-pie -Wa,-gdwarf-5 -o "'$crate'" "'$crate.s'" $linkargs "'-l$std'" diff --git a/compiler/ingerc.cpp b/compiler/ingerc.cpp deleted file mode 100644 index 01a58c1..0000000 --- a/compiler/ingerc.cpp +++ /dev/null @@ -1,485 +0,0 @@ -#include "X86InstrInfo.h" -#include "X86RegisterInfo.h" - -#include "llvm/CodeGen/TargetInstrInfo.h" -#include "llvm/IR/LegacyPassManager.h" -#include "llvm/IR/Module.h" -#include "llvm/MC/MCContext.h" - -#include -#include - -using llvm::legacy::PassManager; -using llvm::DebugLoc; -using llvm::Function; -using llvm::FunctionType; -using llvm::GlobalValue; -using llvm::MachineBasicBlock; -using llvm::MachineFunction; -using llvm::MachineFunctionPass; -using llvm::MachineInstr; -using llvm::MachineInstrBuilder; -using llvm::MachineInstrBundleIterator; -using llvm::MCContext; -using llvm::MCSymbol; -using llvm::Pass; -using llvm::PassInfo; -using llvm::PassRegistry; -using llvm::PointerType; -using llvm::SmallVector; -using llvm::Type; -using llvm::callDefaultCtor; -using llvm::outs; - -namespace { -struct IngerCancel: public MachineFunctionPass { - virtual bool runOnMachineFunction(MachineFunction &mf) override { - outs() << "FUNCTION: " << mf.getName() << '\n'; - - auto changed = false; - auto &pads = mf.getLandingPads(); - if(getEpilogueFunction()) { - auto epilogue = findInst( - mf, - std::bind(isCallTo, std::placeholders::_1, [](auto *fun) { - return fun && fun->getName() == getEpilogueFunction(); - }) - ); - if(epilogue) { - auto *cfi = findFirstPad(mf); - if(!cfi) { - cfi = pads.front().LandingPadBlock->getPrevNode(); - while(!cfi->front().isCFIInstruction()) - cfi = cfi->getPrevNode(); - } - - auto index = cfi->front().getOperand(0).getCFIIndex(); - auto off = mf.getFrameInstructions()[index].getOffset(); - addInst( - *(*epilogue)->getParent(), - *epilogue, - llvm::X86::LEA64r, - [&off](auto &inst, auto &) { - inst.addReg(llvm::X86::RDI); - - // off(%rsp), see X86InstrBuilder.h:addRegOffset() - inst.addReg(llvm::X86::RSP); - inst.addImm(1); - inst.addReg(0); - inst.addImm(off - 8); - inst.addReg(0); - } - ); - ++*epilogue; - addInst( - *(*epilogue)->getParent(), - *epilogue, - llvm::X86::NOOP, - [](auto &, auto &) {} - ); - changed = true; - } - } - - if(mf.getName().contains("drop_in_place")) - return changed; - - for(auto &pad : pads) { - auto &cleanupBlock = *pad.LandingPadBlock; - outs() << "landing pad: " << *pad.LandingPadLabel << '\n'; - - auto dropCall = std::find_if( - cleanupBlock.begin(), - cleanupBlock.end(), - std::bind(isCallTo, std::placeholders::_1, [](auto *fun) { - return fun && fun->getName().contains("drop_in_place"); - }) - ); - - auto *beginBlock = &mf.front(); - auto beginLoc = beginBlock->begin(); - - auto endIt = mf.end(); - --endIt; - if(&*endIt == &cleanupBlock) - --endIt; - - auto *endBlock = &*endIt; - auto endLoc = endBlock->end(); - - if(dropCall == cleanupBlock.end()) { - auto *sRetType = getFunctionSRetType(mf.getFunction()); - const Function *dtor = nullptr; - if(sRetType) - dtor = findDtor(*sRetType, mf); - if(dtor) { - auto argIndex = -1ul; - auto ctor = findInst(mf, [&sRetType, &argIndex](auto &each) { - auto *fun = getFunction(each); - if(!fun) - return false; - return getFunctionSRetType(*fun, &argIndex) == sRetType; - }); - assert(ctor); - beginBlock = endBlock = (*ctor)->getParent(); - ++*ctor; - beginLoc = endLoc = *ctor; - - // Find the mov or lea before the constructor call. - assert(argIndex == 0); - --*ctor; - --*ctor; - while((*ctor)->getOperand(0).getReg() != llvm::X86::RDI) { - assert(*ctor != (*ctor)->getParent()->begin()); - --*ctor; - } - - auto move = cleanupBlock.begin(); - while(move != cleanupBlock.end() && !move->isMoveReg()) - ++move; - assert(move != cleanupBlock.end()); - - // mov %rax, %rbp ; Save _Unwind_Context pointer. - move->getOperand(0).setReg(llvm::X86::RBP); - - auto unwind = move; - while(unwind != cleanupBlock.end() && !unwind->isCall()) - ++unwind; - assert(unwind != cleanupBlock.end()); - - // (mov|lea) ???, %rdi ; Pass victim to destructor. - addInst( - cleanupBlock, - unwind, - (*ctor)->getOpcode(), - [&ctor](auto &inst, auto &) { - inst.addReg(llvm::X86::RDI); - - auto &src = (*ctor)->getOperand(1); - if(src.isReg()) - inst.addReg(src.getReg()); - else - inst.cloneMemRefs(**ctor); - } - ); - - dropCall = addInst( - cleanupBlock, - unwind, - unwind->getOpcode(), - [&dtor](auto &inst, auto &) { - inst.addGlobalAddress(dtor); - } - ); - - // mov %rbp, %rdi ; Pass _Unwind_Context pointer. - addInst( - cleanupBlock, - unwind, - move->getOpcode(), - [](auto &inst, auto &) { - inst.addReg(llvm::X86::RDI); - inst.addReg(llvm::X86::RBP); - } - ); - } - } - - if(dropCall != cleanupBlock.end()) { - auto &beginLabel = getOrCreateLabel( - mf.getOrCreateLandingPadInfo(&cleanupBlock).BeginLabels, - *beginBlock, - beginLoc, - changed - ); - - auto &endLabel = getOrCreateLabel( - mf.getOrCreateLandingPadInfo(&cleanupBlock).EndLabels, - *endBlock, - endLoc, - changed - ); - outs() << "bounding labels: " << beginLabel << ' ' << endLabel << '\n'; - - auto &dropFun = *dropCall->getOperand(0).getGlobal(); - outs() << "dropFun: " << dropFun.getName() << '\n'; - - auto &dropType = *getFunctionType(*dropCall).params().front(); - outs() << "paramType: " << dropType << '\n'; - - auto labelFinder = [](auto &label) { - return [&label](auto &each) { - return std::any_of( - each.operands_begin(), - each.operands_end(), - [&label](auto &each) { - return each.isMCSymbol() - && each.getMCSymbol() == &label; - } - ); - }; - }; - - auto beginInst = findInst(mf, labelFinder(beginLabel)); - assert(beginInst); - outs() << "beginInst: " << **beginInst << '\n'; - - auto endInst = findInst(mf, labelFinder(endLabel)); - assert(endInst); - outs() << "endInst: " << **endInst << '\n'; - - auto prevInst = (*beginInst)->getPrevNode(); - assert(prevInst); - if(!isCallUsing(*prevInst, dropType)) { - auto nextInst = this->nextInst(*beginInst); - while( - nextInst - && !(*nextInst)->isCall() - && *nextInst != endInst - ) - nextInst = this->nextInst(*nextInst); - if(nextInst && isCallUsing(**nextInst, dropType)) { - outs() << "nextInst: " << **nextInst << '\n'; - - auto *movedInst = (*beginInst)->removeFromParent(); - (*nextInst)->getParent()->insertAfter( - *nextInst, - movedInst - ); - changed = true; - } - } - - auto nextInst = this->nextInst(*endInst); - while( - nextInst - && !isCallTo(**nextInst, [&dropFun](auto *fun) { - return fun == &dropFun; - }) - && !(*nextInst)->isCFIInstruction() - && !(*nextInst)->isReturn() - ) - nextInst = this->nextInst(*nextInst); - assert(nextInst); - outs() << "nextInst: " << **nextInst << '\n'; - - if((*nextInst)->isCFIInstruction()) - --*nextInst; - - auto *movedInst = (*endInst)->removeFromParent(); - (*nextInst)->getParent()->insert(*nextInst, movedInst); - changed = true; - } - } - - return changed; - } - - static char ID; - IngerCancel(): MachineFunctionPass(ID) {} - -private: - static MachineInstr &addInst( - MachineBasicBlock &block, - MachineInstrBundleIterator pos, - unsigned opcode, - std::function operands - ) { - auto &mf = *block.getParent(); - auto &info = mf.getSubtarget().getInstrInfo()->get(opcode); - auto &inst = *mf.CreateMachineInstr(info, DebugLoc()); - MachineInstrBuilder build(mf, inst); - operands(build, mf.getContext()); - block.insert(pos, &inst); - return inst; - } - - static const Function *findDtor(const Type &type, const MachineFunction &fun) { - for(auto &defn : *fun.getFunction().getParent()) { - if(defn.getName().contains("drop_in_place")) { - const auto ¶ms = defn.getFunctionType()->params(); - if( - params.size() - && params.front()->isPointerTy() - && static_cast(params.front())->getElementType() == &type - ) - return &defn; - } - } - return nullptr; - } - - static MachineBasicBlock *findFirstPad(MachineFunction &fun) { - for(auto &pad : fun.getLandingPads()) { - auto &block = *pad.LandingPadBlock; - if(block.front().isCFIInstruction()) - return █ - } - return nullptr; - } - - static std::optional> findInst( - MachineFunction &mf, - std::function pred - ) { - for(auto &block : mf) { - auto inst = std::find_if(block.begin(), block.end(), pred); - if(inst != block.end()) - return std::optional(inst); - } - return {}; - } - - static const char *getEpilogueFunction() { - static const char *epilogue = nullptr; - static bool memo = false; - if(!memo) { - epilogue = getenv("INGERC_EPILOGUE"); - memo = true; - } - return epilogue; - } - - static const Function *getFunction(const MachineInstr &call) { - if(!call.isCall()) - return nullptr; - - return static_cast(call.getOperand(0).getGlobal()); - } - - static const Type *getFunctionSRetType(const Function &fun, size_t *param = nullptr) { - auto params = fun.getFunctionType()->params(); - for(auto index = 0u; index != params.size(); ++index) { - auto *type = fun.getParamStructRetType(index); - if(type) { - if(param) - *param = index; - return type; - } - } - return nullptr; - } - - static const FunctionType &getFunctionType(const MachineInstr &call) { - assert(call.isCall()); - - auto &fun = *call.getOperand(0).getGlobal(); - auto &funType = *fun.getType()->getElementType(); - assert(funType.isFunctionTy()); - - return static_cast(funType); - } - - static MCSymbol &getOrCreateLabel( - SmallVector &labels, - MachineBasicBlock &block, - MachineInstrBundleIterator pos, - bool &changed - ) { - if(labels.size()) - return *labels.front(); - - auto &inst = addInst( - block, - pos, - llvm::TargetOpcode::EH_LABEL, - [](auto &inst, auto &syms) { - inst.addSym(syms.createTempSymbol()); - } - ); - auto &label = *inst.getOperand(0).getMCSymbol(); - labels.push_back(&label); - changed = true; - return label; - } - - static bool isCallTo( - const MachineInstr &inst, - std::function name - ) { - if(!inst.isCall()) - return false; - - auto &operand = inst.getOperand(0); - const GlobalValue *fun = nullptr; - if(operand.isGlobal()) - fun = operand.getGlobal(); - // else it's an indirect call so we cannot know the function statically - return name(fun); - } - - static bool isCallUsing(const MachineInstr &inst, const Type &type) { - if(!inst.isCall()) - return false; - - auto &funType = getFunctionType(inst); - return funType.getReturnType() == &type - || std::any_of( - funType.param_begin(), - funType.param_end(), - [&type](auto &each) { - return each == &type; - } - ); - } - - static std::optional> nextInst( - MachineInstrBundleIterator inst - ) { - auto &block = *inst->getParent(); - auto nextInst = inst; - ++nextInst; - if(nextInst != block.end()) - return std::optional(nextInst); - - auto *nextBlock = inst->getParent()->getNextNode(); - if(nextBlock) - return std::optional(nextBlock->begin()); - - return {}; - } -}; -} - -char IngerCancel::ID; - -typedef void (*PassManager_add_t)(PassManager *, Pass *); -static PassManager_add_t _ZN4llvm6legacy11PassManager3addEPNS_4PassE; - -static void add(PassManager *pm, Pass *p) { - if(p->getPassName() == "X86 Assembly Printer") - _ZN4llvm6legacy11PassManager3addEPNS_4PassE(pm, new IngerCancel()); - _ZN4llvm6legacy11PassManager3addEPNS_4PassE(pm, p); -} - -extern "C" { -PassManager_add_t _ZTVN4llvm6legacy11PassManagerE[5]; -} - -namespace llvm { -void initializeIngerCancelPass(PassRegistry &); -} - -extern "C" void LLVMInitializeX86Target() { - void (*LLVMInitializeX86Target)() = (void (*)()) dlsym(RTLD_NEXT, "LLVMInitializeX86Target"); - LLVMInitializeX86Target(); - - PassRegistry &pr = *PassRegistry::getPassRegistry(); - initializeIngerCancelPass(pr); - - _ZN4llvm6legacy11PassManager3addEPNS_4PassE = (PassManager_add_t) - dlsym(RTLD_NEXT, "_ZN4llvm6legacy11PassManager3addEPNS_4PassE"); - const PassManager_add_t *vtable = (PassManager_add_t *) - dlsym(RTLD_NEXT, "_ZTVN4llvm6legacy11PassManagerE"); - for( - size_t index = 0; - index != sizeof _ZTVN4llvm6legacy11PassManagerE / sizeof *_ZTVN4llvm6legacy11PassManagerE; - ++index - ) - if(vtable[index] == _ZN4llvm6legacy11PassManager3addEPNS_4PassE) - _ZTVN4llvm6legacy11PassManagerE[index] = add; - else - _ZTVN4llvm6legacy11PassManagerE[index] = vtable[index]; -} - -INITIALIZE_PASS(IngerCancel, "llc", "IngerCancel", false, false) diff --git a/compiler/ingerc.ts b/compiler/ingerc.ts deleted file mode 100755 index f77f8d8..0000000 --- a/compiler/ingerc.ts +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env -S deno run --allow-read --allow-write - -const EPILOGUE = 'llvm.donothing'; - -function processDefine(fun: string[]): string[] { - let personality = Boolean(fun[0].match(/\bpersonality\b/)); - if(!personality) - fun[0] = fun[0].replace( - /(!dbg .+)?{$/, - 'personality i32 (' - + 'i32, ' - + 'i32, ' - + 'i64, ' - + '%"unwind::libunwind::_Unwind_Exception"*, ' - + '%"unwind::libunwind::_Unwind_Context"*' - + ')* @rust_eh_personality $&', - ); - - let cleanup = false; - for(let line = 0; line < fun.length; ++line) - if(fun[line].match(/^ ret\b/)) { - const label = 'ingerc' + line; - fun.splice( - line, - 0, - ' invoke void @"' - + epilogue - + '"() to label %' - + label - + ' unwind label %cleanup', - label + ':', - ); - line += 2; - } else if(fun[line].match(/^cleanup:/)) - cleanup = true; - - if(!cleanup) - fun.splice( - fun.length - 1, - 0, - 'cleanup:', - ' %ingerc = landingpad { i8*, i32 } cleanup', - ' resume { i8*, i32 } %ingerc', - ); - - return fun; -} - -let epilogue = EPILOGUE; -const args = Deno.args.slice(); -if(args.length == 2) - epilogue = args.pop()!; - -if(args.length != 1) { - console.log( - 'USAGE: ' + import.meta.url.replace(/.*\//, '') + ' [epilogue function]\n' - + '\n' - + 'Modify to force llc to generate an LSDA for each function, even\n' - + 'those that statically cannot raise exceptions.' - + 'With [epilogue function], notify the runtime of epilogue entry by invoking the\n' - + 'named function.' - ); - Deno.exit(1); -} - -const filename = args[0]; -let ll = new TextDecoder().decode(Deno.readFileSync(filename)); -let define = '\ndeclare void @"' + epilogue + '"()'; -if(ll.includes(define)) { - console.log('We\'ve already processed this file! Leaving it unchanged.'); - Deno.exit(2); -} - -const funs = ll.split('\ndefine ').flatMap(function(elem) { - const [head, tail] = elem.split('\n}\n'); - if(tail) - return [('define ' + head + '\n}').split('\n'), tail]; - else - return [head]; -}); - -ll = funs.map(function(elem) { - if(!Array.isArray(elem)) - return elem; - - return '\n' + processDefine(elem).join('\n') + '\n'; -}).join(''); -ll += define; -Deno.writeFileSync(filename, new TextEncoder().encode(ll)); diff --git a/configure b/configure deleted file mode 100755 index fff2a40..0000000 --- a/configure +++ /dev/null @@ -1,116 +0,0 @@ -#!/bin/sh - -readonly PIPE="/tmp/configure" - -version() { - echo -n "Looking for $1..." - - local version - echo -n " broken installation" >"$PIPE.err" - version="`"$1" --version 2>>"$PIPE.err" | grep -o '[[:digit:].]\+[[:digit:]]'`" - rm "$PIPE.err" - echo " found `echo "$version" | head -n1`" -} - -helper() { - local file="$1" - local color="$2" - printf %s "[${color}m" >&2 - tr -d '\n' <"$file" | sed 's/.*://;a\' >&2 - printf %s "" >&2 - rm "$file" -} - -handler() { - [ -e "$PIPE.err" ] && helper "$PIPE.err" 31 - [ -e "$PIPE.warn" ] && helper "$PIPE.warn" 93 -} - -set -e -trap handler EXIT - -version cargo -version rustc - -echo -n "Checking Rust 2018 support..." -echo " not present" >"$PIPE.err" -rustc --help -v | grep -- --edition >/dev/null -echo " present" - -version make - -echo -n "Checking GNU Make language support..." -echo " not present" >"$PIPE.err" -make --version | grep GNU >/dev/null -echo " present" - -version cc -version bindgen -version rustfmt -version ld -version nm -version objcopy -version git - -dir="`dirname "$0"`" - -git() { - "`which git`" -C "$dir" "$@" -} - -cargo() { - local owd="$PWD" - cd "$dir" - "`which cargo`" "$@" - cd "$owd" -} - -echo -n "Checking submodules..." -if git submodule status | grep '^-' >/dev/null -then - echo " initializing" - git submodule update --init --recursive -elif git submodule status | grep '^+' >/dev/null -then - echo " inconsistent" >"$PIPE.warn" - handler -else - echo " consistent" -fi - -echo -n "Configuring toolchain..." -if [ -e "$dir/.cargo/config" ] -then - echo " skipping" -else - cat >"$dir/.cargo/config" <<-tac - `cat "$dir/.cargo/config.toml"` - rustc = ".cargo/rustc" - tac - echo " generated" -fi - -echo -n "Cleaning libinger build..." -cargo clean -echo " done" - -echo -n "Cleaning libgotcha build..." -make -C"$dir/external/libgotcha" clean >/dev/null -echo " done" - -echo -n "Removing lockfile..." -rm -f "$dir/Cargo.lock" -echo " done" - -echo -n "Detecting interpreter..." -if [ -n "$LIBINGER_LINKER" ] -then - file -L "$LIBINGER_LINKER" 2>&1 | tee "$PIPE.err" | grep '\' >/dev/null - echo " using $LIBINGER_LINKER" -elif [ -e "$dir/ld.so" ] -then - echo " using `readlink "$dir/ld.so"`" -else - echo " define LIBINGER_LINKER to a dynamic linker or place one at ./ld.so" >"$PIPE.err" - false -fi diff --git a/external/libgotcha b/external/libgotcha deleted file mode 160000 index 5228729..0000000 --- a/external/libgotcha +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 52287295e3391dc6d3dd670aa3e3797a47142c53 diff --git a/external/libtimetravel b/external/libtimetravel deleted file mode 160000 index 1d73c8c..0000000 --- a/external/libtimetravel +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1d73c8c46f0dbb7958e66f42652ba69e532f1031 diff --git a/internal/libsignal/Cargo.toml b/internal/libsignal/Cargo.toml deleted file mode 100644 index b8aabfc..0000000 --- a/internal/libsignal/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "signal" -version = "0.0.0" -edition = "2018" - -[dependencies] -libc = "*" - -[features] -libgotcha = [] diff --git a/internal/libsignal/src/lib.rs b/internal/libsignal/src/lib.rs deleted file mode 100644 index 6f34f19..0000000 --- a/internal/libsignal/src/lib.rs +++ /dev/null @@ -1,294 +0,0 @@ -mod libgotcha; -pub mod pthread; - -use libc::SIG_BLOCK; -use libc::SIG_SETMASK; -use libc::SIG_UNBLOCK; -use libc::SIGABRT; -use libc::SIGALRM; -use libc::SIGBUS; -use libc::SIGCHLD; -use libc::SIGCONT; -use libc::SIGFPE; -use libc::SIGHUP; -use libc::SIGILL; -use libc::SIGINT; -use libc::SIGKILL; -use libc::SIGPIPE; -use libc::SIGPROF; -use libc::SIGPOLL; -use libc::SIGPWR; -use libc::SIGQUIT; -use libc::SIGSEGV; -use libc::SIGSTKFLT; -use libc::SIGSYS; -use libc::SIGTERM; -use libc::SIGTRAP; -use libc::SIGTSTP; -use libc::SIGTTIN; -use libc::SIGTTOU; -use libc::SIGURG; -use libc::SIGUSR1; -use libc::SIGUSR2; -use libc::SIGVTALRM; -use libc::SIGWINCH; -use libc::SIGXCPU; -use libc::SIGXFSZ; -use libc::ucontext_t; -use std::io::Error; -use std::ptr::null_mut; -pub use libc::sigaction as Sigaction; -pub use libc::siginfo_t; -pub use libc::sigset_t as Sigset; -use std::io::Result; -use std::os::raw::c_int; - -pub type Handler = extern "C" fn(Signal, Option<&siginfo_t>, Option<&mut ucontext_t>); - -#[allow(dead_code)] -#[repr(i32)] -pub enum Operation { - Block = SIG_BLOCK, - Unblock = SIG_UNBLOCK, - SetMask = SIG_SETMASK, -} - -#[allow(dead_code)] -#[derive(Clone)] -#[derive(Copy)] -#[repr(i32)] -pub enum Signal { - Abort = SIGABRT, - Alarm = SIGALRM, - Bus = SIGBUS, - Breakpoint = SIGTRAP, - Child = SIGCHLD, - Continue = SIGCONT, - Coprocessor = SIGSTKFLT, - FilesystemLimit = SIGXFSZ, - FloatingPoint = SIGFPE, - Hangup = SIGHUP, - Illegal = SIGILL, - Interrupt = SIGINT, - Kill = SIGKILL, - Pipe = SIGPIPE, - Pollable = SIGPOLL, - ProfilingTimer = SIGPROF, - Quit = SIGQUIT, - Segfault = SIGSEGV, - Syscall = SIGSYS, - TerminalInput = SIGTTIN, - TerminalOutput = SIGTTOU, - TerminalStop = SIGTSTP, - Terminate = SIGTERM, - PowerFailure = SIGPWR, - ProcessorLimit = SIGXCPU, - UrgentSocket = SIGURG, - User1 = SIGUSR1, - User2 = SIGUSR2, - VirtualAlarm = SIGVTALRM, - WindowResize = SIGWINCH, -} - -impl PartialEq for Signal { - fn eq(&self, other: &Self) -> bool { - *self as c_int == *other as c_int - } -} -impl Eq for Signal {} - -pub trait Set { - fn empty() -> Self; - fn full() -> Self; - fn add(&mut self, _: Signal); - fn del(&mut self, _: Signal); - fn has(&self, _: Signal) -> bool; -} - -fn sigset(fun: fn(&mut Sigset)) -> Sigset { - use std::mem::zeroed; - - let mut my = unsafe { - zeroed() - }; - fun(&mut my); - my -} - -impl Set for Sigset { - fn empty() -> Self { - use libc::sigemptyset; - sigset(|me| unsafe { sigemptyset(me); }) - } - - fn full() -> Self { - use libc::sigfillset; - sigset(|me| unsafe { sigfillset(me); }) - } - - fn add(&mut self, signal: Signal) { - use libc::sigaddset; - unsafe { - sigaddset(self, signal as _); - } - } - - fn del(&mut self, signal: Signal) { - use libc::sigdelset; - unsafe { - sigdelset(self, signal as _); - } - } - - fn has(&self, signal: Signal) -> bool { - use libc::sigismember; - unsafe { - sigismember(self, signal as _) != 0 - } - } -} - -pub trait Action { - fn new(_: Handler, _: Sigset, _: c_int) -> Self; - fn sa_sigaction(&self) -> &Handler; - fn sa_sigaction_mut(&mut self) -> &mut Handler; -} - -impl Action for Sigaction { - fn new(sigaction: Handler, mask: Sigset, flags: c_int) -> Self { - Self { - sa_sigaction: sigaction as _, - sa_mask: mask, - sa_flags: flags, - sa_restorer: None, - } - } - - fn sa_sigaction(&self) -> &Handler { - use std::mem::transmute; - - unsafe { - transmute(self.sa_sigaction) - } - } - - fn sa_sigaction_mut(&mut self) -> &mut Handler { - use std::mem::transmute; - - unsafe { - transmute(self.sa_sigaction) - } - } -} - -pub trait Actionable { - fn maybe(&self) -> Option<&Sigaction>; -} - -impl Actionable for Sigaction { - fn maybe(&self) -> Option<&Self> { - Some(self) - } -} - -impl Actionable for () { - fn maybe(&self) -> Option<&Sigaction> { - None - } -} - -pub fn sigaction(signal: Signal, new: &dyn Actionable, old: Option<&mut Sigaction>) -> Result<()> { - use libc::sigaction; - - if unsafe { - sigaction( - signal as c_int, - if let Some(new) = new.maybe() { new } else { null_mut() }, - if let Some(old) = old { old } else { null_mut() }, - ) - } == 0 { - Ok(()) - } else { - Err(Error::last_os_error()) - } -} - -pub fn sigprocmask(how: Operation, new: &Sigset, old: Option<&mut Sigset>) -> Result<()> { - pthread_sigmask(how, new, old) -} - -pub fn pthread_sigmask(how: Operation, new: &Sigset, old: Option<&mut Sigset>) -> Result<()> { - use libc::pthread_sigmask; - - let code = unsafe { - pthread_sigmask(how as c_int, new, if let Some(old) = old { old } else { null_mut() }) - }; - if code == 0 { - Ok(()) - } else { - Err(Error::from_raw_os_error(code)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use pthread::pthread_kill; - use pthread::pthread_self; - - #[test] - fn sigaction_usr1() { - use std::sync::atomic::AtomicBool; - use std::sync::atomic::Ordering; - - thread_local! { - static RAN: AtomicBool = AtomicBool::new(false); - } - - extern "C" fn handler(signum: Signal, _: Option<&siginfo_t>, _: Option<&mut ucontext_t>) { - RAN.with(|ran| ran.store(signum == Signal::User1, Ordering::Relaxed)); - } - - let conf = Sigaction::new(handler, Sigset::empty(), 0); - sigaction(Signal::User1, &conf, None).unwrap(); - - pthread_kill(pthread_self(), Signal::User1).unwrap(); - - assert!(RAN.with(|ran| ran.load(Ordering::Relaxed))); - } - - #[test] - fn sigprocmask_usr2() { - use libc::sigsuspend; - use std::sync::atomic::AtomicBool; - use std::sync::atomic::Ordering; - - thread_local! { - static RAN: AtomicBool = AtomicBool::new(false); - } - - extern "C" fn handler(signum: Signal, _: Option<&siginfo_t>, _: Option<&mut ucontext_t>) { - RAN.with(|ran| ran.store(signum == Signal::User2, Ordering::Relaxed)); - } - - let mut mask = Sigset::empty(); - mask.add(Signal::User2); - sigprocmask(Operation::Block, &mask, None).unwrap(); - - let conf = Sigaction::new(handler, Sigset::empty(), 0); - sigaction(Signal::User2, &conf, None).unwrap(); - - pthread_kill(pthread_self(), Signal::User2).unwrap(); - - assert!(!RAN.with(|ran| ran.load(Ordering::Relaxed))); - - let mut mask = Sigset::full(); - mask.del(Signal::User2); - unsafe { - sigsuspend(&mask); - } - - assert!( RAN.with(|ran| ran.load(Ordering::Relaxed))); - } -} diff --git a/internal/libsignal/src/libgotcha.rs b/internal/libsignal/src/libgotcha.rs deleted file mode 120000 index d1956ba..0000000 --- a/internal/libsignal/src/libgotcha.rs +++ /dev/null @@ -1 +0,0 @@ -../../../external/libtimetravel/src/libgotcha.rs \ No newline at end of file diff --git a/internal/libsignal/src/pthread.rs b/internal/libsignal/src/pthread.rs deleted file mode 100644 index 8e52e0f..0000000 --- a/internal/libsignal/src/pthread.rs +++ /dev/null @@ -1,29 +0,0 @@ -pub use crate::Signal; - -use libc::c_int; -use libc::pthread_t; -use std::io::Error; -use std::io::Result; - -pub struct PThread (pthread_t); - -pub fn pthread_kill(thread: PThread, signal: Signal) -> Result<()> { - use crate::libgotcha::libgotcha_pthread_kill; - - let code = unsafe { - libgotcha_pthread_kill(thread.0, signal as c_int) - }; - if code == 0 { - Ok(()) - } else { - Err(Error::from_raw_os_error(code)) - } -} - -pub fn pthread_self() -> PThread { - use libc::pthread_self; - - PThread (unsafe { - pthread_self() - }) -} diff --git a/ld b/ld deleted file mode 100755 index 35d12df..0000000 --- a/ld +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -cc="cc" -interp="" -for arg in "$@" -do - case "$arg" in - -linger) - symlink="`dirname "$0"`/`basename "$0"`.so" - if [ -z "$LIBINGER_LINKER" ] && readlink -e "$symlink" >/dev/null - then - LIBINGER_LINKER="$symlink" - fi - - if [ -n "$LIBINGER_LINKER" ] - then - interp="-Wl,-I`readlink -f "$LIBINGER_LINKER"`" - fi - ;; - */libgotcha-*.rlib) - cc="`dirname "$0"`/cc" - ;; - esac -done - -exec "$cc" $interp "$@" diff --git a/ldd b/ldd deleted file mode 100755 index 819da54..0000000 --- a/ldd +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -if [ "$#" -eq "0" ] -then - echo "$0: missing file arguments" - exit 1 -fi - -while [ "$#" -ne "0" ] -do - prog="$1" - shift - - echo "$prog:" - case "$prog" in - /*) - ;; - *) - prog="./$prog" - esac - LD_TRACE_LOADED_OBJECTS= "$prog" -done diff --git a/native/Makefile b/native/Makefile new file mode 100644 index 0000000..dbbac31 --- /dev/null +++ b/native/Makefile @@ -0,0 +1,6 @@ +override ASFLAGS := $(ASFLAGS) +override LDFLAGS := $(LDFLAGS) +override LDLIBS := $(LDLIBS) + +lib%.a: %.o + $(AR) rs $@ $^ diff --git a/native/arch/x86_64/sp.s b/native/arch/x86_64/sp.s new file mode 100644 index 0000000..b0945cc --- /dev/null +++ b/native/arch/x86_64/sp.s @@ -0,0 +1,7 @@ + .text + + .globl sp + .type sp, @function +sp: + mov %rsp, %rax + ret diff --git a/profile b/profile deleted file mode 100755 index ce90928..0000000 --- a/profile +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh - -if [ "$#" -lt "2" ] -then - cat <<-tac - USAGE: $0 ... - - is which benchmark to run (or an empty string for all) - is the profiler and command-line arguments, e.g.,: - time - strace -c - valgrind --tool=callgrind --collect-atstart=no --toggle-collect=bencher::Bencher::iter - - Only release builds of the inger benchmark are supported. Note that you probably - want to enable debug symbols by adding the following to Cargo.toml before building: - [profile.bench] - debug = true - [profile.release] - debug = true - tac - exit 1 -fi - -test="$1" -shift - -LIBGOTCHA_NOGLOBALS= LD_LIBRARY_PATH=target/release/deps exec "$@" target/release/inger-*[!.]? $test diff --git a/salgrind b/salgrind new file mode 100755 index 0000000..a5965a2 --- /dev/null +++ b/salgrind @@ -0,0 +1,7 @@ +#!/bin/sh +set -e + +trap "rm -f src/main.rs target/debug/ucontext" EXIT +ln -s ../tests/tests.rs src/main.rs +cargo rustc --bins -- -Zsanitizer="$1" +target/debug/ucontext diff --git a/src/compile_assert.rs b/src/compile_assert.rs deleted file mode 100644 index 877af17..0000000 --- a/src/compile_assert.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[inline] -pub fn assert_sync(_: &T) {} diff --git a/src/ffi.rs b/src/ffi.rs deleted file mode 100644 index 77bcc68..0000000 --- a/src/ffi.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::linger::Linger as Lingerer; - -use std::ffi::c_void; -use std::process::abort; -use std::thread::Result; - -#[repr(C)] -pub struct Linger { - is_complete: bool, - continuation: Lingerer<(), dyn FnMut(*mut Option>) + Send>, -} - -#[no_mangle] -extern fn launch(fun: unsafe extern fn(*mut c_void), us: u64, args: *mut c_void) -> Linger { - use crate::force::AssertSend; - use crate::linger::launch; - - let args = unsafe { - AssertSend::new(args) - }; - let timed = launch(move || unsafe { - fun(*args) - }, us); - if let Ok(timed) = timed { - Linger { - is_complete: timed.is_completion(), - continuation: timed.erase(), - } - } else { - abort() - } -} - -#[no_mangle] -extern fn resume(timed: Option<&mut Linger>, us: u64) { - use crate::linger::resume; - - if let Some(timed) = timed { - if resume(&mut timed.continuation, us).is_err() { - abort(); - } - timed.is_complete = timed.continuation.is_completion(); - } else { - abort(); - } -} - -#[no_mangle] -extern fn cancel(timed: Option<&mut Linger>) { - if let Some(timed) = timed { - if timed.continuation.is_continuation() { - timed.continuation = Lingerer::Completion(()); - timed.is_complete = true; - } - } else { - abort(); - } -} - -#[no_mangle] -extern fn pause() { - use crate::linger::pause; - - pause(); -} diff --git a/src/force.rs b/src/force.rs deleted file mode 100644 index a960fe2..0000000 --- a/src/force.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::ops::Deref; -use std::ops::DerefMut; - -#[repr(transparent)] -pub struct AssertSend (T); - -unsafe impl Send for AssertSend {} - -impl AssertSend { - pub unsafe fn new(t: T) -> Self { - Self (t) - } -} - -impl Deref for AssertSend { - type Target = T; - - fn deref(&self) -> &Self::Target { - let Self (this) = self; - this - } -} - -impl DerefMut for AssertSend { - fn deref_mut(&mut self) -> &mut Self::Target { - let Self (this) = self; - this - } -} diff --git a/src/future.rs b/src/future.rs deleted file mode 100644 index 8b21c13..0000000 --- a/src/future.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::linger::Linger; - -use std::future::Future; -use std::io::Result; -use std::pin::Pin; -use std::sync::mpsc::SyncSender; -use std::task::Context; -use std::task::Poll; -use std::thread::Result as ThdResult; - -pub struct PreemptiveFuture< - T, - F: FnMut(*mut Option>) + Send, - P: Fn() -> I + Unpin, - I: FnMut() + Unpin, -> { - fun: Option>, - us: u64, - poll: P, - pre: SyncSender, -} - -pub fn poll_fn(fun: impl FnMut() -> Poll + Send, us: u64) --> Result>) + Send, impl Fn() -> fn(), fn()>> { - fn nop() {} - fn nope() -> fn() { nop } - poll_fns(nope, fun, us) -} - -pub fn poll_fns( - poll: impl Fn() -> I + Unpin, - mut fun: impl FnMut() -> Poll + Send, - us: u64, -) -> Result>) + Send, impl Fn() -> I, I>> { - use crate::linger::launch; - use crate::linger::pause; - - use std::hint::unreachable_unchecked; - use std::sync::mpsc::sync_channel; - - let (pre, prep): (SyncSender, _) = sync_channel(1); - let fun = Some(launch(move || { - let mut res; - while { - prep.recv().unwrap()(); - res = fun(); - res.is_pending() - } { - pause(); - } - if let Poll::Ready(res) = res { - res - } else { - unsafe { - unreachable_unchecked() - } - } - }, 0)?); - Ok(PreemptiveFuture { - fun, - us, - poll, - pre, - }) -} - -impl>) + Send, P: Fn() -> I + Unpin, I: FnMut() + Unpin> -Future for PreemptiveFuture { - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, context: &mut Context) -> Poll { - use crate::linger::resume; - - if let Some(mut fun) = self.fun.take() { - self.pre.send((self.poll)()).unwrap(); - if let Err(or) = resume(&mut fun, self.us) { - Poll::Ready(Err(or)) - } else { - if let Linger::Completion(ready) = fun { - Poll::Ready(Ok(ready)) - } else { - let timeout = ! fun.yielded(); - self.fun.replace(fun); - if timeout { - // The preemptible function timed out rather than blocking - // on some other future, so it's already ready to run again. - context.waker().wake_by_ref(); - } - Poll::Pending - } - } - } else { - Poll::Pending - } - } -} diff --git a/src/groups.rs b/src/groups.rs deleted file mode 100644 index f43e404..0000000 --- a/src/groups.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::reusable::SyncResult; - -use gotcha::Group as GotchaGroup; - -pub fn assign_group() -> SyncResult<'static, GotchaGroup> { - use crate::compile_assert::assert_sync; - use crate::reusable::SyncPool; - - use std::convert::TryInto; - use std::sync::Once; - - static mut GROUPS: Option> = None; - static INIT: Once = Once::new(); - INIT.call_once(|| unsafe { - GROUPS.replace(SyncPool::new(GotchaGroup::new)); - }); - - let groups = unsafe { - GROUPS.as_ref() - }.unwrap(); - assert_sync(&groups); - groups.try_into() -} diff --git a/src/invar.rs b/src/invar.rs new file mode 100644 index 0000000..664e9f7 --- /dev/null +++ b/src/invar.rs @@ -0,0 +1,3 @@ +pub trait MoveInvariant { + fn after_move(&mut self) {} +} diff --git a/src/lib.rs b/src/lib.rs index bf42276..a410f93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,44 +1,16 @@ -mod compile_assert; -pub mod ffi; -pub mod force; -pub mod future; -mod groups; -mod lifetime; -mod linger; -mod localstores; -mod preemption; -pub mod profiler; -mod reusable; -mod signals; -mod stacks; -#[cfg(not(feature = "notls"))] -mod tcb; -mod timer; -mod unfurl; - -#[cfg(feature = "notls")] -mod tcbstub; -#[cfg(feature = "notls")] -mod tcb { - pub use crate::tcbstub::*; -} - -pub use linger::*; - -use gotcha::Group; - -const QUANTUM_MICROSECS: u64 = 100; +extern crate libc; +mod invar; +mod platform; +mod sp; #[doc(hidden)] -pub const STACK_N_PREALLOC: usize = Group::LIMIT; -const STACK_SIZE_BYTES: usize = 2 * 1_024 * 1_024; - -#[no_mangle] -static libgotcha_exitanalysis: bool = true; - -pub fn concurrency_limit() -> usize { - Group::limit() -} - -#[cfg(test)] -fn main() {} +mod tests; +mod ucontext; +mod uninit; +mod volatile; +mod zero; + +pub use invar::MoveInvariant; +pub use libc::MINSIGSTKSZ; +pub use libc::SIGSTKSZ; +pub use ucontext::*; diff --git a/src/lifetime.rs b/src/lifetime.rs deleted file mode 100644 index 80a6abf..0000000 --- a/src/lifetime.rs +++ /dev/null @@ -1,9 +0,0 @@ -#![allow(dead_code)] - -pub unsafe fn unbound<'a, T>(bounded: *const T) -> &'a T { - &*bounded -} - -pub unsafe fn unbound_mut<'a, T>(bounded: *mut T) -> &'a mut T { - &mut *bounded -} diff --git a/src/linger.rs b/src/linger.rs deleted file mode 100644 index a71c927..0000000 --- a/src/linger.rs +++ /dev/null @@ -1,509 +0,0 @@ -use crate::preemption::RealThreadId; -use crate::preemption::defer_preemption; -use crate::preemption::disable_preemption; -use crate::preemption::is_preemptible; -use crate::preemption::thread_signal; -use crate::reusable::ReusableSync; -use crate::stacks::DerefAdapter; -use crate::tcb::ThreadControlBlock; - -use gotcha::Group; -use signal::Set; -use signal::Signal; -use signal::siginfo_t; -use std::cell::Cell; -use std::cell::RefCell; -use std::io::Result; -use std::os::raw::c_int; -use std::ptr::NonNull; -use std::thread::Result as ThdResult; -use timetravel::errno::errno; -use timetravel::Context; -use timetravel::HandlerContext; - -#[repr(C)] -pub enum Linger>) + Send + ?Sized> { - Completion(T), - Continuation(Continuation), - Poison, -} - -impl>) + Send + ?Sized> Unpin for Linger {} - -impl>) + Send + ?Sized> Linger { - pub fn is_completion(&self) -> bool { - if let Linger::Completion(_) = self { - true - } else { - false - } - } - - pub fn is_continuation(&self) -> bool { - if let Linger::Continuation(_) = self { - true - } else { - false - } - } - - pub fn yielded(&self) -> bool { - if let Linger::Continuation(continuation) = self { - continuation.stateful.yielded - } else { - false - } - } -} - -impl<'a, T, F: FnMut(*mut Option>) + Send + 'a> Linger { - /// Erase the type of the contained closure, thereby allowing insertion of the instance into - /// a polymorphic data structure containing other instances. After this operation, the - /// instance behaves as before: only its type is changed. - pub fn erase(self) -> Linger>) + Send + 'a> { - use std::mem::MaybeUninit; - - if let Linger::Completion(this) = self { - Linger::Completion(this) - } else if let Linger::Continuation(this) = self { - let this = MaybeUninit::new(this); - let this = this.as_ptr(); - unsafe { - let functional: *const _ = &(*this).functional; - let stateful: *const _ = &(*this).stateful; - let group: *const _ = &(*this).group; - let tls: *const _ = &(*this).tls; - - Linger::Continuation(Continuation { - functional: functional.read(), - stateful: stateful.read(), - group: group.read(), - tls: tls.read(), - }) - } - } else { - Linger::Poison - } - } -} - -pub struct Continuation { - // First they called for the preemptible function to be executed, and I did not read the - // argument because it was not present. Then they called for the return value, and I did - // not call the preemptible function because it was not present. Then they called for me, - // and I did not call or return because there was no one left to call and I had nothing left - // to give. (Only call this twice!) - // - // Because the whole Continuation might be moved between this function's preemption and its - // resumption, we must heap allocate it so its captured environment has a stable address. - functional: Box, - stateful: Task, - group: ReusableSync<'static, Group>, - tls: ReusableSync<'static, Option>, -} - -unsafe impl Send for Continuation {} - -impl Drop for Continuation { - fn drop(&mut self) { - debug_assert!(! is_preemptible(), "libinger: dropped from preemptible function"); - - let group = &self.group; - if self.stateful.errno.is_some() { - // We're canceling a paused preemptible function. Clean up the group! - assert!(group.renew(), "libinger: failed to reinitialize library group"); - } - } -} - -#[derive(Default)] -struct Task { - // Also indicates the state of execution. If we have a Continuation instance, we know the - // computatation cannot have completed earlier, so it must be in one of these states... - // * None on entry to resume() means it hasn't yet started running. - // * None at end of resume() means it has completed. - // * Some at either point means it timed out and is currently paused. - errno: Option, - checkpoint: Option>>>>, - yielded: bool, -} - -/// Run `fun` with the specified time budget, in `us`econds. -/// -/// If the budget is `0`, the timed function is initialized but not invoked; if it is `max_value()`, -/// it is run to completion. -pub fn launch(fun: impl FnOnce() -> T + Send, us: u64) --> Result>) + Send>> { - use crate::localstores::alloc_localstore; - use crate::groups::assign_group; - - use std::panic::AssertUnwindSafe; - use std::panic::catch_unwind; - - enum Completion { - Function(F), - Return(T), - Empty, - } - - impl Completion { - fn take(&mut self) -> Self { - use std::mem::replace; - - replace(self, Completion::Empty) - } - } - - // Danger, W.R.! Although in theory libgotcha ensures there's only one copy of our library, - // exported parameterized functions are actually monomorphized into the *caller's* object - // file! This means that this function has an inconsistent view of the worldstate if called - // from a preemptible function, but that any non-generic (name brand?) functions it calls - // are guaranteed to run in the libgotcha's shared group. To guard against heisenbugs - // arising from the former case, we first assert that no one has attempted a nested call. - debug_assert!(! is_preemptible(), "launch(): called from preemptible function"); - - let mut fun = Completion::Function(AssertUnwindSafe (fun)); - let fun = Box::new(move |ret: *mut Option>| { - fun = match fun.take() { - Completion::Function(fun) => - // We haven't yet moved the closure. This means schedule() is invoking us - // as a *nullary* function, implying ret is undefined and mustn't be used. - Completion::Return(catch_unwind(fun)), - Completion::Return(val) => { - debug_assert!(! ret.is_null()); - let ret = unsafe { - &mut *ret - }; - ret.replace(val); - Completion::Empty - }, - Completion::Empty => - Completion::Empty, - } - }); - - let group = assign_group().expect("launch(): too many active timed functions"); - let mut linger = Linger::Continuation(Continuation { - functional: fun, - stateful: Task::default(), - group, - tls: alloc_localstore(), - }); - if us != 0 { - resume(&mut linger, us)?; - } - Ok(linger) -} - -// Note that it is only safe to use these while the virtual thread-control block is installed! -thread_local! { - static BOOTSTRAP: Cell, Group)>> = Cell::default(); - static TASK: RefCell = RefCell::default(); - static DEADLINE: Cell = Cell::default(); -} - -/// Let `fun` continue running for the specified time budget, in `us`econds. -/// -/// If the budget is `0`, this is a no-op; if it is `max_value()`, the timed function is run to -/// completion. This function is idempotent once the timed function completes. -pub fn resume(fun: &mut Linger>) + Send + ?Sized>, us: u64) --> Result<&mut Linger>) + Send + ?Sized>> { - use std::panic::resume_unwind; - - // Danger, W.R! The same disclaimer from launch() applies here. - debug_assert!(! is_preemptible(), "resume(): called from preemptible function"); - - if let Linger::Continuation(continuation) = fun { - let task = &mut continuation.stateful; - let group = *continuation.group; - let thread = RealThreadId::current(); - - // Install the virtual thread-control block. It is only safe to use thread-locals - // between the end of this block and when we uninstall it again! - let tls = continuation.tls.take().expect("libinger: continuation with missing TCB"); - let tls = unsafe { - tls.install(group)? - }; - - if let Err(or) = setup_thread(thread) { - abort(&format!("resume(): failure in thread_setup(): {}", or)); - } - - // Are we launching this preemptible function for the first time? - if task.errno.is_none() { - let fun = &mut continuation.functional; - task.checkpoint = setup_stack()?; - BOOTSTRAP.with(|bootstrap| { - // The schedule() function is polymorphic across "return" types, but - // we expect a storage area appropriate for our own type. To remove - // the specialization from our signature, we reduce our arity by - // casting away our parameter. This is safe because schedule() - // represents our first caller, so we know not to read the argument. - let fun: *mut (dyn FnMut(_) + Send) = fun; - let fun: *mut (dyn FnMut() + Send) = fun as _; - let no_fun = bootstrap.replace(NonNull::new(fun).map(|fun| - (fun, group) - )); - debug_assert!( - no_fun.is_none(), - "resume(): bootstraps without an intervening schedule()", - ); - }); - } - - DEADLINE.with(|deadline| deadline.replace(us)); - - // Transfer control into the libinger module before running the function! - let finished = switch_stack(task, group)?; - continuation.tls.replace(unsafe { - tls.uninstall()? - }); - if finished { - // The preemptible function finished (either ran to completion or panicked). - // Since we know the closure is no longer running concurrently, it's now - // safe to call it again to retrieve the return value. - let mut retval = None; - (continuation.functional)(&mut retval); - - match retval.expect("resume(): return value was already retrieved") { - Ok(retval) => *fun = Linger::Completion(retval), - Err(panic) => { - *fun = Linger::Poison; - resume_unwind(panic); - }, - } - } - } - - Ok(fun) -} - -/// Set up preemption for a kernel execution thread. Call after installing a virtual TCB! -#[inline(never)] -fn setup_thread(thread: RealThreadId) -> Result<()> { - use crate::preemption::thread_setup; - use super::QUANTUM_MICROSECS; - thread_setup(thread, preempt, QUANTUM_MICROSECS) -} - -/// Set up the oneshot execution stack. Always returns a Some when things are Ok. -#[inline(never)] -fn setup_stack() --> Result>>>>> { - use crate::stacks::alloc_stack; - - use timetravel::makecontext; - - let mut checkpoint = None; - makecontext( - DerefAdapter::from(alloc_stack()), - |goto| drop(checkpoint.replace(goto)), - schedule, - )?; - Ok(checkpoint) -} - -/// Jump to the preemptible function, reenabling preemption if the function was previously paused. -/// Runs on the main execution stack. Returns whether the function "finished" without a timeout. -#[inline(never)] -fn switch_stack(task: &mut Task, group: Group) -> Result { - use crate::lifetime::unbound_mut; - - use gotcha::group_thread_set; - use signal::Operation; - use signal::Sigset; - use signal::pthread_sigmask; - use timetravel::restorecontext; - use timetravel::sigsetcontext; - - let mut error = None; - restorecontext( - task.checkpoint.take().expect("switch_stack(): continuation is missing"), - |pause| { - let resume = TASK.with(|task| { - let mut task = task.borrow_mut(); - debug_assert!( - task.checkpoint.is_none(), - "switch_stack(): this continuation would nest?!" - ); - - let resume = task.checkpoint.get_or_insert(pause); - unsafe { - unbound_mut(resume) - } - }); - - // Are we resuming a paused preemptible function? - if let Some(erryes) = task.errno { - // Do everything to enable preemption short of unblocking the - // signal, which will be don atomically by sigsetcontext() as it - // jumps into the continuation. - let mut old = Sigset::empty(); - let mut new = Sigset::empty(); - let sig = thread_signal().unwrap_or_else(|_| - abort("switch_stack(): error accessing TLS variable") - ); - new.add(sig); - if let Err(or) = pthread_sigmask( - Operation::Block, - &new, - Some(&mut old), - ) { - error.replace(or); - } - old.del(sig); - *resume.mask() = old; - - // The clock is ticking from this "start" point. - stamp(); - - // The order of these two lines, with respect to both each other and - // the rest of the program, is very important. We need to switch to - // the new group before we restore errno so we get the right one, - // and there cannot be any standard library calls between its - // restoration and the call to sigsetcontext(). - group_thread_set!(group); - *errno() = erryes; - } - - // No library calls may be made before this one! - let failure = sigsetcontext(resume); - error.replace(failure.expect("resume(): continuation is invalid")); - }, - )?; - if let Some(error) = error { - Err(error)?; - } - - let descheduled = TASK.with(|task| task.replace(Task::default())); - let preempted = descheduled.errno.is_some(); - if preempted { - *task = descheduled; - } else { - // Prevent namespace reinitialization on drop of Continuation containing the Task. - task.errno.take(); - } - - Ok(! preempted) -} - -/// Enable preemption and call the preemptible function. Runs on the oneshot execution stack. -fn schedule() { - use crate::preemption::enable_preemption; - - let (mut fun, group) = BOOTSTRAP.with(|bootstrap| bootstrap.take()).unwrap_or_else(|| - abort("schedule(): called without bootstrapping") - ); - let fun = unsafe { - fun.as_mut() - }; - stamp(); - if let Ok(signal) = enable_preemption(group.into()) { - debug_assert!(signal.is_some()); - - fun(); - disable_preemption(signal); - } else { - abort("schedule(): error accessing TLS variable"); - } - - // It's important that we haven't saved any errno, since we'll check it to determine whether - // the preemptible function ran to completion. - debug_assert!( - TASK.with(|task| task.borrow().errno.is_none()), - "schedule(): finished leaving errno", - ); -} - -/// Signal handler that pauses the preemptible function on timeout. Runs on the oneshot stack. -extern fn preempt(no: Signal, _: Option<&siginfo_t>, uc: Option<&mut HandlerContext>) { - use crate::unfurl::Unfurl; - - use timetravel::Swap; - - let erryes = *errno(); - let uc = unsafe { - uc.unfurl() - }; - let relevant = thread_signal().map(|signal| no == signal).unwrap_or(false); - if relevant && is_preemptible() { - let deadline = DEADLINE.with(|deadline| deadline.get()); - if nsnow() >= deadline { - TASK.with(|task| { - // It's time to pause the function. We need to save its state. - let mut task = task.borrow_mut(); - - // Did it cooperatively yield instead of being preempted? - task.yielded = deadline == 0; - - // Configure us to return into the checkpoint for its call site. - let checkpoint = task.checkpoint.as_mut(); - let checkpoint = unsafe { - checkpoint.unfurl() - }; - checkpoint.swap(uc); - - // Instead of restoring errno, save it for if and when we resume. - task.errno.replace(erryes); - - // Block this signal and disable preemption. - uc.uc_sigmask.add(no); - disable_preemption(None); - }); - } else { - *errno() = erryes; - } - } else { - if relevant { - // The timed function has called into a nonpreemptible library function. - // We'll need to intercept it immediately upon the function's return. - defer_preemption((&mut uc.uc_sigmask, no).into()); - } else { - // We still want to block this signal so it doesn't disturb us again. - uc.uc_sigmask.add(no); - } - - *errno() = erryes; - } -} - -/// Bump the deadline forward by the current wall-clock time, unless the timeout is unlimited. -fn stamp() { - DEADLINE.with(|deadline| - deadline.replace(deadline.get().checked_mul(1_000).map(|timeout| - nsnow() + timeout - ).unwrap_or(deadline.get())) - ); -} - -/// Immediately yield the calling preemptible function. -#[inline(never)] -pub fn pause() { - // Note that this function is not parameterized, so it is itself nonpreemptible. This is - // key because we need to be able to request preemption atomically (so we only get one). - DEADLINE.with(|deadline| deadline.take()); - - // Get preempted when we return. - defer_preemption(None); -} - -/// Read the current wall-clock time, in nanoseconds. -#[doc(hidden)] -pub fn nsnow() -> u64 { - use std::time::UNIX_EPOCH; - use std::time::SystemTime; - - let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("libinger: wall clock error"); - let mut sum = now.subsec_nanos().into(); - sum += now.as_secs() * 1_000_000_000; - sum -} - -#[doc(hidden)] -pub fn abort(err: &str) -> ! { - use std::process::abort; - - let abort: fn() -> _ = abort; - eprintln!("{}", err); - abort() -} diff --git a/src/localstores.rs b/src/localstores.rs deleted file mode 100644 index c0ce3a4..0000000 --- a/src/localstores.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::reusable::ReusableSync; -use crate::tcb::ThreadControlBlock; - -pub fn alloc_localstore() -> ReusableSync<'static, Option> { - use crate::compile_assert::assert_sync; - use crate::reusable::SyncPool; - - use gotcha::Group; - use std::convert::TryInto; - use std::sync::Once; - - static mut LOCALSTORES: Option>> = None; - static INIT: Once = Once::new(); - INIT.call_once(|| { - let localstores: fn() -> _ = || Some(Some(ThreadControlBlock::new())); - let localstores = SyncPool::new(localstores); - localstores.prealloc(Group::limit()) - .expect("libinger: TCB allocator lock was poisoned during init"); - unsafe { - LOCALSTORES.replace(localstores); - } - }); - - let localstores = unsafe { - LOCALSTORES.as_ref() - }.unwrap(); - assert_sync(&localstores); - localstores.try_into().expect("libinger: TCB allocator lock is poisoned") -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 1a7a9a1..0000000 --- a/src/main.rs +++ /dev/null @@ -1,28 +0,0 @@ -use inger::ffi::Linger; -use std::mem::size_of; - -fn main() { - print!( - "\ - #ifndef LIBINGER_H_\n\ - #define LIBINGER_H_\n\ - \n\ - #include \n\ - #include \n\ - \n\ - typedef struct {{\n\ - bool is_complete;\n\ - uint8_t continuation[{}];\n\ - }} linger_t;\n\ - \n\ - linger_t launch(void (*)(void *), uint64_t, void *);\n\ - void resume(linger_t *, uint64_t);\n\ - void cancel(linger_t *);\n\ - \n\ - void pause(void);\n\ - \n\ - #endif\n\ - ", - size_of::(), - ); -} diff --git a/src/platform.rs b/src/platform.rs new file mode 100644 index 0000000..85bfc04 --- /dev/null +++ b/src/platform.rs @@ -0,0 +1,32 @@ +use invar::MoveInvariant; +use libc::ucontext_t; +pub use self::imp::*; + +#[cfg(target_os = "linux")] +mod imp { + use libc::greg_t; + use libc::_libc_fpstate; + use super::*; + use zero::Zero; + + impl MoveInvariant for ucontext_t { + fn after_move(&mut self) { + let start = self as *mut ucontext_t; + let end = unsafe { + start.add(1) + } as *mut _libc_fpstate; + self.uc_mcontext.fpregs = unsafe { + end.sub(1) + }; + } + } + + unsafe impl Zero for [greg_t; 23] {} +} + +#[cfg(not(target_os = "linux"))] +mod imp { + use super::*; + + impl MoveInvariant for ucontext_t {} +} diff --git a/src/preemption.rs b/src/preemption.rs deleted file mode 100644 index 4a9c3e3..0000000 --- a/src/preemption.rs +++ /dev/null @@ -1,240 +0,0 @@ -use crate::reusable::ReusableSync; -use crate::timer::Timer; - -use gotcha::Group; -use gotcha::group_thread_set; -use signal::pthread::pthread_kill; -use signal::pthread::pthread_self; -use signal::Handler; -use signal::Operation; -use signal::Set; -use signal::Signal; -use signal::Sigset; -use signal::sigaction; -use std::cell::RefCell; -use std::io::Result as IoResult; -use std::os::raw::c_int; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; - -thread_local! { - static SIGNAL: RefCell> = RefCell::default(); - - // Whether we had to delay preemption checks until the end of a nonpreemptible call. - static DEFERRED: AtomicBool = AtomicBool::new(false); -} - -pub fn thread_signal() -> Result { - // Because this is called from signal handlers, it might happen during thread teardown, when - // the thread-local variable is being/has been destructed. In such a case, we simply report - // that the current thread has no preemption signal assigned (any longer). - SIGNAL.try_with(|signal| - signal.borrow().as_ref().map(|signal| { - let RealThreadId (signal) = signal; - signal.borrow().as_ref().map(|signal| - *signal.signal - ) - }).unwrap_or(None).ok_or(()) - ).unwrap_or(Err(())) -} - -extern fn resume_preemption() { - // Skip if this trampoline is running in a destructor during thread teardown. - drop(enable_preemption(None)); -} - -pub fn enable_preemption(group: Option) -> Result, ()> { - use timetravel::errno::errno; - - // We must access errno_group() even when we won't use it so that the initial - // enable_preemption() call bootstraps later resume_preemption() ones! - let erryes = errno_group(group); - let errno = *errno(); - - // We can only call thread_signal() if the preemption signal is already blocked; otherwise, - // the signal handler might race on the thread-local SIGNAL variable. It's fine to do when: - // * We have been passed a group, because in this case preemption was previously disabled. - // - OR - - // * Preemption has been deferred, because when setting the flag, the signal handler will - // have masked out the signal. - let mut unblock = None; - if let Some(group) = group { - // It's important we don't unmask the preemption signal until we've switched groups; - // otherwise, its handler may run immediately and remask it! - group_thread_set!(group); - unblock.replace(thread_signal()?); - } - // else the caller is asserting the group change has already been performed. - - if DEFERRED.with(|deferred| deferred.swap(false, Ordering::Relaxed)) { - let signal = unblock.get_or_insert(thread_signal()?); - drop(pthread_kill(pthread_self(), *signal)); - } - - if let Some(signal) = unblock { - drop(mask(Operation::Unblock, signal)); - } - - // Propagate any errors encountered during our libc replacements back to the calling libset. - if group.is_none() && errno != 0 { - *erryes = errno; - } - Ok(unblock) -} - -pub fn disable_preemption(block: Option) { - group_thread_set!(Group::SHARED); - if let Some(signal) = block { - // Mask the preemption signal without calling thread_signal(), which would by racy. - drop(mask(Operation::Block, signal)); - } - - SIGNAL.with(|signal| signal.replace(None)); - DEFERRED.with(|deferred| deferred.store(false, Ordering::Relaxed)); -} - -pub fn is_preemptible() -> bool { - use gotcha::group_thread_get; - - ! group_thread_get!().is_shared() -} - -// It is only safe to call this function while preemption is (temporarily) disabled! -pub fn defer_preemption(signum: Option<(&mut Sigset, Signal)>) { - debug_assert!(! is_preemptible()); - - // We must first mask the signal so no attempted preemption races on DEFERRED! - if let Some((sigmask, signo)) = signum { - // Caller is asserting we are beneath a signal handler, so we should only update the - // outside world's mask. - sigmask.add(signo); - } else { - drop(mask(Operation::Block, thread_signal().unwrap())); - } - - DEFERRED.with(|deferred| deferred.store(true, Ordering::Relaxed)); -} - -pub fn thread_setup(thread: RealThreadId, handler: Handler, quantum: u64) -> IoResult<()> { - use gotcha::shared_hook; - use std::sync::Once; - - let RealThreadId (signal) = thread; - if signal.borrow().is_none() { - signal.replace(Some(PreemptionSignal::new(handler, quantum)?)); - } - SIGNAL.with(|signal| signal.replace(Some(thread))); - - static INIT: Once = Once::new(); - INIT.call_once(|| shared_hook(resume_preemption)); - - Ok(()) -} - -fn errno_group(group: Option) -> &'static mut c_int { - use gotcha::group_lookup_symbol_fn; - use libc::__errno_location; - thread_local! { - static ERRNO_LOCATION: RefCell *mut c_int>> = - RefCell::new(None); - } - - // We save the location in a thread-local variable so the next time we need to find its - // *location,* we won't clear its *value* in the process! - ERRNO_LOCATION.with(|errno_location| { - let mut errno_location = errno_location.borrow_mut(); - if let Some(group) = group { - errno_location.replace(unsafe { - group_lookup_symbol_fn!(group, __errno_location) - }.unwrap()); - } - let __errno_location = errno_location.unwrap(); - unsafe { - &mut *__errno_location() - } - }) -} - -fn mask(un: Operation, no: Signal) -> IoResult<()> { - use signal::pthread_sigmask; - - let mut set = Sigset::empty(); - set.add(no); - pthread_sigmask(un, &set, None) -} - -pub struct RealThreadId (&'static RefCell>); - -impl RealThreadId { - pub fn current() -> Self { - use crate::lifetime::unbound; - - thread_local! { - static SIGNALER: RefCell> = RefCell::default(); - } - - Self (SIGNALER.with(|signaler| unsafe { - unbound(signaler) - })) - } -} - -struct PreemptionSignal { - signal: ReusableSync<'static, Signal>, - timer: Timer, -} - -impl PreemptionSignal { - fn new(handler: Handler, quantum: u64) -> IoResult { - use crate::signals::assign_signal; - use crate::timer::Clock; - use crate::timer::Sigevent; - use crate::timer::itimerspec; - use crate::timer::timer_create; - use crate::timer::timer_settime; - - use libc::SA_RESTART; - use libc::SA_SIGINFO; - use libc::timespec; - use signal::Action; - use signal::Sigaction; - - let signal = assign_signal().expect("libinger: no available signal for preempting this thread"); - let sa = Sigaction::new(handler, Sigset::empty(), SA_SIGINFO | SA_RESTART); - sigaction(*signal, &sa, None)?; - mask(Operation::Block, *signal)?; - - let mut se = Sigevent::signal(*signal); - let timer = timer_create(Clock::Real, &mut se)?; - let quantum: i64 = quantum as _; - let mut it = itimerspec { - it_interval: timespec { - tv_sec: 0, - tv_nsec: quantum * 1_000, - }, - it_value: timespec { - tv_sec: 0, - tv_nsec: quantum * 1_000, - }, - }; - timer_settime(timer, false, &mut it, None)?; - - Ok(Self { - signal, - timer, - }) - } -} - -impl Drop for PreemptionSignal { - fn drop(&mut self) { - use crate::timer::timer_delete; - - if let Err(or) = timer_delete(self.timer) { - eprintln!("libinger: unable to delete POSIX timer: {}", or); - } - if let Err(or) = sigaction(*self.signal, &(), None) { - eprintln!("libinger: unable to unregister signal handler: {}", or); - } - } -} diff --git a/src/profiler.rs b/src/profiler.rs deleted file mode 100644 index fc13794..0000000 --- a/src/profiler.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::linger::nsnow; - -use libc::ucontext_t; -use signal::Set; -use signal::Signal; -use signal::Sigset; -use signal::siginfo_t; -use std::cell::RefCell; -use std::collections::VecDeque; - -pub fn with_profiler(fun: impl Fn(&mut Profiler) -> T) -> T { - thread_local! { - static PROFILER: RefCell> = RefCell::default(); - } - PROFILER.with(|profiler| - fun(profiler.borrow_mut().get_or_insert_with(Profiler::default)) - ) -} - -#[derive(Default)] -pub struct Profiler { - past: VecDeque, - present: u64, -} - -impl Profiler { - pub fn begin(&mut self) { - use signal::Action; - use signal::Sigaction; - use signal::sigaction; - use std::sync::Once; - - static ONCE: Once = Once::new(); - ONCE.call_once(|| - drop(sigaction(Signal::Interrupt, &mut Sigaction::new(interrupt, Sigset::empty(), 0), None)) - ); - self.present = nsnow(); - } - - pub fn end(&mut self) -> bool { - if self.present != 0 { - self.past.push_back(nsnow() - self.present); - self.present = 0; - true - } else { - false - } - } -} - -impl Drop for Profiler { - fn drop(&mut self) { - use std::ffi::CString; - use std::os::raw::c_char; - - extern { - fn puts(_: *const c_char); - } - - let len: f64 = self.past.len() as _; - let sum: u64 = self.past.iter().sum(); - let sum: f64 = sum as _; - let msg = CString::new(format!("Profiler ave. = {} us", sum / len / 1_000.0)).unwrap(); - unsafe { - puts(msg.as_ptr()); - } - } -} - -extern fn interrupt(_: Signal, _: Option<&siginfo_t>, _: Option<&mut ucontext_t>) { - use signal::Operation; - use signal::pthread_sigmask; - use std::os::raw::c_int; - use std::process::abort; - use std::thread::current; - extern { - fn exit(_: c_int); - } - if current().name().unwrap_or_else(|| abort()) != "main" { - unsafe { - exit(100); - } - } else { - let mut sig = Sigset::empty(); - sig.add(Signal::Interrupt); - drop(pthread_sigmask(Operation::Block, &sig, None)) - } -} diff --git a/src/reusable.rs b/src/reusable.rs deleted file mode 100644 index ee753f0..0000000 --- a/src/reusable.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::cell::BorrowMutError; -use std::cell::RefCell; -use std::cell::RefMut; -use std::convert::TryFrom; -use std::fmt::Debug; -use std::marker::PhantomData; -use std::ops::Deref; -use std::ops::DerefMut; -use std::result::Result as StdResult; -use std::sync::Mutex; -use std::sync::MutexGuard; -use std::sync::PoisonError; - -type Sync = Mutex>; -type Unsync = RefCell>; - -pub struct Reusable<'a, T, A = Unsync> -where &'a A: SharedMut> { - value: Option, - pool: &'a A, -} - -pub type ReusableSync<'a, T> = Reusable<'a, T, Sync>; - -impl<'a, T, A, B: Fn() -> Option> TryFrom<&'a Pool> for Reusable<'a, T, A> -where &'a A: SharedMut> { - type Error = Option<<&'a A as SharedMut>>::Error>; - - fn try_from(pool: &'a Pool) -> StdResult, Self::Error> { - let builder = &pool.builder; - let pool = &pool.allocated; - let value = pool.try_into_inner()?.pop().or_else(builder); - if value.is_some() { - Ok(Self { - value, - pool, - }) - } else { - Err(None) - } - } -} - -impl<'a, T, A> Deref for Reusable<'a, T, A> -where &'a A: SharedMut> { - type Target = T; - - fn deref(&self) -> &Self::Target { - // Can only be None if we're called while being dropped! - self.value.as_ref().unwrap() - } -} - -impl<'a, T, A> DerefMut for Reusable<'a, T, A> -where &'a A: SharedMut> { - fn deref_mut(&mut self) -> &mut Self::Target { - // Can only be None if we're called while being dropped! - self.value.as_mut().unwrap() - } -} - -impl<'a, T, A> Drop for Reusable<'a, T, A> -where &'a A: SharedMut> { - fn drop(&mut self) { - // Can only be None on a double drop! - let value = self.value.take().unwrap(); - - // Panic instead of losing this value. - self.pool.try_into_inner().unwrap().push(value) - } -} - -pub struct Pool Option, A = Unsync> { - _type: PhantomData, - allocated: A, - builder: B, -} - -pub type SyncPool Option> = Pool>; - -impl<'a, T, F: Fn() -> Option, C: Default + 'a> Pool -where &'a C: SharedMut> { - pub fn new(builder: F) -> Self { - Self { - _type: PhantomData::default(), - allocated: C::default(), - builder: builder, - } - } - - pub fn prealloc(&'a self, count: usize) - -> StdResult<(), Option<<&'a C as SharedMut>>::Error>> { - use std::collections::LinkedList; - use std::convert::TryInto; - - let swap: LinkedList, _>> = (0..count).map(|_| - self.try_into() - ).collect(); - for temp in swap { - temp?; - } - Ok(()) - } -} - -impl<'a, T: Default, C: Default + 'a> Default for Pool Option, C> -where &'a C: SharedMut> { - fn default() -> Self { - Self::new(|| Some(T::default())) - } -} - -pub type Result<'a, T, A = Unsync> = StdResult< - Reusable<'a, T, A>, - Option<<&'a A as SharedMut>>::Error>, ->; - -pub type SyncResult<'a, T> = Result<'a, T, Sync>; - -#[doc(hidden)] -pub trait SharedMut { - type Okay: DerefMut; - type Error: Debug; - - fn try_into_inner(self) -> StdResult; -} - -impl<'a, T> SharedMut for &'a RefCell { - type Okay = RefMut<'a, T>; - type Error = BorrowMutError; - - fn try_into_inner(self) -> StdResult { - self.try_borrow_mut() - } -} - -impl<'a, T> SharedMut for &'a Mutex { - type Okay = MutexGuard<'a, T>; - type Error = PoisonError; - - fn try_into_inner(self) -> StdResult { - self.lock() - } -} diff --git a/src/signals.rs b/src/signals.rs deleted file mode 100644 index ccfd14d..0000000 --- a/src/signals.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::reusable::SyncResult; - -use signal::Signal; - -static NOTIFICATION_SIGNALS: [Signal; 16] = [ - Signal::Alarm, - Signal::VirtualAlarm, - Signal::ProfilingTimer, - Signal::ProcessorLimit, - Signal::FilesystemLimit, - Signal::TerminalInput, - Signal::TerminalOutput, - Signal::PowerFailure, - Signal::User1, - Signal::User2, - - // A stretch... - Signal::UrgentSocket, - Signal::Pollable, - Signal::Syscall, - Signal::FloatingPoint, - Signal::Hangup, - Signal::Child, -]; - -pub fn assign_signal() -> SyncResult<'static, Signal> { - use crate::compile_assert::assert_sync; - use crate::reusable::SyncPool; - - use std::convert::TryInto; - use std::sync::atomic::AtomicUsize; - use std::sync::atomic::Ordering; - use std::sync::Once; - - static mut SIGNALS: Option Option + Sync>>> = None; - static INIT: Once = Once::new(); - INIT.call_once(|| unsafe { - let free = AtomicUsize::new(0); - SIGNALS.replace(SyncPool::new(Box::new(move || - NOTIFICATION_SIGNALS.get(free.fetch_add(1, Ordering::Relaxed)).copied() - ))); - }); - - let signals = unsafe { - SIGNALS.as_ref() - }.unwrap(); - assert_sync(&signals); - signals.try_into() -} diff --git a/src/sp.rs b/src/sp.rs new file mode 100644 index 0000000..5276719 --- /dev/null +++ b/src/sp.rs @@ -0,0 +1,12 @@ +use libc::uintptr_t; + +pub fn sp() -> uintptr_t { + #[link(name = "sp")] + extern "C" { + fn sp() -> uintptr_t; + } + + unsafe { + sp() + } +} diff --git a/src/stacks.rs b/src/stacks.rs deleted file mode 100644 index 32d981b..0000000 --- a/src/stacks.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::reusable::ReusableSync; - -use std::marker::PhantomData; -use std::ops::Deref; -use std::ops::DerefMut; -use timetravel::stable::StableAddr; -use timetravel::stable::StableMutAddr; - -pub fn alloc_stack() -> ReusableSync<'static, Box<[u8]>> { - use crate::compile_assert::assert_sync; - use crate::reusable::SyncPool; - use super::STACK_SIZE_BYTES; - - use gotcha::Group; - use std::convert::TryInto; - use std::sync::Once; - - static mut STACKS: Option>> = None; - static INIT: Once = Once::new(); - INIT.call_once(|| { - let stacks: fn() -> _ = || Some(vec![0; STACK_SIZE_BYTES].into_boxed_slice()); - let stacks = SyncPool::new(stacks); - stacks.prealloc(Group::limit()) - .expect("libinger: stack allocator lock was poisoned during init"); - unsafe { - STACKS.replace(stacks); - } - }); - - let stacks = unsafe { - STACKS.as_ref() - }.unwrap(); - assert_sync(&stacks); - stacks.try_into().expect("libinger: stack allocator lock is poisoned") -} - -pub struct DerefAdapter<'a, T> (T, PhantomData<&'a ()>); - -impl From for DerefAdapter<'_, T> { - fn from(t: T) -> Self { - Self (t, PhantomData::default()) - } -} - -impl<'a, T: Deref, U: Deref + 'a, V: ?Sized> Deref for DerefAdapter<'a, T> { - type Target = V; - - fn deref(&self) -> &Self::Target { - let Self (t, _) = self; - &***t - } -} - -impl<'a, T: DerefMut, U: DerefMut + 'a, V: ?Sized> DerefMut for DerefAdapter<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - let Self (t, _) = self; - &mut ***t - } -} - -unsafe impl<'a, T: Deref, U: StableAddr + 'a> StableAddr for DerefAdapter<'a, T> {} -unsafe impl<'a, T: DerefMut, U: StableMutAddr + 'a> StableMutAddr for DerefAdapter<'a, T> {} diff --git a/src/tcb.rs b/src/tcb.rs deleted file mode 100644 index 7b80e90..0000000 --- a/src/tcb.rs +++ /dev/null @@ -1,223 +0,0 @@ -#![allow(unused)] - -use gotcha::prctl::ARCH_GET_CPUID; -use gotcha::prctl::ARCH_GET_FS; -use gotcha::prctl::ARCH_GET_GS; -use gotcha::prctl::ARCH_SET_CPUID; -use gotcha::prctl::ARCH_SET_FS; -use gotcha::prctl::ARCH_SET_GS; -use gotcha::Group; -use std::io::Error; -use std::io::Result; -use std::os::raw::c_int; -use std::os::raw::c_ulong; - -#[must_use] -pub struct ThreadControlBlock (Option>); - -impl ThreadControlBlock { - pub fn current() -> Result { - unsafe { - arch_prctl_get(GetOp::Fs).map(|fs| Self (Some(MaybeMut::Ref(fs)))) - } - } - - pub fn new() -> Self { - extern { - fn _dl_allocate_tls(_: Option<&mut TCB>) -> Option<&mut TCB>; - } - - #[repr(C)] - struct TCB { - tls_ptr: usize, - _unused: usize, - self_ptr: usize, - } - - let fs = unsafe { - _dl_allocate_tls(None) - }.expect("libinger: could not allocate thread-control block"); - let auto: *mut _ = fs; - fs.tls_ptr = auto as _; - fs.self_ptr = auto as _; - - let auto: *mut _ = auto as _; - Self (Some(MaybeMut::Mut(unsafe { - &mut *auto - }))) - } - - pub unsafe fn install(mut self, group: Group) -> Result { - let parent = unguarded_parent(self.install_unguarded(group.into()))?; - Ok(ThreadControlBlockGuard { - this: self, - parent, - }) - } - - unsafe fn install_unguarded(&mut self, group: Option) -> Result> { - use crate::linger::abort; - - use gotcha::group_lookup_symbol_fn; - use std::slice; - extern { - fn __ctype_init(); - } - - const POINTER_GUARD: usize = 6; - const KERNEL_THREAD: usize = 90; - - let Self (fs) = self; - let fs = fs.as_mut().unwrap(); - let mut cur = None; - let mut custom = false; - if let MaybeMut::Mut(fs) = fs { - let fs = unsafe { - slice::from_raw_parts_mut(*fs, KERNEL_THREAD + 1) - }; - let cur = cur.get_or_insert(Self::current()?); - let Self (cur) = &cur; - let cur: &_ = cur.as_ref().unwrap().into(); - let cur = unsafe { - slice::from_raw_parts(cur, KERNEL_THREAD + 1) - }; - fs[POINTER_GUARD] = cur[POINTER_GUARD]; - fs[KERNEL_THREAD] = cur[KERNEL_THREAD]; - custom = true; - } - - let fs = (&*fs).into(); - arch_prctl_set(SetOp::Fs, fs)?; - if custom { - __ctype_init(); - if let Some(group) = group { - let __ctype_init: Option = group_lookup_symbol_fn!(group, __ctype_init); - if let Some(__ctype_init) = __ctype_init { - __ctype_init(); - } else { - abort("install(): could not get address of __ctype_init()"); - } - } - } - Ok(cur) - } - - fn take(&mut self) -> Option> { - let Self (this) = self; - this.take() - } -} - -impl Drop for ThreadControlBlock { - fn drop(&mut self) { - let Self (this) = self; - if let Some(MaybeMut::Mut(_)) = this.as_mut() { - if let Ok (parent) = unguarded_parent(unsafe { - self.install_unguarded(None) - }) { - drop(ThreadControlBlockGuard { - this: ThreadControlBlock (self.take()), - parent, - }); - } else { - eprintln!("libinger: could not install TCB to run TLS destructors"); - } - } - let Self (fs) = self; - } -} - -fn unguarded_parent(this: Result>) -> Result { - this?.ok_or(()).or_else(|_| ThreadControlBlock::current()) -} - -#[must_use] -pub struct ThreadControlBlockGuard { - this: ThreadControlBlock, - parent: ThreadControlBlock, -} - -impl ThreadControlBlockGuard { - pub unsafe fn uninstall(mut self) -> Result { - Ok(ThreadControlBlock (self.this.take())) - } -} - -impl Drop for ThreadControlBlockGuard { - fn drop(&mut self) { - extern { - fn __call_tls_dtors(); - fn _dl_deallocate_tls(_: &mut usize, _: bool); - } - - let mut dealloc = None; - if let Some(MaybeMut::Mut(fs)) = self.this.take() { - unsafe { - __call_tls_dtors(); - } - dealloc = Some(fs); - } - unsafe { - self.parent.install_unguarded(None).unwrap(); - } - if let Some(fs) = dealloc { - unsafe { - _dl_deallocate_tls(fs, true); - } - } - } -} - -enum MaybeMut<'a> { - Ref(&'a usize), - Mut(&'a mut usize), -} - -impl<'a> From<&'a MaybeMut<'a>> for &'a usize { - fn from(other: &'a MaybeMut) -> Self { - match other { - MaybeMut::Ref(other) => other, - MaybeMut::Mut(other) => other, - } - } -} - -enum GetOp { - Cpuid = ARCH_GET_CPUID as _, - Fs = ARCH_GET_FS as _, - Gs = ARCH_GET_GS as _, -} - -enum SetOp { - Cpuid = ARCH_SET_CPUID as _, - Fs = ARCH_SET_FS as _, - Gs = ARCH_SET_GS as _, -} - -unsafe fn arch_prctl_get<'a>(op: GetOp) -> Result<&'a usize> { - use std::mem::MaybeUninit; - extern { - fn arch_prctl(_: c_int, _: *mut c_ulong) -> c_int; - } - - let mut addr = MaybeUninit::uninit(); - if arch_prctl(op as _, addr.as_mut_ptr()) == 0 { - let addr: *const _ = addr.assume_init() as _; - Ok(&*addr) - } else { - Err(Error::last_os_error()) - } -} - -unsafe fn arch_prctl_set(op: SetOp, val: &usize) -> Result<()> { - extern { - fn libgotcha_arch_prctl(_: c_int, _: c_ulong) -> c_int; - } - - let val: *const _ = val; - if libgotcha_arch_prctl(op as _, val as _) == 0 { - Ok(()) - } else { - Err(Error::last_os_error()) - } -} diff --git a/src/tcbstub.rs b/src/tcbstub.rs deleted file mode 100644 index 498d486..0000000 --- a/src/tcbstub.rs +++ /dev/null @@ -1,25 +0,0 @@ -use gotcha::Group; -use std::io::Result; - -#[must_use] -pub struct ThreadControlBlock; - -impl ThreadControlBlock { - pub fn new() -> Self { Self } - pub unsafe fn install(self, _: Group) -> Result { Ok(ThreadControlBlockGuard) } -} - -impl Drop for ThreadControlBlock { - fn drop(&mut self) {} -} - -#[must_use] -pub struct ThreadControlBlockGuard; - -impl ThreadControlBlockGuard { - pub unsafe fn uninstall(self) -> Result { Ok(ThreadControlBlock) } -} - -impl Drop for ThreadControlBlockGuard { - fn drop(&mut self) {} -} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..ae5a73d --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,37 @@ +#![doc(test(attr(deny(warnings))))] + +//! ```compile_fail +//! use std::mem::uninitialized; +//! use ucontext::Context; +//! +//! fn assert_clone(_: T) {} +//! +//! let context: Context = unsafe { +//! uninitialized() +//! }; +//! assert_clone(context); +//! ``` + +//! ```compile_fail +//! use std::mem::uninitialized; +//! use ucontext::Context; + +//! fn assert_send(_: T) {} +//! +//! let context: Context = unsafe { +//! uninitialized() +//! }; +//! assert_send(context); +//! ``` + +//! ```compile_fail +//! use std::mem::uninitialized; +//! use ucontext::Context; +//! +//! fn assert_sync(_: T) {} +//! +//! let context: Context = unsafe { +//! uninitialized() +//! }; +//! assert_sync(context); +//! ``` diff --git a/src/timer.rs b/src/timer.rs deleted file mode 100644 index 62870d1..0000000 --- a/src/timer.rs +++ /dev/null @@ -1,121 +0,0 @@ -use libc::CLOCK_BOOTTIME; -use libc::CLOCK_BOOTTIME_ALARM; -use libc::CLOCK_MONOTONIC; -use libc::CLOCK_PROCESS_CPUTIME_ID; -use libc::CLOCK_REALTIME; -use libc::CLOCK_REALTIME_ALARM; -use libc::CLOCK_THREAD_CPUTIME_ID; -pub use libc::itimerspec; -use libc::sigevent; -use signal::Signal; -use std::io::Error; -use std::io::Result; -use std::mem::MaybeUninit; -use std::os::raw::c_int; - -#[allow(dead_code)] -pub enum Clock { - Boot = CLOCK_BOOTTIME as _, - BootAlarm = CLOCK_BOOTTIME_ALARM as _, - Mono = CLOCK_MONOTONIC as _, - Process = CLOCK_PROCESS_CPUTIME_ID as _, - Real = CLOCK_REALTIME as _, - RealAlarm = CLOCK_REALTIME_ALARM as _, - Thread = CLOCK_THREAD_CPUTIME_ID as _, -} - -#[repr(transparent)] -pub struct Sigevent (sigevent); - -impl Sigevent { - #[allow(dead_code)] - pub fn none() -> Self { - use libc::SIGEV_NONE; - - Self (Self::new(SIGEV_NONE)) - } - - #[allow(dead_code)] - pub fn signal(signal: Signal) -> Self { - use libc::SIGEV_SIGNAL; - - let mut this = Self::new(SIGEV_SIGNAL); - this.sigev_signo = signal as _; - Self (this) - } - - #[allow(dead_code)] - pub fn thread_id(signal: Signal, thread: c_int) -> Self { - use libc::SIGEV_THREAD_ID; - - let mut this = Self::new(SIGEV_THREAD_ID); - this.sigev_signo = signal as _; - this.sigev_notify_thread_id = thread; - Self (this) - } - - fn new(notify: c_int) -> sigevent { - let mut event: sigevent = unsafe { - uninitialized() - }; - event.sigev_notify = notify; - event - } -} - -#[derive(Clone, Copy)] -#[repr(transparent)] -pub struct Timer (usize); - -pub fn timer_create(clockid: Clock, sevp: &mut Sigevent) -> Result { - extern { - fn timer_create(_: c_int, _: *mut sigevent, _: *mut Timer) -> c_int; - } - - let mut timer = MaybeUninit::uninit(); - let Sigevent (sevp) = sevp; - if unsafe { - timer_create(clockid as _, sevp, timer.as_mut_ptr()) - } != 0 { - Err(Error::last_os_error())?; - } - - Ok(unsafe { - timer.assume_init() - }) -} - -pub fn timer_settime(timerid: Timer, absolute: bool, new: &itimerspec, old: Option<&mut itimerspec>) -> Result<()> { - use libc::TIMER_ABSTIME; - extern { - fn timer_settime(_: Timer, _: c_int, _: *const itimerspec, _: Option<&mut itimerspec>) -> c_int; - } - - let absolute = if absolute { TIMER_ABSTIME } else { 0 }; - if unsafe { - timer_settime(timerid, absolute, new, old) - } != 0 { - Err(Error::last_os_error())?; - } - - Ok(()) -} - -#[allow(dead_code)] -pub fn timer_delete(timerid: Timer) -> Result<()> { - extern { - fn timer_delete(_: Timer) -> c_int; - } - - if unsafe { - timer_delete(timerid) - } != 0 { - Err(Error::last_os_error())?; - } - - Ok(()) -} - -unsafe fn uninitialized() -> T { - MaybeUninit::uninit().assume_init() -} diff --git a/src/ucontext.rs b/src/ucontext.rs new file mode 100644 index 0000000..61aa655 --- /dev/null +++ b/src/ucontext.rs @@ -0,0 +1,289 @@ +use invar::MoveInvariant; +use libc::sigset_t; +use libc::ucontext_t; +use libc::uintptr_t; +use std::cell::Cell; +use std::cell::RefCell; +use std::io::Error; +use std::io::Result; +use std::rc::Rc; +use std::rc::Weak; +use uninit::Uninit; +use zero::Zero; + +const REG_CSGSFS: usize = 18; +const REG_RSP: usize = 15; + +thread_local! { + static GUARDS: RefCell>> = RefCell::new(Vec::new()); +} + +/// A continuation that may be resumed using `setcontext()`. +pub struct Context { + context: RefCell, + guard: Cell>>, +} + +impl Context { + /// NB: The returned object contains uninitialized data, and cannot be safely dropped until + /// it has either been initialized or zeroed! + fn new() -> Self { + Self { + context: RefCell::new(ucontext_t::uninit()), + guard: Cell::new(None), + } + + } + + /// Exchange the functional portion of this context with another one. When called on a + /// a particular context within a signal handler, this causes that context to be restored + /// upon return from the handler. Note that the handler's original context is stored back + /// unguarded, but that a subsequent `setcontext()`s is UB according to SUSv2. + pub fn swap(&mut self, other: &mut ucontext_t) { + use std::mem::swap; + + let mut this = self.context.borrow_mut(); + + this.after_move(); + swap(&mut this.uc_mcontext, &mut other.uc_mcontext); + swap(&mut this.uc_mcontext.gregs[REG_CSGSFS], &mut other.uc_mcontext.gregs[REG_CSGSFS]); + + let this_fp = unsafe { + this.uc_mcontext.fpregs.as_mut().unwrap() + }; + let other_fp = unsafe { + other.uc_mcontext.fpregs.as_mut().unwrap() + }; + swap(this_fp, other_fp); + swap(&mut this.uc_mcontext.fpregs, &mut other.uc_mcontext.fpregs); + + swap(&mut this.uc_flags, &mut other.uc_flags); + swap(&mut this.uc_link, &mut other.uc_link); + swap(&mut this.uc_stack, &mut other.uc_stack); + swap(&mut this.uc_sigmask, &mut other.uc_sigmask); + + self.guard.take(); + } +} + +#[inline(always)] +fn checkpoint(context: *mut ucontext_t) -> Result<()> { + use libc::getcontext; + use std::ptr::write; + + if unsafe { + getcontext(context) + } != 0 { + // Zero the uninitialized context before dropping it! + unsafe { + write(context, ucontext_t::zero()); + } + Err(Error::last_os_error())?; + } + + Ok(()) +} + +/// Calls `a()`, which may perform a `setcontext()` on its argument. If and only if it does so, +/// `b()` is executed before this function returns. +pub fn getcontext T, B: FnOnce() -> T>(a: A, b: B) -> Result { + use std::mem::forget; + use volatile::VolBool; + + let context = Context::new(); + + // Storing this flag on the stack is not unsound because guard enforces the invariant that + // this stack frame outlives any resumable context. Storing it on the stack is not leaky + // because client code that never resumes the context was already responsible for cleaning + // up this function's stack. + let mut unused = VolBool::new(true); + let idx = GUARDS.with(|guards| { + let mut guards = guards.borrow_mut(); + let idx = guards.len(); + let guard = Rc::new(idx); + let guardian = Rc::downgrade(&guard); + context.guard.set(Some(guardian)); + guards.push(guard); + idx + }); + checkpoint(context.context.as_ptr())?; + + let res; + if unused.load() { + unused.store(false); + res = a(context); + } else { + forget(context); + res = b(); + } + + GUARDS.with(move |guards| { + guards.borrow_mut().truncate(idx) + }); + Ok(res) +} + +fn validate_guard(context: &Context, stack: uintptr_t) -> Option<()> { + fn between(left: usize, between: usize, right: usize) -> bool { + use std::cmp::max; + use std::cmp::min; + use std::mem::size_of; + + let size = 4 * size_of::(); + min(left, right) - size <= between && between <= max(left, right) + size + } + + let guard = context.guard.take().take()?; + GUARDS.with(|guards| { + let guard = guard.upgrade()?; + guards.borrow_mut().truncate(*guard + 1); + Some(()) + })?; + + let theirs = context.context.borrow().uc_mcontext.gregs[REG_RSP]; + if ! between(stack, &guard as *const _ as _, theirs as _) { + context.guard.set(Some(guard)); + } + + Some(()) +} + +/// "Call gate" that invokes a `function()` on a separate `stack`, optionally resuming the program +/// at the `successor` context upon said function's return (or, by default, exiting). +pub fn makecontext(mut function: T, stack: &mut [u8], successor: Option<&Context>) -> Error { + use libc::makecontext; + use libc::setcontext; + use sp::sp; + use std::mem::transmute; + use std::os::raw::c_uint; + + enum Link<'a> { + WithoutSuccessor(&'a mut dyn FnMut()), + WithSuccessor(LinkedSuccessor<'a>), + } + + struct LinkedSuccessor<'a> { + function: &'a mut dyn FnMut(), + successor: &'a Context, + stack: uintptr_t, + } + + extern "C" fn link(lower: c_uint, upper: c_uint) { + let link: *mut Link = (lower as usize | ((upper as usize) << 32)) as _; + let link = unsafe { + link.as_mut() + }.unwrap(); + + match link { + Link::WithoutSuccessor(link) => link(), + Link::WithSuccessor(link) => { + (link.function)(); + validate_guard(&link.successor, link.stack); + }, + } + } + + let mut context = ucontext_t::uninit(); + if let Err(or) = checkpoint(&mut context) { + return or; + } + + let args; + context.uc_stack.ss_sp = stack.as_mut_ptr() as _; + context.uc_stack.ss_size = stack.len(); + if let Some(successor) = successor { + context.uc_link = successor.context.as_ptr(); + args = Link::WithSuccessor(LinkedSuccessor { + function: &mut function, + successor: successor, + stack: sp(), + }); + } else { + args = Link::WithoutSuccessor(&mut function); + } + + let args: usize = &args as *const _ as _; + unsafe { + makecontext(&mut context, transmute(link as extern "C" fn(c_uint, c_uint)), 2, args, args >> 32); + setcontext(&context); + } + + unreachable!() +} + +/// Attempts to resume `context`, never returning on success. Otherwise, returns `None` if +/// `context`'s stack frame has expired or `Some` to indicate a platform error. +pub fn setcontext(context: &Context) -> Option { + use libc::setcontext; + use sp::sp; + + validate_guard(context, sp())?; + + let mut ucontext = context.context.borrow_mut(); + ucontext.after_move(); + unsafe { + setcontext(&*ucontext); + } + Some(Error::last_os_error()) +} + +pub fn sigsetcontext(context: Context) -> Error { + use libc::SA_SIGINFO; + use libc::SIGVTALRM; + use libc::pthread_kill; + use libc::pthread_self; + use libc::sigaction; + use libc::siginfo_t; + use std::cell::Cell; + use std::os::raw::c_int; + use std::ptr::null_mut; + use std::sync::ONCE_INIT; + use std::sync::Once; + + static INIT: Once = ONCE_INIT; + + thread_local! { + static CONTEXT: Cell> = Cell::new(None); + } + + INIT.call_once(|| { + extern "C" fn handler(_: c_int, _: Option<&siginfo_t>, context: Option<&mut ucontext_t>) { + let context = context.unwrap(); + let mut protext = CONTEXT.with(|protext| protext.take()).unwrap(); + protext.swap(context); + } + + let config = sigaction { + sa_flags: SA_SIGINFO, + sa_sigaction: handler as _, + sa_restorer: None, + sa_mask: sigset_t::zero(), + }; + if unsafe { + sigaction(SIGVTALRM, &config, null_mut()) + } != 0 { + panic!(Error::last_os_error()); + } + }); + + CONTEXT.with(|protext| protext.set(Some(context))); + unsafe { + pthread_kill(pthread_self(), SIGVTALRM); + } + Error::last_os_error() +} + +unsafe impl Uninit for ucontext_t { + fn uninit() -> Self { + use std::mem::uninitialized; + + let mut this: Self = unsafe { + uninitialized() + }; + this.uc_mcontext.gregs = Zero::zero(); + this + } +} + +unsafe impl Zero for sigset_t {} +unsafe impl Zero for ucontext_t {} diff --git a/src/unfurl.rs b/src/unfurl.rs deleted file mode 100644 index 32b4ea3..0000000 --- a/src/unfurl.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub trait Unfurl { - unsafe fn unfurl(self) -> T; -} - -impl Unfurl for Option { - unsafe fn unfurl(self) -> T { - use std::hint::unreachable_unchecked; - - if let Some(t) = self { - t - } else { - unreachable_unchecked() - } - } -} diff --git a/src/uninit.rs b/src/uninit.rs new file mode 100644 index 0000000..10555c9 --- /dev/null +++ b/src/uninit.rs @@ -0,0 +1,9 @@ +pub unsafe trait Uninit: Sized { + fn uninit() -> Self { + use std::mem::uninitialized; + + unsafe { + uninitialized() + } + } +} diff --git a/src/volatile.rs b/src/volatile.rs new file mode 100644 index 0000000..4120094 --- /dev/null +++ b/src/volatile.rs @@ -0,0 +1,25 @@ +pub struct VolBool (bool); + +impl VolBool { + pub fn new(val: bool) -> Self { + VolBool (val) + } + + pub fn load(&self) -> bool { + use std::ptr::read_volatile; + + let VolBool (val) = self; + unsafe { + read_volatile(val) + } + } + + pub fn store(&mut self, val: bool) { + use std::ptr::write_volatile; + + let VolBool (ue) = self; + unsafe { + write_volatile(ue, val); + } + } +} diff --git a/src/zero.rs b/src/zero.rs new file mode 100644 index 0000000..3461cc4 --- /dev/null +++ b/src/zero.rs @@ -0,0 +1,9 @@ +pub unsafe trait Zero: Sized { + fn zero() -> Self { + use std::mem::zeroed; + + unsafe { + zeroed() + } + } +} diff --git a/tests/inger/lock.rs b/tests/inger/lock.rs deleted file mode 100644 index b90c10a..0000000 --- a/tests/inger/lock.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::sync::MutexGuard; - -pub fn exclusive(fun: fn() -> T) { - let lock = lock(); - fun(); - drop(lock); -} - -fn lock() -> MutexGuard<'static, ()> { - use std::sync::Mutex; - use std::sync::Once; - - static INIT: Once = Once::new(); - static mut LOCK: Option> = None; - - INIT.call_once(|| unsafe { - LOCK.replace(Mutex::new(())); - }); - - // The lock might be poisened because a previous test failed. This is safe to ignore - // because we should no longer have a race (since the other test's thread is now - // dead) and we don't need to fail the current test as well. - unsafe { - LOCK.as_ref() - }.unwrap().lock().unwrap_or_else(|poison| poison.into_inner()) -} diff --git a/tests/inger/main.rs b/tests/inger/main.rs deleted file mode 100644 index 614ab3b..0000000 --- a/tests/inger/main.rs +++ /dev/null @@ -1,197 +0,0 @@ -#![cfg_attr(bench, feature(test))] -#[cfg(bench)] -extern crate test; - -mod lock; - -use lock::exclusive; - -use inger::launch; -use inger::nsnow; -use inger::resume; -#[cfg(bench)] -use test::Bencher; - -#[test] -fn launch_completion() { - exclusive(|| - assert!(launch(|| (), 1_000).unwrap().is_completion()) - ); -} - -#[test] -fn launch_continuation() { - exclusive(|| - assert!(launch(|| timeout(1_000_000), 10).unwrap().is_continuation()) - ); -} - -#[test] -fn launch_union() { - exclusive(|| - launch(|| -> Result> { Ok(false) }, 1_000).unwrap() - ); -} - -#[should_panic(expected = "PASS")] -#[test] -fn launch_panic() { - exclusive(|| - drop(launch(|| panic!("PASS"), 1_000)) - // Lock becomes poisoned. - ); -} - -#[ignore] -#[should_panic(expected = "PASS")] -#[test] -fn launch_panic_outer() { - exclusive(|| - drop(launch(|| { - drop(launch(|| (), 1_000)); - panic!("PASS"); - }, 1_000)) - // Lock becomes poisoned. - ); -} - -#[ignore] -#[should_panic(expected = "PASS")] -#[test] -fn launch_panic_inner() { - exclusive(|| - drop(launch(|| drop(launch(|| panic!("PASS"), 1_000)), 1_000)) - // Lock becomes poisoned. - ); -} - -#[ignore] -#[test] -fn launch_completions() { - exclusive(|| - assert!(launch(|| assert!(launch(|| (), 1_000).unwrap().is_completion()), 1_000).unwrap().is_completion()) - ); -} - -#[ignore] -#[test] -fn launch_continuations() { - exclusive(|| { - assert!(launch(|| { - assert!(launch(|| timeout(1_000_000), 10).unwrap().is_continuation()); - timeout(1_000_000); - }, 1_000).unwrap().is_continuation()); - }); -} - -#[ignore] -#[test] -fn resume_completion() { - exclusive(|| { - let mut cont = launch(|| timeout(1_000_000), 10).unwrap(); - assert!(cont.is_continuation(), "completion instead of continuation"); - assert!(resume(&mut cont, 10_000_000).unwrap().is_completion()); - }); -} - -#[ignore] -#[test] -fn resume_completion_drop() { - exclusive(|| { - let mut cont = launch(|| timeout(1_000_000), 100).unwrap(); - assert!(cont.is_continuation(), "completion instead of continuation"); - assert!(resume(&mut cont, 10_000).unwrap().is_continuation()); - }); -} - -#[ignore] -#[test] -fn resume_completion_repeat() { - exclusive(|| { - let mut cont = launch(|| timeout(1_000_000), 10).unwrap(); - assert!(cont.is_continuation(), "launch(): returned completion instead of continuation"); - resume(&mut cont, 10).unwrap(); - assert!(cont.is_continuation(), "resume(): returned completion instead of continuation"); - assert!(resume(&mut cont, 10_000_000).unwrap().is_completion()); - }); -} - -#[test] -fn setup_only() { - use std::sync::atomic::AtomicBool; - use std::sync::atomic::Ordering; - - exclusive(|| { - let run = AtomicBool::new(false); - let mut prep = launch(|| run.store(true, Ordering::Relaxed), 0).unwrap(); - assert!(! run.load(Ordering::Relaxed)); - resume(&mut prep, 1_000).unwrap(); - assert!(run.load(Ordering::Relaxed)); - }); -} - -#[should_panic(expected = "launch(): too many active timed functions: None")] -#[test] -fn launch_toomany() { - exclusive(|| { - use std::collections::LinkedList; - - let mut orphans = LinkedList::default(); - loop { - orphans.push_back(launch(|| timeout(1_000_000), 0).unwrap()); - // Lock becomes poisoned. - } - }); -} - -#[test] -fn launch_toomany_reinit() { - exclusive(|| { - let thing_one = launch(|| timeout(1_000_000), 0).unwrap(); - let _thing_two = launch(|| timeout(1_000_000), 0).unwrap(); - drop(thing_one); - let _thing_three = launch(|| timeout(1_000_000), 0).unwrap(); - }); -} - -#[ignore] -#[test] -fn abuse_preemption() { - for _ in 0..25 { - launch_continuation(); - } -} - -fn timeout(mut useconds: u64) { - useconds *= 1_000; - - let mut elapsed = 0; - let mut last = nsnow(); - while elapsed < useconds { - let mut this = nsnow(); - while this < last || this - last > 1_000 { - last = this; - this = nsnow(); - } - elapsed += this - last; - last = this; - } -} - -#[bench] -#[cfg(bench)] -fn timeout_10(lo: &mut Bencher) { - lo.iter(|| timeout(10)); -} - -#[bench] -#[cfg(bench)] -fn timeout_100(lo: &mut Bencher) { - lo.iter(|| timeout(100)); -} - -#[bench] -#[cfg(bench)] -fn timeout_1000(lo: &mut Bencher) { - lo.iter(|| timeout(1_000)); -} diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..b659bff --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,314 @@ +#![cfg_attr(not(test), allow(dead_code))] + +extern crate libc; +extern crate ucontext; + +use libc::ucontext_t; +use std::cell::RefCell; +use ucontext::Context; +use ucontext::getcontext; +use ucontext::makecontext; +use ucontext::setcontext; +use ucontext::sigsetcontext; + +fn main() { + getcontext_donothing(); + getcontext_setcontext(); + getcontext_succeedatnothing(); + getcontext_nested(); + makecontext_setcontext(); + context_moveinvariant(); + context_swapinvariant(); + killswap_getcontext(); + killswap_makecontext(); + killswap_sigsetcontext(); +} + +#[cfg_attr(test, should_panic(expected = "done"))] +#[cfg_attr(test, test)] +fn getcontext_donothing() { + let mut reached = false; + getcontext(|_| reached = true, || unreachable!()).unwrap(); + assert!(reached); + if cfg!(test) { + panic!("done"); + } +} + +#[cfg_attr(test, should_panic(expected = "done"))] +#[cfg_attr(test, test)] +fn getcontext_setcontext() { + let mut reached = false; + getcontext( + |context| { + setcontext(&context); + unreachable!(); + }, + || reached = true, + ).unwrap(); + assert!(reached); + if cfg!(test) { + panic!("done"); + } +} + +#[cfg_attr(test, should_panic(expected = "done"))] +#[cfg_attr(test, test)] +fn getcontext_succeedatnothing() { + let invalid = getcontext(|context| context, || unreachable!()).unwrap(); + assert!(setcontext(&invalid).is_none()); + if cfg!(test) { + panic!("done"); + } +} + +#[cfg_attr(test, should_panic(expected = "done"))] +#[cfg_attr(test, test)] +fn getcontext_nested() { + use std::cell::Cell; + + let mut reached = true; + let context = Cell::new(None); + getcontext( + |outer| getcontext( + |inner| { + context.set(Some(inner)); + setcontext(&outer); + unreachable!(); + }, + || unreachable!(), + ).unwrap(), + || { + assert!(setcontext(&context.take().unwrap()).is_none()); + reached = true; + }, + ).unwrap(); + assert!(reached); + if cfg!(test) { + panic!("done"); + } +} + +#[cfg_attr(test, should_panic(expected = "done"))] +#[cfg_attr(test, test)] +fn makecontext_setcontext() { + use std::cell::Cell; + use ucontext::MINSIGSTKSZ; + + thread_local! { + static REACHED: Cell = Cell::new(false); + } + + fn call() { + REACHED.with(|reached| reached.set(true)); + } + + let mut reached = false; + getcontext( + |mut successor| { + let mut stack = [0u8; MINSIGSTKSZ]; + makecontext(call, &mut stack, Some(&mut successor)); + unreachable!(); + }, + || reached = true, + ).unwrap(); + assert!(REACHED.with(|reached| reached.get())); + assert!(reached); + if cfg!(test) { + panic!("done"); + } +} + +fn ucontext(context: &mut Context) -> &mut ucontext_t { + use std::mem::transmute; + + let context: &mut RefCell<_> = unsafe { + transmute(context) + }; + context.get_mut() +} + +fn uc_inbounds(within: *const ucontext_t, context: *const ucontext_t) -> bool { + within > context && within < unsafe { + context.add(1) + } +} + +#[cfg_attr(test, test)] +fn context_moveinvariant() { + use ucontext::MoveInvariant; + + let mut context = getcontext(|context| context, || unreachable!()).unwrap(); + let context = ucontext(&mut context); + context.after_move(); + assert!(uc_inbounds(context.uc_mcontext.fpregs as _, context)); +} + +#[cfg_attr(test, test)] +fn context_swapinvariant() { + use ucontext::MoveInvariant; + + let mut first = getcontext(|context| context, || unreachable!()).unwrap(); + let mut second = getcontext(|context| context, || unreachable!()).unwrap(); + + let second = ucontext(&mut second); + { + let first = ucontext(&mut first); + first.after_move(); + second.after_move(); + first.uc_link = first.uc_mcontext.fpregs as _; + second.uc_link = second.uc_mcontext.fpregs as _; + assert!(uc_inbounds(first.uc_link, first)); + assert!(uc_inbounds(second.uc_link, second)); + } + + first.swap(second); + let first = ucontext(&mut first); + assert!(uc_inbounds(first.uc_link, second)); + assert!(uc_inbounds(second.uc_link, first)); + assert!(uc_inbounds(first.uc_mcontext.fpregs as _, first)); + assert!(uc_inbounds(second.uc_mcontext.fpregs as _, second)); +} + +thread_local! { + static CONTEXT: RefCell> = RefCell::new(None); +} + +fn killswap() -> fn(Context) { + use libc::SA_SIGINFO; + use libc::SIGUSR1; + use libc::pthread_kill; + use libc::pthread_self; + use libc::sigaction; + use libc::siginfo_t; + use std::io::Error; + use std::mem::zeroed; + use std::os::raw::c_int; + use std::ptr::null_mut; + + extern "C" fn handler( + _: c_int, + _: Option<&mut siginfo_t>, + context: Option<&mut ucontext_t>, + ) { + let context = context.unwrap(); + CONTEXT.with(|global| global.borrow_mut().as_mut().unwrap().swap(context)); + } + + let config = sigaction { + sa_flags: SA_SIGINFO, + sa_sigaction: handler as _, + sa_restorer: None, + sa_mask: unsafe { + zeroed() + }, + }; + if unsafe { + sigaction(SIGUSR1, &config, null_mut()) + } != 0 { + panic!(Error::last_os_error()); + } + + fn fun(context: Context) { + CONTEXT.with(|global| global.replace(Some(context))); + unsafe { + pthread_kill(pthread_self(), SIGUSR1); + } + } + + fun +} + +#[cfg_attr(test, should_panic(expected = "done"))] +#[cfg_attr(test, test)] +fn killswap_getcontext() { + let mut reached = false; + getcontext(killswap(), || reached = true).unwrap(); + assert!(reached); + if cfg!(test) { + panic!("done"); + } +} + +fn stack_inbounds(within: &ucontext_t, stack: &[u8]) -> bool { + const REG_RSP: usize = 15; + + let within: *const _ = within.uc_mcontext.gregs[REG_RSP] as _; + within > stack.as_ptr() && within < unsafe { + stack.as_ptr().add(stack.len()) + } +} + +#[cfg_attr(test, should_panic(expected = "done"))] +#[cfg_attr(test, test)] +fn killswap_makecontext() { + use libc::MINSIGSTKSZ; + use std::cell::Cell; + + thread_local! { + static SUCCESSOR: Cell> = Cell::new(None); + } + + fn call() { + killswap()(SUCCESSOR.with(|successor| successor.take()).unwrap()); + } + + let mut reached = false; + let mut stack = [0u8; MINSIGSTKSZ]; + getcontext( + |mut context| { + assert!(! stack_inbounds(ucontext(&mut context), &stack)); + SUCCESSOR.with(|successor| successor.set(Some(context))); + makecontext(call, &mut stack, None); + unreachable!(); + }, + || reached = true, + ).unwrap(); + assert!(reached); + + let mut context = getcontext(|context| context, || unreachable!()).unwrap(); + assert!(! stack_inbounds(ucontext(&mut context), &stack)); + if cfg!(test) { + panic!("done"); + } +} + +#[cfg_attr(test, should_panic(expected = "done"))] +#[cfg_attr(test, test)] +fn killswap_sigsetcontext() { + use libc::MINSIGSTKSZ; + use std::cell::Cell; + + thread_local! { + static CHECKPOINT: Cell> = Cell::new(None); + } + + fn call() { + let context = CHECKPOINT.with(|checkpoint| checkpoint.take()).unwrap(); + killswap()(context); + } + + let mut reached = false; + getcontext( + |mut call_complete| { + let mut stack = [0u8; MINSIGSTKSZ]; + getcontext( + |call_pause| { + CHECKPOINT.with(|checkpoint| checkpoint.set(Some(call_pause))); + makecontext(call, &mut stack, Some(&mut call_complete)); + unreachable!(); + }, + || { + let call_resume = CONTEXT.with(|context| context.borrow_mut().take()).unwrap(); + sigsetcontext(call_resume); + unreachable!(); + }, + ).unwrap(); + }, + || reached = true, + ).unwrap(); + assert!(reached); + if cfg!(test) { + panic!("done"); + } +} diff --git a/testsuite/.gitignore b/testsuite/.gitignore deleted file mode 100644 index da31524..0000000 --- a/testsuite/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -/** - -!/.gitignore -!/build -!/custom-glibc.patch -!/test -!/testinger.c diff --git a/testsuite/build b/testsuite/build deleted file mode 100755 index cf760c0..0000000 --- a/testsuite/build +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/sh - -readonly LIBINGER=".." - -if [ "$#" -eq "0" -o \( "$1" != "release" -a "$1" != "debug" -a -z "`echo "$1" | grep ^-`" \) ] -then - echo "USAGE: $0 ...\"> [cc flag]..." - exit 1 -fi -cargoflags="$1" -shift -buildtype="debug" -cflags="-g3 -Og" -case "$cargoflags" in --*-release*) - buildtype="release" - ;; -release) - buildtype="release" - cargoflags="--$cargoflags" - ;; -debug) - cargoflags="" - ;; -esac -if [ "$buildtype" = "release" ] -then - cflags="-O2" -fi - -cd "`dirname "$0"`" -cd "$LIBINGER" - -set -ve -./configure || true -mkdir -p "target/$buildtype" -cargo build $cargoflags -cargo run $cargoflags >"target/$buildtype/libinger.h" -cp external/libgotcha/libgotcha_api.h "target/$buildtype/libgotcha.h" -cp external/libgotcha/libgotcha_repl.h "target/$buildtype" -objcopy -Wsignal --globalize-symbol libgotcha_dlsym --globalize-symbol libgotcha_signal "target/$buildtype/deps/libgotcha-"*.rlib 2>/dev/null -rm "target/$buildtype/deps/libinger.so" -cd - -c99 $cflags -Wall -Wextra -Wpedantic -Werror "$@" -c -fpic -fno-optimize-sibling-calls -D_GNU_SOURCE -Wno-missing-attributes -I"$OLDPWD/target/$buildtype" testinger.c -cd - -cargo rustc $cargoflags --lib -- -Clink-arg="$OLDPWD/testinger.o" -cd - -mv "$OLDPWD/target/$buildtype/libinger.so" libtestinger.so -rm "$OLDPWD/target/$buildtype/deps/libinger.so" -rm testinger.o -cd - -./configure >/dev/null || true diff --git a/testsuite/custom-glibc.patch b/testsuite/custom-glibc.patch deleted file mode 100644 index c98f810..0000000 --- a/testsuite/custom-glibc.patch +++ /dev/null @@ -1,98 +0,0 @@ -diff --git i/testsuite/build w/testsuite/build -index cf760c0..fe575b8 100755 ---- i/testsuite/build -+++ w/testsuite/build -@@ -11,6 +11,7 @@ cargoflags="$1" - shift - buildtype="debug" - cflags="-g3 -Og" -+rustflags="" - case "$cargoflags" in - -*-release*) - buildtype="release" -@@ -27,6 +28,10 @@ if [ "$buildtype" = "release" ] - then - cflags="-O2" - fi -+if [ -e lib ] -+then -+ rustflags="-Clink-arg=-L$PWD/lib" -+fi - - cd "`dirname "$0"`" - cd "$LIBINGER" -@@ -34,8 +39,11 @@ cd "$LIBINGER" - set -ve - ./configure || true - mkdir -p "target/$buildtype" --cargo build $cargoflags - cargo run $cargoflags >"target/$buildtype/libinger.h" -+ -+export RUSTFLAGS="-Cprefer-dynamic=no" -+cargo build $cargoflags --lib -+cargo build $cargoflags --lib - cp external/libgotcha/libgotcha_api.h "target/$buildtype/libgotcha.h" - cp external/libgotcha/libgotcha_repl.h "target/$buildtype" - objcopy -Wsignal --globalize-symbol libgotcha_dlsym --globalize-symbol libgotcha_signal "target/$buildtype/deps/libgotcha-"*.rlib 2>/dev/null -@@ -43,7 +51,7 @@ rm "target/$buildtype/deps/libinger.so" - cd - - c99 $cflags -Wall -Wextra -Wpedantic -Werror "$@" -c -fpic -fno-optimize-sibling-calls -D_GNU_SOURCE -Wno-missing-attributes -I"$OLDPWD/target/$buildtype" testinger.c - cd - --cargo rustc $cargoflags --lib -- -Clink-arg="$OLDPWD/testinger.o" -+eval cargo rustc "$cargoflags" --lib -- -Clink-arg="$OLDPWD/testinger.o" "$rustflags" "`sed -n -e's/",/"/g' -e's/^rustflags = \[\(.\+\)\]$/\1/p' .cargo/config`" - cd - - mv "$OLDPWD/target/$buildtype/libinger.so" libtestinger.so - rm "$OLDPWD/target/$buildtype/deps/libinger.so" -diff --git i/testsuite/test w/testsuite/test -index cfef791..0c83013 100755 ---- i/testsuite/test -+++ w/testsuite/test -@@ -1,5 +1,7 @@ - #!/bin/sh - -+readonly VERSION="2.29" -+ - GNULIB="$*" - if [ -z "$GNULIB" ] - then -@@ -7,9 +9,38 @@ then - fi - - set -ve --[ ! -e libtestinger.so ] && ./build release - [ ! -e gnulib/configure ] && "$GNULIB/gnulib-tool" --create-testdir --dir gnulib --single-configure `"$GNULIB/posix-modules"` --[ ! -e Makefile ] && gnulib/configure CFLAGS="-fpic -g3" -+if [ ! -e Makefile ] -+then -+ cflags="" -+ ldflags="" -+ version="`ldd --version | head -n1 | rev | cut -d" " -f1 | rev`" -+ if [ "$version" != "$VERSION" ] -+ then -+ echo >&2 -+ echo "!!! It looks like your system uses glibc $version." >&2 -+ echo "!!! We recommend running this suite on version $VERSION!" >&2 -+ echo >&2 -+ printf %s "Path to an alternative ld-linux.so (enter to use system's)? " -+ read interp -+ if [ -n "$interp" ] -+ then -+ interp="`realpath "$interp"`" -+ ldflags="-Wl,-I$interp" -+ -+ lib="`dirname "$interp"`" -+ ldflags="-L$lib $ldflags" -+ ln -s "$lib" . -+ rm -f libtestinger.so -+ -+ cflags="-I." -+ mkdir sys -+ echo "#error" >sys/single_threaded.h -+ fi -+ fi -+ gnulib/configure CFLAGS="-fpic -g3 $cflags" LDFLAGS="$ldflags" -+fi -+[ ! -e libtestinger.so ] && ./build release - make -j"`getconf _NPROCESSORS_ONLN`" - [ ! -e gltests/test-suite.log ] && make check || true - make check LD_PRELOAD="$PWD/libtestinger.so" LIBGOTCHA_NUMGROUPS="1" LIBGOTCHA_SKIP="`cat <<-tac diff --git a/testsuite/test b/testsuite/test deleted file mode 100755 index cfef791..0000000 --- a/testsuite/test +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/sh - -GNULIB="$*" -if [ -z "$GNULIB" ] -then - GNULIB="/usr/share/gnulib" -fi - -set -ve -[ ! -e libtestinger.so ] && ./build release -[ ! -e gnulib/configure ] && "$GNULIB/gnulib-tool" --create-testdir --dir gnulib --single-configure `"$GNULIB/posix-modules"` -[ ! -e Makefile ] && gnulib/configure CFLAGS="-fpic -g3" -make -j"`getconf _NPROCESSORS_ONLN`" -[ ! -e gltests/test-suite.log ] && make check || true -make check LD_PRELOAD="$PWD/libtestinger.so" LIBGOTCHA_NUMGROUPS="1" LIBGOTCHA_SKIP="`cat <<-tac - /bin/bash - /usr/bin/cat - /usr/bin/chmod - /usr/bin/cmp - /usr/bin/diff - /usr/bin/env - /usr/bin/expr - /usr/bin/gawk - /usr/bin/grep - /usr/bin/head - /usr/bin/make - /usr/bin/mkdir - /usr/bin/mv - /usr/bin/sed - /usr/bin/sh - /usr/bin/sleep - /usr/bin/rm - /usr/bin/tr - /usr/bin/wc -tac`" diff --git a/testsuite/testinger.c b/testsuite/testinger.c deleted file mode 100644 index 289a1bd..0000000 --- a/testsuite/testinger.c +++ /dev/null @@ -1,173 +0,0 @@ -#include "libgotcha.h" -#include "libgotcha_repl.h" -#include "libinger.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct inout { - int argc; - char **argv; - char **envp; - int retval; -}; - -static int (*mainfunc)(int, char **, char **); - -static void testinging(void *inout) { - struct inout *argret = inout; - argret->retval = mainfunc(argret->argc, argret->argv, argret->envp); -} - -static int testinger(int argc, char **argv, char **envp) { - struct inout argret = { - .argc = argc, - .argv = argv, - .envp = envp, - }; - launch(testinging, UINT64_MAX, &argret); - return argret.retval; -} - -#pragma weak libtestinger_libc_start_main = __libc_start_main -int __libc_start_main(int (*main)(int, char **, char **), int argc, char **argv, int (*init)(int, char **, char **), void (*fini)(void), void (*rtld_fini)(void), void *stack_end) { - const char *skiplist = getenv("LIBGOTCHA_SKIP"); - if(skiplist && strstr(skiplist, *argv)) - return __libc_start_main(main, argc, argv, init, fini, rtld_fini, stack_end); - - struct link_map *lm = dlmopen(1, *argv, RTLD_LAZY); - dlclose(lm); - - const struct link_map *l = dlopen(NULL, RTLD_LAZY); - uintptr_t offset = (uintptr_t) main - l->l_addr; - mainfunc = (int (*)(int, char **, char **)) (lm->l_addr + offset); - - return __libc_start_main(testinger, argc, argv, init, fini, rtld_fini, stack_end); -} - -#pragma weak libtestinger_signal = signal -void (*signal(int signum, void (*handler)(int)))(int) { - if(handler == SIG_DFL) - return handler; - - return libgotcha_signal(signum, handler); -} - -static bool intrsleep; - -#pragma weak libtestinger_alarm = alarm -unsigned int alarm(unsigned int seconds) { - intrsleep = true; - return alarm(seconds); -} - -#pragma weak libtestinger_nanosleep = nanosleep -int nanosleep(const struct timespec *req, struct timespec *rem) { - // This should really be preemptible, but that causes the gnulib testsuite to fail some - // fragile timing assertions. - //libgotcha_group_thread_set(libgotcha_group_caller()); - - struct timespec ours; - if(!rem) - rem = &ours; - - int stat = nanosleep(req, rem); - if(intrsleep) { - intrsleep = false; - return stat; - } - while(stat && errno == EINTR) { - struct timespec next; - memcpy(&next, rem, sizeof next); - stat = nanosleep(&next, rem); - } - return stat; -} - -int libtestinger_nanosleep(const struct timespec *, struct timespec *); - -#pragma weak libtestinger_usleep = usleep -int usleep(useconds_t usec) { - struct timespec nsec = { - .tv_sec = usec / 1000000, - .tv_nsec = (usec % 1000000) * 1000, - }; - return libtestinger_nanosleep(&nsec, NULL); -} - -#pragma weak libtestinger_sleep = sleep -unsigned int sleep(unsigned int seconds) { - struct timespec nsec = { - .tv_sec = seconds, - }; - return libtestinger_nanosleep(&nsec, NULL); -} - -#pragma weak libtestinger_write = write -ssize_t write(int fd, const void *buf, size_t count) { - // This should really be preemptible, but that causes the gnulib testsuite to fail some - // fragile timing assertions. - //libgotcha_group_thread_set(libgotcha_group_caller()); - - int flags = fcntl(fd, F_GETFL); - if(flags & O_NONBLOCK) - return write(fd, buf, count); - - size_t written = 0; - while(written != count) { - ssize_t update = write(fd, (void *) ((uintptr_t) buf + written), count - written); - if(update < 0) - return update; - written += update; - } - return written; -} - -#pragma weak libtestinger_select = select -int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) { - libgotcha_group_thread_set(libgotcha_group_caller()); - - int res = 0; - while((res = select(nfds, readfds, writefds, exceptfds, timeout)) < 0 && errno == EINTR); - return res; -} - -void _dl_signal_error(int, const char *, const char *, const char *); -void libtestinger_dl_signal_exception(int error, const char *const *module, const char *message); - -extern const char *__progname; - -// It seems that glibc has a bug: __libc_dlsym() calls from ancillary namespaces abort the process -// if they cannot find the target symbol, even if they would ordinarily only return an error code! -// This happens because _dl_signal_cexception() calls are always redirected back to the base -// namespace, so we work around it by proxying them and redirecting to the correct namespace. -#pragma weak libtestinger_dl_signal_exception = _dl_signal_exception -void _dl_signal_exception(int error, const char *const *module, const char *message) { - // If we're still bootstrapping dlsym(), just jump back to libc however we can get there! - if(dlsym == libgotcha_dlsym) { - if(_dl_signal_exception == libtestinger_dl_signal_exception) - _dl_signal_error(error, module[0], message, module[1]); - else - _dl_signal_exception(error, module, message); - } - - // Otherwise, jump to the copy of libc in the namespace we came from. - libgotcha_group_t group = libgotcha_group_caller(); - void (*_dl_signal_exception)(int, const char *const *, const char *) = - (void (*)(int, const char *const *, const char *)) (uintptr_t) - libgotcha_group_symbol_from(group, "_dl_signal_exception", "libc.so.6"); - libgotcha_group_thread_set(group); - assert(_dl_signal_exception); - assert(_dl_signal_exception != libtestinger_dl_signal_exception); - if(mainfunc) - fprintf(stderr, "./%s: symbol lookup warning: %s: %s (code %d)\n", __progname, *module, message, error); - _dl_signal_exception(error, module, message); -}