diff --git a/.github/workflows/clock.yml b/.github/workflows/clock.yml new file mode 100644 index 000000000..dc519a766 --- /dev/null +++ b/.github/workflows/clock.yml @@ -0,0 +1,60 @@ +name: Clock gates + +# agent-env's libfaketime (src/agent_env/computers/clock), the library every computer's session programs load to read +# universe time, built with stock 0.9.12 beside it in an Ubuntu 24.04 image and run through tst/clock's gates on x86_64 +# and arm64: every wait API in C, Python, Java and Node at several rates, jumps, cancelled waits, timerfd duplicates, +# condvar and semaphore stress, the waits again with futex_waitv refused, a stalled writer and a dead one, torn page +# reads, the marker, the environment race and Chromium launches. It runs on pull requests that touch the library, its +# tier or Chrome's pin, weekly, and on demand; it isn't a required check. + +on: + pull_request: + paths: + - "src/agent_env/computers/clock/**" + - "src/agent_env/browsers/envs/chromium/chromium.py" + - "tst/clock/**" + - ".github/workflows/clock.yml" + schedule: + - cron: "41 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: clock-${{ github.event_name == 'pull_request' && github.ref || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + PIP_DISABLE_PIP_VERSION_CHECK: "1" + AGENTENV_TEST_CLOCK_CACHE: ${{ github.workspace }}/.clock-downloads + +jobs: + gates: + name: gates (${{ matrix.arch }}) + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + arch: [amd64, arm64] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + - name: Install from uv.lock + run: | + python -m pip install --quiet -r .github/ci-requirements.txt + uv sync --locked --extra dev --python "$(command -v python)" + # Chrome's archive and upstream's tarball, checked by sha256 when the tier uses them; a new pin is a new key. + - name: Downloads + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ env.AGENTENV_TEST_CLOCK_CACHE }} + key: clock-downloads-${{ matrix.arch }}-${{ hashFiles('src/agent_env/browsers/envs/chromium/chromium.py', 'src/agent_env/computers/clock/build.sh') }} + - name: Clock gates + run: .venv/bin/python -m pytest tst/clock -rA --timeout=2400 --junitxml=junit-clock-${{ matrix.arch }}.xml + - name: No skips + if: always() + run: .venv/bin/python .github/scripts/check_skip_policy.py junit-clock-${{ matrix.arch }}.xml diff --git a/AGENTS.md b/AGENTS.md index 8d99e82cf..ac164026a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,7 @@ or one of them only where it is used. | `make int-test-fast` | `tst/integration` minus `int_test_slow`, in parallel; needs Docker and a local OCI registry (`docker run -d -p 5000:5000 public.ecr.aws/docker/library/registry:2`) | | `make int-test-slow` | the `int_test_slow` tests, serially (they build the same Docker tags from module fixtures and race under xdist) | | `make installer-test` | `tst/installer`: `plugin add` / `remove` through the real pip, uv and pipx, offline against wheels built from the checkout, and the container user journey (`python:3.12-slim`, `--network none`) with the PEP 668 refusal; needs uv, pipx and Docker | +| `make clock-test` | `tst/clock`: agent-env's libfaketime (`src/agent_env/computers/clock`) and stock 0.9.12 built in an Ubuntu 24.04 image for this machine's Docker, and its gates run there: every wait API in C, Python, Java and Node at several rates, jumps, cancelled waits, timerfd duplicates, condvar and semaphore stress, the waits again with `futex_waitv` refused (as before Linux 5.16 and under gVisor), a stalled writer, a dead one, torn reads, the marker, the environment race, and Chromium launched 40 times with it and 10 with stock, side by side; needs Docker, and fetches Chrome's archive and upstream's tarball once into `AGENTENV_TEST_CLOCK_CACHE` | | `make trl-test` | `tst/trl`: one `GRPOTrainer` step on CPU through `agent_env.training.trl`, with a tiny model from the Hub; needs trl and CPU torch, which the `dev` extra leaves out | | `make nemo-gym-test` | `tst/nemo_gym`: rollouts through `agent_env.training.nemo_gym`'s resources server in-process; needs the `nemo-gym` extra, on Python 3.13.14 or later, which the `dev` extra leaves out (`uv sync --extra dev --extra nemo-gym`) | | `make verl-test` | `tst/verl`: rows played through `agent_env.training.verl`'s agent loop on verl itself, with Qwen3's tokenizer from the Hub and a stand-in rollout server; needs verl and CPU torch, which the `dev` extra leaves out | @@ -137,7 +138,7 @@ and deserialized through a registry. | `explorer/` | Optional local web UI: `agent-env up`, binds loopback `:8234`. | | `cli/` | Click CLI with the groups `a2a-agent`, `artifact`, `bundle`, `config`, `env`, `eval`, `plugin`, `run`, `task`, `up`. `task run`, `task run-batch` and `eval run` hand their runs to the configured `[runner]` and follow them. | | `agents/` | The `agents` bundle: the agents agent-env ships, one folder each under `agents/agents/`, with a smoke task each under `agents/tasks/`. Its Claude Code, `@local/agentenv-framework/agents/claude-code`, is the default agent. An agent's Python packages are its folder's `requirements.txt`, installed in its image, never in agent-env's own environment. A run that pulls one of its agents reads the whole bundle, so `tst/unit/bundle/shipped_bundles_test.py` parses and checks every shipped bundle. | -| `computers/` | The `computers` bundle: the base computers agent-env ships as `computer` artifacts, `ubuntu-24.04` (XFCE on Xvfb, supervisord as the session user, settings as `AGENTENV_SETTING_`), and `omarchy-4.5`, a disk alone built on the composing machine from Omarchy's own VM image (its `[forms.disk]` `source`) by `disk/omarchy/`'s recipe, its services systemd user units of the graphical session that start through `with-seed`, booted by the launcher as a `linux-desktop` guest (`env/envs/computer/utils/omarchy_disk.py`, on `thin_disk.py`, which Windows' disks use too), aliased through `[artifacts]`. It has no tasks either. `disk/linux/` is the Linux disk form's recipe: `boot/`, whose Dockerfile makes a disk form (a GPT disk of an EFI system partition holding one UKI, Ubuntu's kernel with a busybox initramfs whose init reads a NoCloud seed and `switch_root`s into the image's entrypoint, and an ext4 root filesystem that is a container image's, plus `/etc/agentenv/vm/image.env` from its OCI config), `tools/`, the disk tools that make it unprivileged on the builder's own platform (`mkfs.ext4 -d`), and `launcher/`, the QEMU launcher that boots one under KVM. A base's `[forms.disk]` with no image is made from its container form; the composer makes each arch's disk from its composed container, keeping the base disk's EFI system partition, keyed by the container's key, the base disk and the recipe; gate 3 probes it where a provider boots disks natively (`disk_image_platforms()`), and a deploy's first boot with KVM probes one left `unverified`; a deploy naming `form = "disk"` boots it through `create_vm(image=..., env=...)`, its settings in the seed. | +| `computers/` | The `computers` bundle: the base computers agent-env ships as `computer` artifacts, `ubuntu-24.04` (XFCE on Xvfb, supervisord as the session user, settings as `AGENTENV_SETTING_`), and `omarchy-4.5`, a disk alone built on the composing machine from Omarchy's own VM image (its `[forms.disk]` `source`) by `disk/omarchy/`'s recipe, its services systemd user units of the graphical session that start through `with-seed`, booted by the launcher as a `linux-desktop` guest (`env/envs/computer/utils/omarchy_disk.py`, on `thin_disk.py`, which Windows' disks use too), aliased through `[artifacts]`. It has no tasks either. `disk/linux/` is the Linux disk form's recipe: `boot/`, whose Dockerfile makes a disk form (a GPT disk of an EFI system partition holding one UKI, Ubuntu's kernel with a busybox initramfs whose init reads a NoCloud seed and `switch_root`s into the image's entrypoint, and an ext4 root filesystem that is a container image's, plus `/etc/agentenv/vm/image.env` from its OCI config), `tools/`, the disk tools that make it unprivileged on the builder's own platform (`mkfs.ext4 -d`), and `launcher/`, the QEMU launcher that boots one under KVM. A base's `[forms.disk]` with no image is made from its container form; the composer makes each arch's disk from its composed container, keeping the base disk's EFI system partition, keyed by the container's key, the base disk and the recipe; gate 3 probes it where a provider boots disks natively (`disk_image_platforms()`), and a deploy's first boot with KVM probes one left `unverified`; a deploy naming `form = "disk"` boots it through `create_vm(image=..., env=...)`, its settings in the seed. `clock/` is the computer's clock: agent-env's build of libfaketime 0.9.12 (`libfaketime.patch` and the two `agentenv_clock_*.c` files it includes, GPL-2.0-only as libfaketime is, built by `build.sh` from upstream's sha256-pinned tarball), which session programs load to read universe time and wait for a set clock time from a page its follower writes (`clockpage.py`), monotonic and relative waits staying real, with its default settings (`clock.conf`); `tst/clock` gates it. | | `browsers/` | The `browsers` bundle: the website browsers agent-env ships, today `chromium`, a `computer_app` env (Chrome for Testing at the build Playwright pins, for amd64 and arm64, set up for unattended use by managed policies in `/etc/opt/chrome_for_testing/policies/managed`, opened at boot on its `start_urls`, its profile its data). All of it, its pin, install step, policies and command, is in `envs/chromium/chromium.py`; `tst/integration/computer/chromium_app_test.py` runs it. Like `drivers`, it has no tasks. | | `drivers/` | The `drivers` bundle: the computer drivers agent-env ships as `computer_driver` envs, the reference driver (X11 on Linux, recording through `x11grab`) aliased `reference`, and trycua's cua-driver aliased `cua-driver` (its daemon from the pinned PyPI wheel, its own instructions, tools and skill resources served to agents through `@server_instructions`, `forwarded_tools` and `@resource`, its tools beside `computer_*`, `computer/v1` on its desktop-scope calls, its skill pack leading the computer skill; `tst/integration/computer/cua_driver_test.py` runs it). It has no tasks: a bundle with neither tasks nor evals is refused unless its `aliases.toml` names something for other bundles to use. The shipped `desktop` example composes it with the `ubuntu-24.04` base into a computer env, which `tst/integration/computer/` deploys, probes, records and has an agent use. | | `examples/` | The example bundles agent-env ships (`hello`, and `echo-mcp`, which needs Docker, and `desktop`, a computer env of `ubuntu-24.04` and `reference` with a probe task and an agent task). Tests run them, but they are examples for users, not fixtures. Every shipped bundle is registered under `agent_env.bundles` in `pyproject.toml`, ` = ""` shipping `src///`. | @@ -438,7 +439,7 @@ skips; a `changes` job decides, and a skipped `installer` counts as passed. `.github/workflows/trl.yml` runs `tst/trl` against TRL 1.16 (1.15 can't train on CPU) and the latest, and on transformers 5.17, the newest Unsloth's `GRPOTrainer` allows, on pull requests that touch `agent_env/training`, `agent_env/openenv` or the tier, weekly, and on demand; it allows no skips -and isn't a required check. `.github/workflows/nemo-gym.yml` runs `tst/nemo_gym` on Python 3.13 with the `nemo-gym` +and isn't a required check. `.github/workflows/clock.yml` runs `tst/clock` on x86_64 and arm64 runners on pull requests that touch the library, the tier or Chrome's pin, weekly, and on demand, keeping Chrome's archive and upstream's tarball between runs; it allows no skips and isn't a required check. `.github/workflows/nemo-gym.yml` runs `tst/nemo_gym` on Python 3.13 with the `nemo-gym` extra from `uv.lock` and with the latest nemo-gym, on the same triggers; it allows no skips and isn't a required check. `.github/workflows/verifiers.yml` runs `tst/verifiers`, `vf-eval` through verifiers' NeMo Gym taskset (prime-rl's) against `serve`, on verifiers 0.4.0 and its main branch, installed in a venv of its own since verifiers pins mcp 2, on diff --git a/Makefile b/Makefile index 99fffb01e..78e6fce1f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install lint unit-test int-test int-test-fast int-test-slow installer-test trl-test nemo-gym-test verl-test skyrl-test miles-test slime-test clean-install-test test clean +.PHONY: help install lint unit-test int-test int-test-fast int-test-slow installer-test clock-test trl-test nemo-gym-test verl-test skyrl-test miles-test slime-test clean-install-test test clean VENV := .venv VENV_BIN := $(abspath $(VENV))/bin @@ -66,6 +66,12 @@ installer-test: ## Run plugin add/remove through the real pip, uv and pipx, and # The TRL contract tier trains one GRPOTrainer step on CPU through agent_env.training.trl, with a tiny model from the # Hub. trl and torch aren't in the dev extra: install them first, as .github/workflows/trl.yml does. +# The clock tier builds agent-env's libfaketime (src/agent_env/computers/clock) and stock 0.9.12 in an Ubuntu 24.04 image +# for this machine's Docker and runs its gates there, Chromium launches included; .github/workflows/clock.yml runs it on +# x86_64 and arm64. +clock-test: ## Build agent-env's libfaketime and run its gates in Docker (tst/clock) + $(PYTHON) -m pytest tst/clock --timeout=2400 $(PYTEST_ARGS) + trl-test: ## Run one GRPOTrainer step on CPU through the TRL helper (needs trl and CPU torch, and the Hub) $(PYTHON) -m pytest tst/trl --timeout=900 $(PYTEST_ARGS) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 34f918d06..643a20c47 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -2,8 +2,8 @@ This project is licensed under the Apache License, Version 2.0, but depends on third-party software identified below. These dependencies are installed separately from the Python Package -Index and imported at runtime. Third-party source code is not incorporated into or statically -linked with the source repository. +Index and imported at runtime. Apart from agent-env's changes to libfaketime, described below, third-party +source code is not incorporated into or statically linked with the source repository. Each third-party component remains subject to its applicable license. Copyright notices, license files, source-code offers, and other materials accompanying each component form part of its @@ -2544,6 +2544,18 @@ Platform-specific native binaries that npm installs for the machine running the | @next/swc-win32-arm64-msvc | 15.5.25 | MIT | | @next/swc-win32-x64-msvc | 15.5.25 | MIT | +## libfaketime + +`src/agent_env/computers/clock/` holds agent-env's changes to [libfaketime](https://github.com/wolfcw/libfaketime) +0.9.12: `libfaketime.patch`, `agentenv_clock_page.c` and `agentenv_clock_waits.c`. Being derived from libfaketime, +those three files are licensed as it is, under the GNU General Public License version 2 (`GPL-2.0-only`); the folder's +`COPYING` is that license's text. libfaketime's own source is not in the repository: the folder's `build.sh` downloads +upstream's v0.9.12 tarball, checks its sha256 and applies the changes, and the library it builds runs inside computers. +The folder's other files, like the rest of the repository, are licensed under the Apache License, Version 2.0. + +- Source: +- License: GPL-2.0-only + ## License texts License and notice files as shipped by each package, with copyright lines removed: they are listed with each package above. Packages whose files differ only in those lines share a text. diff --git a/src/agent_env/computers/clock/COPYING b/src/agent_env/computers/clock/COPYING new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/src/agent_env/computers/clock/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/src/agent_env/computers/clock/README.md b/src/agent_env/computers/clock/README.md new file mode 100644 index 000000000..764f2f88f --- /dev/null +++ b/src/agent_env/computers/clock/README.md @@ -0,0 +1,54 @@ +# The computer's clock: agent-env's libfaketime + +Every program on a computer reads its own clock, so a computer can't ask the gateway for universe time the way an MCP +server does. Instead each session program loads agent-env's build of [libfaketime](https://github.com/wolfcw/libfaketime) +(through `/etc/ld.so.preload`), which answers clock reads with universe time from a page a follower process keeps in +step with the gateway. + +What follows the universe, and what doesn't: + +- **Universe time:** clock reads (`CLOCK_REALTIME`, `_COARSE`, `_ALARM`, `CLOCK_TAI`, `gettimeofday`, `time`) and waits + for a set realtime clock time (`clock_nanosleep` with `TIMER_ABSTIME`, realtime futex waits, `sem_timedwait` and + `sem_clockwait`, realtime condition variables, absolute realtime timerfds and POSIX timers). A wait is planned in real + time and planned again whenever the page changes, so a jump past its deadline ends it at the jump. Where the kernel + has no `futex_waitv` (before 5.16, or gVisor), realtime futex and semaphore waits read the page between slices of + `AGENTENV_WAIT_SLICE_MS` (100 ms by default) instead, so they see a change within a slice. +- **Real time:** the monotonic and boot clocks, CPU-time clocks, and every relative sleep and timeout, at any rate. +- **Who:** only the session user's processes (`AGENTENV_SESSION_UID`). A program started with `AGENTENV_REAL_TIME=1` + stays real; the marker is taken out of its environment block, so its children are faked. +- **Settings:** read once as the library loads, from `/etc/agentenv/clock.conf` (`clock.conf` is the default), or from + the environment where there is no such file. While the file exists the environment changes none of them; the marker + above is the one switch a program's environment has. No `getenv()` runs once the program has started: a `getenv()` + racing a program's own `setenv()` made stock libfaketime crash Chromium on about one launch in five. + +## Files + +| File | What it is | License | +|---|---|---| +| `libfaketime.patch` | agent-env's changes to upstream 0.9.12's `src/` (applied with `patch -p1`) | GPL-2.0-only | +| `agentenv_clock_page.c` | settings, who is faked, the clock page reader; included by the patched `libfaketime.c` | GPL-2.0-only | +| `agentenv_clock_waits.c` | universe-time reads and waits, the per-process helper thread; included the same way | GPL-2.0-only | +| `COPYING` | the GNU General Public License version 2, libfaketime's license | | +| `build.sh` | builds `libfaketimeMT.so.1` from upstream's sha256-pinned tarball and the files above | Apache-2.0 | +| `clockpage.py` | the clock page's writer, which the follower uses | Apache-2.0 | +| `clock.conf` | the library's default settings | Apache-2.0 | + +Build it on x86_64 or aarch64 Linux with a C toolchain, make, patch and curl: + + src/agent_env/computers/clock/build.sh OUT [TARBALL] + +## The clock page + +An 80-byte page (default `/dev/shm/agentenv-clock`, version 2) that `clockpage.py` rewrites in place: `seq` odd, the +fields and their check, `seq` even, then `FUTEX_WAKE` on `seq`, which wakes every wait the library has planned and its +helper threads. Readers never cache it and never see a torn value: they wait on `seq` while it is odd and retry when it +changed or the check doesn't match. A write that would step the universe back by less than a second (a rate change +computed from a read a moment earlier) continues from where the page has it instead. + +## Tests + +`tst/clock` (`make clock-test`) builds this library and stock 0.9.12 in an Ubuntu 24.04 container and runs its gates +there: every wait API in C, Python, Java and Node at several rates, jumps, cancelled waits, timerfd duplicates, +condition-variable and semaphore stress, the waits again in a container that refuses `futex_waitv` (as kernels before +5.16 and gVisor do), a stalled writer, one that died mid-write, torn reads, the marker, the environment race and +Chromium launches. `.github/workflows/clock.yml` runs them on x86_64 and arm64. diff --git a/src/agent_env/computers/clock/agentenv_clock_page.c b/src/agent_env/computers/clock/agentenv_clock_page.c new file mode 100644 index 000000000..9167f7ede --- /dev/null +++ b/src/agent_env/computers/clock/agentenv_clock_page.c @@ -0,0 +1,320 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * agent-env's additions to libfaketime 0.9.12, part 1, included by libfaketime.c (libfaketime.patch) before its + * time functions: the process's settings, read once at load from /etc/agentenv/clock.conf, or from the environment + * where there is no such file, so no getenv() runs after main() starts; who is faked (the session user, unless started with AGENTENV_REAL_TIME=1, a marker + * taken out of the environment block in place); and the clock page the follower writes, read without a cache. + * Derived from libfaketime, so licensed as it is, under the GNU General Public License version 2 (COPYING). + */ +/* + * configuration is read once, when the library loads, from AGENTENV_CLOCK_CONF (NAME=VALUE lines) when that + * file exists, else from the environment, and never with getenv() afterwards: a getenv() racing a program's setenv() + * reads a freed environ array, which is how libfaketime's pthread_cond_timedwait crashed Chromium. + */ +#ifndef AGENTENV_CLOCK_CONF +#define AGENTENV_CLOCK_CONF "/etc/agentenv/clock.conf" +#endif +#define FT_CONF_MAX 64 +static char ft_conf_text[8192]; +static const char *ft_conf_name[FT_CONF_MAX]; +static const char *ft_conf_value[FT_CONF_MAX]; +static int ft_conf_count = 0; +static bool ft_conf_loaded = false; +/* this process stays on real time: another user's than AGENTENV_SESSION_UID, or started with + AGENTENV_REAL_TIME=1. */ +static bool ft_disabled = false; +/* bumped with each new time specification parsed, so a wait sees the clock change under it. */ +static volatile unsigned long ft_spec_gen = 0; +static __thread unsigned long ft_tl_gen = 0; +/* real seconds per fake second for what is left of a wait, as of this thread's last clock read: 1 / rate, + or 1 for a frozen clock. */ +static __thread double ft_tl_scale = 1.0; +/* how long a wait that can be cut sleeps before it looks at the clock again (AGENTENV_WAIT_SLICE_MS). */ +static long ft_wait_slice_ns = 100000000L; + +static void ft_conf_add(char *name, char *value) +{ + if (ft_conf_count < FT_CONF_MAX) + { + ft_conf_name[ft_conf_count] = name; + ft_conf_value[ft_conf_count] = value; + ft_conf_count++; + } +} + +static void ft_conf_load(void) +{ + extern char **environ; + size_t used = 0; + int fd; + if (ft_conf_loaded) return; + ft_conf_loaded = true; + fd = open(AGENTENV_CLOCK_CONF, O_RDONLY | O_CLOEXEC); + if (fd >= 0) + { + ssize_t n; + char *line; + while (used < sizeof(ft_conf_text) - 1 && + (n = read(fd, ft_conf_text + used, sizeof(ft_conf_text) - 1 - used)) > 0) + used += n; + close(fd); + ft_conf_text[used] = 0; + line = ft_conf_text; + while (line != NULL && *line != 0) + { + char *next = strchr(line, '\n'); + char *eq; + if (next != NULL) *next++ = 0; + eq = strchr(line, '='); + if (line[0] != '#' && eq != NULL && eq != line) + { + *eq = 0; + ft_conf_add(line, eq + 1); + } + line = next; + } + return; + } + for (char **e = environ; e != NULL && *e != NULL; e++) + { + const char *s = *e; + size_t len; + char *copy, *eq; + if (strncmp(s, "FAKE", 4) && strncmp(s, "NO_FAKE", 7) && strncmp(s, "DONT_FAKE", 9) && strncmp(s, "HOME=", 5) + && strncmp(s, "AGENTENV_", 9)) + continue; + len = strlen(s); + if (used + len + 1 > sizeof(ft_conf_text)) break; + copy = ft_conf_text + used; + memcpy(copy, s, len + 1); + used += len + 1; + eq = strchr(copy, '='); + if (eq == NULL) continue; + *eq = 0; + ft_conf_add(copy, eq + 1); + } +} + +static char *ft_getenv(const char *name) +{ + ft_conf_load(); + for (int i = 0; i < ft_conf_count; i++) + { + if (strcmp(ft_conf_name[i], name) == 0) return (char *) ft_conf_value[i]; + } + return NULL; +} + +/* + * AGENTENV_REAL_TIME=1 taken out of the original environment block in place: its pointer out of the array + * (which is main()'s envp too) and the string itself changed to "=0", so no copy of it is the marker. unsetenv() isn't + * used: a program may define its own (bash does, and before its main() it does nothing). + */ +static void ft_scrub_marker(void) +{ + extern char **environ; + char **e = environ; + if (e == NULL) return; + while (*e != NULL) + { + if (strcmp(*e, "AGENTENV_REAL_TIME=1") == 0) + { + char **d = e; + (*e)[sizeof("AGENTENV_REAL_TIME=") - 1] = '0'; + do d[0] = d[1]; while (*d++ != NULL); + } + else + e++; + } +} + +/* whether a wait's deadline is one to translate: a valid one, in a faked process, outside libfaketime. */ +static bool ft_translates_wait(const struct timespec *abstime) +{ + return abstime != NULL && !ft_disabled && !dont_fake && abstime->tv_nsec >= 0 && abstime->tv_nsec < SEC_TO_nSEC; +} + +/* + * where a CLOCK_REALTIME deadline in fake time falls on a real clock (CLOCK_MONOTONIC or CLOCK_REALTIME): + * now, plus what is left of the fake interval times ft_tl_scale; so a frozen clock counts what is left as real time and + * nothing waits forever, and a deadline already past is now. + */ +static void ft_deadline_from_fake(clockid_t clock, const struct timespec *fake_abs, struct timespec *out) +{ + struct timespec now, real_now, fake_now, left, scaled; + DONT_FAKE_TIME((*real_clock_gettime)(clock, &now)); + DONT_FAKE_TIME((*real_clock_gettime)(CLOCK_REALTIME, &real_now)); + fake_now = real_now; + (void) fake_clock_gettime(CLOCK_REALTIME, &fake_now); + *out = now; + timespecsub(fake_abs, &fake_now, &left); + if (left.tv_sec < 0) return; + if (left.tv_sec > 100L * 365 * 86400) left.tv_sec = 100L * 365 * 86400; + if (ft_tl_scale != 1.0) + { + timespecmul(&left, ft_tl_scale, &scaled); + } + else + { + scaled = left; + } + timespecadd(&now, &scaled, out); +} + +/* the end of a wait's next slice on CLOCK_MONOTONIC; true when that is the deadline itself. */ +static bool ft_slice_end(const struct timespec *mono_abs, struct timespec *slice) +{ + struct timespec now, step = {ft_wait_slice_ns / SEC_TO_nSEC, ft_wait_slice_ns % SEC_TO_nSEC}; + DONT_FAKE_TIME((*real_clock_gettime)(CLOCK_MONOTONIC, &now)); + timespecadd(&now, &step, slice); + if (timespeccmp(slice, mono_abs, <)) return false; + *slice = *mono_abs; + return true; +} + +/* after a slice, the clock read again (the file, with no cache); a new specification since ``gen`` moves the + deadline. True when it moved. */ +static bool ft_deadline_moved(const struct timespec *fake_abs, struct timespec *mono_abs, unsigned long *gen) +{ + struct timespec now; + DONT_FAKE_TIME((*real_clock_gettime)(CLOCK_REALTIME, &now)); + (void) fake_clock_gettime(CLOCK_REALTIME, &now); + if (ft_tl_gen == *gen) return false; + *gen = ft_tl_gen; + ft_deadline_from_fake(CLOCK_MONOTONIC, fake_abs, mono_abs); + return true; +} + +/* + * the clock page. 64 bytes, little-endian, rewritten in place by the follower: seq odd while it writes, + * then even; check is the xor of the other words and a constant. A realtime read in a faked process copies it + * lock-free and computes universe + (now - anchor) * rate (rate 0: universe), so every process agrees with no shared + * start; the first thread to see a new generation also hands it to libfaketime's own state (rates of sleeps, the + * time an absolute spec gives) under the MT lock. The page must be owned by AGENTENV_CLOCK_PAGE_UID (0 by default) and + * writable by no one else, so the session user can't plant one. + */ +struct ft_clock_page +{ + uint32_t magic, version; + uint64_t seq; + int64_t universe_ns, anchor_ns; + double rate; + uint32_t armed, reserved; + uint64_t generation, check; + int64_t mono_universe_ns, mono_anchor_ns; +}; +#define FT_PAGE_MAGIC 0x4B434541u +#define FT_PAGE_VERSION 2u +static const volatile struct ft_clock_page *ft_page = NULL; +static uint64_t ft_page_applied = 0; +static time_t ft_page_last_try = 0; +static void parse_ft_string(const char *user_faked_time); + +static uint64_t ft_page_check(const struct ft_clock_page *p) +{ + uint64_t rate_bits; + memcpy(&rate_bits, &p->rate, sizeof rate_bits); + return p->seq ^ (uint64_t) p->universe_ns ^ (uint64_t) p->anchor_ns ^ rate_bits ^ p->armed ^ p->generation + ^ (uint64_t) p->mono_universe_ns ^ (uint64_t) p->mono_anchor_ns ^ 0x9E3779B97F4A7C15ull; +} + +static bool ft_page_snapshot(struct ft_clock_page *out) +{ + for (int i = 0; i < 1000; i++) + { + uint64_t s1 = __atomic_load_n(&ft_page->seq, __ATOMIC_ACQUIRE); + if (s1 & 1) continue; + out->magic = ft_page->magic; + out->seq = s1; + out->universe_ns = ft_page->universe_ns; + out->anchor_ns = ft_page->anchor_ns; + out->rate = ft_page->rate; + out->armed = ft_page->armed; + out->version = ft_page->version; + out->generation = ft_page->generation; + out->check = ft_page->check; + out->mono_universe_ns = ft_page->mono_universe_ns; + out->mono_anchor_ns = ft_page->mono_anchor_ns; + __atomic_thread_fence(__ATOMIC_ACQUIRE); + if (__atomic_load_n(&ft_page->seq, __ATOMIC_RELAXED) == s1 && out->magic == FT_PAGE_MAGIC + && out->version == FT_PAGE_VERSION + && ft_page_check(out) == out->check) + return true; + } + return false; +} + +static void ft_page_open(void) +{ + const char *path = ft_getenv("AGENTENV_CLOCK_PAGE"); + const char *owner = ft_getenv("AGENTENV_CLOCK_PAGE_UID"); + struct stat st; + void *map; + int fd; + if (path == NULL || *path == 0) return; + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) return; + if (fstat(fd, &st) != 0 || st.st_size < (off_t) sizeof(struct ft_clock_page) + || st.st_uid != (uid_t) (owner ? strtoul(owner, NULL, 10) : 0) || (st.st_mode & 022) != 0) + { + close(fd); + return; + } + map = mmap(NULL, sizeof(struct ft_clock_page), PROT_READ, MAP_SHARED, fd, 0); + close(fd); + if (map == MAP_FAILED) return; + if (((const struct ft_clock_page *) map)->magic != FT_PAGE_MAGIC + || ((const struct ft_clock_page *) map)->version != FT_PAGE_VERSION) + { + munmap(map, sizeof(struct ft_clock_page)); + return; + } + ft_page = map; +} + +/* libfaketime's own state set to the page's clock; called with the MT lock held. */ +static void ft_page_apply_locked(const struct ft_clock_page *s) +{ + char spec[160]; + if (s->generation == ft_page_applied) return; + if (!s->armed) + snprintf(spec, sizeof spec, "+0 x1"); + else + snprintf(spec, sizeof spec, "@%lld.%09lld x%.17g ^%lld.%09lld", + (long long) (s->universe_ns / 1000000000LL), (long long) (s->universe_ns % 1000000000LL), s->rate, + (long long) (s->anchor_ns / 1000000000LL), (long long) (s->anchor_ns % 1000000000LL)); + parse_ft_string(spec); + __atomic_store_n(&ft_page_applied, s->generation, __ATOMIC_RELEASE); +} + +/* "%s" times are read as a number, without strptime() and mktime(), which reads TZ with getenv(). */ +static const char *ft_parse_epoch(const char *s, struct timespec *out) +{ + char *end; + long long sec; + long nsec = 0, scale = 100000000L; + bool negative; + while (*s == ' ') s++; + negative = (*s == '-'); + sec = strtoll(s, &end, 10); + if (end == s) return NULL; + if (*end == '.') + { + end++; + while (*end >= '0' && *end <= '9') + { + nsec += (*end - '0') * scale; + scale /= 10; + end++; + } + } + if (negative && nsec > 0) + { + sec -= 1; + nsec = SEC_TO_nSEC - nsec; + } + out->tv_sec = (time_t) sec; + out->tv_nsec = nsec; + return end; +} + diff --git a/src/agent_env/computers/clock/agentenv_clock_waits.c b/src/agent_env/computers/clock/agentenv_clock_waits.c new file mode 100644 index 000000000..61c836ce7 --- /dev/null +++ b/src/agent_env/computers/clock/agentenv_clock_waits.c @@ -0,0 +1,1159 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * agent-env's additions to libfaketime 0.9.12, part 2, included by libfaketime.c (libfaketime.patch) after its time + * functions: clock reads and waits for a set realtime clock time on universe time, re-planned whenever the page + * changes, and everything else (monotonic and boot clocks, relative sleeps and timeouts) the kernel's. + * Derived from libfaketime, so licensed as it is, under the GNU General Public License version 2 (COPYING). + */ +/* + * Elapsed time is real; clock reads and waits for a realtime clock time follow the universe. + */ +#include +#include +#include +#include + +#if !defined(FAKE_PTHREAD) || !defined(FAKE_TIMERS) || !defined(FAKE_SLEEP) || defined(INTERCEPT_SYSCALL) +#error agent-env waits need FAKE_PTHREAD, FAKE_TIMERS and FAKE_SLEEP, and their own syscall(), not the INTERCEPT_SYSCALL one +#endif +#ifndef SYS_futex_waitv +#define SYS_futex_waitv 449 +#endif + +/* + * the system call itself, with no libc and no libfaketime state, so syscall() below works before the + * library is initialised (Chromium's allocator calls syscall() from inside the dynamic loader). + */ +static long ft_raw_syscall6(long n, long a, long b, long c, long d, long e, long f) +{ + long ret; +#if defined(__x86_64__) + register long r10 __asm__("r10") = d; + register long r8 __asm__("r8") = e; + register long r9 __asm__("r9") = f; + __asm__ volatile("syscall" : "=a"(ret) : "0"(n), "D"(a), "S"(b), "d"(c), "r"(r10), "r"(r8), "r"(r9) + : "rcx", "r11", "memory"); +#elif defined(__aarch64__) + register long x8 __asm__("x8") = n; + register long x0 __asm__("x0") = a; + register long x1 __asm__("x1") = b; + register long x2 __asm__("x2") = c; + register long x3 __asm__("x3") = d; + register long x4 __asm__("x4") = e; + register long x5 __asm__("x5") = f; + __asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5) : "memory"); + ret = x0; +#else +#error the agent-env syscall() needs x86_64 or aarch64 +#endif + if (ret < 0 && ret > -4096) + { + errno = (int) -ret; + return -1; + } + return ret; +} + +#define FT_NS 1000000000LL +#define FT_NEVER INT64_MAX +static __thread struct ft_clock_page ft_tl_snap; +static __thread bool ft_tl_snap_ok = false; + +/* a consistent copy of the page. While the follower holds it odd (preempted mid-write), wait for it on seq, + which it wakes when done, up to a second: a copy older than the page being written would be extrapolated past the + new page's anchor, and the clock would step back when the write lands. Only a follower that died mid-write (seq odd + for over a second, remembered) leaves this thread on its last good copy. */ +static uint64_t ft_dead_seq = 0; +static bool ft_snap(struct ft_clock_page *s) +{ + for (int i = 0; i < 64; i++) + { + if (ft_page_snapshot(s)) + { + ft_tl_snap = *s; + ft_tl_snap_ok = true; + return true; + } + sched_yield(); + } + for (int i = 0; i < 100; i++) + { + uint64_t seq = __atomic_load_n(&ft_page->seq, __ATOMIC_ACQUIRE); + if ((seq & 1) && seq == __atomic_load_n(&ft_dead_seq, __ATOMIC_RELAXED)) break; + if (seq & 1) + { + struct timespec ts = {0, 10000000}; + ft_raw_syscall6(SYS_futex, (long) &ft_page->seq, FUTEX_WAIT, (long) (uint32_t) seq, (long) &ts, 0, 0); + } + if (ft_page_snapshot(s)) + { + ft_tl_snap = *s; + ft_tl_snap_ok = true; + return true; + } + if (i == 99) __atomic_store_n(&ft_dead_seq, seq, __ATOMIC_RELAXED); + } + if (!ft_tl_snap_ok) return false; + *s = ft_tl_snap; + return true; +} + +/* the real time just read, in hand before the loads after it: a load can be carried out before an earlier read of the + CPU's counter (aarch64 and x86_64 both allow it). On aarch64 a load whose address depends on the time, then a load + barrier, as the kernel orders its own counter reads; on x86_64 an lfence. */ +static inline void ft_after_real_read(const struct timespec *tp) +{ +#if defined(__aarch64__) + uint64_t tmp; + __asm__ volatile("eor %0, %1, %1\n\tadd %0, sp, %0\n\tldr xzr, [%0]\n\tdmb ishld" + : "=&r"(tmp) : "r"((uint64_t) tp->tv_nsec) : "memory"); +#elif defined(__x86_64__) + (void) tp; + __asm__ volatile("lfence" ::: "memory"); +#else + (void) tp; + __atomic_thread_fence(__ATOMIC_SEQ_CST); +#endif +} + +/* a consistent copy of the page and the real time read while it was the page: the time is read after the copy and kept + only if the page is still the one copied. A time from before a write paired with the page it wrote (a reader that + waited out the write, or was descheduled between its time and its copy) could put the universe behind what that + reader had shown, since a write is continuous with the page it replaces only at its own real time. */ +static bool ft_snap_at(clockid_t clock, struct ft_clock_page *s, struct timespec *tp) +{ + for (int i = 0; i < 8; i++) + { + int r; + if (!ft_snap(s)) return false; + DONT_FAKE_TIME(r = (*real_clock_gettime)(clock, tp)); + if (r != 0) return false; + ft_after_real_read(tp); + if (__atomic_load_n(&ft_page->seq, __ATOMIC_ACQUIRE) == s->seq) return true; + } + return true; +} + +static inline int64_t ft_ns(const struct timespec *t) +{ + if (t->tv_sec > 9000000000LL) return FT_NEVER / 2; + if (t->tv_sec < -9000000000LL) return -(FT_NEVER / 2); + return (int64_t) t->tv_sec * FT_NS + t->tv_nsec; +} +static inline struct timespec ft_ts(int64_t ns) +{ + struct timespec t; + if (ns < 0) ns = 0; + t.tv_sec = (time_t) (ns / FT_NS); + t.tv_nsec = (long) (ns % FT_NS); + return t; +} +static inline int64_t ft_real_rt(void) +{ + struct timespec t; + (*real_clock_gettime)(CLOCK_REALTIME, &t); + return ft_ns(&t); +} +static inline bool ft_uw_active(void) +{ + return ft_page != NULL && !ft_disabled && !dont_fake; +} +static inline bool ft_faked(void) +{ + return !ft_disabled && !dont_fake; +} +static inline bool ft_valid_ts(const struct timespec *t) +{ + return t != NULL && t->tv_nsec >= 0 && t->tv_nsec < FT_NS && t->tv_sec >= 0; +} +static inline bool ft_rt_clock(clockid_t c) +{ + return c == CLOCK_REALTIME || c == CLOCK_REALTIME_ALARM; +} +/* the clocks the universe has; every other one (monotonic, boot, raw, CPU time) is real. */ +static inline bool ft_universe_clock(clockid_t c) +{ + return c == CLOCK_REALTIME || c == CLOCK_REALTIME_COARSE || c == CLOCK_REALTIME_ALARM || c == CLOCK_TAI; +} + +/* the universe's realtime at real realtime t; before the anchor, the anchor's. */ +static int64_t ft_page_rt(const struct ft_clock_page *s, int64_t t) +{ + if (!s->armed) return t; + if (s->rate == 0 || t <= s->anchor_ns) return s->universe_ns; + if (s->rate == 1.0) return s->universe_ns + (t - s->anchor_ns); + return s->universe_ns + (int64_t) ((double) (t - s->anchor_ns) * s->rate); +} +/* the real realtime at which the universe reaches ``due``; 0 (the past) once it has, FT_NEVER while frozen + short of it. */ +static int64_t ft_real_of(const struct ft_clock_page *s, int64_t due) +{ + double r; + if (!s->armed) return due < 1 ? 1 : due; + if (due <= s->universe_ns) return 1; + if (s->rate <= 0) return FT_NEVER; + r = (double) (due - s->universe_ns) / s->rate; + if (r > 4e18) return FT_NEVER; + return s->anchor_ns + (int64_t) r + 1; +} +static inline bool ft_reached(const struct ft_clock_page *s, int64_t due) +{ + return ft_page_rt(s, ft_real_rt()) >= due; +} +static inline uint32_t ft_seq32(void) +{ + return (uint32_t) __atomic_load_n(&ft_page->seq, __ATOMIC_ACQUIRE); +} + +/* ---- a lock for the tables below, with signals blocked so a handler can't deadlock on it ---- */ +static volatile int ft_tab_lock_word = 0; +static void ft_tab_lock(sigset_t *old) +{ + sigset_t all; + sigfillset(&all); + pthread_sigmask(SIG_SETMASK, &all, old); + while (__atomic_exchange_n(&ft_tab_lock_word, 1, __ATOMIC_ACQUIRE)) sched_yield(); +} +static void ft_tab_unlock(const sigset_t *old) +{ + __atomic_store_n(&ft_tab_lock_word, 0, __ATOMIC_RELEASE); + pthread_sigmask(SIG_SETMASK, old, NULL); +} + +/* + * ---- timers for a realtime clock time: timerfds and POSIX timers armed at TIMER_ABSTIME on a realtime clock. The + * kernel timer is armed at the real realtime the universe reaches the deadline at (in the past once it has, so it fires + * now), its interval at interval / rate, and disarmed while frozen short of it; the helper arms it again on each change. + * A one-shot timer that has fired is left alone. ---- + */ +#define FT_TIMER_FDS 8 +struct ft_timer +{ + int kind; /* 0 free, 1 timerfd, 2 POSIX timer */ + int tracked; /* armed for a realtime clock time */ + int nfds, fds[FT_TIMER_FDS]; /* a timerfd's descriptors: the one it was armed through and its duplicates */ + timer_t id; + int clock; + int flags; /* the caller's flags, TFD_TIMER_CANCEL_ON_SET kept */ + int real_set; /* the kernel timer is armed */ + int64_t due, interval; /* universe ns */ + struct ft_clock_page pg; /* the page it was last armed from */ +}; +#define FT_TIMERS_MAX 1024 +static struct ft_timer ft_timers[FT_TIMERS_MAX]; +static int ft_timers_top = 0; +static volatile int ft_timers_any = 0; +static int (*real_timer_create_new)(clockid_t, struct sigevent *, timer_t *); +static int (*real_timer_delete_new)(timer_t); +static int (*real_timer_settime_new)(timer_t, int, const struct itimerspec *, struct itimerspec *); +static int (*real_timer_gettime_new)(timer_t, struct itimerspec *); +#define FT_TFD_CLOCKS 65536 +static int ft_tfd_clock[FT_TFD_CLOCKS]; /* clock + 1 as timerfd_create was told; 0 unknown */ + +static struct ft_timer *ft_timer_find_fd(int fd) +{ + for (int i = 0; i < ft_timers_top; i++) + if (ft_timers[i].kind == 1) + for (int k = 0; k < ft_timers[i].nfds; k++) + if (ft_timers[i].fds[k] == fd) return &ft_timers[i]; + return NULL; +} +static struct ft_timer *ft_timer_find_id(timer_t id) +{ + for (int i = 0; i < ft_timers_top; i++) + if (ft_timers[i].kind == 2 && ft_timers[i].id == id) return &ft_timers[i]; + return NULL; +} +static struct ft_timer *ft_timer_new(void) +{ + for (int i = 0; i < FT_TIMERS_MAX; i++) + if (ft_timers[i].kind == 0) + { + memset(&ft_timers[i], 0, sizeof ft_timers[i]); + if (i >= ft_timers_top) ft_timers_top = i + 1; + return &ft_timers[i]; + } + return NULL; +} +static int ft_timer_real_get(struct ft_timer *t, struct itimerspec *cur) +{ + return t->kind == 1 ? (*real_timerfd_gettime)(t->fds[0], cur) : (*real_timer_gettime_new)(t->id, cur); +} +/* the next expiration at or after ``after``. */ +static int64_t ft_timer_next(const struct ft_timer *t, int64_t after) +{ + int64_t next = t->due; + if (t->interval > 0 && next < after) next += ((after - next + t->interval - 1) / t->interval) * t->interval; + return next; +} +/* the kernel timer set from page ``s``. A periodic timer goes on from its first expiration past the + universe's time on the page it was armed from, so a jump delivers what it passed at once. Lock held. */ +static int ft_timer_arm(struct ft_timer *t, const struct ft_clock_page *s, bool first) +{ + struct itimerspec real, cur; + double rate = s->armed ? s->rate : 1.0; + int64_t next = t->due, at; + int r; + memset(&real, 0, sizeof real); + if (!first && t->real_set && t->interval == 0 && ft_timer_real_get(t, &cur) == 0 && cur.it_value.tv_sec == 0 + && cur.it_value.tv_nsec == 0) + { + t->tracked = 0; /* fired: the expiration is the kernel's to hand over */ + return 0; + } + if (!first && t->interval > 0) next = ft_timer_next(t, ft_page_rt(&t->pg, ft_real_rt()) + 1); + at = ft_real_of(s, next); + if (at != FT_NEVER) + { + real.it_value = ft_ts(at); + if (real.it_value.tv_sec == 0 && real.it_value.tv_nsec == 0) real.it_value.tv_nsec = 1; + if (t->interval > 0) + { + int64_t iv = (int64_t) ((double) t->interval / rate); + real.it_interval = ft_ts(iv < 1 ? 1 : iv); + } + } + t->due = next; + t->pg = *s; + t->real_set = (at != FT_NEVER); + if (t->kind == 1) + r = (*real_timerfd_settime)(t->fds[0], TFD_TIMER_ABSTIME | (t->flags & TFD_TIMER_CANCEL_ON_SET), &real, NULL); + else + r = (*real_timer_settime_new)(t->id, TIMER_ABSTIME, &real, NULL); + if (r != 0) t->tracked = 0; + return r; +} +/* the timer's value in universe time. Lock held. */ +static void ft_timer_value(struct ft_timer *t, struct itimerspec *out) +{ + struct ft_clock_page s; + struct itimerspec cur; + memset(out, 0, sizeof *out); + if (!t->tracked) return; + if (t->interval == 0 && t->real_set && ft_timer_real_get(t, &cur) == 0 && cur.it_value.tv_sec == 0 + && cur.it_value.tv_nsec == 0) + { + t->tracked = 0; + return; + } + out->it_interval = ft_ts(t->interval); + if (ft_snap(&s)) + { + int64_t now = ft_page_rt(&s, ft_real_rt()), next = ft_timer_next(t, now); + out->it_value = ft_ts(next - now < 1 ? 1 : next - now); + } +} + +/* + * ---- realtime condvar waits in progress: each waits to the real time its deadline falls at; on a page change that + * moves that time, the helper broadcasts its condvar, once, and the wait returns 0 (a spurious wakeup the caller + * rechecks), so the caller waits again on the new page. A broadcast that comes before the waiter is inside glibc's wait + * is sent again until the waiter has seen the change. ---- + */ +struct ft_cw +{ + pthread_cond_t *cond; + int used, woken; + int64_t due, real_due; + uint64_t gen; /* the page generation its real_due came from; FT_CW_UNPLANNED before */ +}; +#define FT_CW_MAX 512 +#define FT_CW_UNPLANNED UINT64_MAX +static struct ft_cw ft_cws[FT_CW_MAX]; +static int ft_cws_top = 0; + +/* ---- the helper thread: one per process, started with the first such timer or wait, blocked on the page's seq ---- */ +static volatile pid_t ft_helper_pid = 0; +static volatile int ft_atfork_set = 0; +static void ft_helper_child(void) +{ + /* POSIX timers and condvar waiters belong to the parent; timerfds are shared with it and stay */ + for (int i = 0; i < ft_timers_top; i++) + if (ft_timers[i].kind == 2) ft_timers[i].kind = 0; + memset(ft_cws, 0, sizeof ft_cws); + ft_cws_top = 0; + ft_tab_lock_word = 0; + ft_helper_pid = 0; +} + +/* on a page change, every timer armed again and every condvar waiter whose real deadline moved woken; true + while a waiter is still to see it (not planned yet, or broadcast to before it was waiting). */ +static bool ft_on_change(const struct ft_clock_page *s, bool rebroadcast) +{ + sigset_t old; + bool pending = false; + ft_tab_lock(&old); + if (!rebroadcast) + for (int i = 0; i < ft_timers_top; i++) + { + struct ft_timer *t = &ft_timers[i]; + if (t->kind != 0 && t->tracked && t->pg.generation != s->generation) ft_timer_arm(t, s, false); + } + for (int i = 0; i < ft_cws_top; i++) + { + struct ft_cw *w = &ft_cws[i]; + if (!w->used || w->gen == s->generation) continue; + if (w->gen == FT_CW_UNPLANNED) + { + pending = true; + continue; + } + if (!w->woken) + { + int64_t nd = ft_real_of(s, w->due), diff; + diff = (nd == FT_NEVER || w->real_due == FT_NEVER) ? (nd == w->real_due ? 0 : FT_NEVER) : nd - w->real_due; + if (diff >= -1000000 && diff <= 1000000) + { + w->gen = s->generation; /* its real end didn't move */ + continue; + } + } + if (!w->woken || rebroadcast) pthread_cond_broadcast(w->cond); + w->woken = 1; + pending = true; + } + ft_tab_unlock(&old); + return pending; +} + +static void *ft_helper_main(void *arg) +{ + struct ft_clock_page s; + uint64_t gen = UINT64_MAX; /* the first pass catches a change made while it started */ + int backoff_ms = 0; + (void) arg; + for (;;) + { + uint32_t seq32 = ft_seq32(); + bool pending = false; + if (ft_snap(&s)) + { + if (s.generation != gen) + { + gen = s.generation; + pending = ft_on_change(&s, false); + backoff_ms = pending ? 1 : 0; + } + else if (backoff_ms > 0) + { + pending = ft_on_change(&s, true); + backoff_ms = pending ? (backoff_ms < 64 ? backoff_ms * 2 : 100) : 0; + } + } + if (backoff_ms > 0) + { + struct timespec ts = {0, (long) backoff_ms * 1000000L}; + ft_raw_syscall6(SYS_futex, (long) &ft_page->seq, FUTEX_WAIT, (long) seq32, (long) &ts, 0, 0); + } + else + ft_raw_syscall6(SYS_futex, (long) &ft_page->seq, FUTEX_WAIT, (long) seq32, 0, 0, 0); + } + return NULL; +} + +static void ft_helper_start(void) +{ + pid_t pid = getpid(); + sigset_t all, old; + pthread_t thread; + pthread_attr_t attr; + if (__atomic_load_n(&ft_helper_pid, __ATOMIC_ACQUIRE) == pid) return; + sigfillset(&all); + pthread_sigmask(SIG_SETMASK, &all, &old); + while (__atomic_exchange_n(&ft_tab_lock_word, 1, __ATOMIC_ACQUIRE)) sched_yield(); + if (ft_helper_pid != pid) + { + if (!ft_atfork_set) + { + pthread_atfork(NULL, NULL, ft_helper_child); + ft_atfork_set = 1; + } + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + pthread_attr_setstacksize(&attr, 128 * 1024); + if (pthread_create(&thread, &attr, ft_helper_main, NULL) == 0) /* inherits the all-blocked signal mask */ + { + __atomic_store_n(&ft_helper_pid, pid, __ATOMIC_RELEASE); + pthread_setname_np(thread, "agentenv-clock"); + } + pthread_attr_destroy(&attr); + } + __atomic_store_n(&ft_tab_lock_word, 0, __ATOMIC_RELEASE); + pthread_sigmask(SIG_SETMASK, &old, NULL); +} + +/* ---- the waits ---- */ + +/* the raw system calls below as the cancellation points their callers are (clock_nanosleep, sem_timedwait): + cancellation acted on at once for the call alone, as glibc's own waits were before its cancellable system call + wrapper. */ +static inline int ft_async_cancel(void) +{ + int old; + pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &old); + return old; +} +static inline void ft_deferred_cancel(int old) +{ + int e = errno, ignored; + pthread_setcanceltype(old, &ignored); + errno = e; +} + +/* a sleep until the universe reaches ``due``, on the page's seq. 0, or EINTR. */ +static int ft_rt_sleep(const struct ft_clock_page *first, int64_t due) +{ + struct ft_clock_page s = *first; + pthread_testcancel(); + for (;;) + { + uint32_t seq32 = (uint32_t) s.seq; + int64_t until; + struct timespec ts; + long r; + int ct; + if (ft_reached(&s, due)) return 0; + until = ft_real_of(&s, due); + ts = ft_ts(until); + ct = ft_async_cancel(); + r = ft_raw_syscall6(SYS_futex, (long) &ft_page->seq, FUTEX_WAIT_BITSET | FUTEX_CLOCK_REALTIME, (long) seq32, + until == FT_NEVER ? 0 : (long) &ts, 0, FUTEX_BITSET_MATCH_ANY); + ft_deferred_cancel(ct); + if (r == -1 && errno == EINTR) return EINTR; + if (!ft_snap(&s)) return 0; + } +} + +struct ft_futex_waitv { uint64_t val, uaddr; uint32_t flags, reserved; }; +static volatile int ft_waitv_missing = 0; +enum { FT_W_WORD, FT_W_PAGE, FT_W_TIMEOUT, FT_W_INTR, FT_W_ERR }; +/* without futex_waitv (before Linux 5.16, under gVisor, or refused by a seccomp filter): the word alone, until the real + deadline or a slice (AGENTENV_WAIT_SLICE_MS) at most, and the caller reads the page again between slices. */ +static int ft_wait1(uint32_t *word, uint32_t val, bool private_word, int64_t until) +{ + int64_t end = ft_real_rt() + ft_wait_slice_ns; + struct timespec ts; + if (until < end) end = until; + ts = ft_ts(end); + if (ft_raw_syscall6(SYS_futex, (long) word, + FUTEX_WAIT_BITSET | FUTEX_CLOCK_REALTIME | (private_word ? FUTEX_PRIVATE_FLAG : 0), (long) val, + (long) &ts, 0, FUTEX_BITSET_MATCH_ANY) == 0 || errno == EAGAIN) + return FT_W_WORD; + if (errno == ETIMEDOUT) return FT_W_PAGE; /* a slice's end, or the deadline: the caller reads the page */ + return errno == EINTR ? FT_W_INTR : FT_W_ERR; +} +/* until *word isn't val, the page changes from seq32, or real realtime ``until``; FT_W_ERR with errno the kernel's + (an unaligned or unmapped word). */ +static int ft_wait2(uint32_t *word, uint32_t val, bool private_word, uint32_t seq32, int64_t until) +{ + struct ft_futex_waitv w[2]; + struct timespec ts = ft_ts(until); + long r; + if (ft_waitv_missing) return ft_wait1(word, val, private_word, until); + /* the page first: futex_waitv returns the last of the futexes woken, so a caller's wake is never taken for the page's */ + w[0].val = seq32; + w[0].uaddr = (uint64_t) (uintptr_t) &ft_page->seq; + w[0].flags = 0x02; /* FUTEX2_SIZE_U32, shared */ + w[0].reserved = 0; + w[1].val = val; + w[1].uaddr = (uint64_t) (uintptr_t) word; + w[1].flags = 0x02 | (private_word ? 128 : 0); + w[1].reserved = 0; + r = ft_raw_syscall6(SYS_futex_waitv, (long) w, 2, 0, until == FT_NEVER ? 0 : (long) &ts, CLOCK_REALTIME, 0); + if (r == 1) return FT_W_WORD; + if (r == 0) return FT_W_PAGE; + if (errno == EAGAIN) return __atomic_load_n(word, __ATOMIC_RELAXED) != val ? FT_W_WORD : FT_W_PAGE; + if (errno == ETIMEDOUT) return FT_W_TIMEOUT; + if (errno == EINTR) return FT_W_INTR; + if (errno != ENOSYS && errno != EPERM) return FT_W_ERR; /* EPERM: a seccomp filter that refuses it so */ + ft_waitv_missing = 1; + return ft_wait1(word, val, private_word, until); +} + +/* futex(2) FUTEX_WAIT_BITSET|FUTEX_CLOCK_REALTIME through syscall(): until the universe reaches the deadline, + beside the page; *handled false leaves the call to the kernel as it is. */ +static long ft_u_futex_rt(uint32_t *uaddr, int op, uint32_t val, const struct timespec *abs, bool *handled) +{ + struct ft_clock_page s; + int64_t due = ft_ns(abs); + *handled = false; + if (!ft_snap(&s)) return 0; + *handled = true; + for (;;) + { + uint32_t seq32 = (uint32_t) s.seq; + int k; + if (ft_reached(&s, due)) + { + errno = ETIMEDOUT; + return -1; + } + k = ft_wait2(uaddr, val, (op & FUTEX_PRIVATE_FLAG) != 0, seq32, ft_real_of(&s, due)); + if (k == FT_W_WORD) + { + if (__atomic_load_n(uaddr, __ATOMIC_RELAXED) != val) + { + errno = EAGAIN; + return -1; + } + return 0; + } + if (k == FT_W_INTR) + { + errno = EINTR; + return -1; + } + if (k == FT_W_ERR) return -1; + if (!ft_snap(&s)) return 0; + } +} + +/* sem_timedwait / sem_clockwait(CLOCK_REALTIME) on glibc's semaphore (its layout is part of the ABI, since + process-shared semaphores outlive glibc versions): the value in the low word of data, the waiters above it, the + futex the value word, which sem_post wakes when there are waiters; ``private`` is glibc's futex flag the other way + round (0 for a process-private semaphore, FUTEX_PRIVATE_FLAG for a shared one). */ +struct ft_new_sem { uint64_t data; int private_flag; int pad; }; +static void ft_sem_unwait(void *sem) +{ + __atomic_fetch_sub(&((struct ft_new_sem *) sem)->data, (uint64_t) 1 << 32, __ATOMIC_RELAXED); +} +/* as one of the semaphore's waiters (counted in data's high word): 0 with a token taken, or the error. A cancelled wait + leaves the waiters it joined, as glibc's does. */ +static int ft_sem_wait_slow(struct ft_new_sem *sem, struct ft_clock_page *s, int64_t due) +{ + volatile int err = 0; + pthread_cleanup_push(ft_sem_unwait, sem); + uint64_t d = __atomic_fetch_add(&sem->data, (uint64_t) 1 << 32, __ATOMIC_RELAXED); + for (;;) + { + if ((d & 0xffffffffu) == 0) + { + int k, ct; + if (ft_reached(s, due)) + { + err = ETIMEDOUT; + break; + } + ct = ft_async_cancel(); + k = ft_wait2((uint32_t *) &sem->data, 0, sem->private_flag == 0, (uint32_t) s->seq, ft_real_of(s, due)); + ft_deferred_cancel(ct); + if (k == FT_W_INTR || k == FT_W_ERR) + { + err = k == FT_W_INTR ? EINTR : errno; + break; + } + if (!ft_snap(s)) + { + err = ETIMEDOUT; + break; + } + d = __atomic_load_n(&sem->data, __ATOMIC_RELAXED); + } + else if (__atomic_compare_exchange_n(&sem->data, &d, d - 1 - ((uint64_t) 1 << 32), true, __ATOMIC_ACQUIRE, + __ATOMIC_RELAXED)) + break; + } + pthread_cleanup_pop(err != 0); + return err; +} +static int ft_u_sem_wait(sem_t *sem_, int64_t due, bool *handled) +{ + struct ft_new_sem *sem = (struct ft_new_sem *) sem_; + struct ft_clock_page s; + uint64_t d; + int err; + *handled = false; + if (sizeof(sem_t) < sizeof(struct ft_new_sem) || !ft_snap(&s)) return 0; + *handled = true; + pthread_testcancel(); + d = __atomic_load_n(&sem->data, __ATOMIC_RELAXED); + while ((d & 0xffffffffu) > 0) + if (__atomic_compare_exchange_n(&sem->data, &d, d - 1, true, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) return 0; + err = ft_sem_wait_slow(sem, &s, due); + if (err == 0) return 0; + errno = err; + return -1; +} + +static void ft_cw_release(void *w) +{ + sigset_t old; + ft_tab_lock(&old); + ((struct ft_cw *) w)->used = 0; + ft_tab_unlock(&old); +} +/* glibc's wait, a cancellation point: a cancelled wait gives up its entry, so the helper never wakes a condvar that + may be gone. */ +static int ft_cw_clockwait(struct ft_cw *w, pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *end) +{ + volatile int r; + pthread_cleanup_push(ft_cw_release, w); + r = real_pthread_cond_clockwait(cond, mutex, CLOCK_REALTIME, end); + pthread_cleanup_pop(0); + return r; +} + +/* a realtime condvar wait (see ft_cw). -1 leaves it to the caller's own path (no page, table full). */ +static int ft_u_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) +{ + struct ft_clock_page s; + struct ft_cw *w = NULL; + struct timespec end; + sigset_t old; + int64_t due = ft_ns(abstime), real_due; + int r, woken; + if (!ft_snap(&s)) return -1; + if (ft_reached(&s, due)) return ETIMEDOUT; + ft_helper_start(); + ft_tab_lock(&old); + for (int i = 0; i < FT_CW_MAX; i++) + if (!ft_cws[i].used) + { + w = &ft_cws[i]; + if (i >= ft_cws_top) ft_cws_top = i + 1; + break; + } + if (w != NULL) + { + w->cond = cond; + w->due = due; + w->woken = 0; + w->gen = FT_CW_UNPLANNED; + w->used = 1; + } + ft_tab_unlock(&old); + if (w == NULL) return -1; + if (!ft_snap(&s)) s.armed = 0; + real_due = ft_real_of(&s, due); + ft_tab_lock(&old); + w->real_due = real_due; + w->gen = s.generation; + ft_tab_unlock(&old); + end = ft_ts(real_due == FT_NEVER ? ft_real_rt() + 86400 * FT_NS : real_due); + r = ft_cw_clockwait(w, cond, mutex, &end); + ft_tab_lock(&old); + woken = w->woken; + w->used = 0; + ft_tab_unlock(&old); + /* a real timeout before the universe's deadline (the page moved it later) is a spurious wakeup; the helper's wake for + a jump that passed the deadline is the timeout */ + if (r == ETIMEDOUT) return (ft_snap(&s) && !ft_reached(&s, due)) ? 0 : ETIMEDOUT; + if (r == 0 && woken && ft_snap(&s) && ft_reached(&s, due)) return ETIMEDOUT; + return r; +} + +/* a realtime mutex wait: glibc's mutex has no way in, so slices (AGENTENV_WAIT_SLICE_MS) of a real + CLOCK_MONOTONIC wait, which loses nothing when it times out, with the page read between them. */ +static int ft_u_mutex_lock(pthread_mutex_t *mutex, int64_t due, bool *handled) +{ + struct ft_clock_page s; + *handled = false; + if (!ft_snap(&s)) return 0; + *handled = true; + for (;;) + { + struct timespec now, end; + int64_t at, sl; + int r; + if (ft_reached(&s, due)) return ETIMEDOUT; + at = ft_real_of(&s, due); + sl = (at == FT_NEVER) ? ft_wait_slice_ns : at - ft_real_rt(); + if (sl > ft_wait_slice_ns) sl = ft_wait_slice_ns; + if (sl < 1) sl = 1; + (*real_clock_gettime)(CLOCK_MONOTONIC, &now); + end = ft_ts(ft_ns(&now) + sl); + r = real_pthread_mutex_clocklock(mutex, CLOCK_MONOTONIC, &end); + if (r == EINVAL) /* a PI mutex on a kernel without FUTEX_LOCK_PI2 */ + { + struct timespec real = ft_ts(ft_real_rt() + sl); + r = real_pthread_mutex_timedlock(mutex, &real); + } + if (r != ETIMEDOUT) return r; + if (!ft_snap(&s)) return ETIMEDOUT; + } +} + +/* syscall(): futex waits for a realtime clock time follow the universe (raw futex users such as Rust's std + and GLib call it), clock_gettime reads it once the library is up; everything else goes straight to the kernel. */ +long syscall(long number, ...) +{ + va_list ap; + long a[6]; + va_start(ap, number); + for (int i = 0; i < 6; i++) a[i] = va_arg(ap, long); + va_end(ap); + if (number == SYS_futex && ft_uw_active()) + { + int op = (int) a[1]; + const struct timespec *ts = (const struct timespec *) a[3]; + if ((op & FUTEX_CMD_MASK) == FUTEX_WAIT_BITSET && (op & FUTEX_CLOCK_REALTIME) && ft_valid_ts(ts) + && (uint32_t) a[5] == FUTEX_BITSET_MATCH_ANY) + { + bool handled; + long r = ft_u_futex_rt((uint32_t *) a[0], op, (uint32_t) a[2], ts, &handled); + if (handled) return r; + } + } + else if (number == SYS_clock_gettime && ft_page != NULL && !ft_disabled && !dont_fake) + return clock_gettime((clockid_t) a[0], (struct timespec *) a[1]); + return ft_raw_syscall6(number, a[0], a[1], a[2], a[3], a[4], a[5]); +} + +/* ---- timerfds ---- */ +static int ft_u_timerfd_settime(int fd, int flags, const struct itimerspec *nv, struct itimerspec *ov, bool *handled) +{ + struct ft_clock_page s; + struct ft_timer *t = NULL; + struct itimerspec prev; + sigset_t old; + int clock, r; + bool was = false; + *handled = false; + if (fd < 0 || fd >= FT_TFD_CLOCKS || nv == NULL) return 0; + clock = ft_tfd_clock[fd] - 1; + *handled = true; + if (ft_timers_any) + { + ft_tab_lock(&old); + t = ft_timer_find_fd(fd); + if (t != NULL && t->tracked) + { + ft_timer_value(t, &prev); + was = true; + t->tracked = 0; /* the record stays, with the timer's descriptors, while any is open */ + } + ft_tab_unlock(&old); + } + if (!ft_rt_clock(clock) || !(flags & TFD_TIMER_ABSTIME) || !ft_valid_ts(&nv->it_value) + || !ft_valid_ts(&nv->it_interval) || (nv->it_value.tv_sec == 0 && nv->it_value.tv_nsec == 0) || !ft_snap(&s)) + { + r = (*real_timerfd_settime)(fd, flags, nv, ov); + if (r == 0 && ov != NULL && was) *ov = prev; + return r; + } + ft_tab_lock(&old); + if ((t = ft_timer_find_fd(fd)) == NULL && (t = ft_timer_new()) != NULL) + { + t->kind = 1; + t->nfds = 1; + t->fds[0] = fd; + } + if (t == NULL) + { + ft_tab_unlock(&old); + *handled = false; /* table full: the caller's own (translated once) path */ + return 0; + } + t->clock = clock; + t->flags = flags; + t->due = ft_ns(&nv->it_value); + t->interval = ft_ns(&nv->it_interval); + t->tracked = 1; + if (ov != NULL) + { + if (was) + *ov = prev; + else + (*real_timerfd_gettime)(fd, ov); + } + r = ft_timer_arm(t, &s, true); + if (r != 0) t->tracked = 0; + ft_timers_any = 1; + ft_tab_unlock(&old); + if (r == 0) ft_helper_start(); + return r; +} +static int ft_u_timerfd_gettime(int fd, struct itimerspec *cur, bool *handled) +{ + struct ft_timer *t; + sigset_t old; + *handled = false; + if (!ft_timers_any || cur == NULL) return 0; + ft_tab_lock(&old); + t = ft_timer_find_fd(fd); + if (t != NULL && t->tracked) + { + ft_timer_value(t, cur); + *handled = true; + } + ft_tab_unlock(&old); + return 0; +} + +/* + * ---- descriptors: a timerfd's record follows its file, not a number. close() takes a descriptor off it (the record + * goes with the last), dup() and its kin add the new one, so the helper never arms a number the program has closed or + * that now names another file. A number closed where this can't see (closefrom, a raw system call) is dropped when + * timerfd_create hands it out again; a timer with more duplicates than a record holds is left armed as it was. ---- + */ +static int (*ft_real_close)(int); +static int (*ft_real_dup)(int); +static int (*ft_real_dup2)(int, int); +static int (*ft_real_dup3)(int, int, int); +static int (*ft_real_fcntl)(int, int, ...); +static int (*ft_real_fcntl64)(int, int, ...); +static int (*ft_real_close_range)(unsigned int, unsigned int, int); +#define FT_RESOLVE(name) do { if (ft_real_##name == NULL) ft_real_##name = dlsym(RTLD_NEXT, #name); } while (0) + +static inline bool ft_tfd_known(int fd) +{ + return fd >= 0 && fd < FT_TFD_CLOCKS && __atomic_load_n(&ft_tfd_clock[fd], __ATOMIC_RELAXED) != 0; +} +/* ``fd`` closed, or about to name another file. Lock held. */ +static void ft_fd_gone_locked(int fd) +{ + struct ft_timer *t; + ft_tfd_clock[fd] = 0; + while ((t = ft_timer_find_fd(fd)) != NULL) + { + int k = 0; + while (t->fds[k] != fd) k++; + t->fds[k] = t->fds[--t->nfds]; + if (t->nfds == 0) t->kind = 0; + } +} +static void ft_fd_gone(int fd) +{ + sigset_t old; + if (!ft_tfd_known(fd)) return; + ft_tab_lock(&old); + ft_fd_gone_locked(fd); + ft_tab_unlock(&old); +} +/* ``to`` now names ``from``'s file, a known timerfd. Lock held. */ +static void ft_fd_duped_locked(int from, int to) +{ + struct ft_timer *t; + if (to < 0 || to >= FT_TFD_CLOCKS) return; + ft_tfd_clock[to] = ft_tfd_clock[from]; + if ((t = ft_timer_find_fd(from)) != NULL) + { + if (t->nfds < FT_TIMER_FDS) + t->fds[t->nfds++] = to; + else + t->kind = 0; + } +} +static void ft_fd_duped(int from, int to) +{ + sigset_t old; + if (!ft_tfd_known(from)) return; + ft_tab_lock(&old); + ft_fd_duped_locked(from, to); + ft_tab_unlock(&old); +} +/* dup2 or dup3 onto ``fd2``. One that replaces a timerfd (whose close doesn't block) makes the change and its + bookkeeping under the lock, so a failed call keeps the record and the helper never arms the number in between. */ +static int ft_dup_onto(int fd, int fd2, int flags, bool three) +{ + sigset_t old; + int r; + FT_RESOLVE(dup2); + FT_RESOLVE(dup3); + if (fd == fd2 || !ft_tfd_known(fd2)) + { + r = three ? (*ft_real_dup3)(fd, fd2, flags) : (*ft_real_dup2)(fd, fd2); + if (r >= 0 && fd != fd2) ft_fd_duped(fd, r); + return r; + } + ft_tab_lock(&old); + r = three ? (*ft_real_dup3)(fd, fd2, flags) : (*ft_real_dup2)(fd, fd2); + if (r >= 0) + { + ft_fd_gone_locked(fd2); + if (ft_tfd_known(fd)) ft_fd_duped_locked(fd, fd2); + } + ft_tab_unlock(&old); + return r; +} + +int close(int fd) +{ + ft_fd_gone(fd); + FT_RESOLVE(close); + return (*ft_real_close)(fd); +} +int dup(int fd) +{ + int r; + FT_RESOLVE(dup); + r = (*ft_real_dup)(fd); + if (r >= 0) ft_fd_duped(fd, r); + return r; +} +int dup2(int fd, int fd2) +{ + return ft_dup_onto(fd, fd2, 0, false); +} +int dup3(int fd, int fd2, int flags) +{ + return ft_dup_onto(fd, fd2, flags, true); +} +/* the argument passed on as glibc's own fcntl reads it, a pointer-sized word whatever the command */ +int fcntl(int fd, int cmd, ...) +{ + va_list ap; + void *arg; + int r; + va_start(ap, cmd); + arg = va_arg(ap, void *); + va_end(ap); + FT_RESOLVE(fcntl); + r = (*ft_real_fcntl)(fd, cmd, arg); + if (r >= 0 && (cmd == F_DUPFD || cmd == F_DUPFD_CLOEXEC)) ft_fd_duped(fd, r); + return r; +} +int fcntl64(int fd, int cmd, ...) +{ + va_list ap; + void *arg; + int r; + va_start(ap, cmd); + arg = va_arg(ap, void *); + va_end(ap); + FT_RESOLVE(fcntl64); + r = (*ft_real_fcntl64)(fd, cmd, arg); + if (r >= 0 && (cmd == F_DUPFD || cmd == F_DUPFD_CLOEXEC)) ft_fd_duped(fd, r); + return r; +} +/* one that closes a timerfd runs under the lock, as dup2 does, so a failed call keeps the records. In practice it runs + in a child of fork before exec, where no other thread wants the lock. */ +int close_range(unsigned int first, unsigned int last, int flags) +{ + sigset_t old; + bool timerfd = false; + int r; + FT_RESOLVE(close_range); + if (!(flags & CLOSE_RANGE_CLOEXEC)) + for (unsigned int fd = first; fd <= last && fd < FT_TFD_CLOCKS && !timerfd; fd++) timerfd = ft_tfd_known((int) fd); + if (!timerfd) return (*ft_real_close_range)(first, last, flags); + ft_tab_lock(&old); + r = (*ft_real_close_range)(first, last, flags); + if (r == 0) + for (unsigned int fd = first; fd <= last && fd < FT_TFD_CLOCKS; fd++) + if (ft_tfd_known((int) fd)) ft_fd_gone_locked((int) fd); + ft_tab_unlock(&old); + return r; +} +/* resolved at load, so a first close() in a signal handler doesn't call dlsym */ +static void __attribute__((constructor)) ft_fd_init(void) +{ + FT_RESOLVE(close); + FT_RESOLVE(dup); + FT_RESOLVE(dup2); + FT_RESOLVE(dup3); + FT_RESOLVE(fcntl); + FT_RESOLVE(fcntl64); + FT_RESOLVE(close_range); +} + +/* ---- POSIX timers (unversioned: upstream's GLIBC_2.2 / 2.3.3 versions don't bind glibc 2.34's timer_settime) ---- */ +int timer_create(clockid_t clock, struct sigevent *sev, timer_t *id) +{ + int r; + ftpl_init(); + if (real_timer_create_new == NULL) real_timer_create_new = dlsym(RTLD_NEXT, "timer_create"); + if (real_timer_create_new == NULL) + { + errno = ENOSYS; + return -1; + } + r = (*real_timer_create_new)(clock, sev, id); + if (r == 0 && id != NULL && ft_rt_clock(clock) && ft_faked()) + { + sigset_t old; + struct ft_timer *t; + ft_tab_lock(&old); + t = ft_timer_find_id(*id); + if (t == NULL) t = ft_timer_new(); + if (t != NULL) + { + memset(t, 0, sizeof *t); + t->kind = 2; + t->id = *id; + t->clock = clock; + } + ft_timers_any = 1; + ft_tab_unlock(&old); + } + return r; +} +int timer_delete(timer_t id) +{ + ftpl_init(); + if (real_timer_delete_new == NULL) real_timer_delete_new = dlsym(RTLD_NEXT, "timer_delete"); + if (ft_timers_any) + { + sigset_t old; + struct ft_timer *t; + ft_tab_lock(&old); + if ((t = ft_timer_find_id(id)) != NULL) t->kind = 0; + ft_tab_unlock(&old); + } + return (*real_timer_delete_new)(id); +} +int timer_settime(timer_t id, int flags, const struct itimerspec *nv, struct itimerspec *ov) +{ + struct ft_clock_page s; + struct ft_timer *t; + struct itimerspec prev; + sigset_t old; + bool was = false; + int r; + ftpl_init(); + if (real_timer_settime_new == NULL) real_timer_settime_new = dlsym(RTLD_NEXT, "timer_settime"); + if (real_timer_gettime_new == NULL) real_timer_gettime_new = dlsym(RTLD_NEXT, "timer_gettime"); + if (!ft_timers_any || nv == NULL) return (*real_timer_settime_new)(id, flags, nv, ov); + ft_tab_lock(&old); + t = ft_timer_find_id(id); + if (t != NULL && t->tracked) + { + ft_timer_value(t, &prev); + was = true; + t->tracked = 0; + } + ft_tab_unlock(&old); + if (t == NULL || !ft_uw_active() || !(flags & TIMER_ABSTIME) || !ft_valid_ts(&nv->it_value) + || !ft_valid_ts(&nv->it_interval) || (nv->it_value.tv_sec == 0 && nv->it_value.tv_nsec == 0) || !ft_snap(&s)) + { + r = (*real_timer_settime_new)(id, flags, nv, ov); + if (r == 0 && ov != NULL && was) *ov = prev; + return r; + } + ft_tab_lock(&old); + if (ov != NULL) + { + if (was) + *ov = prev; + else + (*real_timer_gettime_new)(id, ov); + } + t->due = ft_ns(&nv->it_value); + t->interval = ft_ns(&nv->it_interval); + t->tracked = 1; + r = ft_timer_arm(t, &s, true); + ft_tab_unlock(&old); + if (r == 0) ft_helper_start(); + return r; +} +int timer_gettime(timer_t id, struct itimerspec *cur) +{ + sigset_t old; + struct ft_timer *t; + ftpl_init(); + if (real_timer_gettime_new == NULL) real_timer_gettime_new = dlsym(RTLD_NEXT, "timer_gettime"); + if (ft_timers_any && cur != NULL) + { + ft_tab_lock(&old); + t = ft_timer_find_id(id); + if (t != NULL && t->tracked) + { + ft_timer_value(t, cur); + ft_tab_unlock(&old); + return 0; + } + ft_tab_unlock(&old); + } + return (*real_timer_gettime_new)(id, cur); +} + diff --git a/src/agent_env/computers/clock/build.sh b/src/agent_env/computers/clock/build.sh new file mode 100755 index 000000000..b50c2bf39 --- /dev/null +++ b/src/agent_env/computers/clock/build.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Builds agent-env's libfaketime: upstream 0.9.12 from its sha256-pinned tarball, libfaketime.patch applied and the two +# engine files beside it copied in, the multithreaded library with stat and utime faking compiled out (file times stay +# real). Writes OUT/libfaketimeMT.so.1. Needs a C toolchain, make, patch and curl; builds on x86_64 and aarch64. +# build.sh OUT [TARBALL] (TARBALL: a local copy of upstream's v0.9.12 tarball, else it is downloaded) +set -eu +out=$1 +here=$(cd "$(dirname "$0")" && pwd) +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +tarball=${2:-$work/libfaketime.tar.gz} +[ -f "$tarball" ] || curl -fsSL -o "$tarball" https://github.com/wolfcw/libfaketime/archive/refs/tags/v0.9.12.tar.gz +echo "4fc32218697c052adcdc5ee395581f2554ca56d086ac817ced2be0d6f1f8a9fa $tarball" | sha256sum -c - >/dev/null +tar xzf "$tarball" -C "$work" +src=$work/libfaketime-0.9.12 +patch -s -d "$src" -p1 < "$here/libfaketime.patch" +cp "$here/agentenv_clock_page.c" "$here/agentenv_clock_waits.c" "$src/src/" +make -s -C "$src/src" FAKETIME_COMPILE_CFLAGS="-UFAKE_STAT -UFAKE_UTIME" libfaketimeMT.so.1 +mkdir -p "$out" +cp "$src/src/libfaketimeMT.so.1" "$out/" diff --git a/src/agent_env/computers/clock/clock.conf b/src/agent_env/computers/clock/clock.conf new file mode 100644 index 000000000..715987d43 --- /dev/null +++ b/src/agent_env/computers/clock/clock.conf @@ -0,0 +1,11 @@ +# Read once by agent-env's libfaketime as each process loads it. While this file exists it is the only source of these +# settings: the environment changes none of them (AGENTENV_REAL_TIME=1 is the one switch a program's environment has). +# Only the session user's processes are faked; the clock comes from the page the follower writes, never from a cache. +AGENTENV_SESSION_UID=1000 +FAKETIME_TIMESTAMP_FILE=/run/agentenv-clock/clock +FAKETIME_NO_CACHE=1 +FAKETIME_DONT_FAKE_MONOTONIC=1 +FAKETIME_FMT=%s +FAKETIME_DISABLE_SHM=1 +AGENTENV_CLOCK_PAGE=/dev/shm/agentenv-clock +AGENTENV_CLOCK_PAGE_UID=0 diff --git a/src/agent_env/computers/clock/clockpage.py b/src/agent_env/computers/clock/clockpage.py new file mode 100644 index 000000000..9c8238bf0 --- /dev/null +++ b/src/agent_env/computers/clock/clockpage.py @@ -0,0 +1,150 @@ +"""The follower's side of the clock page (version 2) and of the clock file: writes the universe's realtime +clock to both. Elapsed time is real, so the page's monotonic fields are written as the real monotonic clock (identity) +and no library reads them; they stay so the page keeps its 80-byte layout. + + clockpage.py set UNIVERSE RATE [ANCHOR] armed: UNIVERSE (epoch s) at real time ANCHOR (default now), at RATE + clockpage.py clear real time + clockpage.py create OWNER as root at boot: the page, OWNER's from its first byte, unarmed + clockpage.py show + +The page (default /dev/shm/agentenv-clock, a tmpfs, so readers that mapped it see every write) is created once, 0644, +owned by whoever writes it, and rewritten in place: seq odd, the fields and their check, seq even, then FUTEX_WAKE on +seq, which wakes every wait agent-env's libfaketime has for a clock time, and its helper threads, so they plan again. +Readers never see a torn value: they retry (waiting on seq) while seq is odd or changed, or the check doesn't match. +A page a writer died writing (seq left odd) is taken up by the next write, which then writes the universe as given. + +A write that would put the universe less than CONTINUITY_NS behind where the page has it now (a rate change computed +from a universe read a moment before the write) continues it from where it is instead: the write's real time is taken +last, while seq is odd, and the universe at it read from the page being replaced, so no reader of either page ever sees +the clock step back. A larger step back is a set_time and is written as given.""" +import ctypes +import mmap +import os +import platform +import pwd +import stat +import struct +import sys +import time +from decimal import Decimal + +PAGE = os.environ.get("AGENTENV_CLOCK_PAGE", "/dev/shm/agentenv-clock") +FILE = os.environ.get("AGENTENV_CLOCK_FILE", "/run/agentenv-clock/clock") +MAGIC, VERSION = 0x4B434541, 2 +# magic version seq universe_ns anchor_ns rate armed reserved generation check mono_universe_ns mono_anchor_ns +LAYOUT = struct.Struct(" int: + rate_bits = struct.unpack(" mmap.mmap: + try: + fd = os.open(PAGE, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o644) + os.ftruncate(fd, LAYOUT.size) + os.write(fd, LAYOUT.pack(MAGIC, VERSION, 0, 0, 0, 1.0, 0, 0, 0, check(0, 0, 0, 1.0, 0, 0, 0, 0), 0, 0)) + except FileExistsError: + fd = os.open(PAGE, os.O_RDWR) + if os.fstat(fd).st_uid != os.getuid(): + raise SystemExit(f"{PAGE} isn't ours") + if os.fstat(fd).st_size < LAYOUT.size: + os.close(fd) + raise SystemExit(f"{PAGE} is an older page") + m = mmap.mmap(fd, LAYOUT.size) + os.close(fd) + return m + + +def _wake(m: mmap.mmap) -> None: + """FUTEX_WAKE every waiter on seq's low word (a shared futex: other processes' read-only mappings are woken too).""" + if _SYS_FUTEX is None: + return + buf = (ctypes.c_char * LAYOUT.size).from_buffer(m) + try: + _libc.syscall(ctypes.c_long(_SYS_FUTEX), ctypes.c_void_p(ctypes.addressof(buf) + 8), ctypes.c_int(_FUTEX_WAKE), + ctypes.c_int(0x7FFFFFFF), None, None, ctypes.c_int(0)) + finally: + del buf + + +def _universe_at(armed, universe_ns, anchor_ns, rate, now_ns) -> int: + if not armed: + return now_ns + if rate == 0 or now_ns <= anchor_ns: + return universe_ns + return universe_ns + int((now_ns - anchor_ns) * rate) + + +def write(armed: int, universe: float, rate: float, anchor: float) -> dict: + m = open_page() + _, _, seq, o_u, o_a, o_rate, o_armed, _, generation, _, _, _ = LAYOUT.unpack(m[:LAYOUT.size]) + universe_ns, anchor_ns = int(Decimal(str(universe)) * 10**9), int(Decimal(str(anchor)) * 10**9) + if not armed: + rate = 1.0 + generation += 1 + dead = seq & 1 # a writer died mid-write: its fields may be torn, so this write goes on from the next even seq + seq += dead + m[8:16] = struct.pack(" 1 and not dead and 0 < old_now - new_now < CONTINUITY_NS + if continued: + universe_ns, anchor_ns = old_now, now_rt + body = LAYOUT.pack(MAGIC, VERSION, seq + 2, universe_ns, anchor_ns, rate, armed, 0, generation, + check(seq + 2, universe_ns, anchor_ns, rate, armed, generation, now_mono, now_mono), + now_mono, now_mono) + m[16:] = body[16:] + m[8:16] = struct.pack(" 1 else 0, + "continued": bool(continued), "universe_ns": universe_ns, "anchor_ns": anchor_ns} + + +def create(owner: str) -> None: + """As root at boot: the page, ``owner``'s from its first byte (unarmed), replacing one anyone else made.""" + user = pwd.getpwnam(owner) + try: + st = os.lstat(PAGE) + if stat.S_ISREG(st.st_mode) and st.st_uid == user.pw_uid and st.st_size == LAYOUT.size: + return + os.unlink(PAGE) + except FileNotFoundError: + pass + fd = os.open(PAGE, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + try: + os.fchown(fd, user.pw_uid, user.pw_gid) + os.write(fd, LAYOUT.pack(MAGIC, VERSION, 0, 0, 0, 1.0, 0, 0, 0, check(0, 0, 0, 1.0, 0, 0, 0, 0), 0, 0)) + os.fchmod(fd, 0o644) + finally: + os.close(fd) + + +if __name__ == "__main__": + cmd = sys.argv[1] + if cmd == "set": + anchor = float(sys.argv[4]) if len(sys.argv) > 4 else time.time() + print(write(1, float(sys.argv[2]), float(sys.argv[3]), anchor)) + elif cmd == "create": + create(sys.argv[2]) + elif cmd == "clear": + print(write(0, 0.0, 1.0, time.time())) + else: + with open(PAGE, "rb") as f: + print(LAYOUT.unpack(f.read(LAYOUT.size))) diff --git a/src/agent_env/computers/clock/libfaketime.patch b/src/agent_env/computers/clock/libfaketime.patch new file mode 100644 index 000000000..02be05f5c --- /dev/null +++ b/src/agent_env/computers/clock/libfaketime.patch @@ -0,0 +1,1527 @@ +SPDX-License-Identifier: GPL-2.0-only +agent-env's changes to libfaketime 0.9.12 (src/), applied from the tarball's root with patch -p1 by build.sh, which +also copies in agentenv_clock_page.c and agentenv_clock_waits.c, included where marked below. A change to libfaketime, +so licensed as libfaketime is, under the GNU General Public License version 2 (COPYING). + +diff -ruN a/src/libfaketime.c b/src/libfaketime.c +--- a/src/libfaketime.c ++++ b/src/libfaketime.c +@@ -231,11 +231,14 @@ + static int (*real___clock_gettime) (clockid_t clk_id, struct timespec *tp); + #endif + #ifdef FAKE_PTHREAD +-static int (*real_pthread_cond_timedwait_225) (pthread_cond_t *, pthread_mutex_t*, struct timespec *); +-static int (*real_pthread_cond_timedwait_232) (pthread_cond_t *, pthread_mutex_t*, struct timespec *); +-static int (*real_pthread_cond_init_232) (pthread_cond_t *restrict, const pthread_condattr_t *restrict); +-static int (*real_pthread_cond_destroy_232) (pthread_cond_t *); +-static pthread_rwlock_t monotonic_conds_lock; ++static int (*real_pthread_cond_timedwait_225) (pthread_cond_t *, pthread_mutex_t*, const struct timespec *); ++static int (*real_pthread_cond_timedwait_232) (pthread_cond_t *, pthread_mutex_t*, const struct timespec *); ++static int (*real_pthread_cond_clockwait) (pthread_cond_t *, pthread_mutex_t*, clockid_t, const struct timespec *); ++static int (*real_pthread_mutex_timedlock) (pthread_mutex_t *, const struct timespec *); ++static int (*real_pthread_mutex_clocklock) (pthread_mutex_t *, clockid_t, const struct timespec *); ++#endif ++#ifdef FAKE_TIMERS ++static int (*real_timerfd_create) (int, int); + #endif + + #ifndef __APPLE__ +@@ -334,6 +337,7 @@ + bool str_array_contains(const char *haystack, const char *needle); + void *ft_dlvsym(void *handle, const char *symbol, const char *version, const char *full_name, char *ignore_list, bool should_debug_dlsym); + ++#include "agentenv_clock_page.c" /* agent-env: settings, who is faked, the clock page */ + /** Semaphore protecting shared data */ + static sem_t *shared_sem = NULL; + +@@ -420,6 +424,29 @@ + static bool parse_config_file = true; + + static void ft_cleanup (void) __attribute__ ((destructor)); ++ ++#ifdef PTHREAD_SINGLETHREADED_TIME ++/* agentenv: the lock fake_clock_gettime holds, at file scope so a fork() can't hand the child a held lock. */ ++static pthread_mutex_t time_mutex = PTHREAD_MUTEX_INITIALIZER; ++static sigset_t ft_fork_mask; ++static void ft_atfork_prepare(void) ++{ ++ sigset_t all; ++ sigfillset(&all); ++ pthread_sigmask(SIG_SETMASK, &all, &ft_fork_mask); ++ pthread_mutex_lock(&time_mutex); ++} ++static void ft_atfork_parent(void) ++{ ++ pthread_mutex_unlock(&time_mutex); ++ pthread_sigmask(SIG_SETMASK, &ft_fork_mask, NULL); ++} ++static void ft_atfork_child(void) ++{ ++ pthread_mutex_init(&time_mutex, NULL); ++ pthread_sigmask(SIG_SETMASK, &ft_fork_mask, NULL); ++} ++#endif + static void ftpl_init (void) __attribute__ ((constructor)); + + +@@ -519,7 +546,7 @@ + + static void ft_shm_destroy(void) + { +- char sem_name[256], shm_name[256], *ft_shared_env = getenv("FAKETIME_SHARED"); ++ char sem_name[256], shm_name[256], *ft_shared_env = ft_getenv("FAKETIME_SHARED"); + + if (ft_shared_env != NULL) + { +@@ -553,7 +580,7 @@ + static void ft_shm_really_init (void) + { + int ticks_shm_fd; +- char sem_name[256], shm_name[256], *ft_shared_env = getenv("FAKETIME_SHARED"); ++ char sem_name[256], shm_name[256], *ft_shared_env = ft_getenv("FAKETIME_SHARED"); + sem_t *shared_semR = NULL; + static int nt=1; + +@@ -561,7 +588,7 @@ + if (ft_shared_env == NULL) + { + ft_shm_create(); +- ft_shared_env = getenv("FAKETIME_SHARED"); ++ ft_shared_env = ft_getenv("FAKETIME_SHARED"); + } + + /* check for stale semaphore / shared memory information */ +@@ -575,7 +602,7 @@ + if (SEM_FAILED == (shared_semR = sem_open(sem_name, 0))) /* gone stale? */ + { + ft_shm_create(); +- ft_shared_env = getenv("FAKETIME_SHARED"); ++ ft_shared_env = ft_getenv("FAKETIME_SHARED"); + } + else + { +@@ -631,7 +658,7 @@ + exit(1); + } + } +- if (getenv("FAKETIME_FLSHM") != NULL) ++ if (ft_getenv("FAKETIME_FLSHM") != NULL) + { /* force the deletion of the shm sync env variable */ + unsetenv("FAKETIME_SHARED"); + } +@@ -653,12 +680,6 @@ + sem_close(shared_sem); + shared_sem = NULL; + } +-#ifdef FAKE_PTHREAD +- if (pthread_rwlock_destroy(&monotonic_conds_lock) != 0) { +- fprintf(stderr, "libfaketime: In ft_cleanup(), monotonic_conds_lock destroy failed\n"); +- exit(-1); +- } +-#endif + if (shmCreator == true) ft_shm_destroy(); + } + +@@ -672,8 +693,8 @@ + static void get_fake_monotonic_setting(int* current_value) + { + char *tmp_env; +- if ((tmp_env = getenv("FAKETIME_DONT_FAKE_MONOTONIC")) != NULL +- || (tmp_env = getenv("DONT_FAKE_MONOTONIC")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_DONT_FAKE_MONOTONIC")) != NULL ++ || (tmp_env = ft_getenv("DONT_FAKE_MONOTONIC")) != NULL) + { + if (0 == strcmp(tmp_env, "1")) + { +@@ -1140,7 +1161,7 @@ + #endif /* ifndef __APPLE__ */ + #endif /* ifdef FAKE_STAT */ + +-#ifdef STATX_TYPE ++#if defined STATX_TYPE && defined FAKE_STAT + static inline void fake_statx_timestamp(struct statx_timestamp* p) + { + struct timespec t = {p->tv_sec,p->tv_nsec}; +@@ -1329,6 +1350,7 @@ + * Contributed by Balint Reczey in v0.9.5 + */ + ++#include "agentenv_clock_waits.c" /* agent-env: universe-time reads and waits */ + #ifdef FAKE_SLEEP + /* + * Faked nanosleep() +@@ -1343,6 +1365,7 @@ + struct timespec real_req; + + ftpl_init(); ++ if (real_nanosleep != NULL) return (*real_nanosleep)(req, rem); + if (real_nanosleep == NULL) + { + return -1; +@@ -1395,13 +1418,39 @@ + struct timespec real_req; + + ftpl_init(); ++ if (real_clock_nanosleep != NULL && (!(flags & TIMER_ABSTIME) || !ft_rt_clock(clock_id) || !ft_faked())) ++ return (*real_clock_nanosleep)(clock_id, flags, req, rem); ++ if (ft_uw_active() && ft_valid_ts(req)) ++ { ++ struct ft_clock_page s; ++ if (ft_snap(&s)) return ft_rt_sleep(&s, ft_ns(req)); ++ } + if (real_clock_nanosleep == NULL) + { + return -1; + } + if (req != NULL) + { +- if (flags & TIMER_ABSTIME) /* sleep until absolute time */ ++ if ((flags & TIMER_ABSTIME) && clock_id == CLOCK_REALTIME && ft_translates_wait(req)) ++ { ++ struct timespec mono, slice; ++ unsigned long gen; ++ bool last; ++ ft_deadline_from_fake(CLOCK_MONOTONIC, req, &mono); ++ gen = ft_tl_gen; ++ for (;;) ++ { ++ last = ft_slice_end(&mono, &slice); ++ DONT_FAKE_TIME(result = (*real_clock_nanosleep)(CLOCK_MONOTONIC, TIMER_ABSTIME, &slice, NULL)); ++ if (result != 0) return result; ++ if (!ft_deadline_moved(req, &mono, &gen) && last) return 0; ++ } ++ } ++ if ((flags & TIMER_ABSTIME) && (ft_disabled || dont_fake || clock_id == CLOCK_REALTIME)) ++ { ++ real_req = *req; ++ } ++ else if (flags & TIMER_ABSTIME) /* sleep until absolute time */ + { + struct timespec tdiff, timeadj; + timespecsub(req, &user_faked_time_timespec, &timeadj); +@@ -1482,6 +1531,7 @@ + int result; + + ftpl_init(); ++ if (real_usleep != NULL) return (*real_usleep)(usec); + if (user_rate_set && !dont_fake) + { + struct timespec real_req; +@@ -1531,6 +1581,7 @@ + #endif + { + ftpl_init(); ++ if (real_sleep != NULL) return (*real_sleep)(seconds); + if (user_rate_set && !dont_fake) + { + if (real_nanosleep == NULL) +@@ -1551,7 +1602,7 @@ + else + { + int result; +- struct timespec real_req = {seconds, 0}, rem; ++ struct timespec real_req = {seconds, 0}, rem = {0, 0}; + timespecmul(&real_req, 1.0 / user_rate, &real_req); + #ifdef MACOS_DYLD_INTERPOSE + DONT_FAKE_TIME(result = (*nanosleep)(&real_req, &rem)); +@@ -1604,6 +1655,8 @@ + { + return -1; + } ++ (void) seconds_real; ++ return (*real_alarm)(seconds); + + #ifdef MACOS_DYLD_INTERPOSE + DONT_FAKE_TIME(ret = (*alarm)(seconds_real)); +@@ -1627,6 +1680,7 @@ + { + return -1; + } ++ return (*real_ppoll)(fds, nfds, timeout_ts, sigmask); + if (timeout_ts != NULL) + { + if (user_rate_set && !dont_fake && ((timeout_ts->tv_sec > 0) || (timeout_ts->tv_nsec > 0))) +@@ -1662,6 +1716,7 @@ + { + return -1; + } ++ return (*real_epoll_wait)(epfd, events, maxevents, timeout); + if (user_rate_set && !dont_fake && timeout > 0) + { + real_timeout = (int) timeout * 1.0/user_rate; +@@ -1686,6 +1741,7 @@ + { + return -1; + } ++ return (*real_epoll_pwait)(epfd, events, maxevents, timeout, sigmask); + if (user_rate_set && !dont_fake && timeout > 0) + { + real_timeout = (int) timeout * 1.0/user_rate; +@@ -1715,6 +1771,8 @@ + { + return -1; + } ++ (void) timeout_real; ++ return (*real_poll)(fds, nfds, timeout); + + #ifdef MACOS_DYLD_INTERPOSE + DONT_FAKE_TIME(ret = (*poll)(fds, nfds, timeout_real)); +@@ -1748,6 +1806,7 @@ + { + return -1; + } ++ return (*real_select)(nfds, readfds, writefds, errorfds, timeout); + + if (timeout != NULL) + { +@@ -1812,6 +1871,7 @@ + { + return -1; + } ++ return (*real_pselect)(nfds, readfds, writefds, errorfds, timeout, sigmask); + + if (timeout != NULL) + { +@@ -1831,98 +1891,63 @@ + } + #endif + +-int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout) ++static int ft_sem_wait_until(sem_t *sem, const struct timespec *fake_abs) + { +- int result; +- struct timespec real_abs_timeout, *real_abs_timeout_pt; +- +- /* sanity check */ +- if (abs_timeout == NULL) +- { +- return -1; +- } +- +- if (!CHECK_MISSING_REAL(sem_timedwait)) return -1; +- +- if (!dont_fake) +- { +- struct timespec tdiff, timeadj; +- +- timespecsub(abs_timeout, &user_faked_time_timespec, &tdiff); +- +- if (user_rate_set) ++ struct timespec mono, slice; ++ unsigned long gen; ++ bool last; ++ int result; ++ ft_deadline_from_fake(CLOCK_MONOTONIC, fake_abs, &mono); ++ gen = ft_tl_gen; ++ for (;;) ++ { ++ last = ft_slice_end(&mono, &slice); ++ DONT_FAKE_TIME(result = (*real_sem_clockwait)(sem, CLOCK_MONOTONIC, &slice)); ++ if (result == 0 || errno != ETIMEDOUT) return result; ++ if (!ft_deadline_moved(fake_abs, &mono, &gen) && last) + { +- timespecmul(&tdiff, user_rate, &timeadj); ++ errno = ETIMEDOUT; ++ return -1; + } +- else +- { +- timeadj = tdiff; +- } +- timespecadd(&ftpl_starttime.real, &timeadj, &real_abs_timeout); +- real_abs_timeout_pt = &real_abs_timeout; +- } +- else +- { +- /* cast away constness */ +- real_abs_timeout_pt = (struct timespec *)abs_timeout; + } +- +- DONT_FAKE_TIME(result = (*real_sem_timedwait)(sem, real_abs_timeout_pt)); +- return result; + } + +-/* EXPERIMENTAL */ +-int sem_clockwait(sem_t *sem, clockid_t clockid, const struct timespec *abstime) ++int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout) + { + int result; +- struct timespec real_abstime, *real_abstime_pt; +- +- if ((!fake_monotonic_clock) && (clockid == CLOCK_MONOTONIC)) ++ ftpl_init(); ++ if (!CHECK_MISSING_REAL(sem_timedwait)) return -1; ++ if (ft_uw_active() && ft_translates_wait(abs_timeout)) + { +- DONT_FAKE_TIME(result = (*real_sem_clockwait)(sem, clockid, abstime)); +- return result; ++ bool handled; ++ result = ft_u_sem_wait(sem, ft_ns(abs_timeout), &handled); ++ if (handled) return result; + } +- +- /* sanity check */ +- if (abstime == NULL) ++ if (!ft_translates_wait(abs_timeout) || real_sem_clockwait == NULL) + { +- return -1; ++ DONT_FAKE_TIME(result = (*real_sem_timedwait)(sem, abs_timeout)); ++ return result; + } ++ return ft_sem_wait_until(sem, abs_timeout); ++} + ++int sem_clockwait(sem_t *sem, clockid_t clockid, const struct timespec *abstime) ++{ ++ int result; ++ ftpl_init(); + if (!CHECK_MISSING_REAL(sem_clockwait)) return -1; +- +- if (!dont_fake) ++ if (ft_uw_active() && ft_translates_wait(abstime) && clockid == CLOCK_REALTIME) + { +- struct timespec tdiff, timeadj; +- +- timespecsub(abstime, &user_faked_time_timespec, &tdiff); +- +- if (user_rate_set) +- { +- timespecmul(&tdiff, 1.0 / user_rate, &timeadj); +- } +- else +- { +- timeadj = tdiff; +- } +- if (clockid == CLOCK_REALTIME) +- { +- timespecadd(&ftpl_starttime.real, &timeadj, &real_abstime); +- } +- if (clockid == CLOCK_MONOTONIC) +- { +- timespecadd(&ftpl_starttime.mon, &timeadj, &real_abstime); +- } +- real_abstime_pt = &real_abstime; ++ bool handled; ++ result = ft_u_sem_wait(sem, ft_ns(abstime), &handled); ++ if (handled) return result; + } +- else ++ if (clockid != CLOCK_REALTIME || !ft_translates_wait(abstime)) + { +- /* cast away constness */ +- real_abstime_pt = (struct timespec *)abstime; ++ DONT_FAKE_TIME(result = (*real_sem_clockwait)(sem, clockid, abstime)); ++ return result; + } +- +- DONT_FAKE_TIME(result = (*real_sem_clockwait)(sem, clockid, real_abstime_pt)); +- return result; ++ return ft_sem_wait_until(sem, abstime); + } + #endif + +@@ -1949,6 +1974,23 @@ + * Faked timer_settime() + * Does not affect timer speed when stepping clock with each time() call. + */ ++#ifdef __linux__ ++/* agentenv: the clock each timerfd was made on (ft_tfd_clock); a record still naming the number is stale. */ ++int timerfd_create(int clockid, int flags) ++{ ++ int fd; ++ ftpl_init(); ++ if (real_timerfd_create == NULL) return -1; ++ fd = real_timerfd_create(clockid, flags); ++ if (fd >= 0 && fd < FT_TFD_CLOCKS) ++ { ++ ft_fd_gone(fd); ++ ft_tfd_clock[fd] = clockid + 1; ++ } ++ return fd; ++} ++#endif ++ + static int + timer_settime_common(timer_t_or_int timerid, int flags, + const struct itimerspec *new_value, +@@ -1977,18 +2019,11 @@ + { + if (flags & abstime_flag) + { +- struct timespec tdiff, timeadj; +- timespecsub(&new_value->it_value, &user_faked_time_timespec, &timeadj); +- if (user_rate_set) +- { +- timespecmul(&timeadj, 1.0/user_rate, &tdiff); +- } ++ if (ft_disabled || (compat == FT_FD && ft_tfd_known(timerid.int_member) ++ && !ft_rt_clock(ft_tfd_clock[timerid.int_member] - 1))) ++ new_real.it_value = new_value->it_value; + else +- { +- tdiff = timeadj; +- } +- /* only CLOCK_REALTIME is handled */ +- timespecadd(&ftpl_starttime.real, &tdiff, &new_real.it_value); ++ ft_deadline_from_fake(CLOCK_REALTIME, &new_value->it_value, &new_real.it_value); + } + else + { +@@ -2061,47 +2096,6 @@ + return result; + } + +-/* +- * Faked timer_settime() compatible with implementation in GLIBC 2.2 +- */ +-int timer_settime_22(int timerid, int flags, +- const struct itimerspec *new_value, +- struct itimerspec *old_value) +-{ +- ftpl_init(); +- if (real_timer_settime_22 == NULL) +- { +- return -1; +- } +- else +- { +- timer_t_or_int temp; +- temp.int_member = timerid; +- return (timer_settime_common(temp, flags, new_value, old_value, +- FT_COMPAT_GLIBC_2_2, TIMER_ABSTIME)); +- } +-} +- +-/* +- * Faked timer_settime() compatible with implementation in GLIBC 2.3.3 +- */ +-int timer_settime_233(timer_t timerid, int flags, +- const struct itimerspec *new_value, +- struct itimerspec *old_value) +-{ +- ftpl_init(); +- if (real_timer_settime_233 == NULL) +- { +- return -1; +- } +- else +- { +- timer_t_or_int temp; +- temp.timer_t_member = timerid; +- return (timer_settime_common(temp, flags, new_value, old_value, +- FT_COMPAT_GLIBC_2_3_3, TIMER_ABSTIME)); +- } +-} + + /* + * Faked timer_gettime() +@@ -2151,48 +2145,6 @@ + return result; + } + +-/* +- * Faked timer_gettime() compatible with implementation in GLIBC 2.2 +- */ +-int timer_gettime_22(timer_t timerid, struct itimerspec *curr_value) +-{ +- ftpl_init(); +- if (real_timer_gettime_22 == NULL) +- { +- return -1; +- } +- else +- { +- timer_t_or_int temp; +- temp.timer_t_member = timerid; +- return (timer_gettime_common(temp, curr_value, +- FT_COMPAT_GLIBC_2_2)); +- } +-} +- +-/* +- * Faked timer_gettime() compatible with implementation in GLIBC 2.3.3 +- */ +-int timer_gettime_233(timer_t timerid, struct itimerspec *curr_value) +-{ +- ftpl_init(); +- if (real_timer_gettime_233 == NULL) +- { +- return -1; +- } +- else +- { +- timer_t_or_int temp; +- temp.timer_t_member = timerid; +- return (timer_gettime_common(temp, curr_value, +- FT_COMPAT_GLIBC_2_3_3)); +- } +-} +- +-__asm__(".symver timer_gettime_22, timer_gettime@GLIBC_2.2"); +-__asm__(".symver timer_gettime_233, timer_gettime@@GLIBC_2.3.3"); +-__asm__(".symver timer_settime_22, timer_settime@GLIBC_2.2"); +-__asm__(".symver timer_settime_233, timer_settime@@GLIBC_2.3.3"); + + #ifdef __linux__ + /* +@@ -2203,6 +2155,14 @@ + struct itimerspec *old_value) + { + ftpl_init(); ++ if (real_timerfd_settime != NULL && ft_faked()) ++ { ++ bool handled = false; ++ int r = ft_uw_active() ? ft_u_timerfd_settime(fd, flags, new_value, old_value, &handled) : 0; ++ if (handled) return r; ++ if (!(flags & TFD_TIMER_ABSTIME) || fd < 0 || fd >= FT_TFD_CLOCKS || !ft_rt_clock(ft_tfd_clock[fd] - 1)) ++ return (*real_timerfd_settime)(fd, flags, new_value, old_value); ++ } + if (real_timerfd_settime == NULL) + { + return -1; +@@ -2222,6 +2182,13 @@ + int timerfd_gettime(int fd, struct itimerspec *curr_value) + { + ftpl_init(); ++ if (real_timerfd_gettime != NULL && ft_faked()) ++ { ++ bool handled = false; ++ int r = ft_u_timerfd_gettime(fd, curr_value, &handled); ++ if (handled) return r; ++ return (*real_timerfd_gettime)(fd, curr_value); ++ } + if (real_timerfd_gettime == NULL) + { + return -1; +@@ -2379,6 +2346,8 @@ + #endif + if (result == -1) return result; /* original function failed */ + ++ if (!ft_universe_clock(clk_id)) return result; ++ + /* pass the real current time to our faking version, overwriting it */ + if (fake_monotonic_clock || (clk_id != CLOCK_MONOTONIC && clk_id != CLOCK_MONOTONIC_RAW + #ifdef CLOCK_MONOTONIC_COARSE +@@ -2495,12 +2464,25 @@ + return; + } + ++ /* agentenv: a specification without "x" is at rate 1, not at the rate of the one before it */ ++ double prev_rate = user_rate; ++ bool prev_rate_set = user_rate_set; ++ if (user_faked_time[0] == 0) return; ++ user_rate = 1.0; ++ user_rate_set = false; ++ + /* check whether the user gave us an absolute time to fake */ + switch (user_faked_time[0]) + { + + default: /* Try and interpret this as a specified time */ + if (ft_mode != FT_NOOP) ft_mode = FT_FREEZE; ++ if (strcmp(user_faked_time_fmt, "%s") == 0) ++ { ++ if (ft_parse_epoch(user_faked_time, &user_faked_time_timespec) == NULL) goto parse_failed; ++ user_faked_time_set = true; ++ goto parse_modifiers; ++ } + user_faked_time_tm.tm_isdst = -1; + nstime_str = strptime(user_faked_time, user_faked_time_fmt, &user_faked_time_tm); + +@@ -2518,9 +2500,7 @@ + } + else + { +- fprintf(stderr, "libfaketime: In parse_ft_string(), failed to parse FAKETIME timestamp.\n" +- "Please check specification %s with format %s\n", user_faked_time, user_faked_time_fmt); +- exit(EXIT_FAILURE); ++ goto parse_failed; + } + goto parse_modifiers; + break; +@@ -2548,6 +2528,11 @@ + /* Contributed by David North, TDI in version 0.7 */ + case '@': /* Specific time, but clock along relative to that starttime */ + ft_mode = FT_START_AT; ++ if (strcmp(user_faked_time_fmt, "%s") == 0) ++ { ++ if (ft_parse_epoch(&user_faked_time[1], &user_faked_time_timespec) == NULL) goto parse_failed; ++ goto parse_anchor; ++ } + user_faked_time_tm.tm_isdst = -1; + nstime_str = strptime(&user_faked_time[1], user_faked_time_fmt, &user_faked_time_tm); + if (NULL != nstime_str) +@@ -2563,12 +2548,17 @@ + } + else + { +- fprintf(stderr, "libfaketime: In parse_ft_string(), failed to parse FAKETIME timestamp.\n"); +- exit(EXIT_FAILURE); ++ goto parse_failed; + } + +- /* Reset starttime */ +- if (NULL == getenv("FAKETIME_DONT_RESET")) ++parse_anchor: ++ /* agentenv: "^" is the real time at which the clock read the time given, so every process reads the ++ same time whenever it started, with no shared memory; without it, the process's own start, as before. */ ++ if (strchr(user_faked_time, '^') != NULL) ++ { ++ if (ft_parse_epoch(strchr(user_faked_time, '^') + 1, &ftpl_starttime.real) == NULL) goto parse_failed; ++ } ++ else if (NULL == ft_getenv("FAKETIME_DONT_RESET")) + reset_time(); + goto parse_modifiers; + break; +@@ -2577,17 +2567,17 @@ + ft_mode = FT_START_AT; + struct stat master_file_stats; + int ret; +- if (NULL == getenv("FAKETIME_FOLLOW_FILE")) ++ if (NULL == ft_getenv("FAKETIME_FOLLOW_FILE")) + { + fprintf(stderr, "libfaketime: %% operator in FAKETIME setting requires environment variable FAKETIME_FOLLOW_FILE set.\n"); + exit(1); + } + else + { +- DONT_FAKE_TIME(ret = stat(getenv("FAKETIME_FOLLOW_FILE"), &master_file_stats)); ++ DONT_FAKE_TIME(ret = stat(ft_getenv("FAKETIME_FOLLOW_FILE"), &master_file_stats)); + if (ret == -1) + { +- fprintf(stderr, "libfaketime: Cannot get timestamp of file %s as requested by %% operator.\n", getenv("FAKETIME_FOLLOW_FILE")); ++ fprintf(stderr, "libfaketime: Cannot get timestamp of file %s as requested by %% operator.\n", ft_getenv("FAKETIME_FOLLOW_FILE")); + exit(1); + } + else +@@ -2596,7 +2586,7 @@ + user_faked_time_timespec.tv_nsec = 0; + } + } +- if (NULL == getenv("FAKETIME_DONT_RESET")) ++ if (NULL == ft_getenv("FAKETIME_DONT_RESET")) + reset_time(); + goto parse_modifiers; + break; +@@ -2609,7 +2599,15 @@ + { + user_rate = atof(strchr(user_faked_time, 'x')+1); + user_rate_set = true; +- if (NULL != getenv("FAKETIME_XRESET")) { ++ if (user_rate <= 0.0) ++ { ++ /* agentenv: rate 0 is the time given, frozen; sleeps and waits then take the real time they ask for */ ++ ft_mode = FT_FREEZE; ++ user_faked_time_set = true; ++ user_rate = 1.0; ++ user_rate_set = false; ++ } ++ if (NULL != ft_getenv("FAKETIME_XRESET")) { + if (ftpl_timecache.real.tv_nsec >= 0) { + user_faked_time_timespec.tv_sec = ftpl_faketimecache.real.tv_sec; + user_faked_time_timespec.tv_nsec = ftpl_faketimecache.real.tv_nsec; +@@ -2639,9 +2637,15 @@ + + strncpy(user_faked_time_saved, user_faked_time, BUFFERLEN-1); + user_faked_time_saved[BUFFERLEN-1] = 0; ++ ft_spec_gen++; + #ifdef DEBUG + fprintf(stderr, "new FAKETIME: %s\n", user_faked_time_saved); + #endif ++ return; ++ ++parse_failed: ++ user_rate = prev_rate; ++ user_rate_set = prev_rate_set; + } + + +@@ -2666,8 +2670,8 @@ + const char *progname = __progname; + #endif + +- char *ignore_list = getenv("FAKETIME_IGNORE_SYMBOLS"); +- bool should_debug_dlsym = getenv("FAKETIME_DEBUG_DLSYM"); ++ char *ignore_list = ft_getenv("FAKETIME_IGNORE_SYMBOLS"); ++ bool should_debug_dlsym = ft_getenv("FAKETIME_DEBUG_DLSYM"); + #define dlsym(handle, symbol) ft_dlvsym(handle, symbol, NULL, symbol, ignore_list, should_debug_dlsym) + #define dlvsym(handle, symbol, version) ft_dlvsym(handle, symbol, version, symbol "@" version, ignore_list, should_debug_dlsym) + +@@ -2749,32 +2753,25 @@ + #endif + + #ifdef FAKE_PTHREAD +- + #ifdef __GLIBC__ + real_pthread_cond_timedwait_225 = dlvsym(RTLD_NEXT, "pthread_cond_timedwait", "GLIBC_2.2.5"); +- + real_pthread_cond_timedwait_232 = dlvsym(RTLD_NEXT, "pthread_cond_timedwait", "GLIBC_2.3.2"); +- real_pthread_cond_init_232 = dlvsym(RTLD_NEXT, "pthread_cond_init", "GLIBC_2.3.2"); +- real_pthread_cond_destroy_232 = dlvsym(RTLD_NEXT, "pthread_cond_destroy", "GLIBC_2.3.2"); ++ real_pthread_cond_clockwait = dlvsym(RTLD_NEXT, "pthread_cond_clockwait", "GLIBC_2.30"); ++ real_pthread_mutex_clocklock = dlvsym(RTLD_NEXT, "pthread_mutex_clocklock", "GLIBC_2.30"); + #endif +- + if (NULL == real_pthread_cond_timedwait_232) + { +- real_pthread_cond_timedwait_232 = dlsym(RTLD_NEXT, "pthread_cond_timedwait"); ++ real_pthread_cond_timedwait_232 = dlsym(RTLD_NEXT, "pthread_cond_timedwait"); + } +- if (NULL == real_pthread_cond_init_232) ++ if (NULL == real_pthread_cond_clockwait) + { +- real_pthread_cond_init_232 = dlsym(RTLD_NEXT, "pthread_cond_init"); ++ real_pthread_cond_clockwait = dlsym(RTLD_NEXT, "pthread_cond_clockwait"); + } +- if (NULL == real_pthread_cond_destroy_232) ++ if (NULL == real_pthread_mutex_clocklock) + { +- real_pthread_cond_destroy_232 = dlsym(RTLD_NEXT, "pthread_cond_destroy"); +- } +- +- if (pthread_rwlock_init(&monotonic_conds_lock,NULL) != 0) { +- fprintf(stderr,"monotonic_conds_lock init failed\n"); +- exit(-1); ++ real_pthread_mutex_clocklock = dlsym(RTLD_NEXT, "pthread_mutex_clocklock"); + } ++ real_pthread_mutex_timedlock = dlsym(RTLD_NEXT, "pthread_mutex_timedlock"); + #endif + #ifdef __APPLE__ + real_clock_get_time = dlsym(RTLD_NEXT, "clock_get_time"); +@@ -2814,6 +2811,7 @@ + #endif + #ifdef __linux__ + real_timerfd_gettime = dlsym(RTLD_NEXT, "timerfd_gettime"); ++ real_timerfd_create = dlsym(RTLD_NEXT, "timerfd_create"); + real_timerfd_settime = dlsym(RTLD_NEXT, "timerfd_settime"); + #endif + #endif +@@ -2826,16 +2824,42 @@ + #undef dlsym + #undef dlvsym + ++ /* agentenv: a process of another user than the session's, or one started with AGENTENV_REAL_TIME=1, stays on real ++ time; the marker is taken out of its environment, so what it starts is faked. */ ++ { ++ const char *uid = ft_getenv("AGENTENV_SESSION_UID"); ++ const char *marker = getenv("AGENTENV_REAL_TIME"); ++ if (uid != NULL && *uid != 0 && (uid_t) strtoul(uid, NULL, 10) != getuid()) ++ ft_disabled = true; ++ if (marker != NULL && strcmp(marker, "1") == 0) ++ { ++ ft_disabled = true; ++ ft_scrub_marker(); ++ } ++ if ((tmp_env = ft_getenv("AGENTENV_WAIT_SLICE_MS")) != NULL && atol(tmp_env) > 0) ++ ft_wait_slice_ns = atol(tmp_env) * 1000000L; ++ } ++ if (ft_disabled) ++ { ++ ft_mode = FT_NOOP; ++ dont_fake = dont_fake_final; ++ return; ++ } ++#ifdef PTHREAD_SINGLETHREADED_TIME ++ pthread_atfork(ft_atfork_prepare, ft_atfork_parent, ft_atfork_child); ++#endif ++ ft_page_open(); ++ + #ifdef FAKE_STATELESS + if (0) ft_shm_init(); + #else +- tmp_env = getenv("FAKETIME_DISABLE_SHM"); ++ tmp_env = ft_getenv("FAKETIME_DISABLE_SHM"); + if (!tmp_env || tmp_env[0] == '0') { + ft_shm_init(); + } + #endif + #ifdef FAKE_STAT +- if (getenv("NO_FAKE_STAT")!=NULL) ++ if (ft_getenv("NO_FAKE_STAT")!=NULL) + { + fake_stat_disabled = 1; //Note that this is NOT re-checked + } +@@ -2843,7 +2867,7 @@ + #if defined FAKE_FILE_TIMESTAMPS + #ifdef FAKE_UTIME + // fake_utime_disabled is 0 by default +- if ((tmp_env = getenv("FAKE_UTIME")) != NULL) //Note that this is NOT re-checked ++ if ((tmp_env = ft_getenv("FAKE_UTIME")) != NULL) //Note that this is NOT re-checked + { + if (!*tmp_env || *tmp_env == 'y' || *tmp_env == 'Y' || *tmp_env == 't' || *tmp_env == 'T') + { /* an empty string or a yes/true value turns off disabling */ +@@ -2860,11 +2884,11 @@ + #endif + #endif + +- if ((tmp_env = getenv("FAKETIME_CACHE_DURATION")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_CACHE_DURATION")) != NULL) + { + cache_duration = atoi(tmp_env); + } +- if ((tmp_env = getenv("FAKETIME_NO_CACHE")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_NO_CACHE")) != NULL) + { + if (0 == strcmp(tmp_env, "1")) + { +@@ -2875,7 +2899,7 @@ + /* Check whether we actually should be faking the returned timestamp. */ + + /* We can prevent faking time for specified commands */ +- if ((tmp_env = getenv("FAKETIME_SKIP_CMDS")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_SKIP_CMDS")) != NULL) + { + char *skip_cmd, *saveptr, *tmpvar; + /* Don't mess with the env variable directly. */ +@@ -2904,12 +2928,12 @@ + } + + /* We can limit faking time to specified commands */ +- if ((tmp_env = getenv("FAKETIME_ONLY_CMDS")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_ONLY_CMDS")) != NULL) + { + char *only_cmd, *saveptr, *tmpvar; + bool cmd_matched = false; + +- if (getenv("FAKETIME_SKIP_CMDS") != NULL) ++ if (ft_getenv("FAKETIME_SKIP_CMDS") != NULL) + { + fprintf(stderr, "Error: Both FAKETIME_SKIP_CMDS and FAKETIME_ONLY_CMDS can't be set.\n"); + exit(EXIT_FAILURE); +@@ -2941,44 +2965,44 @@ + } + } + +- if ((tmp_env = getenv("FAKETIME_START_AFTER_SECONDS")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_START_AFTER_SECONDS")) != NULL) + { + ft_start_after_secs = atol(tmp_env); + limited_faking = true; + } +- if ((tmp_env = getenv("FAKETIME_STOP_AFTER_SECONDS")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_STOP_AFTER_SECONDS")) != NULL) + { + ft_stop_after_secs = atol(tmp_env); + limited_faking = true; + } +- if ((tmp_env = getenv("FAKETIME_START_AFTER_NUMCALLS")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_START_AFTER_NUMCALLS")) != NULL) + { + ft_start_after_ncalls = atol(tmp_env); + limited_faking = true; + } +- if ((tmp_env = getenv("FAKETIME_STOP_AFTER_NUMCALLS")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_STOP_AFTER_NUMCALLS")) != NULL) + { + ft_stop_after_ncalls = atol(tmp_env); + limited_faking = true; + } + + /* check whether we should spawn an external command */ +- if ((tmp_env = getenv("FAKETIME_SPAWN_TARGET")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_SPAWN_TARGET")) != NULL) + { + spawnsupport = true; +- (void) strncpy(ft_spawn_target, getenv("FAKETIME_SPAWN_TARGET"), sizeof(ft_spawn_target) - 1); ++ (void) strncpy(ft_spawn_target, ft_getenv("FAKETIME_SPAWN_TARGET"), sizeof(ft_spawn_target) - 1); + ft_spawn_target[sizeof(ft_spawn_target) - 1] = 0; +- if ((tmp_env = getenv("FAKETIME_SPAWN_SECONDS")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_SPAWN_SECONDS")) != NULL) + { + ft_spawn_secs = atol(tmp_env); + } +- if ((tmp_env = getenv("FAKETIME_SPAWN_NUMCALLS")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_SPAWN_NUMCALLS")) != NULL) + { + ft_spawn_ncalls = atol(tmp_env); + } + } + +- if ((tmp_env = getenv("FAKETIME_SAVE_FILE")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_SAVE_FILE")) != NULL) + { + if (-1 == (outfile = open(tmp_env, O_RDWR | O_APPEND | O_CLOEXEC | O_CREAT, + S_IWUSR | S_IRUSR))) +@@ -2989,7 +3013,7 @@ + } + + /* load file only if reading timestamps from it is not finished yet */ +- if ((tmp_env = getenv("FAKETIME_LOAD_FILE")) != NULL) ++ if ((tmp_env = ft_getenv("FAKETIME_LOAD_FILE")) != NULL) + { + int infile = -1; + struct stat sb; +@@ -3021,7 +3045,7 @@ + infile_set = true; + } + +- tmp_env = getenv("FAKETIME_FMT"); ++ tmp_env = ft_getenv("FAKETIME_FMT"); + if (tmp_env == NULL) + { + strcpy(user_faked_time_fmt, "%Y-%m-%d %T"); +@@ -3061,7 +3085,7 @@ + system_time_from_system(&ftpl_starttime); + } + /* fake time supplied as environment variable? */ +- if (NULL != (tmp_env = getenv("FAKETIME"))) ++ if (NULL != (tmp_env = ft_getenv("FAKETIME"))) + { + parse_config_file = false; + parse_ft_string(tmp_env); +@@ -3070,6 +3094,11 @@ + { + read_config_file(); + } ++ if (ft_page != NULL) ++ { ++ struct ft_clock_page s; ++ if (ft_page_snapshot(&s)) ft_page_apply_locked(&s); ++ } + + dont_fake = dont_fake_final; + } +@@ -3209,8 +3238,8 @@ + * a system-wide /etc/faketimerc present. + * The /etc/faketimerc handling has been contributed by David Burley, + * Jacob Moorman, and Wayne Davison of SourceForge, Inc. in version 0.6 */ +- (void) snprintf(custom_filename, BUFSIZ, "%s", getenv("FAKETIME_TIMESTAMP_FILE")); +- (void) snprintf(filename, BUFSIZ, "%s/.faketimerc", getenv("HOME")); ++ (void) snprintf(custom_filename, BUFSIZ, "%s", ft_getenv("FAKETIME_TIMESTAMP_FILE")); ++ (void) snprintf(filename, BUFSIZ, "%s/.faketimerc", ft_getenv("HOME")); + if ((faketimerc = open(custom_filename, O_RDONLY)) != -1 || + (faketimerc = open(filename, O_RDONLY)) != -1 || + (faketimerc = open("/etc/faketimerc", O_RDONLY)) != -1) +@@ -3248,6 +3277,48 @@ + tp_save.tv_nsec = tp->tv_nsec; + + if (dont_fake) return 0; ++ if (ft_disabled) return 0; ++ if (ft_page != NULL && (clk_id == CLOCK_REALTIME || clk_id == CLOCK_REALTIME_COARSE)) ++ { ++ struct ft_clock_page s; ++ if (ft_snap_at(clk_id, &s, tp)) ++ { ++ if (s.generation != __atomic_load_n(&ft_page_applied, __ATOMIC_ACQUIRE)) ++ { ++#ifdef PTHREAD_SINGLETHREADED_TIME ++ sigset_t all, old; ++ sigfillset(&all); ++ pthread_sigmask(SIG_SETMASK, &all, &old); ++ pthread_mutex_lock(&time_mutex); ++ ft_page_apply_locked(&s); ++ pthread_mutex_unlock(&time_mutex); ++ pthread_sigmask(SIG_SETMASK, &old, NULL); ++#else ++ ft_page_apply_locked(&s); ++#endif ++ } ++ ft_tl_gen = s.generation; ++ ft_tl_scale = (s.armed && s.rate > 0) ? 1.0 / s.rate : 1.0; ++ if (s.armed) ++ { ++ int64_t now = (int64_t) tp->tv_sec * 1000000000LL + tp->tv_nsec, fake; ++ if (s.rate == 0 || now <= s.anchor_ns) ++ fake = s.universe_ns; ++ else if (s.rate == 1.0) ++ fake = s.universe_ns + (now - s.anchor_ns); ++ else ++ fake = s.universe_ns + (int64_t) ((double) (now - s.anchor_ns) * s.rate); ++ tp->tv_sec = fake / 1000000000LL; ++ tp->tv_nsec = fake % 1000000000LL; ++ if (tp->tv_nsec < 0) ++ { ++ tp->tv_sec -= 1; ++ tp->tv_nsec += 1000000000LL; ++ } ++ } ++ return 0; ++ } ++ } + /* Per process timers are only sped up or slowed down */ + if ((clk_id == CLOCK_PROCESS_CPUTIME_ID ) || (clk_id == CLOCK_THREAD_CPUTIME_ID)) + { +@@ -3262,7 +3333,6 @@ + volatile int ret = INT_MAX; + + #ifdef PTHREAD_SINGLETHREADED_TIME +- static pthread_mutex_t time_mutex = PTHREAD_MUTEX_INITIALIZER; + + // block all signals while locked. prevents deadlocks if signal interrupts in in mid-operation. + sigset_t all_signals, original_mask; +@@ -3367,6 +3437,19 @@ + force_cache_expiration = 0; + } + ++ /* agentenv: with the clock page mapped, the file isn't read; until it is, look for it once a second */ ++ if (ft_page == NULL && ft_getenv("AGENTENV_CLOCK_PAGE") != NULL && current_ts.tv_sec != ft_page_last_try) ++ { ++ ft_page_last_try = current_ts.tv_sec; ++ ft_page_open(); ++ if (ft_page != NULL) ++ { ++ struct ft_clock_page s; ++ if (ft_page_snapshot(&s)) ft_page_apply_locked(&s); ++ } ++ } ++ if (ft_page != NULL) cache_expired = 0; ++ + if (cache_expired == 1) + { + static char user_faked_time[BUFFERLEN]; /* changed to static for caching in v0.6 */ +@@ -3377,7 +3460,7 @@ + fprintf(stderr, "***************++ Cache expired ++**************\n"); + */ + +- if (NULL != (tmp_env = getenv("FAKETIME"))) ++ if (NULL != (tmp_env = ft_getenv("FAKETIME"))) + { + strncpy(user_faked_time, tmp_env, BUFFERLEN - 1); + user_faked_time[BUFFERLEN - 1] = 0; +@@ -3401,6 +3484,9 @@ + get_fake_monotonic_setting(&fake_monotonic_clock); + } /* cache had expired */ + ++ ft_tl_gen = ft_spec_gen; ++ ft_tl_scale = (ft_mode == FT_START_AT && user_rate_set && user_rate > 0) ? 1.0 / user_rate : 1.0; ++ + if (infile_set) + { + if (load_time(tp)) +@@ -3520,6 +3606,7 @@ + { + struct timespec ts; + int ret; ++ if (ft_disabled) return 0; + ts.tv_sec = tv->tv_sec; + ts.tv_nsec = tv->tv_usec * 1000 + ftpl_starttime.real.tv_nsec % 1000; + +@@ -3627,6 +3714,8 @@ + DONT_FAKE_TIME(result = (*real___clock_gettime)(clk_id, tp)); + if (result == -1) return result; /* original function failed */ + ++ if (!ft_universe_clock(clk_id)) return result; ++ + /* pass the real current time to our faking version, overwriting it */ + if (fake_monotonic_clock || (clk_id != CLOCK_MONOTONIC && clk_id != CLOCK_MONOTONIC_RAW + #ifdef CLOCK_MONOTONIC_COARSE +@@ -3723,238 +3812,124 @@ + + #ifdef FAKE_PTHREAD + +-typedef enum {FT_COMPAT_GLIBC_2_2_5, FT_COMPAT_GLIBC_2_3_2} ft_lib_compat_pthread; +- +-struct pthread_cond_monotonic { +- pthread_cond_t *ptr; +- UT_hash_handle hh; +-}; +- +-static struct pthread_cond_monotonic *monotonic_conds = NULL; ++/* ++ * agentenv: a wait with a CLOCK_REALTIME deadline waits on CLOCK_MONOTONIC instead, until ft_deadline_from_fake's ++ * deadline. A condvar's clock is read from glibc's own condvar word (bit 1 of __wrefs, which glibc's waits read, since ++ * glibc 2.25) instead of a table kept by intercepting pthread_cond_init and pthread_cond_destroy; nothing here calls ++ * getenv() or malloc() or takes a lock. A condvar wait isn't cut into slices, since a slice ending would be a wakeup ++ * its caller must recheck, so a jump or a change of rate reaches it only when it ends; a mutex wait is cut. ++ */ ++#define FT_COND_CLOCK_MONOTONIC 2 + +-int pthread_cond_init_232(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr) ++int pthread_cond_timedwait_232(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) + { +- clockid_t clock_id; +- int result; +- ++ struct timespec mono; + ftpl_init(); +- if (!CHECK_MISSING_REAL(pthread_cond_init_232)) return -1; +- result = real_pthread_cond_init_232(cond, attr); +- +- if (result != 0 || attr == NULL) +- return result; +- +- pthread_condattr_getclock(attr, &clock_id); +- +- if (clock_id == CLOCK_MONOTONIC) { +- struct pthread_cond_monotonic *e = (struct pthread_cond_monotonic*)malloc(sizeof(struct pthread_cond_monotonic)); +- e->ptr = cond; +- +- if (pthread_rwlock_wrlock(&monotonic_conds_lock) != 0) { +- fprintf(stderr,"can't acquire write monotonic_conds_lock\n"); +- exit(-1); +- } +- HASH_ADD_PTR(monotonic_conds, ptr, e); +- pthread_rwlock_unlock(&monotonic_conds_lock); ++ if (ft_uw_active() && ft_translates_wait(abstime) && real_pthread_cond_clockwait != NULL ++ && !(__atomic_load_n(&cond->__data.__wrefs, __ATOMIC_RELAXED) & FT_COND_CLOCK_MONOTONIC)) ++ { ++ int r = ft_u_cond_wait(cond, mutex, abstime); ++ if (r != -1) return r; + } +- +- return result; ++ if (!ft_translates_wait(abstime) || real_pthread_cond_clockwait == NULL ++ || (__atomic_load_n(&cond->__data.__wrefs, __ATOMIC_RELAXED) & FT_COND_CLOCK_MONOTONIC)) ++ return real_pthread_cond_timedwait_232(cond, mutex, abstime); ++ ft_deadline_from_fake(CLOCK_MONOTONIC, abstime, &mono); ++ return real_pthread_cond_clockwait(cond, mutex, CLOCK_MONOTONIC, &mono); + } + +-int pthread_cond_destroy_232(pthread_cond_t *cond) ++int pthread_cond_timedwait_225(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) + { +- struct pthread_cond_monotonic* e; +- ++ struct timespec real; + ftpl_init(); +- +- if (pthread_rwlock_wrlock(&monotonic_conds_lock) != 0) { +- fprintf(stderr,"can't acquire write monotonic_conds_lock\n"); +- exit(-1); +- } +- HASH_FIND_PTR(monotonic_conds, &cond, e); +- if (e) { +- HASH_DEL(monotonic_conds, e); +- free(e); ++ if (ft_uw_active() && ft_translates_wait(abstime) && real_pthread_cond_clockwait != NULL) ++ { ++ int r = ft_u_cond_wait(cond, mutex, abstime); ++ if (r != -1) return r; + } +- pthread_rwlock_unlock(&monotonic_conds_lock); +- +- return real_pthread_cond_destroy_232(cond); ++ if (!ft_translates_wait(abstime)) ++ return real_pthread_cond_timedwait_225(cond, mutex, abstime); ++ ft_deadline_from_fake(CLOCK_REALTIME, abstime, &real); ++ return real_pthread_cond_timedwait_225(cond, mutex, &real); + } + +-/* +- * Check whether we need a run-time activation of the +- * forced monotonic fix to avoid faked / unfaked timestamp +- * confusion between the application and glibc internals. +- */ +-bool needs_forced_monotonic_fix(char *function_name) +-{ +- bool result = false; +- char *env_var; +-#ifdef __GLIBC__ +- const char *glibc_version_string = gnu_get_libc_version(); ++__asm__(".symver pthread_cond_timedwait_225, pthread_cond_timedwait@GLIBC_2.2.5"); ++#if defined __ARM_ARCH || defined FORCE_PTHREAD_NONVER ++__asm__(".symver pthread_cond_timedwait_232, pthread_cond_timedwait@@"); ++#else ++__asm__(".symver pthread_cond_timedwait_232, pthread_cond_timedwait@@GLIBC_2.3.2"); + #endif + +- if (function_name == NULL) return false; +- +- /* The forced monotonic fix can be activated by setting an +- * environment variable to 1, or disabled by setting it to 0 */ +- if ((env_var = getenv("FAKETIME_FORCE_MONOTONIC_FIX")) != NULL) ++int pthread_cond_clockwait(pthread_cond_t *cond, pthread_mutex_t *mutex, clockid_t clockid, ++ const struct timespec *abstime) ++{ ++ struct timespec mono; ++ ftpl_init(); ++ if (real_pthread_cond_clockwait == NULL) return EINVAL; ++ if (ft_uw_active() && ft_translates_wait(abstime) && clockid == CLOCK_REALTIME) + { +- if (env_var[0] == '0') +- result = false; +- else +- result = true; ++ int r = ft_u_cond_wait(cond, mutex, abstime); ++ if (r != -1) return r; + } +- else +- { +-#ifdef __GLIBC__ +- /* Here we try to derive the necessity for a forced monotonic fix * +- * based on glibc version. What could possibly go wrong? */ +- +- int glibc_major, glibc_minor; +- sscanf(glibc_version_string, "%d.%d", &glibc_major, &glibc_minor); +- +- /* The following decision logic is yet purely based on experiences +- * with pthread_cond_timedwait(). The used boundaries may still be +- * imprecise. */ +- if ( (glibc_major == 2) && +- ((glibc_minor <= 24) || (glibc_minor >= 30)) ) +- { +- result = true; +- } +- else +-#endif +- result = false; // avoid forced monotonic fixes unless really necessary +- } +- +- if (getenv("FAKETIME_DEBUG") != NULL) +-#ifdef __GLIBC__ +- fprintf(stderr, "libfaketime: forced monotonic fix for %s = %s (glibc version %s)\n", +- function_name, result ? "yes":"no", glibc_version_string); +-#else +- fprintf(stderr, "libfaketime: forced monotonic fix for %s = %s (not glibc-compiled)\n", +- function_name, result ? "yes":"no"); +-#endif +- +- return result; ++ if (clockid != CLOCK_REALTIME || !ft_translates_wait(abstime)) ++ return real_pthread_cond_clockwait(cond, mutex, clockid, abstime); ++ ft_deadline_from_fake(CLOCK_MONOTONIC, abstime, &mono); ++ return real_pthread_cond_clockwait(cond, mutex, CLOCK_MONOTONIC, &mono); + } + +-int pthread_cond_timedwait_common(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime, ft_lib_compat_pthread compat) ++static int ft_mutex_lock_until(pthread_mutex_t *mutex, const struct timespec *fake_abs) + { +- struct timespec tp, tdiff_actual, realtime, faketime; +- struct timespec *tf = NULL; +- struct pthread_cond_monotonic* e; +- char *tmp_env; +- int wait_ms; +- clockid_t clk_id; +- int result = 0; +- +- ftpl_init(); +- +- if (abstime != NULL) ++ struct timespec mono, slice; ++ unsigned long gen; ++ bool last; ++ int result; ++ ft_deadline_from_fake(CLOCK_MONOTONIC, fake_abs, &mono); ++ gen = ft_tl_gen; ++ for (;;) + { +- if (pthread_rwlock_rdlock(&monotonic_conds_lock) != 0) { +- fprintf(stderr,"can't acquire read monotonic_conds_lock\n"); +- exit(-1); +- } +- HASH_FIND_PTR(monotonic_conds, &cond, e); +- pthread_rwlock_unlock(&monotonic_conds_lock); +- if (e != NULL) +- clk_id = CLOCK_MONOTONIC; +- else +- clk_id = CLOCK_REALTIME; +- +- DONT_FAKE_TIME(result = (*real_clock_gettime)(clk_id, &realtime)); +- if (result == -1) +- { +- return EINVAL; +- } +- faketime = realtime; +- (void)fake_clock_gettime(clk_id, &faketime); +- +- if ((tmp_env = getenv("FAKETIME_WAIT_MS")) != NULL) +- { +- wait_ms = atol(tmp_env); +- DONT_FAKE_TIME(result = (*real_clock_gettime)(clk_id, &realtime)); +- if (result == -1) +- { +- return EINVAL; +- } +- +- tdiff_actual.tv_sec = wait_ms / 1000; +- tdiff_actual.tv_nsec = (wait_ms % 1000) * 1000000; +- timespecadd(&realtime, &tdiff_actual, &tp); +- +- tf = &tp; ++ last = ft_slice_end(&mono, &slice); ++ result = real_pthread_mutex_clocklock(mutex, CLOCK_MONOTONIC, &slice); ++ if (result == EINVAL && real_pthread_mutex_timedlock != NULL) ++ { /* a PI mutex on a kernel without FUTEX_LOCK_PI2 takes only CLOCK_REALTIME */ ++ struct timespec real; ++ ft_deadline_from_fake(CLOCK_REALTIME, fake_abs, &real); ++ return real_pthread_mutex_timedlock(mutex, &real); + } +- else +- { +- timespecsub(abstime, &faketime, &tp); +- if (user_rate_set) +- { +- timespecmul(&tp, 1.0 / user_rate, &tdiff_actual); +- } +- else +- { +- tdiff_actual = tp; +- } +- } +- +- /* For CLOCK_MONOTONIC, pthread_cond_timedwait uses clock_gettime +- internally to calculate the appropriate duration for the +- waiting time. This already uses the faked functions, hence, the +- fake time needs to be passed to pthread_cond_timedwait for +- CLOCK_MONOTONIC. */ +-#ifndef __ARM_ARCH +-#ifndef FORCE_MONOTONIC_FIX +- if (clk_id == CLOCK_MONOTONIC) { +- if (needs_forced_monotonic_fix("pthread_cond_timedwait") == true) { +- timespecadd(&realtime, &tdiff_actual, &tp); +- } +- else { +- timespecadd(&faketime, &tdiff_actual, &tp); +- } +- } +- else +-#endif +-#endif +- timespecadd(&realtime, &tdiff_actual, &tp); +- +- tf = &tp; +- } +- +- switch (compat) { +- case FT_COMPAT_GLIBC_2_3_2: +- result = real_pthread_cond_timedwait_232(cond, mutex, tf); +- break; +- case FT_COMPAT_GLIBC_2_2_5: +- result = real_pthread_cond_timedwait_225(cond, mutex, tf); +- break; ++ if (result != ETIMEDOUT) return result; ++ if (!ft_deadline_moved(fake_abs, &mono, &gen) && last) return ETIMEDOUT; + } +- return result; + } + +-int pthread_cond_timedwait_225(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) ++int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abstime) + { +- return pthread_cond_timedwait_common(cond, mutex, abstime, FT_COMPAT_GLIBC_2_2_5); ++ ftpl_init(); ++ if (real_pthread_mutex_timedlock == NULL) return EINVAL; ++ if (ft_uw_active() && ft_translates_wait(abstime) && real_pthread_mutex_clocklock != NULL) ++ { ++ bool handled; ++ int r = ft_u_mutex_lock(mutex, ft_ns(abstime), &handled); ++ if (handled) return r; ++ } ++ if (!ft_translates_wait(abstime) || real_pthread_mutex_clocklock == NULL) ++ return real_pthread_mutex_timedlock(mutex, abstime); ++ return ft_mutex_lock_until(mutex, abstime); + } + +-int pthread_cond_timedwait_232(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) ++int pthread_mutex_clocklock(pthread_mutex_t *mutex, clockid_t clockid, const struct timespec *abstime) + { +- return pthread_cond_timedwait_common(cond, mutex, abstime, FT_COMPAT_GLIBC_2_3_2); ++ ftpl_init(); ++ if (real_pthread_mutex_clocklock == NULL) return EINVAL; ++ if (ft_uw_active() && ft_translates_wait(abstime) && clockid == CLOCK_REALTIME) ++ { ++ bool handled; ++ int r = ft_u_mutex_lock(mutex, ft_ns(abstime), &handled); ++ if (handled) return r; ++ } ++ if (clockid != CLOCK_REALTIME || !ft_translates_wait(abstime)) ++ return real_pthread_mutex_clocklock(mutex, clockid, abstime); ++ return ft_mutex_lock_until(mutex, abstime); + } + +-__asm__(".symver pthread_cond_timedwait_225, pthread_cond_timedwait@GLIBC_2.2.5"); +-#if defined __ARM_ARCH || defined FORCE_PTHREAD_NONVER +-__asm__(".symver pthread_cond_timedwait_232, pthread_cond_timedwait@@"); +-__asm__(".symver pthread_cond_init_232, pthread_cond_init@@"); +-__asm__(".symver pthread_cond_destroy_232, pthread_cond_destroy@@"); +-#else +-__asm__(".symver pthread_cond_timedwait_232, pthread_cond_timedwait@@GLIBC_2.3.2"); +-__asm__(".symver pthread_cond_init_232, pthread_cond_init@@GLIBC_2.3.2"); +-__asm__(".symver pthread_cond_destroy_232, pthread_cond_destroy@@GLIBC_2.3.2"); +-#endif +- + #endif + + /* +@@ -4005,15 +3980,15 @@ + /* If FAKETIME_TIMESTAMP_FILE was given in environment, + * and if FAKETIME_UPDATE_TIMESTAMP_FILE=1, then update it. + * This allows other process instances to share the same time. */ +- if ( (getenv("FAKETIME_TIMESTAMP_FILE") != NULL) +- && (*getenv("FAKETIME_TIMESTAMP_FILE") != '\0') +- && (getenv("FAKETIME_UPDATE_TIMESTAMP_FILE") != NULL) +- && (strcmp(getenv("FAKETIME_UPDATE_TIMESTAMP_FILE"), "1") == 0)) ++ if ( (ft_getenv("FAKETIME_TIMESTAMP_FILE") != NULL) ++ && (*ft_getenv("FAKETIME_TIMESTAMP_FILE") != '\0') ++ && (ft_getenv("FAKETIME_UPDATE_TIMESTAMP_FILE") != NULL) ++ && (strcmp(ft_getenv("FAKETIME_UPDATE_TIMESTAMP_FILE"), "1") == 0)) + { + const char *error = NULL; + FILE *envfile; + static char custom_filename[BUFSIZ]; +- (void) snprintf(custom_filename, BUFSIZ, "%s", getenv("FAKETIME_TIMESTAMP_FILE")); ++ (void) snprintf(custom_filename, BUFSIZ, "%s", ft_getenv("FAKETIME_TIMESTAMP_FILE")); + + if ((envfile = fopen(custom_filename, "wt")) != NULL) + { +@@ -4034,7 +4009,7 @@ + { + fprintf(stderr, "libfaketime: In clock_settime(), failed to " + "%s while updating FAKETIME_TIMESTAMP_FILE (`%s'): %s\n", +- error, getenv("FAKETIME_TIMESTAMP_FILE"), strerror(errno)); ++ error, ft_getenv("FAKETIME_TIMESTAMP_FILE"), strerror(errno)); + } + } + +@@ -4125,7 +4100,7 @@ + + /* return 0 if no FAKERANDOM_SEED was seen */ + static int bypass_randomness(void* buf, size_t buflen) { +- char *seedstring = getenv("FAKERANDOM_SEED"); ++ char *seedstring = ft_getenv("FAKERANDOM_SEED"); + char *b = buf; + + if (seedstring != NULL) { +@@ -4169,7 +4144,7 @@ + #else + pid_t getpid() { + #endif +- const char *pidstring = getenv("FAKETIME_FAKEPID"); ++ const char *pidstring = ft_getenv("FAKETIME_FAKEPID"); + if (pidstring != NULL) { + long int pid = strtol(pidstring, NULL, 0); + return (pid_t)(pid); +@@ -4270,7 +4245,7 @@ + va_list ap; + va_start(ap, number); + #ifdef FAKE_RANDOM +- if (number == __NR_getrandom && getenv("FAKERANDOM_SEED")) { ++ if (number == __NR_getrandom && ft_getenv("FAKERANDOM_SEED")) { + void *buf; + size_t buflen; + unsigned int flags; +@@ -4282,7 +4257,7 @@ + } + #endif + // static int (*real_clock_gettime) (clockid_t clk_id, struct timespec *tp); +- if (number == __NR_clock_gettime && (getenv("FAKETIME") || getenv("FAKETIME_TIMESTAMP_FILE"))) { ++ if (number == __NR_clock_gettime && (ft_getenv("FAKETIME") || ft_getenv("FAKETIME_TIMESTAMP_FILE"))) { + clockid_t clk_id; + struct timespec *tp; + clk_id = va_arg(ap, clockid_t); +diff -ruN a/src/libfaketime.map b/src/libfaketime.map +--- a/src/libfaketime.map ++++ b/src/libfaketime.map +@@ -1,21 +1,9 @@ +-GLIBC_2.2 { +- global: +- +- timer_gettime; timer_settime; +- local: timer_settime_*; timer_gettime_*; +-}; +- +-GLIBC_2.3.3 { +- # Changed timer_t. +- timer_gettime; timer_settime; +-} GLIBC_2.2; +- + GLIBC_2.2.5 { + global: pthread_cond_timedwait; + local: pthread_cond_timedwait_*; pthread_cond_init_*; pthread_cond_destroy*; + }; + + GLIBC_2.3.2 { +- pthread_cond_timedwait; pthread_cond_init; pthread_cond_destroy; ++ pthread_cond_timedwait; + } GLIBC_2.2.5; + diff --git a/tst/clock/Dockerfile b/tst/clock/Dockerfile new file mode 100644 index 000000000..dc80ec238 --- /dev/null +++ b/tst/clock/Dockerfile @@ -0,0 +1,28 @@ +# The clock gates' image: Ubuntu 24.04 as the base computer has it, agent-env's libfaketime built by its build.sh and +# stock 0.9.12 from the same tarball beside it, the gate programs in C, Python, Java and Node, and Chrome for Testing +# at the browsers bundle's pin. tst/clock/conftest.py assembles its context, with both downloads, checked. +FROM public.ecr.aws/docker/library/ubuntu:24.04@sha256:24737cef17f4e22b1977df7d9a05e77c4e28b7da5b8911f8d8ccda136bf3573c +ARG CHROME_SHA256 +ARG CHROME_PACKAGES +RUN apt-get update -q \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y -q --no-install-recommends build-essential patch curl \ + ca-certificates python3 openjdk-21-jdk-headless nodejs util-linux unzip $CHROME_PACKAGES >/dev/null \ + && rm -rf /var/lib/apt/lists/* \ + && userdel -r ubuntu && useradd -m -u 1000 user +COPY downloads/chrome.zip /tmp/chrome.zip +RUN echo "$CHROME_SHA256 /tmp/chrome.zip" | sha256sum -c - \ + && unzip -q /tmp/chrome.zip -d /opt && mv /opt/chrome-linux* /opt/chrome && rm /tmp/chrome.zip +COPY clock /opt/clock-src +COPY downloads/libfaketime.tar.gz /tmp/lft.tgz +RUN /opt/clock-src/build.sh /opt/clock/lib /tmp/lft.tgz \ + && tar xzf /tmp/lft.tgz -C /tmp && make -s -C /tmp/libfaketime-0.9.12/src libfaketimeMT.so.1 \ + && mkdir -p /opt/clock/stock && cp /tmp/libfaketime-0.9.12/src/libfaketimeMT.so.1 /opt/clock/stock/ \ + && rm -rf /tmp/lft.tgz /tmp/libfaketime-0.9.12 \ + && cp /opt/clock-src/clockpage.py /opt/clock/ \ + && mkdir -p /etc/agentenv /run/agentenv-clock && cp /opt/clock-src/clock.conf /etc/agentenv/clock.conf +COPY programs /opt/clock/programs +RUN mkdir -p /opt/clock/bin /opt/clock/java && cd /opt/clock/programs \ + && for p in uwait jumpwaits condstress2 semstress envrace rtread torn cancel tfdalias; do \ + gcc -O2 -o /opt/clock/bin/$p $p.c -lpthread || exit 1; done \ + && javac -d /opt/clock/java UWait.java && chmod 755 marker.sh +COPY harness.py /opt/clock/harness.py diff --git a/tst/clock/__init__.py b/tst/clock/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tst/clock/chromium_test.py b/tst/clock/chromium_test.py new file mode 100644 index 000000000..4cd15fc88 --- /dev/null +++ b/tst/clock/chromium_test.py @@ -0,0 +1,12 @@ +"""Chromium, the client stock libfaketime crashed on about one launch in five: launched directly, it runs every time with +this library (40 launches, which that rate would leave clean about once in 7,500 runs), and stock 0.9.12, the positive +control, fails it. The two run side by side, in containers of their own, and the gate takes under 30 s.""" +from concurrent.futures import ThreadPoolExecutor + + +def test_chrome_runs_every_launch_with_this_library_and_stock_fails_it(gate): + with ThreadPoolExecutor(2) as pool: + ours, stock = pool.map(lambda args: gate("chromium", *args), [("agentenv", 40, 8, 20.0), ("stock", 10, 10, 3.0)]) + assert max(ours["seconds"], stock["seconds"]) <= 30, (ours, stock) + assert (ours["launches"], ours["failed"]) == (40, 0), ours + assert stock["launches"] == 10 and stock["failed"] >= 1, stock diff --git a/tst/clock/conftest.py b/tst/clock/conftest.py new file mode 100644 index 000000000..1a35d2625 --- /dev/null +++ b/tst/clock/conftest.py @@ -0,0 +1,112 @@ +"""The clock tier: agent-env's libfaketime (src/agent_env/computers/clock) built in an Ubuntu 24.04 image with stock +0.9.12 beside it and the gate programs, each gate run in a container of that image by harness.py. Needs Docker; the +image is for the Docker daemon's own arch, and Chrome is the browsers bundle's pinned build for it. Chrome's archive and +upstream libfaketime's tarball are fetched once into AGENTENV_TEST_CLOCK_CACHE (~/.cache/agentenv-clock by default), +checked by sha256, which CI keeps between runs.""" +import hashlib +import importlib.util +import json +import os +import re +import shutil +import subprocess +import urllib.request +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] +RECIPE = REPO / "src" / "agent_env" / "computers" / "clock" +CHROMIUM_APP = REPO / "src" / "agent_env" / "browsers" / "envs" / "chromium" / "chromium.py" +IMAGE = "agentenv-clock-gates:test" +CACHE = Path(os.environ.get("AGENTENV_TEST_CLOCK_CACHE") or Path.home() / ".cache" / "agentenv-clock") + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with open(path, "rb") as f: + for block in iter(lambda: f.read(1 << 20), b""): + digest.update(block) + return digest.hexdigest() + + +def _fetched(url: str, sha256: str, name: str) -> Path: + """``url`` in the cache as ``name``, downloaded once and checked against ``sha256``.""" + path = CACHE / name + if path.exists() and _sha256(path) == sha256: + return path + CACHE.mkdir(parents=True, exist_ok=True) + part = path.with_name(name + ".part") + urllib.request.urlretrieve(url, part) + assert _sha256(part) == sha256, f"{url} doesn't have the pinned sha256 {sha256}" + part.replace(path) + return path + + +def _libfaketime_pin() -> tuple[str, str]: + """The upstream tarball and sha256 build.sh pins, so the two can't drift apart.""" + text = (RECIPE / "build.sh").read_text() + return re.search(r"https://\S+\.tar\.gz", text).group(0), re.search(r"\b[0-9a-f]{64}\b", text).group(0) + + +def _chrome_pin(arch: str) -> tuple[str, str, str]: + """The browsers bundle's Chrome for Testing archive for ``arch``, its sha256, and the packages it links against.""" + spec = importlib.util.spec_from_file_location("clock_tier_chromium_app", CHROMIUM_APP) + app = importlib.util.module_from_spec(spec) + spec.loader.exec_module(app) + path, sha256 = app._ARCHIVES[arch] + url = f"https://storage.googleapis.com/chrome-for-testing-public/{app.VERSION}/{path}" + return url, sha256, " ".join(p for p in app.PACKAGES if p != "unzip") + + +@pytest.fixture(scope="session") +def gate(tmp_path_factory): + """Run one of harness.py's gates in a fresh container: gate(NAME, *ARGS, docker_args=()) -> its JSON.""" + arch = subprocess.run(["docker", "version", "--format", "{{.Server.Arch}}"], capture_output=True, text=True, + check=True).stdout.strip() + url, sha256, packages = _chrome_pin(arch) + context = tmp_path_factory.mktemp("clock-gates") + shutil.copytree(RECIPE, context / "clock", ignore=shutil.ignore_patterns("__pycache__")) + shutil.copytree(HERE / "programs", context / "programs") + (context / "downloads").mkdir() + shutil.copy(_fetched(url, sha256, f"chrome-{arch}-{sha256[:12]}.zip"), context / "downloads" / "chrome.zip") + shutil.copy(_fetched(*_libfaketime_pin(), "libfaketime-0.9.12.tar.gz"), context / "downloads" / "libfaketime.tar.gz") + for name in ("Dockerfile", "harness.py"): + shutil.copy(HERE / name, context / name) + built = subprocess.run(["docker", "build", "-q", "-t", IMAGE, "--build-arg", f"CHROME_SHA256={sha256}", + "--build-arg", f"CHROME_PACKAGES={packages}", str(context)], capture_output=True, text=True) + assert built.returncode == 0, f"the gate image didn't build:\n{built.stderr[-4000:]}" + + def run(name: str, *args, timeout: float = 600, docker_args=()): + r = subprocess.run(["docker", "run", "--rm", "--shm-size=1g", *docker_args, IMAGE, "python3", + "/opt/clock/harness.py", name, *map(str, args)], capture_output=True, text=True, + timeout=timeout) + assert r.returncode == 0, f"gate {name} failed:\n{r.stderr[-3000:]}" + return json.loads(r.stdout) + + return run + + +#: futex_waitv answered ENOSYS, as a kernel before 5.16 or gVisor answers it; everything else allowed. +NO_FUTEX_WAITV = {"defaultAction": "SCMP_ACT_ALLOW", + "syscalls": [{"names": ["futex_waitv"], "action": "SCMP_ACT_ERRNO", "errnoRet": 38}]} + + +@pytest.fixture(scope="session") +def gate_without_waitv(gate, tmp_path_factory): + """``gate`` in containers whose seccomp profile refuses futex_waitv, checked to refuse it.""" + profile = tmp_path_factory.mktemp("seccomp") / "no-futex-waitv.json" + profile.write_text(json.dumps(NO_FUTEX_WAITV)) + + def run(name: str, *args, timeout: float = 600): + return gate(name, *args, timeout=timeout, docker_args=["--security-opt", f"seccomp={profile}"]) + + assert run("probe") == {"futex_waitv_errno": 38} + return run + + +@pytest.fixture(scope="module", params=["futex_waitv", "without futex_waitv"]) +def waits_gate(request, gate, gate_without_waitv): + """The gates for waits that use futex_waitv, run with it and without it (where they wait in slices instead).""" + return gate if request.param == "futex_waitv" else gate_without_waitv diff --git a/tst/clock/harness.py b/tst/clock/harness.py new file mode 100644 index 000000000..033381d3f --- /dev/null +++ b/tst/clock/harness.py @@ -0,0 +1,570 @@ +"""The clock gates' driver, run as root inside the gate image (tst/clock/Dockerfile). The library leaves root's +processes real, so this driver is on real time while it writes the clock page as the follower would, and every program +under test runs as the session user, whom the library fakes. Each gate prints one JSON document on stdout. + + harness.py waits | ratechange | jumps [ROUNDS] | cancel | timerfds | condstress [SECONDS] | semstress [SECONDS] + | stall | torn [SECONDS] | deadwriter | marker | identity | envrace [SECONDS] + | chromium [agentenv|stock LAUNCHES PARALLEL LIMIT] | probe +""" +import ctypes +import json +import os +import random +import select +import struct +import subprocess +import sys +import time +from concurrent.futures import ThreadPoolExecutor + +sys.path.insert(0, "/opt/clock") +import clockpage # noqa: E402 + +LIB = "/opt/clock/lib/libfaketimeMT.so.1" +STOCK = "/opt/clock/stock/libfaketimeMT.so.1" +PROGRAMS = "/opt/clock/programs" +CHROME = "/opt/chrome/chrome" +ENV = {"PATH": "/opt/clock/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HOME": "/home/user", + "LANG": "C.UTF-8"} +DAY = 86400 +RAW = time.CLOCK_MONOTONIC_RAW + +C_APIS = ["nanosleep", "clock_nanosleep_rt_rel", "clock_nanosleep_mono_rel", "clock_nanosleep_mono_abs", + "clock_nanosleep_rt_abs", "clock_nanosleep_boot_abs", "usleep", "sleep", "select", "pselect", "poll", "ppoll", + "epoll_wait", "epoll_pwait", "epoll_pwait2", "timerfd_mono_rel", "timerfd_mono_abs", "timerfd_rt_abs", + "timerfd_mono_rel_read", "cond_timedwait_rt", "cond_timedwait_mono", "cond_clockwait_mono", + "cond_clockwait_rt", "cond_timedwait_rt_loop", "cond_timedwait_mono_loop", "sem_timedwait", + "sem_clockwait_mono", "mutex_timedlock", "mutex_clocklock_mono", "futex_wait_rel", "futex_bitset_mono_abs", + "alarm", "setitimer_real", "timerfd_rt_abs_read", "timerfd_rt_abs_epoll", "ptimer_rt_abs_signal", + "ptimer_mono_rel_signal", "ptimer_rt_abs_thread", "futex_bitset_rt_abs", "sem_clockwait_rt", + "mutex_clocklock_rt", "cond_timedwait_gtod", "gtod_poll_loop"] +PY_APIS = ["py_time_sleep", "py_event_wait", "py_condition_wait", "py_lock_acquire_timeout", "py_queue_get_timeout", + "py_select", "py_selectors_epoll", "py_asyncio_sleep", "py_poll_monotonic_deadline", "py_until_datetime"] +JAVA_APIS = ["java_thread_sleep", "java_object_wait", "java_park_nanos", "java_queue_poll", "java_scheduled_executor", + "java_condition_await", "java_park_until", "java_await_until"] +NODE_APIS = ["node_settimeout", "node_setinterval", "node_promises_settimeout"] +APIS = C_APIS + PY_APIS + JAVA_APIS + NODE_APIS +#: Waits for a set clock time, which follow the universe; every other wait is elapsed time, which stays real. +CLOCK_TIME = {"clock_nanosleep_rt_abs", "timerfd_rt_abs", "timerfd_rt_abs_read", "timerfd_rt_abs_epoll", + "ptimer_rt_abs_signal", "ptimer_rt_abs_thread", "futex_bitset_rt_abs", "cond_timedwait_rt", + "cond_clockwait_rt", "cond_timedwait_rt_loop", "cond_timedwait_gtod", "sem_timedwait", + "sem_clockwait_rt", "mutex_timedlock", "mutex_clocklock_rt", "gtod_poll_loop", "py_until_datetime", + "java_park_until", "java_await_until"} + + +def raw() -> float: + return time.clock_gettime(RAW) + + +def preload(lib=LIB) -> None: + with open("/etc/ld.so.preload", "w") as f: + f.write(lib + "\n" if lib else "") + + +def user_cmd(argv, env=None): + """``argv`` as the session user, in a clean environment of ``ENV`` and ``env``.""" + e = dict(ENV, **(env or {})) + return ["setpriv", "--reuid=1000", "--regid=1000", "--clear-groups", "env", "-i", + *(f"{k}={v}" for k, v in e.items()), *argv] + + +def page(): + with open(clockpage.PAGE, "rb") as f: + return clockpage.LAYOUT.unpack(f.read(clockpage.LAYOUT.size)) + + +def universe_now() -> tuple[float, float]: + _, _, _, u, a, rate, armed, _, _, _, _, _ = page() + now = time.time() + return (now, 1.0) if not armed else (u / 1e9 + (now - a / 1e9) * rate, rate) + + +def set_clock(universe: float, rate: float) -> dict: + now = time.time() + written = clockpage.write(1, universe, rate, now) + return {**written, "universe": universe, "rate": rate, "anchor": now, "raw": raw()} + + +def advance(delta: float) -> dict: + u, rate = universe_now() + return set_clock(u + delta, rate) + + +def set_rate(rate: float) -> dict: + u, _ = universe_now() + return set_clock(u, rate) + + +def fresh_page() -> None: + if os.path.exists(clockpage.PAGE): + os.remove(clockpage.PAGE) + clockpage.open_page().close() + clockpage.write(0, 0.0, 1.0, time.time()) + + +def spawn(api: str, seconds: float) -> dict: + if api in C_APIS: + argv = ["uwait", api, str(seconds)] + elif api in PY_APIS: + argv = ["python3", f"{PROGRAMS}/uwait.py", api, str(seconds)] + elif api in JAVA_APIS: + argv = ["java", "-Xshare:auto", "-XX:TieredStopAtLevel=1", "-cp", "/opt/clock/java", "UWait", api, str(seconds)] + else: + argv = ["node", f"{PROGRAMS}/uwait.js", api, str(seconds)] + p = subprocess.Popen(user_cmd(argv), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd="/tmp") + os.set_blocking(p.stdout.fileno(), False) + return {"api": api, "p": p, "buf": "", "ready": None, "done": None, "line": None} + + +def collect(procs, deadline: float, actions=()): + """Every process's output until all are done or ``deadline`` (raw), running ``actions`` [(raw_time, fn, label)] on + the way; when each started and ended.""" + actions = sorted(actions, key=lambda a: a[0]) + ran = [] + while True: + now = raw() + while actions and now >= actions[0][0]: + _, fn, label = actions.pop(0) + started = raw() + info = fn() + ran.append({"label": label, "start_raw": started, "raw": raw(), + "info": {k: v for k, v in info.items() if k != "spec"}}) + live = [p for p in procs if p["done"] is None] + if not live or now >= deadline: + break + ready, _, _ = select.select([p["p"].stdout for p in live], [], [], 0.002) + for p in live: + if p["p"].stdout in ready: + p["buf"] += p["p"].stdout.read() or "" + t = raw() + for line in p["buf"].split("\n")[:-1]: + if line == "READY" and p["ready"] is None: + p["ready"] = t + elif (line.startswith("DONE") or line.startswith("{")) and p["done"] is None: + p["done"], p["line"] = t, line + if p["p"].poll() is not None and p["done"] is None: + p["buf"] += p["p"].stdout.read() or "" + for line in p["buf"].splitlines(): + if line.startswith("{") or line.startswith("DONE"): + p["done"], p["line"] = raw(), line + if p["done"] is None: + p["done"], p["line"] = raw(), "EXIT " + (p["p"].stderr.read() or "")[-300:] + for p in procs: + if p["p"].poll() is None: + p["p"].kill() + return ran + + +def result(p, change=None) -> dict: + """``p``'s outcome; with ``change`` (collect's record of it), when it ended after the change's write ended and + after it started (ms).""" + out = {"api": p["api"], "class": "clock_time" if p["api"] in CLOCK_TIME else "elapsed"} + line = p["line"] + if line is None: + return {**out, "status": "still waiting"} + if line.startswith("{"): + j = json.loads(line) + out.update(real=round(j["real"], 4), u_mono=round(j["u_mono"], 4), u_rt=round(j["u_rt"], 4), rc=j["rc"]) + ended = j["start_raw"] + j["real"] + elif line.startswith("DONE"): + _, mono, wall = line.split() + out.update(real=round(p["done"] - p["ready"], 4), u_mono=round(float(mono), 4), u_rt=round(float(wall), 4)) + ended = p["done"] + else: + out["status"] = line[:300] + return out + if change is not None: + out["end_after_change_ms"] = round((ended - change["raw"]) * 1000, 1) + out["end_after_change_start_ms"] = round((ended - change["start_raw"]) * 1000, 1) + return out + + +def scenario(name, start_rate, seconds, change=None, change_at=None, limit=8.0, start_universe=None, apis=APIS): + set_clock(start_universe if start_universe is not None else time.time() - 180 * DAY, start_rate) + t0 = raw() + procs = [spawn(a, seconds) for a in apis] + ran = collect(procs, t0 + limit, [(t0 + change_at, change, "change")] if change else []) + return {"scenario": name, "start_rate": start_rate, "seconds": seconds, "changes": ran, + "results": [result(p, ran[0] if ran else None) for p in procs]} + + +def waits(): + """Every API's 1 s wait at rates 1, 2 and 60, and frozen with +1 h at 2.5 s.""" + preload() + fresh_page() + out = [scenario(f"rate {rate}", rate, 1.0, limit=4.0) for rate in (1, 2, 60)] + out.append(scenario("frozen, +1 h at 2.5 s", 0, 1.0, lambda: advance(3600), 2.5, limit=6.0)) + preload(None) + return out + + +def ratechange(): + """Waits for a clock time 10 s ahead at rate 1, the rate going to 60 at 1.0 s (so they end at 1 + 9 / 60 = 1.15 s); + and waits 2 s ahead when a set_time 182 days back comes at 0.5 s (they wait on, for their new deadline).""" + preload() + fresh_page() + clock = [a for a in APIS if a in CLOCK_TIME] + out = [scenario("rate 1, rate 60 at 1.0 s", 1, 10.0, lambda: set_rate(60), 1.0, limit=4.0, apis=clock), + scenario("rate 1, set_time -182 d at 0.5 s", 1, 2.0, lambda: set_clock(universe_now()[0] - 182 * DAY, 1), + 0.5, limit=4.0, start_universe=time.time(), apis=clock)] + preload(None) + return out + + +def jumps(rounds=30, seed=5420): + """Forward jumps of +8 h, +1 d or +1 week at rate 1, one a round at a random moment; before each, waits for a clock + time inside the jumped window ("in"), one past it ("after"), and relative waits ("rel"), from jumpwaits.""" + rng = random.Random(seed) + preload() + fresh_page() + set_clock(time.time() - 182 * DAY, 1) + p = subprocess.Popen(user_cmd(["jumpwaits"]), stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1, + cwd="/tmp") + out = [] + for i in range(rounds): + j = rng.choice([8 * 3600, DAY, 7 * DAY]) + delay = rng.uniform(0.2, 2.0) + p.stdin.write(f"arm {j / 2} {j + 5}\n") + p.stdin.flush() + armed = p.stdout.readline().split() + assert armed[0] == "ARMED", armed + time.sleep(delay) + w0 = raw() + advance(j) + w1 = raw() + d = json.loads(p.stdout.readline()) + t0 = d["t0"] / 1e9 + row = {"round": i, "jump_s": j, "delay_s": round(delay, 3), "write_ms": round((w1 - w0) * 1e3, 3), "waiters": {}} + for name, (kind, fired, rc) in d["w"].items(): + f = fired / 1e9 + if kind == 0: + row["waiters"][name] = {"kind": "in", "after_write_start_ms": round((f - w0) * 1e3, 3), + "after_write_end_ms": round((f - w1) * 1e3, 3), "rc": rc} + elif kind == 1: + row["waiters"][name] = {"kind": "after", "vs_expected_ms": round((f - (t0 + 5)) * 1e3, 3), "rc": rc} + else: + row["waiters"][name] = {"kind": "rel", "real_s": round(f - t0, 4), "rc": rc} + row["mono_back"], row["mono_rate"] = d["mono_back"], d["mono_rate"] + out.append(row) + p.stdin.close() + p.wait(timeout=30) + preload(None) + return {"rounds": out, "mono_back_total": out[-1]["mono_back"], "mono_rate_final": out[-1]["mono_rate"]} + + +def _jumped(argv, jump: float) -> dict: + """``argv`` as the session user, jumped ``jump`` seconds 0.3 s after it prints READY: its two JSON lines, before + READY and after the jump, and when the jump's write started and ended (raw).""" + p = subprocess.Popen(user_cmd(argv), stdout=subprocess.PIPE, text=True, cwd="/tmp") + first = None + for line in p.stdout: + if line.strip() == "READY": + break + first = json.loads(line) + time.sleep(0.3) + started = raw() + advance(jump) + written = raw() + out, _ = p.communicate(timeout=30) + return {"before": first, "after": json.loads(out), "write_started_raw": started, "written_raw": written} + + +def _after_write(fired_ns: int, r: dict) -> dict: + """When a wait the jump ended fired, from the write's start and from its end (ms); None if it never did.""" + if not fired_ns: + return {"after_write_start_ms": None, "after_write_end_ms": None} + return {"after_write_start_ms": round((fired_ns / 1e9 - r["write_started_raw"]) * 1e3, 3), + "after_write_end_ms": round((fired_ns / 1e9 - r["written_raw"]) * 1e3, 3)} + + +def cancel(): + """Waits for a clock time cancelled (cancel.c): sem_timedwait and clock_nanosleep threads go at once, the semaphore + keeps no waiter for one, and after 600 cancelled condvar waits a condvar wait still ends at a jump past its deadline.""" + preload() + fresh_page() + set_clock(time.time() - 182 * DAY, 1) + r = _jumped(["cancel"], 2 * 3600) + preload(None) + return {**r["before"], "rc": r["after"]["rc"], **_after_write(r["after"]["fired_raw"], r)} + + +def timerfds(): + """A timerfd's tracking through its duplicates (tfdalias.c), one run per way of making one and one of calls that fail, + each jumped 2 h.""" + preload() + fresh_page() + set_clock(time.time() - 182 * DAY, 1) + out = {} + for how in ("dup", "dup2", "dup3", "fcntl", "close_range", "failed"): + r = _jumped(["tfdalias", how], 2 * 3600) + a = r["after"] + out[how] = {"same_number": a["same_number"], "failed_calls": a["failed_calls"], "a2_count": a["a2_count"], + "o_fired_early": a["o_fired_early"], **_after_write(a["a2_fired_raw"], r)} + preload(None) + return out + + +def _with_page_changes(p) -> int: + """+10 min every 0.5 s while ``p`` runs; how many.""" + n = 0 + while p.poll() is None: + time.sleep(0.5) + advance(600) + n += 1 + return n + + +def condstress(seconds=10.0): + """Realtime condvar handoffs at 1 ms and 100 ms gaps, with and without page changes; then single timed waits: 3 s + unchanged, 3 s under same-clock rewrites every 0.3 s, and 30 s with +1 h at 1 s, which passes them.""" + preload() + out = [] + for gap in (1000, 100000): + for changes in (False, True): + fresh_page() + set_clock(time.time() - 182 * DAY, 1) + p = subprocess.Popen(user_cmd(["condstress2", "stress", str(seconds), str(gap)]), stdout=subprocess.PIPE, + text=True) + n = _with_page_changes(p) if changes else 0 + o, _ = p.communicate(timeout=seconds + 60) + out.append({"test": "stress", "gap_us": gap, "page_changes": n, **json.loads(o)}) + + def single(wait_s, label, during=None): + fresh_page() + set_clock(time.time() - 182 * DAY, 1) + p = subprocess.Popen(user_cmd(["condstress2", "single", "20", str(wait_s)]), stdout=subprocess.PIPE, text=True) + writes = during(p) if during else 0 + o, _ = p.communicate(timeout=120) + return {"test": "single", "label": label, "writes": writes, **json.loads(o)} + + def rewrite(p): + n = 0 + while p.poll() is None: + time.sleep(0.3) + set_clock(*universe_now()) + n += 1 + return n + + def jump(p): + time.sleep(1.0) + advance(3600) + return 1 + + out += [single(3, "unchanged"), single(3, "rewrites", rewrite), single(30, "jump", jump)] + preload(None) + return out + + +def semstress(seconds=10.0): + """sem_timedwait with a realtime deadline against sem_post at 1 ms and 100 ms gaps, with and without page changes.""" + preload() + out = [] + for gap in (1000, 100000): + for changes in (False, True): + fresh_page() + set_clock(time.time() - 182 * DAY, 1) + p = subprocess.Popen(user_cmd(["semstress", str(seconds), str(gap)]), stdout=subprocess.PIPE, text=True) + n = _with_page_changes(p) if changes else 0 + o, _ = p.communicate(timeout=seconds + 60) + out.append({"gap_us": gap, "page_changes": n, **json.loads(o)}) + preload(None) + return out + + +def stall(writes=2000): + """A writer stalled 5 ms after reading its real time, as a preempted follower is, making ``writes`` writes that + alternate rate 60 and frozen, while 4 tight readers read CLOCK_REALTIME and CLOCK_MONOTONIC: neither may step + back, which a page copied before a write whose anchor is earlier than the read's real time would make it do.""" + + class Stalled: + def __getattr__(self, name): + return getattr(time, name) + + def clock_gettime_ns(self, clock): + value = time.clock_gettime_ns(clock) + time.sleep(0.005) + return value + + preload() + fresh_page() + set_clock(time.time() - 182 * DAY, 1) + seconds = writes * 0.0055 + 2 + readers = [subprocess.Popen(user_cmd(["rtread", str(seconds), "1", f"/tmp/rtread_{k}.json"]), cwd="/tmp") + for k in range(4)] + time.sleep(1.0) + clockpage.time = Stalled() + started = time.time() + try: + for k in range(writes): + set_rate(60 if k % 2 == 0 else 0) + finally: + clockpage.time = time + wrote = time.time() - started + for p in readers: + p.wait() + threads = [] + for k in range(4): + with open(f"/tmp/rtread_{k}.json") as f: + threads += json.load(f)["threads"] + preload(None) + return {"writes": writes, "write_seconds": round(wrote, 3), "reads": sum(t["reads"] for t in threads), + "rt_back": sum(t["rt_back"] for t in threads), "rt_max_back_ns": max(t["rt_max_back_ns"] for t in threads), + "mono_back": sum(t["mono_back"] for t in threads), "mono_rate": [t["mono_rate"] for t in threads]} + + +def torn(seconds=10.0): + """The page rewritten as fast as possible, alternating universes 182 days back and 21 days ahead, while 4 threads + read CLOCK_REALTIME as fast as they can: every read must be one of the two (torn.c).""" + preload() + fresh_page() + off_a, off_b = -182 * DAY, 21 * DAY + set_clock(time.time() + off_a, 1) + p = subprocess.Popen(user_cmd(["torn", str(seconds), "4", str(off_a), str(off_b)]), stdout=subprocess.PIPE, + text=True, cwd="/tmp") + writes = 0 + while p.poll() is None: + now = time.time() + clockpage.write(1, now + (off_b if writes % 2 == 0 else off_a), 1.0, now) + writes += 1 + o, _ = p.communicate(timeout=30) + preload(None) + return {"writes": writes, **json.loads(o)} + + +def deadwriter(): + """A writer that died mid-write leaves seq odd; the next write takes the page up, as given, and readers follow it.""" + preload() + fresh_page() + set_clock(time.time() - 182 * DAY, 1) + m = clockpage.open_page() + seq = struct.unpack(" object: + if abs(value - before) < 30: + return "real" + if abs(value - (before + universe_offset)) < 30: + return "universe" + return value + + +def marker(): + """A program started with AGENTENV_REAL_TIME=1 is real, the marker is gone from its environment, and its children, + a marked bash's included, are faked (marker.sh).""" + preload() + fresh_page() + set_clock(time.time() - 182 * DAY, 1) + before = time.time() + r = subprocess.run(user_cmd([f"{PROGRAMS}/marker.sh"]), capture_output=True, text=True, cwd="/tmp") + lines = {} + for line in r.stdout.splitlines(): + key, _, value = line.partition(" ") + try: + lines[key] = _classify(float(value), before, -182 * DAY) + except ValueError: + lines[key] = value + preload(None) + return {"lines": lines, "rc": r.returncode, "stderr": r.stderr[-300:]} + + +def identity(): + """Who is faked: the session user is, root and another user aren't, and an unarmed page is real for everyone.""" + preload() + fresh_page() + before = time.time() + unarmed = float(subprocess.run(user_cmd(["date", "+%s.%N"]), capture_output=True, text=True).stdout) + set_clock(time.time() - 182 * DAY, 1) + date = ["date", "+%s.%N"] + session = float(subprocess.run(user_cmd(date), capture_output=True, text=True).stdout) + root = float(subprocess.run(date, capture_output=True, text=True).stdout) + other = float(subprocess.run(["setpriv", "--reuid=1001", "--regid=1001", "--clear-groups", *date], + capture_output=True, text=True).stdout) + preload(None) + return {name: _classify(value, before, -182 * DAY) + for name, value in (("unarmed_session", unarmed), ("session", session), ("root", root), ("other_user", other))} + + +def envrace(seconds=10.0): + """envrace three times: setenv and unsetenv on one thread while others read the clock and make timed waits, which + stock libfaketime follows with getenv(), the race behind its Chromium crashes. This library reads its settings once, + at load, so no run may crash.""" + preload() + fresh_page() + set_clock(time.time() - 182 * DAY, 1) + runs = [] + for _ in range(3): + r = subprocess.run(user_cmd(["envrace", str(seconds)]), capture_output=True, text=True, timeout=seconds + 60) + runs.append({"rc": r.returncode, "out": r.stdout.strip()[-200:]}) + preload(None) + return runs + + +def _stock_env() -> dict: + return {"LD_PRELOAD": STOCK, "FAKETIME_TIMESTAMP_FILE": "/tmp/stockclock", "FAKETIME_DONT_FAKE_MONOTONIC": "1", + "FAKETIME_FMT": "%s"} + + +def chromium(variant="agentenv", launches=40, parallel=8, limit=20.0): + """Chrome launched directly ``launches`` times, ``parallel`` at a time: with this library in /etc/ld.so.preload + (``agentenv``), where each launch must load about:blank headless, print it and exit 0 within ``limit`` seconds; or + with stock 0.9.12 by LD_PRELOAD (``stock``), the positive control, whose launches fail (crash, or hang past + ``limit``). The test runs the two variants in containers side by side.""" + with open("/tmp/stockclock", "w") as f: + f.write(f"@{int(time.time() - 182 * DAY)}\n") + os.chmod("/tmp/stockclock", 0o644) + fresh_page() + set_clock(time.time() - 182 * DAY, 1) + preload(LIB if variant == "agentenv" else None) + + def launch(k: int) -> dict: + profile = f"/tmp/chrome-{k:04d}" # fixed width, so no profile is a prefix of another + argv = [CHROME, "--headless=new", "--no-sandbox", "--disable-gpu", "--no-first-run", "--no-default-browser-check", + f"--user-data-dir={profile}", "--dump-dom", "about:blank"] + started = raw() + try: + r = subprocess.run(user_cmd(argv, {} if variant == "agentenv" else _stock_env()), capture_output=True, + text=True, timeout=limit, cwd="/tmp") + rc, out, err = r.returncode, r.stdout, r.stderr + except subprocess.TimeoutExpired as e: + rc, out, err = "hung", "", "".join(part for part in (e.stdout, e.stderr) if isinstance(part, str)) + subprocess.run(["pkill", "-9", "-f", profile]) + subprocess.run(["rm", "-rf", profile]) + signalled = "Received signal" in err + ok = rc == 0 and not signalled and "(1).poll(ms, TimeUnit.MILLISECONDS); break; + case "java_scheduled_executor": { + ScheduledExecutorService s = Executors.newSingleThreadScheduledExecutor(); + s.schedule(() -> 0, ms, TimeUnit.MILLISECONDS).get(); + s.shutdown(); + break; + } + case "java_condition_await": { + ReentrantLock l = new ReentrantLock(); + Condition c = l.newCondition(); + l.lock(); + try { c.await(ms, TimeUnit.MILLISECONDS); } finally { l.unlock(); } + break; + } + case "java_park_until": { + long until = System.currentTimeMillis() + ms; + while (System.currentTimeMillis() < until) LockSupport.parkUntil(until); + break; + } + case "java_await_until": { + ReentrantLock l = new ReentrantLock(); + Condition c = l.newCondition(); + java.util.Date until = new java.util.Date(System.currentTimeMillis() + ms); + l.lock(); + try { while (c.awaitUntil(until)) { } } finally { l.unlock(); } + break; + } + default: throw new IllegalArgumentException(api); + } + System.out.printf("DONE %.6f %.6f%n", (System.nanoTime() - n0) / 1e9, (System.currentTimeMillis() - w0) / 1e3); + } +} diff --git a/tst/clock/programs/cancel.c b/tst/clock/programs/cancel.c new file mode 100644 index 000000000..7140d30db --- /dev/null +++ b/tst/clock/programs/cancel.c @@ -0,0 +1,98 @@ +/* Waits for a clock time cancelled: a thread cancelled in sem_timedwait or clock_nanosleep goes at once, and the + semaphore keeps no waiter for it; 600 threads cancelled in pthread_cond_timedwait, more than the library's table of + condvar waits holds, leave it as they found it, which the wait after them shows by ending at the jump past its + deadline. A realtime futex wait on an unaligned word fails at once, as the kernel's does. Prints {"sem_cancelled", + "sem_waiters", "cns_cancelled", "cond_cancelled", "misaligned_errno", "misaligned_ms"}, then READY with the last wait + in progress, then {"fired_raw", "rc"} once it ends. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CONDS 600 + +static int64_t ns(clockid_t c) { struct timespec t; clock_gettime(c, &t); return (int64_t)t.tv_sec * 1000000000LL + t.tv_nsec; } +static struct timespec ts_of(int64_t v) { struct timespec t = {v / 1000000000LL, v % 1000000000LL}; return t; } +static struct timespec hour_ahead(void) { return ts_of(ns(CLOCK_REALTIME) + 3600000000000LL); } + +static sem_t sem; +static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t c = PTHREAD_COND_INITIALIZER; +static volatile int entered; +static int64_t fired_raw; +static long last_rc; + +static void unlock(void *mutex) { pthread_mutex_unlock(mutex); } +static void *sem_waiter(void *a) { struct timespec t = hour_ahead(); (void)a; entered = 1; sem_timedwait(&sem, &t); return NULL; } +static void *cns_waiter(void *a) { struct timespec t = hour_ahead(); (void)a; entered = 1; clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &t, NULL); return NULL; } +static void *cond_waiter(void *a) { + struct timespec t = hour_ahead(); long r; + pthread_mutex_lock(&m); + pthread_cleanup_push(unlock, &m); + entered = 1; + r = pthread_cond_timedwait(&c, &m, &t); + pthread_cleanup_pop(1); + if (a) { fired_raw = ns(CLOCK_MONOTONIC_RAW); last_rc = r; } + return NULL; +} + +/* a thread started on fn, in its wait: a condvar waiter once it has released the mutex, others after 50 ms */ +static pthread_t started(void *(*fn)(void *), void *arg) { + pthread_t t; + entered = 0; + pthread_create(&t, NULL, fn, arg); + while (!entered) usleep(100); + if (fn == cond_waiter) { pthread_mutex_lock(&m); pthread_mutex_unlock(&m); } + else usleep(50000); + return t; +} +/* joined within ``seconds`` of real time, and how it ended: 1 cancelled, 0 not */ +static int joined(pthread_t t, int seconds, void **res) { + struct timespec dl = ts_of(ns(CLOCK_MONOTONIC) + seconds * 1000000000LL); + return pthread_clockjoin_np(t, res, CLOCK_MONOTONIC, &dl) == 0; +} +static int cancelled(void *(*fn)(void *)) { + void *res = NULL; + pthread_t t = started(fn, NULL); + pthread_cancel(t); + return joined(t, 2, &res) && res == PTHREAD_CANCELED; +} + +/* a realtime futex wait an hour ahead on a word one byte off its alignment: its errno, and how long it took */ +static long misaligned(double *ms) { + static uint32_t words[2]; + struct timespec t = hour_ahead(); + int64_t t0 = ns(CLOCK_MONOTONIC); + long r = syscall(SYS_futex, (char *)words + 1, FUTEX_WAIT_BITSET_PRIVATE | FUTEX_CLOCK_REALTIME, 0, &t, NULL, + FUTEX_BITSET_MATCH_ANY); + *ms = (ns(CLOCK_MONOTONIC) - t0) / 1e6; + return r == -1 ? errno : 0; +} + +int main(void) { + int sem_ok, cns_ok, cond_ok = 0; + uint64_t waiters; + double ms; + long bad = misaligned(&ms); + void *res; + sem_init(&sem, 0, 0); + sem_ok = cancelled(sem_waiter); + waiters = *(volatile uint64_t *)&sem >> 32; /* glibc's semaphore: its waiters in the high word */ + cns_ok = cancelled(cns_waiter); + for (int i = 0; i < CONDS; i++) cond_ok += cancelled(cond_waiter); + printf("{\"sem_cancelled\": %d, \"sem_waiters\": %llu, \"cns_cancelled\": %d, \"cond_cancelled\": %d, " + "\"misaligned_errno\": %ld, \"misaligned_ms\": %.3f}\n", sem_ok, (unsigned long long)waiters, cns_ok, cond_ok, + bad, ms); + pthread_t last = started(cond_waiter, (void *)1); + printf("READY\n"); + fflush(stdout); + if (!joined(last, 10, &res)) { printf("{\"fired_raw\": 0, \"rc\": -1}\n"); return 0; } + printf("{\"fired_raw\": %lld, \"rc\": %ld}\n", (long long)fired_raw, last_rc); + return 0; +} diff --git a/tst/clock/programs/condstress2.c b/tst/clock/programs/condstress2.c new file mode 100644 index 000000000..4173ae216 --- /dev/null +++ b/tst/clock/programs/condstress2.c @@ -0,0 +1,70 @@ +/* Realtime condvar waits under signals and clock changes. + * condstress2 stress SECONDS GAP_US a waiter waits for a flag with a 1 h CLOCK_REALTIME deadline (predicate loop, + * the deadline recomputed from the clock each time round); a signaler sets the flag and signals every GAP_US + * (+-20 %) once the last one was taken. Prints handoffs, signal-to-take latency (CLOCK_MONOTONIC_RAW) and the + * waiter's returns that found no flag (spurious) or timed out. + * condstress2 single N SECONDS N threads each make one pthread_cond_timedwait with a deadline SECONDS of clock + * time ahead and no signal; prints each one's result and real seconds. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include "rawsys.h" +static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t c = PTHREAD_COND_INITIALIZER; +static int flag = 0, stop = 0; static int64_t sent; static int64_t lat[4000000]; static int nlat; static long spurious, timeouts, calls; +static int64_t raw(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC_RAW, &t); return (int64_t)t.tv_sec * 1000000000LL + t.tv_nsec; } +static void *waiter(void *a) { + (void)a; + pthread_mutex_lock(&m); + while (!stop) { + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3600; + while (!flag && !stop) { int r = pthread_cond_timedwait(&c, &m, &ts); calls++; if (r == ETIMEDOUT) { timeouts++; break; } if (!flag && !stop) spurious++; } + if (flag) { if (nlat < 4000000) lat[nlat++] = raw() - sent; flag = 0; } + } + pthread_mutex_unlock(&m); + return NULL; +} +static int cmp(const void *a, const void *b) { int64_t x = *(int64_t *)a, y = *(int64_t *)b; return x < y ? -1 : x > y; } +static double single_s; static pthread_cond_t sc = PTHREAD_COND_INITIALIZER; +struct sres { int rc; double real; }; +static void *single(void *a) { + struct sres *r = a; struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + long long d = (long long)ts.tv_nsec + (long long)(single_s * 1e9); ts.tv_sec += d / 1000000000LL; ts.tv_nsec = d % 1000000000LL; + int64_t t0 = raw(); pthread_mutex_lock(&m); r->rc = pthread_cond_timedwait(&sc, &m, &ts); pthread_mutex_unlock(&m); r->real = (raw() - t0) / 1e9; + return NULL; +} +int main(int argc, char **argv) { + if (!strcmp(argv[1], "single")) { + int n = atoi(argv[2]); single_s = atof(argv[3]); pthread_t t[256]; struct sres r[256]; + for (int i = 0; i < n; i++) pthread_create(&t[i], NULL, single, &r[i]); + for (int i = 0; i < n; i++) pthread_join(t[i], NULL); + printf("{\"n\": %d, \"seconds\": %g, \"results\": [", n, single_s); + for (int i = 0; i < n; i++) printf("%s[\"%s\", %.4f]", i ? ", " : "", r[i].rc == ETIMEDOUT ? "ETIMEDOUT" : r[i].rc == 0 ? "0" : "other", r[i].real); + printf("]}\n"); + return 0; + } + double s = atof(argv[2]); long gap = atol(argv[3]); pthread_t t; pthread_create(&t, NULL, waiter, NULL); + int64_t end = raw() + (int64_t)(s * 1e9); unsigned seed = 1; + while (raw() < end) { + long us = gap + (long)((rand_r(&seed) % 401 - 200) / 1000.0 * gap); + struct timespec d = {us / 1000000, (us % 1000000) * 1000}; + raw_nanosleep(&d); + pthread_mutex_lock(&m); + if (!flag) { flag = 1; sent = raw(); pthread_cond_signal(&c); } + pthread_mutex_unlock(&m); + int64_t w0 = raw(); + for (;;) { pthread_mutex_lock(&m); int f = flag; pthread_mutex_unlock(&m); if (!f || raw() - w0 > 2000000000LL) break; sched_yield(); } + } + pthread_mutex_lock(&m); stop = 1; pthread_cond_signal(&c); pthread_mutex_unlock(&m); + pthread_join(t, NULL); + qsort(lat, nlat, sizeof lat[0], cmp); + int over = 0; for (int i = 0; i < nlat; i++) if (lat[i] > 50000000) over++; + printf("{\"handoffs\": %d, \"max_ms\": %.3f, \"p999_ms\": %.3f, \"p50_ms\": %.4f, \"over_50ms\": %d, \"spurious\": %ld, \"timeouts\": %ld, \"calls\": %ld}\n", nlat, + nlat ? lat[nlat - 1] / 1e6 : 0, nlat ? lat[(int)(nlat * 0.999)] / 1e6 : 0, nlat ? lat[nlat / 2] / 1e6 : 0, over, spurious, timeouts, calls); + return 0; +} diff --git a/tst/clock/programs/envrace.c b/tst/clock/programs/envrace.c new file mode 100644 index 000000000..210039c8c --- /dev/null +++ b/tst/clock/programs/envrace.c @@ -0,0 +1,44 @@ +/* The mechanism of the Chromium crash, alone: one thread grows and shrinks the environment (setenv/unsetenv, which + * reallocate environ) while others make timed condvar waits and realtime clock reads, each of which stock libfaketime + * follows with getenv(). Runs for argv[1] seconds (default 3) and exits 0; a crash shows as a signal. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +static volatile int stop = 0; +static void *mutate(void *arg) { + (void)arg; + char name[32], value[64]; + for (unsigned long i = 0; !stop; i++) { + snprintf(name, sizeof name, "ENVRACE_%lu", i % 512); + snprintf(value, sizeof value, "%lu-%lu", i, i * 7); + setenv(name, value, 1); + if (i % 3 == 0) unsetenv(name); + } + return NULL; +} +static void *wait_loop(void *arg) { + (void)arg; + pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t c = PTHREAD_COND_INITIALIZER; + struct timespec ts; + while (!stop) { + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec += 1000; if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; } + pthread_mutex_lock(&m); pthread_cond_timedwait(&c, &m, &ts); pthread_mutex_unlock(&m); + } + return NULL; +} +int main(int argc, char **argv) { + int seconds = argc > 1 ? atoi(argv[1]) : 3; + pthread_t t[5]; + pthread_create(&t[0], NULL, mutate, NULL); + for (int i = 1; i < 5; i++) pthread_create(&t[i], NULL, wait_loop, NULL); + sleep(seconds); + stop = 1; + for (int i = 0; i < 5; i++) pthread_join(t[i], NULL); + puts("survived"); + return 0; +} diff --git a/tst/clock/programs/jumpwaits.c b/tst/clock/programs/jumpwaits.c new file mode 100644 index 000000000..19e98b970 --- /dev/null +++ b/tst/clock/programs/jumpwaits.c @@ -0,0 +1,153 @@ +/* Waits across universe jumps, as the session user: jumpwaits. Reads "arm DIN DAFTER" lines on stdin (seconds of + * universe time from now); for each it starts one thread per waiter below, prints "ARMED " once they are + * waiting, and when all have returned prints one JSON line with each waiter's return (CLOCK_MONOTONIC_RAW ns) and + * result. "in" waiters wait for the clock time now + DIN (a jump the driver makes passes it), "after" ones for now + + * DAFTER (past the jump: they should end DAFTER - jump real seconds after arming), "rel" ones for 3 s of elapsed time. + * A reader thread reads CLOCK_MONOTONIC and CLOCK_MONOTONIC_RAW all along and reports backward steps and drift. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int64_t ns(clockid_t c) { struct timespec t; clock_gettime(c, &t); return (int64_t)t.tv_sec * 1000000000LL + t.tv_nsec; } +static struct timespec ts_of(int64_t v) { struct timespec t = {v / 1000000000LL, v % 1000000000LL}; return t; } + +enum { IN, AFTER, REL }; +struct waiter { const char *name; int kind; int64_t fired; long rc; }; +static struct waiter W[] = { + {"cns_in", IN}, {"tfd_poll_in", IN}, {"tfd_read_in", IN}, {"tfd_epoll_in", IN}, {"ptimer_thread_in", IN}, + {"ptimer_signal_in", IN}, {"cond_rt_in", IN}, {"cond_clock_rt_in", IN}, {"cond_loop_in", IN}, {"sem_rt_in", IN}, + {"futex_rt_in", IN}, {"mutex_rt_in", IN}, + {"cns_after", AFTER}, {"tfd_poll_after", AFTER}, {"cond_loop_after", AFTER}, {"ptimer_thread_after", AFTER}, + {"nanosleep_rel", REL}, {"poll_rel", REL}, {"cond_mono_rel", REL}, {"tfd_mono_rel", REL}, {"cns_mono_rel", REL}, +}; +#define NW (int)(sizeof W / sizeof W[0]) +static int64_t D_in, D_after; +static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER, held = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t c_rt = PTHREAD_COND_INITIALIZER, c_rt2 = PTHREAD_COND_INITIALIZER, c_rt3 = PTHREAD_COND_INITIALIZER, + c_rt4 = PTHREAD_COND_INITIALIZER, c_mono; +static sem_t sem; +static volatile int ready = 0, stop_reader = 0; + +static void *holder(void *a) { (void)a; pthread_mutex_lock(&held); for (;;) pause(); return NULL; } +static void on_thread_timer(union sigval v) { sem_post((sem_t *)v.sival_ptr); } + +static long tfd_wait(int64_t due, int how) { + int fd = timerfd_create(CLOCK_REALTIME, 0); unsigned long long n = 0; + struct itimerspec it = {{0, 0}, ts_of(due)}; + if (timerfd_settime(fd, TFD_TIMER_ABSTIME, &it, NULL)) return errno; + __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); + if (how == 1) { struct pollfd p = {fd, POLLIN, 0}; poll(&p, 1, -1); } + if (how == 2) { int e = epoll_create1(0); struct epoll_event ev = {.events = EPOLLIN}; epoll_ctl(e, EPOLL_CTL_ADD, fd, &ev); epoll_wait(e, &ev, 1, -1); close(e); } + long r = read(fd, &n, 8) == 8 ? (long)n : -errno; + close(fd); + return r; +} +static long ptimer_thread(int64_t due) { + sem_t s; sem_init(&s, 0, 0); timer_t id; + struct sigevent sev = {.sigev_notify = SIGEV_THREAD}; sev.sigev_notify_function = on_thread_timer; sev.sigev_value.sival_ptr = &s; + if (timer_create(CLOCK_REALTIME, &sev, &id)) return -errno; + struct itimerspec it = {{0, 0}, ts_of(due)}; + if (timer_settime(id, TIMER_ABSTIME, &it, NULL)) return -errno; + __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); + while (sem_wait(&s) != 0) ; + timer_delete(id); + return 0; +} +static long cond_loop(pthread_cond_t *c, int64_t due) { + struct timespec t = ts_of(due); long n = 0; + pthread_mutex_lock(&m); + __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); + for (;;) { int r = pthread_cond_timedwait(c, &m, &t); n++; if (r == ETIMEDOUT || ns(CLOCK_REALTIME) >= due) break; } + pthread_mutex_unlock(&m); + return n; /* calls it took */ +} + +static void *run(void *arg) { + struct waiter *w = arg; long r = 0; struct timespec t; uint32_t word = 0; + const char *n = w->name; + if (!strcmp(n, "cns_in") || !strcmp(n, "cns_after")) { t = ts_of(w->kind == IN ? D_in : D_after); __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); r = clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &t, NULL); } + else if (!strcmp(n, "tfd_poll_in")) r = tfd_wait(D_in, 1); + else if (!strcmp(n, "tfd_poll_after")) r = tfd_wait(D_after, 1); + else if (!strcmp(n, "tfd_read_in")) r = tfd_wait(D_in, 0); + else if (!strcmp(n, "tfd_epoll_in")) r = tfd_wait(D_in, 2); + else if (!strcmp(n, "ptimer_thread_in")) r = ptimer_thread(D_in); + else if (!strcmp(n, "ptimer_thread_after")) r = ptimer_thread(D_after); + else if (!strcmp(n, "ptimer_signal_in")) { + sigset_t set; sigemptyset(&set); sigaddset(&set, SIGRTMIN + 1); timer_t id; siginfo_t si; + struct sigevent sev = {.sigev_notify = SIGEV_THREAD_ID, .sigev_signo = SIGRTMIN + 1}; sev._sigev_un._tid = gettid(); + if (timer_create(CLOCK_REALTIME, &sev, &id)) r = -errno; + else { struct itimerspec it = {{0, 0}, ts_of(D_in)}; timer_settime(id, TIMER_ABSTIME, &it, NULL); __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); + r = sigwaitinfo(&set, &si) == SIGRTMIN + 1 ? 0 : -errno; timer_delete(id); } + } + else if (!strcmp(n, "cond_rt_in")) { t = ts_of(D_in); pthread_mutex_lock(&m); __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); r = pthread_cond_timedwait(&c_rt, &m, &t); pthread_mutex_unlock(&m); } + else if (!strcmp(n, "cond_clock_rt_in")) { t = ts_of(D_in); pthread_mutex_lock(&m); __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); r = pthread_cond_clockwait(&c_rt2, &m, CLOCK_REALTIME, &t); pthread_mutex_unlock(&m); } + else if (!strcmp(n, "cond_loop_in")) r = cond_loop(&c_rt3, D_in); + else if (!strcmp(n, "cond_loop_after")) r = cond_loop(&c_rt4, D_after); + else if (!strcmp(n, "sem_rt_in")) { t = ts_of(D_in); __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); r = sem_timedwait(&sem, &t) ? errno : 0; } + else if (!strcmp(n, "futex_rt_in")) { t = ts_of(D_in); __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); r = syscall(SYS_futex, &word, FUTEX_WAIT_BITSET_PRIVATE | FUTEX_CLOCK_REALTIME, 0, &t, NULL, FUTEX_BITSET_MATCH_ANY) ? errno : 0; } + else if (!strcmp(n, "mutex_rt_in")) { t = ts_of(D_in); __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); r = pthread_mutex_timedlock(&held, &t); } + else if (!strcmp(n, "nanosleep_rel")) { t = ts_of(3000000000LL); __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); r = nanosleep(&t, NULL); } + else if (!strcmp(n, "poll_rel")) { __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); r = poll(NULL, 0, 3000); } + else if (!strcmp(n, "cond_mono_rel")) { t = ts_of(ns(CLOCK_MONOTONIC) + 3000000000LL); pthread_mutex_lock(&m); __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); r = pthread_cond_timedwait(&c_mono, &m, &t); pthread_mutex_unlock(&m); } + else if (!strcmp(n, "tfd_mono_rel")) { int fd = timerfd_create(CLOCK_MONOTONIC, 0); struct itimerspec it = {{0, 0}, ts_of(3000000000LL)}; unsigned long long k; timerfd_settime(fd, 0, &it, NULL); __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); r = read(fd, &k, 8) == 8 ? 0 : -errno; close(fd); } + else if (!strcmp(n, "cns_mono_rel")) { t = ts_of(3000000000LL); __atomic_add_fetch(&ready, 1, __ATOMIC_SEQ_CST); r = clock_nanosleep(CLOCK_MONOTONIC, 0, &t, NULL); } + w->fired = ns(CLOCK_MONOTONIC_RAW); + w->rc = r; + return NULL; +} + +static long mono_back; static int64_t mono_back_max, drift_min = INT64_MAX, drift_max = INT64_MIN; +static void *reader(void *a) { + (void)a; int64_t last = ns(CLOCK_MONOTONIC), off0 = ns(CLOCK_MONOTONIC) - ns(CLOCK_MONOTONIC_RAW); + for (long i = 0; !stop_reader; i++) { + int64_t mo = ns(CLOCK_MONOTONIC); + if (mo < last) { mono_back++; if (last - mo > mono_back_max) mono_back_max = last - mo; } + last = mo; + if ((i & 1023) == 0) { int64_t d = mo - ns(CLOCK_MONOTONIC_RAW) - off0; if (d < drift_min) drift_min = d; if (d > drift_max) drift_max = d; } + } + return NULL; +} + +int main(void) { + pthread_condattr_t at; pthread_condattr_init(&at); pthread_condattr_setclock(&at, CLOCK_MONOTONIC); pthread_cond_init(&c_mono, &at); + sem_init(&sem, 0, 0); + sigset_t set; sigemptyset(&set); sigaddset(&set, SIGRTMIN + 1); pthread_sigmask(SIG_BLOCK, &set, NULL); + pthread_t h, rd; pthread_create(&h, NULL, holder, NULL); pthread_create(&rd, NULL, reader, NULL); + while (pthread_mutex_trylock(&held) == 0) { pthread_mutex_unlock(&held); sched_yield(); } + setvbuf(stdout, NULL, _IOLBF, 0); + char line[256]; int round = 0; int64_t raw_start = ns(CLOCK_MONOTONIC_RAW), mono_start = ns(CLOCK_MONOTONIC); + while (fgets(line, sizeof line, stdin)) { + double din, dafter; + if (sscanf(line, "arm %lf %lf", &din, &dafter) != 2) break; + int64_t u0 = ns(CLOCK_REALTIME), t0 = ns(CLOCK_MONOTONIC_RAW); + D_in = u0 + (int64_t)(din * 1e9); D_after = u0 + (int64_t)(dafter * 1e9); + ready = 0; + pthread_t t[64]; + for (int i = 0; i < NW; i++) { W[i].fired = 0; W[i].rc = -999; pthread_create(&t[i], NULL, run, &W[i]); } + while (__atomic_load_n(&ready, __ATOMIC_SEQ_CST) < NW) usleep(1000); + usleep(50000); + printf("ARMED %lld %lld\n", (long long)t0, (long long)ns(CLOCK_MONOTONIC_RAW)); + for (int i = 0; i < NW; i++) pthread_join(t[i], NULL); + printf("{\"round\": %d, \"t0\": %lld, \"u0\": %lld, \"w\": {", round++, (long long)t0, (long long)u0); + for (int i = 0; i < NW; i++) printf("%s\"%s\": [%d, %lld, %ld]", i ? ", " : "", W[i].name, W[i].kind, (long long)W[i].fired, W[i].rc); + printf("}, \"mono_back\": %ld, \"mono_back_max_ns\": %lld, \"mono_minus_raw_drift_ns\": [%lld, %lld], \"mono_rate\": %.9f}\n", + mono_back, (long long)mono_back_max, (long long)drift_min, (long long)drift_max, + (double)(ns(CLOCK_MONOTONIC) - mono_start) / (double)(ns(CLOCK_MONOTONIC_RAW) - raw_start)); + } + stop_reader = 1; pthread_join(rd, NULL); + return 0; +} diff --git a/tst/clock/programs/marker.sh b/tst/clock/programs/marker.sh new file mode 100644 index 000000000..bf66c9af5 --- /dev/null +++ b/tst/clock/programs/marker.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# The marker scrub, as the session user: a marked bash's own time (printf %()T is bash's builtin, in the process +# itself), its direct child's and its grandchild's, against the universe's (an unmarked date) and real (/proc/uptime-free: +# printed by the root driver around it). Prints "label epoch_seconds". +echo "unmarked_date $(date +%s.%N)" +env AGENTENV_REAL_TIME=1 bash -c 'printf "marked_bash_itself %(%s)T\n" -1; echo "marked_bash_child_date $(date +%s.%N)"; echo "marked_bash_grandchild_sh_date $(sh -c "date +%s.%N")"; echo "marked_bash_exec_child $(bash -c "exec date +%s.%N")"; echo "marked_bash_environ $(tr "\0" "\n" < /proc/$$/environ | grep -c "^AGENTENV_REAL_TIME=1$")"; echo "child_environ $(sh -c "tr \"\\0\" \"\\n\" < /proc/self/environ | grep -c ^AGENTENV_REAL_TIME")"' +env AGENTENV_REAL_TIME=1 python3 -c 'import subprocess,time; print("marked_python_itself", time.time()); print("marked_python_child", subprocess.run(["date","+%s.%N"],capture_output=True,text=True).stdout.strip())' diff --git a/tst/clock/programs/rawsys.h b/tst/clock/programs/rawsys.h new file mode 100644 index 000000000..eb73b3e85 --- /dev/null +++ b/tst/clock/programs/rawsys.h @@ -0,0 +1,20 @@ +/* rawsys.h: system calls through the raw instruction (x86_64 or aarch64), which libfaketime can't see. */ +#include +static inline long raw_syscall2(long n_x86, long n_arm, long a, long b) +{ +#if defined(__aarch64__) + register long x8 __asm__("x8") = n_arm; + register long x0 __asm__("x0") = a; + register long x1 __asm__("x1") = b; + __asm__ volatile("svc 0" : "+r"(x0) : "r"(x8), "r"(x1) : "memory"); + return x0; +#else + long r; + __asm__ volatile("syscall" : "=a"(r) : "0"(n_x86), "D"(a), "S"(b) : "rcx", "r11", "memory"); + return r; +#endif +} +/* A relative sleep the library can't stretch or shorten. */ +static inline long raw_nanosleep(const struct timespec *t) { return raw_syscall2(35, 101, (long) t, 0); } +/* The kernel's CLOCK_REALTIME, whatever the library makes of the libc call. */ +static inline long raw_clock_gettime(clockid_t clock, struct timespec *t) { return raw_syscall2(228, 113, (long) clock, (long) t); } diff --git a/tst/clock/programs/rtread.c b/tst/clock/programs/rtread.c new file mode 100644 index 000000000..3ef8efbe3 --- /dev/null +++ b/tst/clock/programs/rtread.c @@ -0,0 +1,39 @@ +/* Tight clock readers: rtread SECONDS THREADS OUT. Each thread reads CLOCK_REALTIME and CLOCK_MONOTONIC as fast as it + * can for SECONDS (CLOCK_MONOTONIC_RAW) and counts reads that went back, with the largest step back; the first thread + * also gives CLOCK_MONOTONIC's rate against CLOCK_MONOTONIC_RAW over the run. Writes JSON to OUT. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +static double dur; +static int64_t ns(clockid_t c) { struct timespec t; clock_gettime(c, &t); return (int64_t)t.tv_sec * 1000000000LL + t.tv_nsec; } +struct res { long reads, rt_back, mono_back; int64_t rt_max_back, mono_max_back; double mono_rate; }; +static struct res R[16]; +static void *reader(void *arg) { + struct res *r = arg; + int64_t raw0 = ns(CLOCK_MONOTONIC_RAW), mono0 = ns(CLOCK_MONOTONIC), end = raw0 + (int64_t)(dur * 1e9); + int64_t lrt = ns(CLOCK_REALTIME), lmo = mono0; + for (;;) { + int64_t rt = ns(CLOCK_REALTIME), mo = ns(CLOCK_MONOTONIC); + r->reads++; + if (rt < lrt) { r->rt_back++; if (lrt - rt > r->rt_max_back) r->rt_max_back = lrt - rt; } + if (mo < lmo) { r->mono_back++; if (lmo - mo > r->mono_max_back) r->mono_max_back = lmo - mo; } + lrt = rt; lmo = mo; + if ((r->reads & 255) == 0 && ns(CLOCK_MONOTONIC_RAW) >= end) break; + } + r->mono_rate = (double)(ns(CLOCK_MONOTONIC) - mono0) / (double)(ns(CLOCK_MONOTONIC_RAW) - raw0); + return NULL; +} +int main(int argc, char **argv) { + dur = atof(argv[1]); int n = atoi(argv[2]); pthread_t t[16]; + for (int i = 0; i < n; i++) pthread_create(&t[i], NULL, reader, &R[i]); + for (int i = 0; i < n; i++) pthread_join(t[i], NULL); + FILE *f = fopen(argv[3], "w"); + fprintf(f, "{\"threads\": ["); + for (int i = 0; i < n; i++) fprintf(f, "%s{\"reads\": %ld, \"rt_back\": %ld, \"rt_max_back_ns\": %lld, \"mono_back\": %ld, \"mono_max_back_ns\": %lld, \"mono_rate\": %.9f}", i ? ", " : "", + R[i].reads, R[i].rt_back, (long long)R[i].rt_max_back, R[i].mono_back, (long long)R[i].mono_max_back, R[i].mono_rate); + fprintf(f, "]}\n"); fclose(f); + return 0; +} diff --git a/tst/clock/programs/semstress.c b/tst/clock/programs/semstress.c new file mode 100644 index 000000000..185426ba6 --- /dev/null +++ b/tst/clock/programs/semstress.c @@ -0,0 +1,39 @@ +/* sem_timedwait (CLOCK_REALTIME deadline 1 h ahead) against sem_post: semstress SECONDS GAP_US. A consumer takes posts + * with sem_timedwait; a producer posts every GAP_US (+-20 %) once the last post was taken, so the consumer is asleep in + * the futex wait for most posts. Prints posts, takes, signal-to-take latency (CLOCK_MONOTONIC_RAW) and errors. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +static sem_t s; static volatile int64_t sent; static volatile long taken; static volatile int stop; static int64_t lat[2000000]; static int nlat; static long errs, timeouts; +static int64_t raw(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC_RAW, &t); return (int64_t)t.tv_sec * 1000000000LL + t.tv_nsec; } +static void *consumer(void *a) { + (void)a; + while (!stop) { + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3600; + if (sem_timedwait(&s, &ts) == 0) { if (nlat < 2000000) lat[nlat++] = raw() - sent; __atomic_add_fetch(&taken, 1, __ATOMIC_SEQ_CST); } + else if (errno == ETIMEDOUT) timeouts++; else if (errno != EINTR) errs++; + } + return NULL; +} +static int cmp(const void *a, const void *b) { int64_t x = *(int64_t *)a, y = *(int64_t *)b; return x < y ? -1 : x > y; } +int main(int argc, char **argv) { + double secs = atof(argv[1]); long gap = atol(argv[2]); pthread_t t; sem_init(&s, 0, 0); pthread_create(&t, NULL, consumer, NULL); + int64_t end = raw() + (int64_t)(secs * 1e9); unsigned seed = 7; long posts = 0, stuck = 0; + while (raw() < end) { + long us = gap + (long)((rand_r(&seed) % 401 - 200) / 1000.0 * gap); + struct timespec d = {us / 1000000, (us % 1000000) * 1000}; clock_nanosleep(CLOCK_MONOTONIC, 0, &d, NULL); + sent = raw(); sem_post(&s); posts++; + int64_t w0 = raw(); while (__atomic_load_n(&taken, __ATOMIC_SEQ_CST) < posts) { if (raw() - w0 > 2000000000LL) { stuck++; break; } sched_yield(); } + } + stop = 1; sem_post(&s); pthread_join(t, NULL); + qsort(lat, nlat, sizeof lat[0], cmp); + int over = 0; for (int i = 0; i < nlat; i++) if (lat[i] > 50000000) over++; + printf("{\"posts\": %ld, \"taken\": %ld, \"stuck_2s\": %ld, \"max_ms\": %.3f, \"p999_ms\": %.3f, \"p50_ms\": %.4f, \"over_50ms\": %d, \"timeouts\": %ld, \"errors\": %ld}\n", + posts, taken - 1 < posts ? taken : taken - 1, stuck, nlat ? lat[nlat - 1] / 1e6 : 0, nlat ? lat[(int)(nlat * 0.999)] / 1e6 : 0, nlat ? lat[nlat / 2] / 1e6 : 0, over, timeouts, errs); + return 0; +} diff --git a/tst/clock/programs/tfdalias.c b/tst/clock/programs/tfdalias.c new file mode 100644 index 000000000..1f4fbcdc2 --- /dev/null +++ b/tst/clock/programs/tfdalias.c @@ -0,0 +1,51 @@ +/* A timerfd's tracking follows its file, not its number: A, armed for a clock time an hour ahead, is duplicated as A2 + (by METHOD: dup, dup2, dup3, fcntl's F_DUPFD_CLOEXEC, or dup then close_range) and closed, and A's number goes to a + duplicate of O, a monotonic timerfd armed 10 s out. With METHOD failed, A2 is A and dup2, dup3 and close_range calls + on it fail, which must leave its tracking as it was. After READY the driver jumps 2 h: A2 must fire at the jump and O + must not fire early. Prints READY, then {"same_number", "failed_calls", "a2_fired_raw", "a2_count", + "o_fired_early"}. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +static int64_t ns(clockid_t c) { struct timespec t; clock_gettime(c, &t); return (int64_t)t.tv_sec * 1000000000LL + t.tv_nsec; } +static struct timespec ts_of(int64_t v) { struct timespec t = {v / 1000000000LL, v % 1000000000LL}; return t; } + +int main(int argc, char **argv) { + const char *how = argc > 1 ? argv[1] : "dup"; + int o = timerfd_create(CLOCK_MONOTONIC, 0), a = timerfd_create(CLOCK_REALTIME, 0), a2, o2; + struct itimerspec ten = {{0, 0}, ts_of(10000000000LL)}, hour = {{0, 0}, ts_of(ns(CLOCK_REALTIME) + 3600000000000LL)}; + unsigned long long count = 0; + int64_t fired = 0; + int failed = 0; + timerfd_settime(o, 0, &ten, NULL); + timerfd_settime(a, TFD_TIMER_ABSTIME, &hour, NULL); + if (!strcmp(how, "failed")) { + a2 = a; + failed += dup2(-1, a) == -1; + failed += dup3(-1, a, 0) == -1; + failed += close_range(a, a, 0x40000000) == -1; /* no such flag */ + } + else if (!strcmp(how, "dup2")) a2 = dup2(a, 100); + else if (!strcmp(how, "dup3")) a2 = dup3(a, 101, O_CLOEXEC); + else if (!strcmp(how, "fcntl")) a2 = fcntl(a, F_DUPFD_CLOEXEC, 50); + else a2 = dup(a); + if (!strcmp(how, "close_range")) close_range(a, a, 0); + else if (strcmp(how, "failed")) close(a); + o2 = dup(o); + printf("READY\n"); + fflush(stdout); + struct pollfd p = {a2, POLLIN, 0}; + if (poll(&p, 1, 5000) == 1 && read(a2, &count, 8) == 8) fired = ns(CLOCK_MONOTONIC_RAW); + struct pollfd q = {o2, POLLIN, 0}; + int early = poll(&q, 1, 1000) == 1; + printf("{\"same_number\": %d, \"failed_calls\": %d, \"a2_fired_raw\": %lld, \"a2_count\": %llu, \"o_fired_early\": %d}\n", + o2 == a, failed, (long long)fired, count, early); + return 0; +} diff --git a/tst/clock/programs/torn.c b/tst/clock/programs/torn.c new file mode 100644 index 000000000..0f80aff0a --- /dev/null +++ b/tst/clock/programs/torn.c @@ -0,0 +1,48 @@ +/* Torn page reads, black box: torn SECONDS THREADS OFF_A_S OFF_B_S. The driver rewrites the clock page as fast as it can, + * alternating a universe OFF_A_S and one OFF_B_S from real time, both at rate 1, while each thread here reads + * CLOCK_REALTIME through the library as fast as it can. Real time is the raw system call's CLOCK_REALTIME at the start + * plus CLOCK_MONOTONIC_RAW since, neither of which the library touches; a read more than 1 s from both offsets mixes two + * pages, so it's torn. Prints one JSON line. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include "rawsys.h" +#define NS 1000000000LL +static double seconds; static int64_t off_a, off_b, rt0, raw0; +static int64_t ns(clockid_t c) { struct timespec t; clock_gettime(c, &t); return (int64_t)t.tv_sec * NS + t.tv_nsec; } +static int64_t far(int64_t d) { return d < 0 ? -d : d; } +struct res { long reads, a, b, torn; int64_t worst; }; +static struct res R[16]; +static void *reader(void *arg) { + struct res *r = arg; + int64_t end = ns(CLOCK_MONOTONIC_RAW) + (int64_t)(seconds * NS); + for (;;) { + int64_t raw = ns(CLOCK_MONOTONIC_RAW), off = ns(CLOCK_REALTIME) - (rt0 + raw - raw0); + r->reads++; + if (far(off - off_a) <= NS) r->a++; + else if (far(off - off_b) <= NS) r->b++; + else { r->torn++; if (far(off) > far(r->worst)) r->worst = off; } + if (raw >= end) break; + } + return NULL; +} +int main(int argc, char **argv) { + if (argc != 5) { fprintf(stderr, "usage: torn SECONDS THREADS OFF_A_S OFF_B_S\n"); return 2; } + seconds = atof(argv[1]); int n = atoi(argv[2]); off_a = (int64_t)(atof(argv[3]) * NS); off_b = (int64_t)(atof(argv[4]) * NS); + if (n < 1 || n > 16) return 2; + struct timespec t; raw_clock_gettime(CLOCK_REALTIME, &t); raw0 = ns(CLOCK_MONOTONIC_RAW); rt0 = (int64_t)t.tv_sec * NS + t.tv_nsec; + pthread_t th[16]; + for (int i = 0; i < n; i++) pthread_create(&th[i], NULL, reader, &R[i]); + long reads = 0, a = 0, b = 0, torn = 0; int64_t worst = 0; + for (int i = 0; i < n; i++) { + pthread_join(th[i], NULL); + reads += R[i].reads; a += R[i].a; b += R[i].b; torn += R[i].torn; + if (far(R[i].worst) > far(worst)) worst = R[i].worst; + } + printf("{\"reads\": %ld, \"universe_a\": %ld, \"universe_b\": %ld, \"torn\": %ld, \"worst_torn_offset_s\": %.3f}\n", + reads, a, b, torn, (double)worst / NS); + return 0; +} diff --git a/tst/clock/programs/uwait.c b/tst/clock/programs/uwait.c new file mode 100644 index 000000000..7df68d3d2 --- /dev/null +++ b/tst/clock/programs/uwait.c @@ -0,0 +1,140 @@ +/* One wait of SECONDS of the program's own (universe) time through API, in this process: + * uwait API SECONDS + * prints one JSON line: real seconds it took (CLOCK_MONOTONIC_RAW, which the library leaves real), the universe's + * realtime and monotonic seconds it covered, and the call's result. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static double secs(clockid_t c) { struct timespec t; clock_gettime(c, &t); return t.tv_sec + t.tv_nsec / 1e9; } +static struct timespec plus(struct timespec t, double s) { + long long ns = (long long)(s * 1e9) + t.tv_nsec; + t.tv_sec += ns / 1000000000LL; t.tv_nsec = ns % 1000000000LL; + return t; +} +static struct timespec clk(clockid_t c, double s) { struct timespec t; clock_gettime(c, &t); return plus(t, s); } +static struct timespec rel(double s) { struct timespec t = {0, 0}; return plus(t, s); } +static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER, held = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t c_rt = PTHREAD_COND_INITIALIZER, c_mono; +static sem_t sem; +static volatile int got_alarm = 0; +static void on_alarm(int s) { (void)s; got_alarm = 1; } +static sem_t tsem; +static void on_ptimer(union sigval v) { (void)v; sem_post(&tsem); } +static void *holder(void *a) { (void)a; pthread_mutex_lock(&held); pause(); return NULL; } +static void hold(void) { pthread_t t; pthread_create(&t, NULL, holder, NULL); while (pthread_mutex_trylock(&held) == 0) { pthread_mutex_unlock(&held); sched_yield(); } } +static long tfd_wait(int fd, int use_poll) { + unsigned long long n = 0; + if (use_poll) { struct pollfd p = {fd, POLLIN, 0}; if (poll(&p, 1, -1) < 0) return errno; } + return read(fd, &n, 8) == 8 ? 0 : errno; +} + +static long run(const char *a, double s) { + struct timespec ts; + uint32_t word = 0; + if (!strcmp(a, "nanosleep")) { ts = rel(s); return nanosleep(&ts, NULL) ? errno : 0; } + if (!strcmp(a, "clock_nanosleep_rt_rel")) { ts = rel(s); return clock_nanosleep(CLOCK_REALTIME, 0, &ts, NULL); } + if (!strcmp(a, "clock_nanosleep_mono_rel")) { ts = rel(s); return clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, NULL); } + if (!strcmp(a, "clock_nanosleep_mono_abs")) { ts = clk(CLOCK_MONOTONIC, s); return clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL); } + if (!strcmp(a, "clock_nanosleep_rt_abs")) { ts = clk(CLOCK_REALTIME, s); return clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &ts, NULL); } + if (!strcmp(a, "clock_nanosleep_boot_abs")) { ts = clk(CLOCK_BOOTTIME, s); return clock_nanosleep(CLOCK_BOOTTIME, TIMER_ABSTIME, &ts, NULL); } + if (!strcmp(a, "usleep")) { return usleep((useconds_t)(s * 1e6)) ? errno : 0; } + if (!strcmp(a, "sleep")) { return sleep((unsigned)s); } + if (!strcmp(a, "select")) { ts = rel(s); struct timeval tv = {ts.tv_sec, ts.tv_nsec / 1000}; return select(0, NULL, NULL, NULL, &tv) < 0 ? errno : 0; } + if (!strcmp(a, "pselect")) { ts = rel(s); return pselect(0, NULL, NULL, NULL, &ts, NULL) < 0 ? errno : 0; } + if (!strcmp(a, "poll")) { return poll(NULL, 0, (int)(s * 1000)) < 0 ? errno : 0; } + if (!strcmp(a, "ppoll")) { ts = rel(s); return ppoll(NULL, 0, &ts, NULL) < 0 ? errno : 0; } + if (!strcmp(a, "epoll_wait")) { int e = epoll_create1(0); struct epoll_event ev; return epoll_wait(e, &ev, 1, (int)(s * 1000)) < 0 ? errno : 0; } + if (!strcmp(a, "epoll_pwait")) { int e = epoll_create1(0); struct epoll_event ev; return epoll_pwait(e, &ev, 1, (int)(s * 1000), NULL) < 0 ? errno : 0; } + if (!strcmp(a, "epoll_pwait2")) { int e = epoll_create1(0); struct epoll_event ev; ts = rel(s); return epoll_pwait2(e, &ev, 1, &ts, NULL) < 0 ? errno : 0; } + if (!strncmp(a, "timerfd_", 8)) { + int mono = strstr(a, "_mono") != NULL, abs_ = strstr(a, "_abs") != NULL, by_poll = strstr(a, "_read") == NULL; + int fd = timerfd_create(mono ? CLOCK_MONOTONIC : CLOCK_REALTIME, 0); + struct itimerspec it = {{0, 0}, abs_ ? clk(mono ? CLOCK_MONOTONIC : CLOCK_REALTIME, s) : rel(s)}; + if (timerfd_settime(fd, abs_ ? TFD_TIMER_ABSTIME : 0, &it, NULL)) return errno; + return tfd_wait(fd, by_poll); + } + if (!strcmp(a, "cond_timedwait_rt")) { ts = clk(CLOCK_REALTIME, s); pthread_mutex_lock(&m); return pthread_cond_timedwait(&c_rt, &m, &ts); } + if (!strcmp(a, "cond_timedwait_mono")) { ts = clk(CLOCK_MONOTONIC, s); pthread_mutex_lock(&m); return pthread_cond_timedwait(&c_mono, &m, &ts); } + if (!strcmp(a, "cond_clockwait_mono")) { ts = clk(CLOCK_MONOTONIC, s); pthread_mutex_lock(&m); return pthread_cond_clockwait(&c_rt, &m, CLOCK_MONOTONIC, &ts); } + if (!strcmp(a, "cond_clockwait_rt")) { ts = clk(CLOCK_REALTIME, s); pthread_mutex_lock(&m); return pthread_cond_clockwait(&c_rt, &m, CLOCK_REALTIME, &ts); } + if (!strcmp(a, "cond_timedwait_rt_loop") || !strcmp(a, "cond_timedwait_mono_loop")) { + /* as a caller with a predicate does: a wakeup before the deadline (by its own clock) waits again */ + int mono = strstr(a, "mono") != NULL; clockid_t c = mono ? CLOCK_MONOTONIC : CLOCK_REALTIME; struct timespec now; + ts = clk(c, s); pthread_mutex_lock(&m); + for (;;) { int r = pthread_cond_timedwait(mono ? &c_mono : &c_rt, &m, &ts); clock_gettime(c, &now); + if (r == ETIMEDOUT || now.tv_sec > ts.tv_sec || (now.tv_sec == ts.tv_sec && now.tv_nsec >= ts.tv_nsec)) return 0; } + } + if (!strcmp(a, "sem_timedwait")) { ts = clk(CLOCK_REALTIME, s); return sem_timedwait(&sem, &ts) ? errno : 0; } + if (!strcmp(a, "sem_clockwait_mono")) { ts = clk(CLOCK_MONOTONIC, s); return sem_clockwait(&sem, CLOCK_MONOTONIC, &ts) ? errno : 0; } + if (!strcmp(a, "mutex_timedlock")) { hold(); ts = clk(CLOCK_REALTIME, s); return pthread_mutex_timedlock(&held, &ts); } + if (!strcmp(a, "mutex_clocklock_mono")) { hold(); ts = clk(CLOCK_MONOTONIC, s); return pthread_mutex_clocklock(&held, CLOCK_MONOTONIC, &ts); } + if (!strcmp(a, "futex_wait_rel")) { ts = rel(s); return syscall(SYS_futex, &word, FUTEX_WAIT_PRIVATE, 0, &ts, NULL, 0) ? errno : 0; } + if (!strcmp(a, "futex_bitset_mono_abs")) { ts = clk(CLOCK_MONOTONIC, s); return syscall(SYS_futex, &word, FUTEX_WAIT_BITSET_PRIVATE, 0, &ts, NULL, FUTEX_BITSET_MATCH_ANY) ? errno : 0; } + if (!strcmp(a, "alarm")) { signal(SIGALRM, on_alarm); alarm((unsigned)s); while (!got_alarm) pause(); return 0; } + /* waits added for the clock-time APIs and timers */ + if (!strcmp(a, "setitimer_real")) { struct itimerval v = {{0, 0}, {(long)s, (long)((s - (long)s) * 1e6)}}; signal(SIGALRM, on_alarm); setitimer(ITIMER_REAL, &v, NULL); while (!got_alarm) pause(); return 0; } + if (!strcmp(a, "timerfd_rt_abs_read") || !strcmp(a, "timerfd_rt_abs_epoll")) { + int fd = timerfd_create(CLOCK_REALTIME, 0); + struct itimerspec it = {{0, 0}, clk(CLOCK_REALTIME, s)}; + if (timerfd_settime(fd, TFD_TIMER_ABSTIME, &it, NULL)) return errno; + if (strstr(a, "epoll")) { int e = epoll_create1(0); struct epoll_event ev = {.events = EPOLLIN}; epoll_ctl(e, EPOLL_CTL_ADD, fd, &ev); if (epoll_wait(e, &ev, 1, -1) != 1) return errno; } + unsigned long long n = 0; return read(fd, &n, 8) == 8 ? 0 : errno; + } + if (!strcmp(a, "ptimer_rt_abs_signal") || !strcmp(a, "ptimer_mono_rel_signal")) { + int rt = strstr(a, "_rt_") != NULL; sigset_t set; sigemptyset(&set); sigaddset(&set, SIGRTMIN); sigprocmask(SIG_BLOCK, &set, NULL); + struct sigevent sev = {.sigev_notify = SIGEV_SIGNAL, .sigev_signo = SIGRTMIN}; timer_t id; + if (timer_create(rt ? CLOCK_REALTIME : CLOCK_MONOTONIC, &sev, &id)) return errno; + struct itimerspec it = {{0, 0}, rt ? clk(CLOCK_REALTIME, s) : rel(s)}; + if (timer_settime(id, rt ? TIMER_ABSTIME : 0, &it, NULL)) return errno; + siginfo_t si; return sigwaitinfo(&set, &si) == SIGRTMIN ? 0 : errno; + } + if (!strcmp(a, "ptimer_rt_abs_thread")) { + struct sigevent sev = {.sigev_notify = SIGEV_THREAD}; sev.sigev_notify_function = on_ptimer; timer_t id; + if (timer_create(CLOCK_REALTIME, &sev, &id)) return errno; + struct itimerspec it = {{0, 0}, clk(CLOCK_REALTIME, s)}; + if (timer_settime(id, TIMER_ABSTIME, &it, NULL)) return errno; + while (sem_wait(&tsem) != 0) ; + return 0; + } + if (!strcmp(a, "futex_bitset_rt_abs")) { ts = clk(CLOCK_REALTIME, s); return syscall(SYS_futex, &word, FUTEX_WAIT_BITSET_PRIVATE | FUTEX_CLOCK_REALTIME, 0, &ts, NULL, FUTEX_BITSET_MATCH_ANY) ? errno : 0; } + if (!strcmp(a, "sem_clockwait_rt")) { ts = clk(CLOCK_REALTIME, s); return sem_clockwait(&sem, CLOCK_REALTIME, &ts) ? errno : 0; } + if (!strcmp(a, "mutex_clocklock_rt")) { hold(); ts = clk(CLOCK_REALTIME, s); return pthread_mutex_clocklock(&held, CLOCK_REALTIME, &ts); } + if (!strcmp(a, "cond_timedwait_gtod")) { struct timeval tv; gettimeofday(&tv, NULL); ts.tv_sec = tv.tv_sec; ts.tv_nsec = tv.tv_usec * 1000; ts = plus(ts, s); pthread_mutex_lock(&m); return pthread_cond_timedwait(&c_rt, &m, &ts); } + if (!strcmp(a, "gtod_poll_loop")) { + /* a deadline from gettimeofday, polled with relative sleeps of at most 100 ms */ + struct timeval tv; gettimeofday(&tv, NULL); double end = tv.tv_sec + tv.tv_usec / 1e6 + s; + for (;;) { gettimeofday(&tv, NULL); double left = end - (tv.tv_sec + tv.tv_usec / 1e6); if (left <= 0) return 0; usleep((useconds_t)((left < 0.1 ? left : 0.1) * 1e6)); } + } + fprintf(stderr, "unknown %s\n", a); + return -1; +} + +int main(int argc, char **argv) { + pthread_condattr_t at; pthread_condattr_init(&at); pthread_condattr_setclock(&at, CLOCK_MONOTONIC); pthread_cond_init(&c_mono, &at); + sem_init(&sem, 0, 0); sem_init(&tsem, 0, 0); + double s = argc > 2 ? atof(argv[2]) : 1.0; + double r0 = secs(CLOCK_MONOTONIC_RAW), u0 = secs(CLOCK_REALTIME), m0 = secs(CLOCK_MONOTONIC); + long r = run(argv[1], s); + double r1 = secs(CLOCK_MONOTONIC_RAW), u1 = secs(CLOCK_REALTIME), m1 = secs(CLOCK_MONOTONIC); + if (r == ETIMEDOUT) r = 0; + printf("{\"api\": \"%s\", \"lang\": \"c\", \"real\": %.6f, \"u_rt\": %.6f, \"u_mono\": %.6f, \"rc\": %ld, \"start_raw\": %.6f}\n", + argv[1], r1 - r0, u1 - u0, m1 - m0, r, r0); + return 0; +} diff --git a/tst/clock/programs/uwait.js b/tst/clock/programs/uwait.js new file mode 100644 index 000000000..68213eb3c --- /dev/null +++ b/tst/clock/programs/uwait.js @@ -0,0 +1,8 @@ +// One wait of SECONDS of universe time in Node: node uwait.js API SECONDS. Prints READY, then DONE . +const [api, s] = [process.argv[2], parseFloat(process.argv[3])]; +const n0 = process.hrtime.bigint(), w0 = Date.now(); +console.log("READY"); +const done = () => console.log(`DONE ${Number(process.hrtime.bigint() - n0) / 1e9} ${(Date.now() - w0) / 1e3}`); +if (api === "node_settimeout") setTimeout(done, s * 1000); +else if (api === "node_setinterval") { const t = setInterval(() => { clearInterval(t); done(); }, s * 1000); } +else if (api === "node_promises_settimeout") require("timers/promises").setTimeout(s * 1000).then(done); diff --git a/tst/clock/programs/uwait.py b/tst/clock/programs/uwait.py new file mode 100644 index 000000000..74df6af72 --- /dev/null +++ b/tst/clock/programs/uwait.py @@ -0,0 +1,52 @@ +"""One wait of SECONDS of universe time through a Python API: uwait.py API SECONDS. Prints one JSON line (real +seconds from CLOCK_MONOTONIC_RAW, which the library leaves real).""" +import asyncio +import json +import queue +import select +import selectors +import sys +import threading +import time + +RAW = time.CLOCK_MONOTONIC_RAW + + +def swallow(fn): + try: + fn() + except queue.Empty: + pass + + +def poll_monotonic(s): + end = time.monotonic() + s + while time.monotonic() < end: + time.sleep(0.005) + + +def until_datetime(s): + import datetime + end = datetime.datetime.now() + datetime.timedelta(seconds=s) + while (left := (end - datetime.datetime.now()).total_seconds()) > 0: + time.sleep(min(left, 0.1)) + + +TESTS = { + "py_until_datetime": until_datetime, + "py_time_sleep": lambda s: time.sleep(s), + "py_event_wait": lambda s: threading.Event().wait(s), + "py_condition_wait": lambda s: (lambda c: (c.acquire(), c.wait(s), c.release()))(threading.Condition()), + "py_lock_acquire_timeout": lambda s: (lambda l: (l.acquire(), l.acquire(timeout=s)))(threading.Lock()), + "py_queue_get_timeout": lambda s: swallow(lambda: queue.Queue().get(timeout=s)), + "py_select": lambda s: select.select([], [], [], s), + "py_selectors_epoll": lambda s: selectors.DefaultSelector().select(s), + "py_asyncio_sleep": lambda s: asyncio.run(asyncio.sleep(s)), + "py_poll_monotonic_deadline": poll_monotonic, +} +api, s = sys.argv[1], float(sys.argv[2]) +r0, u0, m0 = time.clock_gettime(RAW), time.time(), time.monotonic() +TESTS[api](s) +r1, u1, m1 = time.clock_gettime(RAW), time.time(), time.monotonic() +print(json.dumps({"api": api, "lang": "python", "real": r1 - r0, "u_rt": u1 - u0, "u_mono": m1 - m0, "rc": 0, + "start_raw": r0}), flush=True) diff --git a/tst/clock/reads_test.py b/tst/clock/reads_test.py new file mode 100644 index 000000000..65fac6a1f --- /dev/null +++ b/tst/clock/reads_test.py @@ -0,0 +1,21 @@ +"""Clock reads: never back, whatever the writer's timing, and never a mix of two pages.""" + + +def test_a_stalled_writer_never_steps_realtime_or_monotonic_back(gate): + """2,000 writes alternating rate 60 and frozen, each stalled 5 ms after reading its real time, under 4 tight readers.""" + r = gate("stall") + assert r["writes"] == 2000 and r["reads"] > 1_000_000 + assert (r["rt_back"], r["mono_back"]) == (0, 0), r + assert all(abs(rate - 1) < 1e-3 for rate in r["mono_rate"]), r + + +def test_a_read_is_never_torn_between_two_pages(gate): + r = gate("torn", 10.0) + assert r["writes"] > 1000 and r["reads"] > 1_000_000 and r["universe_a"] > 0 and r["universe_b"] > 0, r + assert r["torn"] == 0, r + + +def test_a_page_a_writer_died_writing_is_taken_up_by_the_next_write(gate): + """seq left odd: the next write goes on from the even seq after it, writes the universe as given and is read.""" + r = gate("deadwriter") + assert r["seq"] == r["dead_seq"] + 3 and r["seq"] % 2 == 0 and not r["continued"] and r["read"] == "universe", r diff --git a/tst/clock/stress_test.py b/tst/clock/stress_test.py new file mode 100644 index 000000000..8991923a5 --- /dev/null +++ b/tst/clock/stress_test.py @@ -0,0 +1,35 @@ +"""Stress: realtime condvar handoffs and sem_timedwait posts (with futex_waitv and without it), with the page changing +under them, lose nothing.""" +import pytest + +SECONDS = 10 + + +@pytest.fixture(scope="module") +def condvars(gate): + return gate("condstress", float(SECONDS)) + + +def test_a_realtime_condvar_handoff_is_never_lost_or_late(condvars): + stress = [r for r in condvars if r["test"] == "stress"] + assert len(stress) == 4 + for r in stress: + assert (r["over_50ms"], r["timeouts"]) == (0, 0), r + assert r["handoffs"] >= 0.5 * SECONDS * 1e6 / (r["gap_us"] + 30), r + if r["page_changes"] == 0: + assert r["spurious"] == 0, r + + +def test_a_single_timed_wait_times_out_at_its_clock_time(condvars): + singles = {r["label"]: r for r in condvars if r["test"] == "single"} + for label, (low, high) in {"unchanged": (2.99, 3.3), "rewrites": (2.99, 3.3), "jump": (0.99, 1.3)}.items(): + results = singles[label]["results"] + assert len(results) == 20 and all(rc == "ETIMEDOUT" and low <= real <= high for rc, real in results), (label, results) + + +def test_a_sem_post_is_never_lost_under_a_realtime_timed_wait(waits_gate): + rows = waits_gate("semstress", float(SECONDS)) + assert len(rows) == 4 + for r in rows: + assert r["posts"] == r["taken"] > 0, r + assert (r["errors"], r["timeouts"], r["stuck_2s"], r["over_50ms"]) == (0, 0, 0, 0), r diff --git a/tst/clock/waits_test.py b/tst/clock/waits_test.py new file mode 100644 index 000000000..65b73e559 --- /dev/null +++ b/tst/clock/waits_test.py @@ -0,0 +1,129 @@ +"""Waits: every wait API in C, Python, Java and Node is either elapsed time, real at any rate, or a wait for a set clock +time, which follows the universe through rates, a freeze, a rate change, a set_time back and forward jumps, with +futex_waitv and without it; cancelled, it leaves nothing behind.""" +import pytest + +#: Single pthread_cond_timedwait/clockwait calls return 0 once when a change moves their deadline without passing it +#: (a rate change, a set_time back), as POSIX allows of a timed wait; every caller's predicate loop absorbs it. +SINGLE_CALL_CONDVARS = {"cond_timedwait_rt", "cond_clockwait_rt", "cond_timedwait_gtod"} +ETIMEDOUT, EINVAL = 110, 22 # Linux's, where the programs run +#: What each jumpwaits waiter returns: a timed wait's error, a timerfd's expirations, a loop's calls (two when the jump +#: moves its deadline without passing it, one spurious return), 0 for sleeps and POSIX timers. +JUMP_RC = {"cns_in": {0}, "tfd_poll_in": {1}, "tfd_read_in": {1}, "tfd_epoll_in": {1}, "ptimer_thread_in": {0}, + "ptimer_signal_in": {0}, "cond_rt_in": {ETIMEDOUT}, "cond_clock_rt_in": {ETIMEDOUT}, "cond_loop_in": {1}, + "sem_rt_in": {ETIMEDOUT}, "futex_rt_in": {ETIMEDOUT}, "mutex_rt_in": {ETIMEDOUT}, "cns_after": {0}, + "tfd_poll_after": {1}, "cond_loop_after": {1, 2}, "ptimer_thread_after": {0}, "nanosleep_rel": {0}, + "poll_rel": {0}, "cond_mono_rel": {ETIMEDOUT}, "tfd_mono_rel": {0}, "cns_mono_rel": {0}} + + +def within(r: dict, low: float, high: float) -> bool: + """Ended ``low`` ms or more after the change's write started and ``high`` ms or less after it ended: the page + changes, and waits wake, before the write's last step, which a slow machine can take tens of ms over.""" + return "end_after_change_ms" in r and r["end_after_change_start_ms"] >= low and r["end_after_change_ms"] <= high + + +def at_jump(w: dict) -> bool: + """A wait the jump ended: not before its write started (the page changes, and waits wake, before the write's last + step), and within 250 ms of the write's end.""" + return w["after_write_start_ms"] is not None and w["after_write_start_ms"] >= -1 and w["after_write_end_ms"] <= 250 + + +def failed(results) -> dict: + """Results with an error: C and Python report one (a timeout is 0); Java and Node end without one.""" + return {r["api"]: r.get("rc") for r in results + if "status" not in r and not r["api"].startswith(("java_", "node_")) and r.get("rc") != 0} + + +@pytest.fixture(scope="module") +def waits(waits_gate): + return {sc["scenario"]: sc["results"] for sc in waits_gate("waits")} + + +@pytest.mark.parametrize("rate", [1, 2, 60]) +def test_a_wait_is_real_elapsed_time_or_a_clock_time_reached_at_the_rate(waits, rate): + results = waits[f"rate {rate}"] + assert len(results) == 64 and not [r for r in results if "status" in r] + elapsed = {r["api"]: r["real"] for r in results if r["class"] == "elapsed"} + clock = {r["api"]: r["real"] for r in results if r["class"] == "clock_time"} + assert len(clock) == 19 and failed(results) == {} + assert {api: s for api, s in elapsed.items() if not 0.99 <= s <= 1.3} == {} + assert {api: s for api, s in clock.items() if not 1 / rate - 0.01 <= s <= 1 / rate + 0.15} == {} + + +def test_frozen_a_clock_time_wait_ends_at_the_advance_and_elapsed_time_runs_on(waits): + results = waits["frozen, +1 h at 2.5 s"] + assert len(results) == 64 and not [r for r in results if "status" in r] and failed(results) == {} + assert {r["api"]: r["real"] for r in results if r["class"] == "elapsed" and not 0.99 <= r["real"] <= 1.3} == {} + assert {r["api"]: r["end_after_change_ms"] for r in results + if r["class"] == "clock_time" and not within(r, -1, 150)} == {} + + +@pytest.fixture(scope="module") +def changes(waits_gate): + return {sc["scenario"]: sc["results"] for sc in waits_gate("ratechange")} + + +def test_a_rate_change_moves_a_clock_time_wait_to_the_new_rate(changes): + """10 s of clock time at rate 1, rate 60 at 1.0 s: the 9 s left take 0.15 s.""" + results = {r["api"]: r for r in changes["rate 1, rate 60 at 1.0 s"]} + assert len(results) == 19 and not [r for r in results.values() if "status" in r] + assert failed(results.values()) == {} + assert {api: r["end_after_change_ms"] for api, r in results.items() + if api in SINGLE_CALL_CONDVARS and not (within(r, -1, 5) and r["rc"] == 0)} == {} + assert {api: r["end_after_change_ms"] for api, r in results.items() + if api not in SINGLE_CALL_CONDVARS and not within(r, 140, 300)} == {} + + +def test_a_set_time_back_makes_a_clock_time_wait_wait_on_for_its_deadline(changes): + results = {r["api"]: r for r in changes["rate 1, set_time -182 d at 0.5 s"]} + assert len(results) == 19 + assert {api: r.get("status") for api, r in results.items() + if api not in SINGLE_CALL_CONDVARS and r.get("status") != "still waiting"} == {} + assert {api: r.get("end_after_change_ms") for api, r in results.items() + if api in SINGLE_CALL_CONDVARS and not within(r, -1, 5)} == {} + assert failed(results.values()) == {} + + +def test_a_jump_ends_the_waits_it_passes_and_none_other_and_monotonic_time_runs_on(waits_gate): + """30 forward jumps of 8 h, a day or a week at rate 1: a wait for a clock time inside the jump ends at it (once the + write has started, and within 250 ms of its end), one past it ends when its clock time comes and never early, and a + relative wait takes its 3 s of real time; each ends as it should (a timeout, an expiration, a sleep done).""" + jumps = waits_gate("jumps") + assert len(jumps["rounds"]) == 30 and jumps["mono_back_total"] == 0 + assert abs(jumps["mono_rate_final"] - 1) < 1e-3 + wrong = [] + for row in jumps["rounds"]: + assert row["waiters"].keys() == JUMP_RC.keys() + for name, w in row["waiters"].items(): + if w["rc"] not in JUMP_RC[name]: + wrong.append((row["round"], name, w)) + elif w["kind"] == "in" and not at_jump(w): + wrong.append((row["round"], name, w)) + elif w["kind"] == "after" and not -1 <= w["vs_expected_ms"] <= 1000: + wrong.append((row["round"], name, w)) + elif w["kind"] == "rel" and not 2.999 <= w["real_s"] <= 3.25: + wrong.append((row["round"], name, w)) + assert wrong == [] + + +def test_a_cancelled_wait_goes_at_once_and_leaves_nothing_behind(waits_gate): + """sem_timedwait and clock_nanosleep are cancellation points; 600 cancelled condvar waits, more than the library's + table holds, leave no entry, so the condvar wait after them still ends at a jump past its deadline. A realtime futex + wait on an unaligned word fails at once, as the kernel's does.""" + r = waits_gate("cancel") + assert (r["sem_cancelled"], r["sem_waiters"], r["cns_cancelled"], r["cond_cancelled"]) == (1, 0, 1, 600), r + assert r["misaligned_errno"] == EINVAL and r["misaligned_ms"] < 50, r + assert r["rc"] == ETIMEDOUT and at_jump(r), r + + +def test_a_timerfd_is_followed_through_its_duplicates_not_its_number(gate): + """A timerfd armed for a clock time, duplicated and closed, fires at a jump past it through its duplicate, and the + monotonic timerfd given its old number doesn't fire with it; dup2, dup3 and close_range calls on it that fail leave + it followed.""" + runs = gate("timerfds") + assert runs.keys() == {"dup", "dup2", "dup3", "fcntl", "close_range", "failed"} + for how, r in runs.items(): + expected = (0, 3) if how == "failed" else (1, 0) + assert (r["same_number"], r["failed_calls"]) == expected, (how, r) + assert (r["a2_count"], r["o_fired_early"]) == (1, 0), (how, r) + assert at_jump(r), (how, r)