From ddd65381f12ef670ee08ddd6b94799016dbc2ec9 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 11 Nov 2026 09:18:44 -0800 Subject: [PATCH 1/5] feat(computers): agent-env's libfaketime for the computer's clock, gated on x86_64 and arm64 (FD-5474) src/agent_env/computers/clock is the library every computer's session programs will load to read universe time: upstream libfaketime 0.9.12 from its sha256-pinned tarball with agent-env's changes, as a small libfaketime.patch and two engine files it includes (agentenv_clock_page.c, agentenv_clock_waits.c; GPL-2.0-only, as libfaketime is), built by build.sh, with the page writer the follower uses (clockpage.py) and the library's default settings (clock.conf). Clock reads and waits for a set realtime clock time follow the universe; monotonic and boot clocks and every relative wait stay real; only the session user is faked; settings are read once at load. A clock read now copies the page before reading the real time and keeps the pair only if the page is still the one copied: a reader that waited out a write paired a time from before it with the page it wrote, which stepped CLOCK_REALTIME back by up to ~280 ns at a rate change. tst/clock (make clock-test, .github/workflows/clock.yml on x86_64 and arm64) builds it and stock 0.9.12 in an Ubuntu 24.04 image and gates it: every wait API in C, Python, Java and Node at several rates, a rate change, a set_time back and 30 jumps, condvar and semaphore stress, a stalled writer, torn reads, who is faked, the marker, the environment race and Chromium launched directly 60 times (stock 0.9.12 as the positive control), in under 30 s. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/clock.yml | 59 + AGENTS.md | 5 +- Makefile | 8 +- THIRD_PARTY_NOTICES.md | 16 +- src/agent_env/computers/clock/COPYING | 339 ++++ src/agent_env/computers/clock/README.md | 50 + .../computers/clock/agentenv_clock_page.c | 320 ++++ .../computers/clock/agentenv_clock_waits.c | 926 ++++++++++ src/agent_env/computers/clock/build.sh | 20 + src/agent_env/computers/clock/clock.conf | 10 + src/agent_env/computers/clock/clockpage.py | 147 ++ .../computers/clock/libfaketime.patch | 1533 +++++++++++++++++ tst/clock/Dockerfile | 28 + tst/clock/__init__.py | 0 tst/clock/chromium_test.py | 9 + tst/clock/conftest.py | 87 + tst/clock/harness.py | 489 ++++++ tst/clock/process_test.py | 18 + tst/clock/programs/UWait.java | 50 + tst/clock/programs/condstress2.c | 70 + tst/clock/programs/envrace.c | 44 + tst/clock/programs/jumpwaits.c | 153 ++ tst/clock/programs/marker.sh | 7 + tst/clock/programs/rawsys.h | 20 + tst/clock/programs/rtread.c | 39 + tst/clock/programs/semstress.c | 39 + tst/clock/programs/torn.c | 48 + tst/clock/programs/uwait.c | 140 ++ tst/clock/programs/uwait.js | 8 + tst/clock/programs/uwait.py | 52 + tst/clock/reads_test.py | 15 + tst/clock/stress_test.py | 34 + tst/clock/waits_test.py | 73 + 33 files changed, 4851 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/clock.yml create mode 100644 src/agent_env/computers/clock/COPYING create mode 100644 src/agent_env/computers/clock/README.md create mode 100644 src/agent_env/computers/clock/agentenv_clock_page.c create mode 100644 src/agent_env/computers/clock/agentenv_clock_waits.c create mode 100755 src/agent_env/computers/clock/build.sh create mode 100644 src/agent_env/computers/clock/clock.conf create mode 100644 src/agent_env/computers/clock/clockpage.py create mode 100644 src/agent_env/computers/clock/libfaketime.patch create mode 100644 tst/clock/Dockerfile create mode 100644 tst/clock/__init__.py create mode 100644 tst/clock/chromium_test.py create mode 100644 tst/clock/conftest.py create mode 100644 tst/clock/harness.py create mode 100644 tst/clock/process_test.py create mode 100644 tst/clock/programs/UWait.java create mode 100644 tst/clock/programs/condstress2.c create mode 100644 tst/clock/programs/envrace.c create mode 100644 tst/clock/programs/jumpwaits.c create mode 100644 tst/clock/programs/marker.sh create mode 100644 tst/clock/programs/rawsys.h create mode 100644 tst/clock/programs/rtread.c create mode 100644 tst/clock/programs/semstress.c create mode 100644 tst/clock/programs/torn.c create mode 100644 tst/clock/programs/uwait.c create mode 100644 tst/clock/programs/uwait.js create mode 100644 tst/clock/programs/uwait.py create mode 100644 tst/clock/reads_test.py create mode 100644 tst/clock/stress_test.py create mode 100644 tst/clock/waits_test.py diff --git a/.github/workflows/clock.yml b/.github/workflows/clock.yml new file mode 100644 index 000000000..8e911bdcc --- /dev/null +++ b/.github/workflows/clock.yml @@ -0,0 +1,59 @@ +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, condvar and semaphore stress, a stalled +# writer, 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..37bd18616 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, condvar and semaphore stress, a stalled writer, torn reads, the marker, the environment race, and Chromium launched 60 times with it and 30 with stock; 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..d9d2b3999 --- /dev/null +++ b/src/agent_env/computers/clock/README.md @@ -0,0 +1,50 @@ +# 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. +- **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) and the + environment, so 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, condition-variable and semaphore stress, a +stalled writer, 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..1303b0737 --- /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 and the environment, 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..01afcf20a --- /dev/null +++ b/src/agent_env/computers/clock/agentenv_clock_waits.c @@ -0,0 +1,926 @@ +/* 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. ---- + */ +struct ft_timer +{ + int kind; /* 0 free, 1 timerfd, 2 POSIX timer */ + int tracked; /* armed for a realtime clock time */ + int fd; + 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 && ft_timers[i].fd == 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->fd, 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->fd, 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 ---- */ + +/* 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; + for (;;) + { + uint32_t seq32 = (uint32_t) s.seq; + int64_t until; + struct timespec ts; + if (ft_reached(&s, due)) return 0; + until = ft_real_of(&s, due); + ts = ft_ts(until); + if (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) == -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_NOSYS }; +/* until *word isn't val, the page changes from seq32, or real realtime ``until``. */ +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_W_NOSYS; + /* 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; + ft_waitv_missing = 1; + return FT_W_NOSYS; +} + +/* 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_waitv_missing || !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_NOSYS) + { + struct timespec t = ft_ts(ft_real_of(&s, due)); + return ft_raw_syscall6(SYS_futex, (long) uaddr, op, (long) val, (long) &t, 0, FUTEX_BITSET_MATCH_ANY); + } + 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 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 = 0; + *handled = false; + if (ft_waitv_missing || sizeof(sem_t) < sizeof(struct ft_new_sem) || !ft_snap(&s)) return 0; + *handled = true; + 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; + d = __atomic_fetch_add(&sem->data, (uint64_t) 1 << 32, __ATOMIC_RELAXED); + for (;;) + { + if ((d & 0xffffffffu) == 0) + { + int k; + if (ft_reached(&s, due)) + { + err = ETIMEDOUT; + break; + } + k = ft_wait2((uint32_t *) &sem->data, 0, sem->private_flag == 0, (uint32_t) s.seq, ft_real_of(&s, due)); + if (k == FT_W_INTR) + { + err = EINTR; + break; + } + if (k == FT_W_NOSYS) + { + struct timespec t = {0, 100000000}; + ft_raw_syscall6(SYS_futex, (long) &sem->data, (FUTEX_WAIT | FUTEX_PRIVATE_FLAG) ^ sem->private_flag, 0, + (long) &t, 0, 0); + } + 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)) + return 0; + } + __atomic_fetch_sub(&sem->data, (uint64_t) 1 << 32, __ATOMIC_RELAXED); + errno = err; + return -1; +} + +/* 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 = real_pthread_cond_clockwait(cond, mutex, CLOCK_REALTIME, &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; + 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) + { + if (t->tracked) + { + ft_timer_value(t, &prev); + was = true; + } + t->kind = 0; + } + 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); + t = ft_timer_new(); + if (t == NULL) + { + ft_tab_unlock(&old); + *handled = false; /* table full: the caller's own (translated once) path */ + return 0; + } + t->kind = 1; + t->fd = fd; + 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->kind = 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; +} + +/* ---- 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..6c64f6ab4 --- /dev/null +++ b/src/agent_env/computers/clock/clock.conf @@ -0,0 +1,10 @@ +# Read once by agent-env's libfaketime as each process loads it (the environment can override a setting). 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..7579b139d --- /dev/null +++ b/src/agent_env/computers/clock/clockpage.py @@ -0,0 +1,147 @@ +"""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 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 + m[8:16] = struct.pack(" 1 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..c882fe1cf --- /dev/null +++ b/src/agent_env/computers/clock/libfaketime.patch @@ -0,0 +1,1533 @@ +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 +--- pa/src/libfaketime.c 2026-11-11 09:07:01 ++++ pb/src/libfaketime.c 2026-11-11 09:07:01 +@@ -231,12 +231,15 @@ + 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__ + #ifdef FAKE_TIMERS +@@ -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,14 +1418,40 @@ + 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); + if (user_rate_set) +@@ -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) + { ++ struct timespec mono, slice; ++ unsigned long gen; ++ bool last; + int result; +- struct timespec real_abs_timeout, *real_abs_timeout_pt; +- +- /* sanity check */ +- if (abs_timeout == NULL) ++ ft_deadline_from_fake(CLOCK_MONOTONIC, fake_abs, &mono); ++ gen = ft_tl_gen; ++ for (;;) + { +- 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) ++ 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,24 @@ + * Faked timer_settime() + * Does not affect timer speed when stepping clock with each time() call. + */ ++/* agentenv: per timerfd, whether its clock is other than CLOCK_REALTIME, as timerfd_create was told. */ ++#define FT_TIMERFDS 65536 ++static unsigned char ft_timerfd_other_clock[FT_TIMERFDS]; ++ ++#ifdef __linux__ ++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_tfd_clock[fd] = clockid + 1; ++ if (fd >= 0 && fd < FT_TIMERFDS) ++ ft_timerfd_other_clock[fd] = (clockid != CLOCK_REALTIME && clockid != CLOCK_REALTIME_ALARM); ++ return fd; ++} ++#endif ++ + static int + timer_settime_common(timer_t_or_int timerid, int flags, + const struct itimerspec *new_value, +@@ -1977,18 +2020,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 && timerid.int_member >= 0 && timerid.int_member < FT_TIMERFDS ++ && ft_timerfd_other_clock[timerid.int_member])) ++ 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,49 +2097,8 @@ + 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() + * Does not affect timer speed when stepping clock with each time() call. + */ +@@ -2151,49 +2146,7 @@ + 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__ + /* + * Faked timerfd_settime +@@ -2203,6 +2156,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 +2183,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 +2347,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 +2465,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 +2501,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 +2529,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 +2549,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 +2568,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 +2587,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 +2600,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 +2638,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 +2671,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 +2754,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"); ++ real_pthread_mutex_clocklock = dlsym(RTLD_NEXT, "pthread_mutex_clocklock"); + } +- +- if (pthread_rwlock_init(&monotonic_conds_lock,NULL) != 0) { +- fprintf(stderr,"monotonic_conds_lock init failed\n"); +- exit(-1); +- } ++ 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 +2812,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 +2825,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 +2868,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 +2885,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 +2900,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 +2929,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 +2966,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 +3014,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 +3046,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 +3086,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 +3095,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 +3239,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 +3278,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 +3334,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 +3438,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 +3461,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 +3485,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 +3607,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 +3715,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,240 +3813,126 @@ + + #ifdef FAKE_PTHREAD + +-typedef enum {FT_COMPAT_GLIBC_2_2_5, FT_COMPAT_GLIBC_2_3_2} ft_lib_compat_pthread; ++/* ++ * 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 + +-struct pthread_cond_monotonic { +- pthread_cond_t *ptr; +- UT_hash_handle hh; +-}; +- +-static struct pthread_cond_monotonic *monotonic_conds = NULL; +- +-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); ++ 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; + } +- HASH_FIND_PTR(monotonic_conds, &cond, e); +- if (e) { +- HASH_DEL(monotonic_conds, e); +- free(e); +- } +- 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? */ ++ 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 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; ++static int ft_mutex_lock_until(pthread_mutex_t *mutex, const struct timespec *fake_abs) ++{ ++ 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); ++ 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 +-#endif +- result = false; // avoid forced monotonic fixes unless really necessary ++ if (result != ETIMEDOUT) return result; ++ if (!ft_deadline_moved(fake_abs, &mono, &gen) && last) return ETIMEDOUT; + } +- +- 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; + } + +-int pthread_cond_timedwait_common(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime, ft_lib_compat_pthread compat) ++int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abstime) + { +- 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) ++ if (real_pthread_mutex_timedlock == NULL) return EINVAL; ++ if (ft_uw_active() && ft_translates_wait(abstime) && real_pthread_mutex_clocklock != NULL) + { +- 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; +- } +- 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; ++ bool handled; ++ int r = ft_u_mutex_lock(mutex, ft_ns(abstime), &handled); ++ if (handled) return r; + } +- +- 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; +- } +- return result; ++ 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_225(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_2_5); ++ 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); + } + +-int pthread_cond_timedwait_232(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) +-{ +- return pthread_cond_timedwait_common(cond, mutex, abstime, FT_COMPAT_GLIBC_2_3_2); +-} +- +-__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 +- + /* + * Intercept calls to time-setting functions if compiled with FAKE_SETTIME set. + * Based on suggestion and prototype by @ojura, see https://github.com/wolfcw/libfaketime/issues/179 +@@ -4005,15 +3981,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 +4010,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 +4101,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 +4145,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 +4246,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 +4258,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 +--- pa/src/libfaketime.map 2026-11-11 09:07:01 ++++ pb/src/libfaketime.map 2026-11-11 09:07:01 +@@ -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..260456ef8 --- /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; 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..3e5b1be2c --- /dev/null +++ b/tst/clock/chromium_test.py @@ -0,0 +1,9 @@ +"""Chromium, the client stock libfaketime crashed on about one launch in five: launched directly, it runs every time with +this library, and stock 0.9.12, the positive control, fails it. The whole gate runs in under 30 s.""" + + +def test_chrome_runs_every_launch_with_this_library_and_stock_fails_it(gate): + r = gate("chromium") + assert r["seconds"] <= 30, r + assert (r["agentenv"]["launches"], r["agentenv"]["failed"]) == (60, 0), r["agentenv"] + assert r["stock"]["launches"] == 10 and r["stock"]["failed"] >= 1, r["stock"] diff --git a/tst/clock/conftest.py b/tst/clock/conftest.py new file mode 100644 index 000000000..13dc6d209 --- /dev/null +++ b/tst/clock/conftest.py @@ -0,0 +1,87 @@ +"""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) -> 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): + r = subprocess.run(["docker", "run", "--rm", "--shm-size=1g", 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 diff --git a/tst/clock/harness.py b/tst/clock/harness.py new file mode 100644 index 000000000..e2a83ed6a --- /dev/null +++ b/tst/clock/harness.py @@ -0,0 +1,489 @@ +"""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] | condstress [SECONDS] | semstress [SECONDS] | stall | torn [SECONDS] + | marker | identity | envrace [SECONDS] | chromium [OURS STOCK PARALLEL] +""" +import json +import os +import random +import select +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 ran.""" + 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) + info = fn() + ran.append({"label": label, "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_raw=None) -> dict: + 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"]) + if change_raw is not None: + out["end_after_change_ms"] = round((j["start_raw"] + j["real"] - change_raw) * 1000, 1) + 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)) + if change_raw is not None: + out["end_after_change_ms"] = round((p["done"] - change_raw) * 1000, 1) + else: + out["status"] = line[:300] + 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 []) + change_raw = ran[0]["raw"] if ran else None + return {"scenario": name, "start_rate": start_rate, "seconds": seconds, "changes": ran, + "results": [result(p, change_raw) 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_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 _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 _classify(value: float, before: float, universe_offset: float) -> 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(ours=60, stock=10, parallel=8, stock_limit=3.0): + """Chrome launched directly, ``ours`` times with this library in /etc/ld.so.preload, each of which must load + about:blank headless, print it and exit 0; then ``stock`` times with stock 0.9.12 by LD_PRELOAD, the positive + control, which must fail (crash or hang past ``stock_limit`` seconds), ``parallel`` at a time.""" + 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) + + def launch(variant: str, k: int) -> dict: + profile = f"/tmp/chrome-{variant}-{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=20 if variant == "agentenv" else stock_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 " list: + preload(LIB if variant == "agentenv" else None) + with ThreadPoolExecutor(parallel) as pool: + return list(pool.map(lambda k: launch(variant, k), range(n))) + + started = raw() + results = batch("agentenv", ours) + batch("stock", stock) + took = raw() - started + preload(None) + summary = {} + for variant in ("agentenv", "stock"): + mine = [r for r in results if r["variant"] == variant] + summary[variant] = {"launches": len(mine), "failed": sum(not r["ok"] for r in mine), + "signalled": sum(r["signal"] for r in mine), "hung": sum(r["rc"] == "hung" for r in mine), + "launch_s_max": max((r["seconds"] for r in mine), default=0), + "failures": [r for r in mine if not r["ok"]][:5]} + return {"seconds": round(took, 2), "parallel": parallel, **summary} + + +GATES = {f.__name__: f for f in (waits, ratechange, jumps, condstress, semstress, stall, torn, marker, identity, envrace, + chromium)} + +if __name__ == "__main__": + gate, args = GATES[sys.argv[1]], [float(a) if "." in a else int(a) for a in sys.argv[2:]] + print(json.dumps(gate(*args), indent=1)) diff --git a/tst/clock/process_test.py b/tst/clock/process_test.py new file mode 100644 index 000000000..52c56015f --- /dev/null +++ b/tst/clock/process_test.py @@ -0,0 +1,18 @@ +"""Who is faked, the marker that keeps a program real, and the environment read once at load.""" + + +def test_only_the_session_users_processes_follow_an_armed_page(gate): + assert gate("identity") == {"unarmed_session": "real", "session": "universe", "root": "real", "other_user": "real"} + + +def test_a_marked_program_stays_real_and_loses_the_marker_so_its_children_are_faked(gate): + r = gate("marker") + assert r["rc"] == 0, r + assert r["lines"] == {"unmarked_date": "universe", "marked_bash_itself": "real", "marked_bash_child_date": "universe", + "marked_bash_grandchild_sh_date": "universe", "marked_bash_exec_child": "universe", + "marked_bash_environ": 0.0, "child_environ": 0.0, "marked_python_itself": "real", + "marked_python_child": "universe"} + + +def test_setenv_racing_clock_reads_and_timed_waits_never_crashes_it(gate): + assert gate("envrace", 10.0) == [{"rc": 0, "out": "survived"}] * 3 diff --git a/tst/clock/programs/UWait.java b/tst/clock/programs/UWait.java new file mode 100644 index 000000000..57e285e46 --- /dev/null +++ b/tst/clock/programs/UWait.java @@ -0,0 +1,50 @@ +import java.util.concurrent.*; +import java.util.concurrent.locks.*; + +/** One wait of SECONDS of universe time through a Java API: java UWait API SECONDS. Real time is read from + * /proc/uptime-free CLOCK_MONOTONIC_RAW via a tiny JNI-free trick: the parent driver times the process instead, so this + * prints the universe's elapsed time (System.nanoTime, faked) and currentTimeMillis. */ +public class UWait { + public static void main(String[] a) throws Exception { + String api = a[0]; + long ms = (long) (Double.parseDouble(a[1]) * 1000); + Object o = new Object(); + long n0 = System.nanoTime(), w0 = System.currentTimeMillis(); + System.out.println("READY"); + System.out.flush(); + switch (api) { + case "java_thread_sleep": Thread.sleep(ms); break; + case "java_object_wait": synchronized (o) { o.wait(ms); } break; + case "java_park_nanos": LockSupport.parkNanos(ms * 1_000_000L); break; + case "java_queue_poll": new ArrayBlockingQueue(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/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/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..f3593b932 --- /dev/null +++ b/tst/clock/reads_test.py @@ -0,0 +1,15 @@ +"""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 diff --git a/tst/clock/stress_test.py b/tst/clock/stress_test.py new file mode 100644 index 000000000..bee7a0efd --- /dev/null +++ b/tst/clock/stress_test.py @@ -0,0 +1,34 @@ +"""Stress: realtime condvar handoffs and sem_timedwait posts, 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(gate): + rows = 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..7970de1d9 --- /dev/null +++ b/tst/clock/waits_test.py @@ -0,0 +1,73 @@ +"""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.""" +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"} + + +@pytest.fixture(scope="module") +def waits(gate): + return {sc["scenario"]: sc["results"] for sc in 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 + 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] + 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 -5 <= r["end_after_change_ms"] <= 150} == {} + + +@pytest.fixture(scope="module") +def changes(gate): + return {sc["scenario"]: sc["results"] for sc in 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 {api: r["end_after_change_ms"] for api, r in results.items() + if api in SINGLE_CALL_CONDVARS and not (abs(r["end_after_change_ms"]) <= 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 140 <= r["end_after_change_ms"] <= 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 abs(r.get("end_after_change_ms", 99)) > 5} == {} + + +def test_a_jump_ends_the_waits_it_passes_and_none_other_and_monotonic_time_runs_on(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, one past it + ends when its clock time comes and never early, and a relative wait takes its 3 s of real time.""" + jumps = 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"]: + for name, w in row["waiters"].items(): + if w["kind"] == "in" and not -5 <= w["after_write_end_ms"] <= 250: + 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 == [] From 0a54493e8e3298b0430fbb4bb307b8dee718c01c Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 11 Nov 2026 09:55:36 -0800 Subject: [PATCH 2/5] fix(computers): cancellable waits, waits without futex_waitv, timerfds by file, a dead page writer (FD-5474) Greptile's review of the clock library: - a condvar wait cancelled mid-wait gives up its table entry (cleanup handler); - sem_timedwait and the universe clock_nanosleep are cancellation points again, and a cancelled semaphore wait leaves the waiter count as it found it; - without futex_waitv (before Linux 5.16, gVisor, a seccomp filter), realtime futex and semaphore waits wait on the word in slices and plan again from the page between them, instead of a deadline translated once or not at all; - a timerfd's record holds its descriptors, kept through dup, dup2, dup3, fcntl, close and close_range; - clockpage.write takes up a page a dead writer left with seq odd; - clock.conf and the README say settings are file-only while the file exists. The gates check every wait's return code, and new gates cover cancellation, timerfd duplicates, a dead writer, and the waits again in containers that refuse futex_waitv. The patch's headers are a/ and b/ again. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/clock.yml | 7 +- AGENTS.md | 2 +- src/agent_env/computers/clock/README.md | 18 +- .../computers/clock/agentenv_clock_page.c | 4 +- .../computers/clock/agentenv_clock_waits.c | 303 +++++++++++++++--- src/agent_env/computers/clock/clock.conf | 5 +- src/agent_env/computers/clock/clockpage.py | 5 +- .../computers/clock/libfaketime.patch | 286 ++++++++--------- tst/clock/Dockerfile | 2 +- tst/clock/conftest.py | 33 +- tst/clock/harness.py | 80 ++++- tst/clock/programs/cancel.c | 81 +++++ tst/clock/programs/tfdalias.c | 43 +++ tst/clock/reads_test.py | 6 + tst/clock/stress_test.py | 7 +- tst/clock/waits_test.py | 61 +++- 16 files changed, 705 insertions(+), 238 deletions(-) create mode 100644 tst/clock/programs/cancel.c create mode 100644 tst/clock/programs/tfdalias.c diff --git a/.github/workflows/clock.yml b/.github/workflows/clock.yml index 8e911bdcc..dc519a766 100644 --- a/.github/workflows/clock.yml +++ b/.github/workflows/clock.yml @@ -2,9 +2,10 @@ 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, condvar and semaphore stress, a stalled -# writer, 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. +# 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: diff --git a/AGENTS.md b/AGENTS.md index 37bd18616..b49a03fc8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +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, condvar and semaphore stress, a stalled writer, torn reads, the marker, the environment race, and Chromium launched 60 times with it and 30 with stock; needs Docker, and fetches Chrome's archive and upstream's tarball once into `AGENTENV_TEST_CLOCK_CACHE` | +| `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 60 times with it and 10 with stock; 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 | diff --git a/src/agent_env/computers/clock/README.md b/src/agent_env/computers/clock/README.md index d9d2b3999..764f2f88f 100644 --- a/src/agent_env/computers/clock/README.md +++ b/src/agent_env/computers/clock/README.md @@ -10,13 +10,16 @@ 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. + 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) and the - environment, so 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. +- **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 @@ -45,6 +48,7 @@ computed from a read a moment earlier) continues from where the page has it inst ## 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, condition-variable and semaphore stress, a -stalled writer, torn reads, the marker, the environment race and Chromium launches. `.github/workflows/clock.yml` runs -them on x86_64 and arm64. +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 index 1303b0737..9167f7ede 100644 --- a/src/agent_env/computers/clock/agentenv_clock_page.c +++ b/src/agent_env/computers/clock/agentenv_clock_page.c @@ -1,8 +1,8 @@ /* 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 and the environment, so no - * getenv() runs after main() starts; who is faked (the session user, unless started with AGENTENV_REAL_TIME=1, a marker + * 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). */ diff --git a/src/agent_env/computers/clock/agentenv_clock_waits.c b/src/agent_env/computers/clock/agentenv_clock_waits.c index 01afcf20a..bafb615d6 100644 --- a/src/agent_env/computers/clock/agentenv_clock_waits.c +++ b/src/agent_env/computers/clock/agentenv_clock_waits.c @@ -226,11 +226,12 @@ static void ft_tab_unlock(const sigset_t *old) * 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 fd; + 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 */ @@ -252,7 +253,9 @@ static int ft_tfd_clock[FT_TFD_CLOCKS]; /* clock + 1 as timerfd_create was tol 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 && ft_timers[i].fd == fd) return &ft_timers[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) @@ -274,7 +277,7 @@ static struct ft_timer *ft_timer_new(void) } static int ft_timer_real_get(struct ft_timer *t, struct itimerspec *cur) { - return t->kind == 1 ? (*real_timerfd_gettime)(t->fd, cur) : (*real_timer_gettime_new)(t->id, 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) @@ -314,7 +317,7 @@ static int ft_timer_arm(struct ft_timer *t, const struct ft_clock_page *s, bool t->pg = *s; t->real_set = (at != FT_NEVER); if (t->kind == 1) - r = (*real_timerfd_settime)(t->fd, TFD_TIMER_ABSTIME | (t->flags & TFD_TIMER_CANCEL_ON_SET), &real, NULL); + 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; @@ -481,35 +484,69 @@ static void ft_helper_start(void) /* ---- 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); - if (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) == -1 && errno == EINTR) - return EINTR; + 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_NOSYS }; +enum { FT_W_WORD, FT_W_PAGE, FT_W_TIMEOUT, FT_W_INTR }; +/* 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; + return errno == EINTR ? FT_W_INTR : FT_W_PAGE; +} /* until *word isn't val, the page changes from seq32, or real realtime ``until``. */ 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_W_NOSYS; + 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; @@ -526,7 +563,7 @@ static int ft_wait2(uint32_t *word, uint32_t val, bool private_word, uint32_t se if (errno == ETIMEDOUT) return FT_W_TIMEOUT; if (errno == EINTR) return FT_W_INTR; ft_waitv_missing = 1; - return FT_W_NOSYS; + return ft_wait1(word, val, private_word, until); } /* futex(2) FUTEX_WAIT_BITSET|FUTEX_CLOCK_REALTIME through syscall(): until the universe reaches the deadline, @@ -536,7 +573,7 @@ static long ft_u_futex_rt(uint32_t *uaddr, int op, uint32_t val, const struct ti struct ft_clock_page s; int64_t due = ft_ns(abs); *handled = false; - if (ft_waitv_missing || !ft_snap(&s)) return 0; + if (!ft_snap(&s)) return 0; *handled = true; for (;;) { @@ -562,11 +599,6 @@ static long ft_u_futex_rt(uint32_t *uaddr, int op, uint32_t val, const struct ti errno = EINTR; return -1; } - if (k == FT_W_NOSYS) - { - struct timespec t = ft_ts(ft_real_of(&s, due)); - return ft_raw_syscall6(SYS_futex, (long) uaddr, op, (long) val, (long) &t, 0, FUTEX_BITSET_MATCH_ANY); - } if (!ft_snap(&s)) return 0; } } @@ -576,42 +608,36 @@ static long ft_u_futex_rt(uint32_t *uaddr, int op, uint32_t val, const struct ti 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 int ft_u_sem_wait(sem_t *sem_, int64_t due, bool *handled) +static void ft_sem_unwait(void *sem) { - struct ft_new_sem *sem = (struct ft_new_sem *) sem_; - struct ft_clock_page s; - uint64_t d; - int err = 0; - *handled = false; - if (ft_waitv_missing || sizeof(sem_t) < sizeof(struct ft_new_sem) || !ft_snap(&s)) return 0; - *handled = true; - 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; - d = __atomic_fetch_add(&sem->data, (uint64_t) 1 << 32, __ATOMIC_RELAXED); + __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; - if (ft_reached(&s, due)) + int k, ct; + if (ft_reached(s, due)) { err = ETIMEDOUT; break; } - k = ft_wait2((uint32_t *) &sem->data, 0, sem->private_flag == 0, (uint32_t) s.seq, ft_real_of(&s, due)); + 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) { err = EINTR; break; } - if (k == FT_W_NOSYS) - { - struct timespec t = {0, 100000000}; - ft_raw_syscall6(SYS_futex, (long) &sem->data, (FUTEX_WAIT | FUTEX_PRIVATE_FLAG) ^ sem->private_flag, 0, - (long) &t, 0, 0); - } - if (!ft_snap(&s)) + if (!ft_snap(s)) { err = ETIMEDOUT; break; @@ -620,13 +646,48 @@ static int ft_u_sem_wait(sem_t *sem_, int64_t due, bool *handled) } else if (__atomic_compare_exchange_n(&sem->data, &d, d - 1 - ((uint64_t) 1 << 32), true, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) - return 0; + break; } - __atomic_fetch_sub(&sem->data, (uint64_t) 1 << 32, __ATOMIC_RELAXED); + 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) { @@ -664,7 +725,7 @@ static int ft_u_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex, const st w->gen = s.generation; ft_tab_unlock(&old); end = ft_ts(real_due == FT_NEVER ? ft_real_rt() + 86400 * FT_NS : real_due); - r = real_pthread_cond_clockwait(cond, mutex, CLOCK_REALTIME, &end); + r = ft_cw_clockwait(w, cond, mutex, &end); ft_tab_lock(&old); woken = w->woken; w->used = 0; @@ -737,7 +798,7 @@ long syscall(long number, ...) 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; + struct ft_timer *t = NULL; struct itimerspec prev; sigset_t old; int clock, r; @@ -750,14 +811,11 @@ static int ft_u_timerfd_settime(int fd, int flags, const struct itimerspec *nv, { ft_tab_lock(&old); t = ft_timer_find_fd(fd); - if (t != NULL) + if (t != NULL && t->tracked) { - if (t->tracked) - { - ft_timer_value(t, &prev); - was = true; - } - t->kind = 0; + 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); } @@ -769,15 +827,18 @@ static int ft_u_timerfd_settime(int fd, int flags, const struct itimerspec *nv, return r; } ft_tab_lock(&old); - t = ft_timer_new(); + 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->kind = 1; - t->fd = fd; t->clock = clock; t->flags = flags; t->due = ft_ns(&nv->it_value); @@ -791,7 +852,7 @@ static int ft_u_timerfd_settime(int fd, int flags, const struct itimerspec *nv, (*real_timerfd_gettime)(fd, ov); } r = ft_timer_arm(t, &s, true); - if (r != 0) t->kind = 0; + if (r != 0) t->tracked = 0; ft_timers_any = 1; ft_tab_unlock(&old); if (r == 0) ft_helper_start(); @@ -814,6 +875,142 @@ static int ft_u_timerfd_gettime(int fd, struct itimerspec *cur, bool *handled) 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. */ +static void ft_fd_duped(int from, int to) +{ + sigset_t old; + struct ft_timer *t; + if (!ft_tfd_known(from) || to < 0 || to >= FT_TFD_CLOCKS) return; + ft_tab_lock(&old); + 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; + } + ft_tab_unlock(&old); +} + +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) +{ + int r; + if (fd != fd2) ft_fd_gone(fd2); + FT_RESOLVE(dup2); + r = (*ft_real_dup2)(fd, fd2); + if (r >= 0 && fd != fd2) ft_fd_duped(fd, r); + return r; +} +int dup3(int fd, int fd2, int flags) +{ + int r; + if (fd != fd2) ft_fd_gone(fd2); + FT_RESOLVE(dup3); + r = (*ft_real_dup3)(fd, fd2, flags); + if (r >= 0) ft_fd_duped(fd, r); + return r; +} +/* 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; +} +int close_range(unsigned int first, unsigned int last, int flags) +{ + if (!(flags & CLOSE_RANGE_CLOEXEC)) + for (unsigned int fd = first; fd <= last && fd < FT_TFD_CLOCKS; fd++) ft_fd_gone((int) fd); + FT_RESOLVE(close_range); + return (*ft_real_close_range)(first, last, flags); +} +/* 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) { diff --git a/src/agent_env/computers/clock/clock.conf b/src/agent_env/computers/clock/clock.conf index 6c64f6ab4..715987d43 100644 --- a/src/agent_env/computers/clock/clock.conf +++ b/src/agent_env/computers/clock/clock.conf @@ -1,5 +1,6 @@ -# Read once by agent-env's libfaketime as each process loads it (the environment can override a setting). Only the -# session user's processes are faked; the clock comes from the page the follower writes, never from a cache. +# 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 diff --git a/src/agent_env/computers/clock/clockpage.py b/src/agent_env/computers/clock/clockpage.py index 7579b139d..9c8238bf0 100644 --- a/src/agent_env/computers/clock/clockpage.py +++ b/src/agent_env/computers/clock/clockpage.py @@ -11,6 +11,7 @@ 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 @@ -89,13 +90,15 @@ def write(armed: int, universe: float, rate: float, anchor: float) -> dict: 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 0 < old_now - new_now < CONTINUITY_NS + continued = armed and generation > 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, diff --git a/src/agent_env/computers/clock/libfaketime.patch b/src/agent_env/computers/clock/libfaketime.patch index c882fe1cf..02be05f5c 100644 --- a/src/agent_env/computers/clock/libfaketime.patch +++ b/src/agent_env/computers/clock/libfaketime.patch @@ -4,9 +4,9 @@ also copies in agentenv_clock_page.c and agentenv_clock_waits.c, included where so licensed as libfaketime is, under the GNU General Public License version 2 (COPYING). diff -ruN a/src/libfaketime.c b/src/libfaketime.c ---- pa/src/libfaketime.c 2026-11-11 09:07:01 -+++ pb/src/libfaketime.c 2026-11-11 09:07:01 -@@ -231,12 +231,15 @@ +--- 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 @@ -20,13 +20,12 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c +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 ++#endif +#ifdef FAKE_TIMERS +static int (*real_timerfd_create) (int, int); -+#endif + #endif #ifndef __APPLE__ - #ifdef FAKE_TIMERS @@ -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); @@ -159,7 +158,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c if (real_nanosleep == NULL) { return -1; -@@ -1395,14 +1418,40 @@ +@@ -1395,13 +1418,39 @@ struct timespec real_req; ftpl_init(); @@ -178,7 +177,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c { - 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; @@ -197,10 +196,9 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c + real_req = *req; + } + else if (flags & TIMER_ABSTIME) /* sleep until absolute time */ -+ { + { struct timespec tdiff, timeadj; timespecsub(req, &user_faked_time_timespec, &timeadj); - if (user_rate_set) @@ -1482,6 +1531,7 @@ int result; @@ -291,18 +289,12 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c -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) { -+ struct timespec mono, slice; -+ unsigned long gen; -+ bool last; - int result; +- int result; - struct timespec real_abs_timeout, *real_abs_timeout_pt; - - /* sanity check */ - if (abs_timeout == NULL) -+ ft_deadline_from_fake(CLOCK_MONOTONIC, fake_abs, &mono); -+ gen = ft_tl_gen; -+ for (;;) - { +- { - return -1; - } - @@ -315,6 +307,14 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c - 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; @@ -330,12 +330,12 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c - } - 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; @@ -420,24 +420,23 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c } #endif -@@ -1949,6 +1974,24 @@ +@@ -1949,6 +1974,23 @@ * Faked timer_settime() * Does not affect timer speed when stepping clock with each time() call. */ -+/* agentenv: per timerfd, whether its clock is other than CLOCK_REALTIME, as timerfd_create was told. */ -+#define FT_TIMERFDS 65536 -+static unsigned char ft_timerfd_other_clock[FT_TIMERFDS]; -+ +#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_tfd_clock[fd] = clockid + 1; -+ if (fd >= 0 && fd < FT_TIMERFDS) -+ ft_timerfd_other_clock[fd] = (clockid != CLOCK_REALTIME && clockid != CLOCK_REALTIME_ALARM); ++ if (fd >= 0 && fd < FT_TFD_CLOCKS) ++ { ++ ft_fd_gone(fd); ++ ft_tfd_clock[fd] = clockid + 1; ++ } + return fd; +} +#endif @@ -445,7 +444,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c static int timer_settime_common(timer_t_or_int timerid, int flags, const struct itimerspec *new_value, -@@ -1977,18 +2020,11 @@ +@@ -1977,18 +2019,11 @@ { if (flags & abstime_flag) { @@ -455,8 +454,8 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c - { - timespecmul(&timeadj, 1.0/user_rate, &tdiff); - } -+ if (ft_disabled || (compat == FT_FD && timerid.int_member >= 0 && timerid.int_member < FT_TIMERFDS -+ && ft_timerfd_other_clock[timerid.int_member])) ++ 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 - { @@ -468,7 +467,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c } else { -@@ -2061,49 +2097,8 @@ +@@ -2061,47 +2096,6 @@ return result; } @@ -492,8 +491,8 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c - 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, @@ -513,12 +512,10 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c - FT_COMPAT_GLIBC_2_3_3, TIMER_ABSTIME)); - } -} -- --/* + + /* * Faked timer_gettime() - * Does not affect timer speed when stepping clock with each time() call. - */ -@@ -2151,49 +2146,7 @@ +@@ -2151,48 +2145,6 @@ return result; } @@ -540,7 +537,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c - FT_COMPAT_GLIBC_2_2)); - } -} - +- -/* - * Faked timer_gettime() compatible with implementation in GLIBC 2.3.3 - */ @@ -564,11 +561,10 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c -__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__ /* - * Faked timerfd_settime -@@ -2203,6 +2156,14 @@ +@@ -2203,6 +2155,14 @@ struct itimerspec *old_value) { ftpl_init(); @@ -583,7 +579,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c if (real_timerfd_settime == NULL) { return -1; -@@ -2222,6 +2183,13 @@ +@@ -2222,6 +2182,13 @@ int timerfd_gettime(int fd, struct itimerspec *curr_value) { ftpl_init(); @@ -597,7 +593,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c if (real_timerfd_gettime == NULL) { return -1; -@@ -2379,6 +2347,8 @@ +@@ -2379,6 +2346,8 @@ #endif if (result == -1) return result; /* original function failed */ @@ -606,7 +602,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c /* 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 +2465,25 @@ +@@ -2495,12 +2464,25 @@ return; } @@ -632,7 +628,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c user_faked_time_tm.tm_isdst = -1; nstime_str = strptime(user_faked_time, user_faked_time_fmt, &user_faked_time_tm); -@@ -2518,9 +2501,7 @@ +@@ -2518,9 +2500,7 @@ } else { @@ -643,7 +639,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c } goto parse_modifiers; break; -@@ -2548,6 +2529,11 @@ +@@ -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; @@ -655,7 +651,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c 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 +2549,17 @@ +@@ -2563,12 +2548,17 @@ } else { @@ -677,7 +673,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c reset_time(); goto parse_modifiers; break; -@@ -2577,17 +2568,17 @@ +@@ -2577,17 +2567,17 @@ ft_mode = FT_START_AT; struct stat master_file_stats; int ret; @@ -698,7 +694,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c exit(1); } else -@@ -2596,7 +2587,7 @@ +@@ -2596,7 +2586,7 @@ user_faked_time_timespec.tv_nsec = 0; } } @@ -707,7 +703,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c reset_time(); goto parse_modifiers; break; -@@ -2609,7 +2600,15 @@ +@@ -2609,7 +2599,15 @@ { user_rate = atof(strchr(user_faked_time, 'x')+1); user_rate_set = true; @@ -724,7 +720,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c 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 +2638,15 @@ +@@ -2639,9 +2637,15 @@ strncpy(user_faked_time_saved, user_faked_time, BUFFERLEN-1); user_faked_time_saved[BUFFERLEN-1] = 0; @@ -740,7 +736,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c } -@@ -2666,8 +2671,8 @@ +@@ -2666,8 +2670,8 @@ const char *progname = __progname; #endif @@ -751,7 +747,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c #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 +2754,25 @@ +@@ -2749,32 +2753,25 @@ #endif #ifdef FAKE_PTHREAD @@ -781,18 +777,18 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c + if (NULL == real_pthread_mutex_clocklock) { - real_pthread_cond_destroy_232 = dlsym(RTLD_NEXT, "pthread_cond_destroy"); -+ real_pthread_mutex_clocklock = dlsym(RTLD_NEXT, "pthread_mutex_clocklock"); - } +- } - - 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 +2812,7 @@ +@@ -2814,6 +2811,7 @@ #endif #ifdef __linux__ real_timerfd_gettime = dlsym(RTLD_NEXT, "timerfd_gettime"); @@ -800,7 +796,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c real_timerfd_settime = dlsym(RTLD_NEXT, "timerfd_settime"); #endif #endif -@@ -2826,16 +2825,42 @@ +@@ -2826,16 +2824,42 @@ #undef dlsym #undef dlvsym @@ -845,7 +841,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c { fake_stat_disabled = 1; //Note that this is NOT re-checked } -@@ -2843,7 +2868,7 @@ +@@ -2843,7 +2867,7 @@ #if defined FAKE_FILE_TIMESTAMPS #ifdef FAKE_UTIME // fake_utime_disabled is 0 by default @@ -854,7 +850,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c { 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 +2885,11 @@ +@@ -2860,11 +2884,11 @@ #endif #endif @@ -868,7 +864,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c { if (0 == strcmp(tmp_env, "1")) { -@@ -2875,7 +2900,7 @@ +@@ -2875,7 +2899,7 @@ /* Check whether we actually should be faking the returned timestamp. */ /* We can prevent faking time for specified commands */ @@ -877,7 +873,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c { char *skip_cmd, *saveptr, *tmpvar; /* Don't mess with the env variable directly. */ -@@ -2904,12 +2929,12 @@ +@@ -2904,12 +2928,12 @@ } /* We can limit faking time to specified commands */ @@ -892,7 +888,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c { fprintf(stderr, "Error: Both FAKETIME_SKIP_CMDS and FAKETIME_ONLY_CMDS can't be set.\n"); exit(EXIT_FAILURE); -@@ -2941,44 +2966,44 @@ +@@ -2941,44 +2965,44 @@ } } @@ -946,7 +942,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c { if (-1 == (outfile = open(tmp_env, O_RDWR | O_APPEND | O_CLOEXEC | O_CREAT, S_IWUSR | S_IRUSR))) -@@ -2989,7 +3014,7 @@ +@@ -2989,7 +3013,7 @@ } /* load file only if reading timestamps from it is not finished yet */ @@ -955,7 +951,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c { int infile = -1; struct stat sb; -@@ -3021,7 +3046,7 @@ +@@ -3021,7 +3045,7 @@ infile_set = true; } @@ -964,7 +960,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c if (tmp_env == NULL) { strcpy(user_faked_time_fmt, "%Y-%m-%d %T"); -@@ -3061,7 +3086,7 @@ +@@ -3061,7 +3085,7 @@ system_time_from_system(&ftpl_starttime); } /* fake time supplied as environment variable? */ @@ -973,7 +969,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c { parse_config_file = false; parse_ft_string(tmp_env); -@@ -3070,6 +3095,11 @@ +@@ -3070,6 +3094,11 @@ { read_config_file(); } @@ -985,7 +981,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c dont_fake = dont_fake_final; } -@@ -3209,8 +3239,8 @@ +@@ -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 */ @@ -996,7 +992,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c if ((faketimerc = open(custom_filename, O_RDONLY)) != -1 || (faketimerc = open(filename, O_RDONLY)) != -1 || (faketimerc = open("/etc/faketimerc", O_RDONLY)) != -1) -@@ -3248,6 +3278,48 @@ +@@ -3248,6 +3277,48 @@ tp_save.tv_nsec = tp->tv_nsec; if (dont_fake) return 0; @@ -1045,7 +1041,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c /* 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 +3334,6 @@ +@@ -3262,7 +3333,6 @@ volatile int ret = INT_MAX; #ifdef PTHREAD_SINGLETHREADED_TIME @@ -1053,7 +1049,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c // block all signals while locked. prevents deadlocks if signal interrupts in in mid-operation. sigset_t all_signals, original_mask; -@@ -3367,6 +3438,19 @@ +@@ -3367,6 +3437,19 @@ force_cache_expiration = 0; } @@ -1073,7 +1069,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c if (cache_expired == 1) { static char user_faked_time[BUFFERLEN]; /* changed to static for caching in v0.6 */ -@@ -3377,7 +3461,7 @@ +@@ -3377,7 +3460,7 @@ fprintf(stderr, "***************++ Cache expired ++**************\n"); */ @@ -1082,7 +1078,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c { strncpy(user_faked_time, tmp_env, BUFFERLEN - 1); user_faked_time[BUFFERLEN - 1] = 0; -@@ -3401,6 +3485,9 @@ +@@ -3401,6 +3484,9 @@ get_fake_monotonic_setting(&fake_monotonic_clock); } /* cache had expired */ @@ -1092,7 +1088,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c if (infile_set) { if (load_time(tp)) -@@ -3520,6 +3607,7 @@ +@@ -3520,6 +3606,7 @@ { struct timespec ts; int ret; @@ -1100,7 +1096,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c ts.tv_sec = tv->tv_sec; ts.tv_nsec = tv->tv_usec * 1000 + ftpl_starttime.real.tv_nsec % 1000; -@@ -3627,6 +3715,8 @@ +@@ -3627,6 +3714,8 @@ DONT_FAKE_TIME(result = (*real___clock_gettime)(clk_id, tp)); if (result == -1) return result; /* original function failed */ @@ -1109,11 +1105,18 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c /* 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,240 +3813,126 @@ +@@ -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 @@ -1123,13 +1126,6 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c + */ +#define FT_COND_CLOCK_MONOTONIC 2 --struct pthread_cond_monotonic { -- pthread_cond_t *ptr; -- UT_hash_handle hh; --}; -- --static struct pthread_cond_monotonic *monotonic_conds = NULL; -- -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) { @@ -1182,16 +1178,16 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c - 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; } -- HASH_FIND_PTR(monotonic_conds, &cond, e); -- if (e) { -- HASH_DEL(monotonic_conds, e); -- free(e); -- } - pthread_rwlock_unlock(&monotonic_conds_lock); - - return real_pthread_cond_destroy_232(cond); @@ -1244,12 +1240,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c -#ifdef __GLIBC__ - /* Here we try to derive the necessity for a forced monotonic fix * - * based on glibc version. What could possibly go wrong? */ -+ 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 glibc_major, glibc_minor; - sscanf(glibc_version_string, "%d.%d", &glibc_major, &glibc_minor); - @@ -1260,30 +1251,11 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c - ((glibc_minor <= 24) || (glibc_minor >= 30)) ) - { - result = true; -+static int ft_mutex_lock_until(pthread_mutex_t *mutex, const struct timespec *fake_abs) -+{ -+ 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); -+ 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 -#endif - result = false; // avoid forced monotonic fixes unless really necessary -+ if (result != ETIMEDOUT) return result; -+ if (!ft_deadline_moved(fake_abs, &mono, &gen) && last) return ETIMEDOUT; - } +- } - - if (getenv("FAKETIME_DEBUG") != NULL) -#ifdef __GLIBC__ @@ -1295,10 +1267,14 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c -#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) -+int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abstime) ++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; @@ -1308,11 +1284,16 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c - clockid_t clk_id; - int result = 0; - - ftpl_init(); +- ftpl_init(); - - if (abstime != NULL) -+ if (real_pthread_mutex_timedlock == NULL) return EINVAL; -+ if (ft_uw_active() && ft_translates_wait(abstime) && real_pthread_mutex_clocklock != 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"); @@ -1347,7 +1328,14 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c - 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); @@ -1382,10 +1370,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c - timespecadd(&realtime, &tdiff_actual, &tp); - - tf = &tp; -+ bool handled; -+ int r = ft_u_mutex_lock(mutex, ft_ns(abstime), &handled); -+ if (handled) return r; - } +- } - - switch (compat) { - case FT_COMPAT_GLIBC_2_3_2: @@ -1394,17 +1379,33 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c - 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_225(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *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_2_5); +- 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) @@ -1418,11 +1419,6 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c + return ft_mutex_lock_until(mutex, abstime); } --int pthread_cond_timedwait_232(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) --{ -- return pthread_cond_timedwait_common(cond, mutex, abstime, FT_COMPAT_GLIBC_2_3_2); --} -- -__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@@"); @@ -1432,14 +1428,12 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c -__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 - + #endif + /* - * Intercept calls to time-setting functions if compiled with FAKE_SETTIME set. - * Based on suggestion and prototype by @ojura, see https://github.com/wolfcw/libfaketime/issues/179 -@@ -4005,15 +3981,15 @@ +@@ -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. */ @@ -1460,7 +1454,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c if ((envfile = fopen(custom_filename, "wt")) != NULL) { -@@ -4034,7 +4010,7 @@ +@@ -4034,7 +4009,7 @@ { fprintf(stderr, "libfaketime: In clock_settime(), failed to " "%s while updating FAKETIME_TIMESTAMP_FILE (`%s'): %s\n", @@ -1469,7 +1463,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c } } -@@ -4125,7 +4101,7 @@ +@@ -4125,7 +4100,7 @@ /* return 0 if no FAKERANDOM_SEED was seen */ static int bypass_randomness(void* buf, size_t buflen) { @@ -1478,7 +1472,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c char *b = buf; if (seedstring != NULL) { -@@ -4169,7 +4145,7 @@ +@@ -4169,7 +4144,7 @@ #else pid_t getpid() { #endif @@ -1487,7 +1481,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c if (pidstring != NULL) { long int pid = strtol(pidstring, NULL, 0); return (pid_t)(pid); -@@ -4270,7 +4246,7 @@ +@@ -4270,7 +4245,7 @@ va_list ap; va_start(ap, number); #ifdef FAKE_RANDOM @@ -1496,7 +1490,7 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c void *buf; size_t buflen; unsigned int flags; -@@ -4282,7 +4258,7 @@ +@@ -4282,7 +4257,7 @@ } #endif // static int (*real_clock_gettime) (clockid_t clk_id, struct timespec *tp); @@ -1506,8 +1500,8 @@ diff -ruN a/src/libfaketime.c b/src/libfaketime.c struct timespec *tp; clk_id = va_arg(ap, clockid_t); diff -ruN a/src/libfaketime.map b/src/libfaketime.map ---- pa/src/libfaketime.map 2026-11-11 09:07:01 -+++ pb/src/libfaketime.map 2026-11-11 09:07:01 +--- a/src/libfaketime.map ++++ b/src/libfaketime.map @@ -1,21 +1,9 @@ -GLIBC_2.2 { - global: diff --git a/tst/clock/Dockerfile b/tst/clock/Dockerfile index 260456ef8..dc80ec238 100644 --- a/tst/clock/Dockerfile +++ b/tst/clock/Dockerfile @@ -22,7 +22,7 @@ RUN /opt/clock-src/build.sh /opt/clock/lib /tmp/lft.tgz \ && 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; do \ + && 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/conftest.py b/tst/clock/conftest.py index 13dc6d209..1a35d2625 100644 --- a/tst/clock/conftest.py +++ b/tst/clock/conftest.py @@ -62,7 +62,7 @@ def _chrome_pin(arch: str) -> tuple[str, str, str]: @pytest.fixture(scope="session") def gate(tmp_path_factory): - """Run one of harness.py's gates in a fresh container: gate(NAME, *ARGS) -> its JSON.""" + """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) @@ -78,10 +78,35 @@ def gate(tmp_path_factory): "--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): - r = subprocess.run(["docker", "run", "--rm", "--shm-size=1g", IMAGE, "python3", "/opt/clock/harness.py", name, - *map(str, args)], capture_output=True, text=True, timeout=timeout) + 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 index e2a83ed6a..3fbe652c4 100644 --- a/tst/clock/harness.py +++ b/tst/clock/harness.py @@ -2,13 +2,16 @@ 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] | condstress [SECONDS] | semstress [SECONDS] | stall | torn [SECONDS] - | marker | identity | envrace [SECONDS] | chromium [OURS STOCK PARALLEL] + harness.py waits | ratechange | jumps [ROUNDS] | cancel | timerfds | condstress [SECONDS] | semstress [SECONDS] + | stall | torn [SECONDS] | deadwriter | marker | identity | envrace [SECONDS] + | chromium [OURS STOCK PARALLEL] | probe """ +import ctypes import json import os import random import select +import struct import subprocess import sys import time @@ -243,6 +246,51 @@ def jumps(rounds=30, seed=5420): 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 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) + advance(jump) + written = raw() + out, _ = p.communicate(timeout=30) + return {"before": first, "after": json.loads(out), "written_raw": written} + + +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) + after = r["after"] + return {**r["before"], "rc": after["rc"], + "after_jump_ms": round((after["fired_raw"] / 1e9 - r["written_raw"]) * 1e3, 3) if after["fired_raw"] else None} + + +def timerfds(): + """A timerfd's tracking through its duplicates (tfdalias.c), one run per way of making one, each jumped 2 h.""" + preload() + fresh_page() + set_clock(time.time() - 182 * DAY, 1) + out = {} + for how in ("dup", "dup2", "dup3", "fcntl", "close_range"): + r = _jumped(["tfdalias", how], 2 * 3600) + a = r["after"] + out[how] = {"same_number": a["same_number"], "a2_count": a["a2_count"], "o_fired_early": a["o_fired_early"], + "a2_after_jump_ms": round((a["a2_fired_raw"] / 1e9 - r["written_raw"]) * 1e3, 3) + if a["a2_fired_raw"] else None} + preload(None) + return out + + def _with_page_changes(p) -> int: """+10 min every 0.5 s while ``p`` runs; how many.""" n = 0 @@ -370,6 +418,23 @@ def torn(seconds=10.0): 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" @@ -481,8 +546,15 @@ def batch(variant: str, n: int) -> list: return {"seconds": round(took, 2), "parallel": parallel, **summary} -GATES = {f.__name__: f for f in (waits, ratechange, jumps, condstress, semstress, stall, torn, marker, identity, envrace, - chromium)} +def probe(): + """This container's answer to futex_waitv: the errno of an empty call (EINVAL where the kernel has it).""" + libc = ctypes.CDLL(None, use_errno=True) + r = libc.syscall(ctypes.c_long(449), None, ctypes.c_uint(0), ctypes.c_uint(0), None, ctypes.c_int(0)) + return {"futex_waitv_errno": ctypes.get_errno() if r == -1 else 0} + + +GATES = {f.__name__: f for f in (waits, ratechange, jumps, cancel, timerfds, condstress, semstress, stall, torn, + deadwriter, marker, identity, envrace, chromium, probe)} if __name__ == "__main__": gate, args = GATES[sys.argv[1]], [float(a) if "." in a else int(a) for a in sys.argv[2:]] diff --git a/tst/clock/programs/cancel.c b/tst/clock/programs/cancel.c new file mode 100644 index 000000000..0f0e05ce0 --- /dev/null +++ b/tst/clock/programs/cancel.c @@ -0,0 +1,81 @@ +/* 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. Prints {"sem_cancelled", "sem_waiters", "cns_cancelled", "cond_cancelled"}, 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 + +#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; +} + +int main(void) { + int sem_ok, cns_ok, cond_ok = 0; + uint64_t waiters; + 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}\n", sem_ok, + (unsigned long long)waiters, cns_ok, cond_ok); + 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/tfdalias.c b/tst/clock/programs/tfdalias.c new file mode 100644 index 000000000..fe5a09309 --- /dev/null +++ b/tst/clock/programs/tfdalias.c @@ -0,0 +1,43 @@ +/* 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. After READY the driver jumps 2 h: A2 must fire at the jump and O, + now on A's old number, must not fire early. Prints READY, then {"same_number", "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; + timerfd_settime(o, 0, &ten, NULL); + timerfd_settime(a, TFD_TIMER_ABSTIME, &hour, NULL); + 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 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, \"a2_fired_raw\": %lld, \"a2_count\": %llu, \"o_fired_early\": %d}\n", o2 == a, + (long long)fired, count, early); + return 0; +} diff --git a/tst/clock/reads_test.py b/tst/clock/reads_test.py index f3593b932..65fac6a1f 100644 --- a/tst/clock/reads_test.py +++ b/tst/clock/reads_test.py @@ -13,3 +13,9 @@ 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 index bee7a0efd..8991923a5 100644 --- a/tst/clock/stress_test.py +++ b/tst/clock/stress_test.py @@ -1,4 +1,5 @@ -"""Stress: realtime condvar handoffs and sem_timedwait posts, with the page changing under them, lose nothing.""" +"""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 @@ -26,8 +27,8 @@ def test_a_single_timed_wait_times_out_at_its_clock_time(condvars): 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(gate): - rows = gate("semstress", float(SECONDS)) +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 diff --git a/tst/clock/waits_test.py b/tst/clock/waits_test.py index 7970de1d9..21582180a 100644 --- a/tst/clock/waits_test.py +++ b/tst/clock/waits_test.py @@ -1,15 +1,30 @@ """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.""" +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 = 110 # 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 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(gate): - return {sc["scenario"]: sc["results"] for sc in gate("waits")} +def waits(waits_gate): + return {sc["scenario"]: sc["results"] for sc in waits_gate("waits")} @pytest.mark.parametrize("rate", [1, 2, 60]) @@ -18,28 +33,29 @@ def test_a_wait_is_real_elapsed_time_or_a_clock_time_reached_at_the_rate(waits, 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 + 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] + 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 -5 <= r["end_after_change_ms"] <= 150} == {} @pytest.fixture(scope="module") -def changes(gate): - return {sc["scenario"]: sc["results"] for sc in gate("ratechange")} +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 (abs(r["end_after_change_ms"]) <= 5 and r["rc"] == 0)} == {} assert {api: r["end_after_change_ms"] for api, r in results.items() @@ -53,21 +69,44 @@ def test_a_set_time_back_makes_a_clock_time_wait_wait_on_for_its_deadline(change 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 abs(r.get("end_after_change_ms", 99)) > 5} == {} + assert failed(results.values()) == {} -def test_a_jump_ends_the_waits_it_passes_and_none_other_and_monotonic_time_runs_on(gate): +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, one past it - ends when its clock time comes and never early, and a relative wait takes its 3 s of real time.""" - jumps = gate("jumps") + 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["kind"] == "in" and not -5 <= w["after_write_end_ms"] <= 250: + if w["rc"] not in JUMP_RC[name]: + wrong.append((row["round"], name, w)) + elif w["kind"] == "in" and not -5 <= w["after_write_end_ms"] <= 250: 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.""" + r = waits_gate("cancel") + assert (r["sem_cancelled"], r["sem_waiters"], r["cns_cancelled"], r["cond_cancelled"]) == (1, 0, 1, 600), r + assert r["rc"] == ETIMEDOUT and -5 <= r["after_jump_ms"] <= 250, 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.""" + runs = gate("timerfds") + assert runs.keys() == {"dup", "dup2", "dup3", "fcntl", "close_range"} + for how, r in runs.items(): + assert (r["same_number"], r["a2_count"], r["o_fired_early"]) == (1, 1, 0), (how, r) + assert -5 <= r["a2_after_jump_ms"] <= 250, (how, r) From 23bc73680ab353ed18e303f7c0b2e53e51a95d2b Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 11 Nov 2026 10:29:43 -0800 Subject: [PATCH 3/5] fix(computers): futex errors returned, failed dup2/dup3/close_range keep a timerfd's record, Chromium variants side by side (FD-5474) - Without futex_waitv, a slice's end is the only reason to read the page again; any other futex error (EINVAL for an unaligned word) is the caller's, and futex_waitv is taken as missing only on ENOSYS or a seccomp EPERM. - dup2 and dup3 that replace a timerfd, and close_range that closes one, make the call and its bookkeeping under the table lock, so a failed call keeps the record. - The Chromium gate runs its 60 launches with this library and stock's 10 in containers side by side: CI's amd64 runner took 32.6 s in sequence, over the 30 s budget. Co-Authored-By: Claude Opus 5.5 --- .../computers/clock/agentenv_clock_waits.c | 96 +++++++++++++------ tst/clock/chromium_test.py | 13 ++- tst/clock/harness.py | 52 +++++----- tst/clock/programs/cancel.c | 23 ++++- tst/clock/programs/tfdalias.c | 20 ++-- tst/clock/waits_test.py | 14 ++- 6 files changed, 142 insertions(+), 76 deletions(-) diff --git a/src/agent_env/computers/clock/agentenv_clock_waits.c b/src/agent_env/computers/clock/agentenv_clock_waits.c index bafb615d6..61c836ce7 100644 --- a/src/agent_env/computers/clock/agentenv_clock_waits.c +++ b/src/agent_env/computers/clock/agentenv_clock_waits.c @@ -484,8 +484,9 @@ static void ft_helper_start(void) /* ---- 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. */ +/* 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; @@ -525,7 +526,7 @@ static int ft_rt_sleep(const struct ft_clock_page *first, int64_t due) 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 }; +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) @@ -538,9 +539,11 @@ static int ft_wait1(uint32_t *word, uint32_t val, bool private_word, int64_t unt 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; - return errno == EINTR ? FT_W_INTR : FT_W_PAGE; + 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``. */ +/* 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]; @@ -562,6 +565,7 @@ static int ft_wait2(uint32_t *word, uint32_t val, bool private_word, uint32_t se 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); } @@ -599,6 +603,7 @@ static long ft_u_futex_rt(uint32_t *uaddr, int op, uint32_t val, const struct ti errno = EINTR; return -1; } + if (k == FT_W_ERR) return -1; if (!ft_snap(&s)) return 0; } } @@ -632,9 +637,9 @@ static int ft_sem_wait_slow(struct ft_new_sem *sem, struct ft_clock_page *s, int 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) + if (k == FT_W_INTR || k == FT_W_ERR) { - err = EINTR; + err = k == FT_W_INTR ? EINTR : errno; break; } if (!ft_snap(s)) @@ -876,9 +881,9 @@ static int ft_u_timerfd_gettime(int fd, struct itimerspec *cur, bool *handled) } /* - * ---- 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 + * ---- 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); @@ -915,13 +920,11 @@ static void ft_fd_gone(int fd) ft_fd_gone_locked(fd); ft_tab_unlock(&old); } -/* ``to`` now names ``from``'s file. */ -static void ft_fd_duped(int from, int to) +/* ``to`` now names ``from``'s file, a known timerfd. Lock held. */ +static void ft_fd_duped_locked(int from, int to) { - sigset_t old; struct ft_timer *t; - if (!ft_tfd_known(from) || to < 0 || to >= FT_TFD_CLOCKS) return; - ft_tab_lock(&old); + if (to < 0 || to >= FT_TFD_CLOCKS) return; ft_tfd_clock[to] = ft_tfd_clock[from]; if ((t = ft_timer_find_fd(from)) != NULL) { @@ -930,7 +933,38 @@ static void ft_fd_duped(int from, int 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) @@ -949,21 +983,11 @@ int dup(int fd) } int dup2(int fd, int fd2) { - int r; - if (fd != fd2) ft_fd_gone(fd2); - FT_RESOLVE(dup2); - r = (*ft_real_dup2)(fd, fd2); - if (r >= 0 && fd != fd2) ft_fd_duped(fd, r); - return r; + return ft_dup_onto(fd, fd2, 0, false); } int dup3(int fd, int fd2, int flags) { - int r; - if (fd != fd2) ft_fd_gone(fd2); - FT_RESOLVE(dup3); - r = (*ft_real_dup3)(fd, fd2, flags); - if (r >= 0) ft_fd_duped(fd, r); - return r; + 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, ...) @@ -992,12 +1016,24 @@ int fcntl64(int fd, int cmd, ...) 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) { - if (!(flags & CLOSE_RANGE_CLOEXEC)) - for (unsigned int fd = first; fd <= last && fd < FT_TFD_CLOCKS; fd++) ft_fd_gone((int) fd); + sigset_t old; + bool timerfd = false; + int r; FT_RESOLVE(close_range); - return (*ft_real_close_range)(first, last, flags); + 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) diff --git a/tst/clock/chromium_test.py b/tst/clock/chromium_test.py index 3e5b1be2c..44b44bd63 100644 --- a/tst/clock/chromium_test.py +++ b/tst/clock/chromium_test.py @@ -1,9 +1,12 @@ """Chromium, the client stock libfaketime crashed on about one launch in five: launched directly, it runs every time with -this library, and stock 0.9.12, the positive control, fails it. The whole gate runs in under 30 s.""" +this library, 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): - r = gate("chromium") - assert r["seconds"] <= 30, r - assert (r["agentenv"]["launches"], r["agentenv"]["failed"]) == (60, 0), r["agentenv"] - assert r["stock"]["launches"] == 10 and r["stock"]["failed"] >= 1, r["stock"] + with ThreadPoolExecutor(2) as pool: + ours, stock = pool.map(lambda args: gate("chromium", *args), [("agentenv", 60, 8, 20.0), ("stock", 10, 10, 3.0)]) + assert max(ours["seconds"], stock["seconds"]) <= 30, (ours, stock) + assert (ours["launches"], ours["failed"]) == (60, 0), ours + assert stock["launches"] == 10 and stock["failed"] >= 1, stock diff --git a/tst/clock/harness.py b/tst/clock/harness.py index 3fbe652c4..a999392dc 100644 --- a/tst/clock/harness.py +++ b/tst/clock/harness.py @@ -4,7 +4,7 @@ harness.py waits | ratechange | jumps [ROUNDS] | cancel | timerfds | condstress [SECONDS] | semstress [SECONDS] | stall | torn [SECONDS] | deadwriter | marker | identity | envrace [SECONDS] - | chromium [OURS STOCK PARALLEL] | probe + | chromium [agentenv|stock LAUNCHES PARALLEL LIMIT] | probe """ import ctypes import json @@ -276,15 +276,17 @@ def cancel(): def timerfds(): - """A timerfd's tracking through its duplicates (tfdalias.c), one run per way of making one, each jumped 2 h.""" + """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"): + 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"], "a2_count": a["a2_count"], "o_fired_early": a["o_fired_early"], + out[how] = {"same_number": a["same_number"], "failed_calls": a["failed_calls"], "a2_count": a["a2_count"], + "o_fired_early": a["o_fired_early"], "a2_after_jump_ms": round((a["a2_fired_raw"] / 1e9 - r["written_raw"]) * 1e3, 3) if a["a2_fired_raw"] else None} preload(None) @@ -499,24 +501,26 @@ def _stock_env() -> dict: "FAKETIME_FMT": "%s"} -def chromium(ours=60, stock=10, parallel=8, stock_limit=3.0): - """Chrome launched directly, ``ours`` times with this library in /etc/ld.so.preload, each of which must load - about:blank headless, print it and exit 0; then ``stock`` times with stock 0.9.12 by LD_PRELOAD, the positive - control, which must fail (crash or hang past ``stock_limit`` seconds), ``parallel`` at a time.""" +def chromium(variant="agentenv", launches=60, 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(variant: str, k: int) -> dict: - profile = f"/tmp/chrome-{variant}-{k:04d}" # fixed width, so no profile is a prefix of another + 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=20 if variant == "agentenv" else stock_limit, cwd="/tmp") + 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)) @@ -524,26 +528,19 @@ def launch(variant: str, k: int) -> dict: subprocess.run(["rm", "-rf", profile]) signalled = "Received signal" in err ok = rc == 0 and not signalled and " list: - preload(LIB if variant == "agentenv" else None) - with ThreadPoolExecutor(parallel) as pool: - return list(pool.map(lambda k: launch(variant, k), range(n))) - started = raw() - results = batch("agentenv", ours) + batch("stock", stock) + with ThreadPoolExecutor(parallel) as pool: + results = list(pool.map(launch, range(launches))) took = raw() - started preload(None) - summary = {} - for variant in ("agentenv", "stock"): - mine = [r for r in results if r["variant"] == variant] - summary[variant] = {"launches": len(mine), "failed": sum(not r["ok"] for r in mine), - "signalled": sum(r["signal"] for r in mine), "hung": sum(r["rc"] == "hung" for r in mine), - "launch_s_max": max((r["seconds"] for r in mine), default=0), - "failures": [r for r in mine if not r["ok"]][:5]} - return {"seconds": round(took, 2), "parallel": parallel, **summary} + return {"variant": variant, "seconds": round(took, 2), "parallel": parallel, "launches": len(results), + "failed": sum(not r["ok"] for r in results), "signalled": sum(r["signal"] for r in results), + "hung": sum(r["rc"] == "hung" for r in results), + "launch_s_max": max((r["seconds"] for r in results), default=0), + "failures": [r for r in results if not r["ok"]][:5]} def probe(): @@ -557,5 +554,6 @@ def probe(): deadwriter, marker, identity, envrace, chromium, probe)} if __name__ == "__main__": - gate, args = GATES[sys.argv[1]], [float(a) if "." in a else int(a) for a in sys.argv[2:]] + gate = GATES[sys.argv[1]] + args = [a if a.isalpha() else float(a) if "." in a else int(a) for a in sys.argv[2:]] print(json.dumps(gate(*args), indent=1)) diff --git a/tst/clock/programs/cancel.c b/tst/clock/programs/cancel.c index 0f0e05ce0..7140d30db 100644 --- a/tst/clock/programs/cancel.c +++ b/tst/clock/programs/cancel.c @@ -1,14 +1,17 @@ /* 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. Prints {"sem_cancelled", "sem_waiters", "cns_cancelled", "cond_cancelled"}, then READY with the last wait + 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 @@ -61,17 +64,31 @@ static int cancelled(void *(*fn)(void *)) { 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}\n", sem_ok, - (unsigned long long)waiters, cns_ok, cond_ok); + 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); diff --git a/tst/clock/programs/tfdalias.c b/tst/clock/programs/tfdalias.c index fe5a09309..1f4fbcdc2 100644 --- a/tst/clock/programs/tfdalias.c +++ b/tst/clock/programs/tfdalias.c @@ -1,7 +1,8 @@ /* 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. After READY the driver jumps 2 h: A2 must fire at the jump and O, - now on A's old number, must not fire early. Prints READY, then {"same_number", "a2_fired_raw", "a2_count", + 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 @@ -22,14 +23,21 @@ int main(int argc, char **argv) { 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, "dup2")) a2 = dup2(a, 100); + 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 close(a); + else if (strcmp(how, "failed")) close(a); o2 = dup(o); printf("READY\n"); fflush(stdout); @@ -37,7 +45,7 @@ int main(int argc, char **argv) { 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, \"a2_fired_raw\": %lld, \"a2_count\": %llu, \"o_fired_early\": %d}\n", o2 == a, - (long long)fired, count, early); + 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/waits_test.py b/tst/clock/waits_test.py index 21582180a..d07361f42 100644 --- a/tst/clock/waits_test.py +++ b/tst/clock/waits_test.py @@ -6,7 +6,7 @@ #: 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 = 110 # Linux's, where the programs run +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}, @@ -96,17 +96,21 @@ def test_a_jump_ends_the_waits_it_passes_and_none_other_and_monotonic_time_runs_ 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.""" + 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 -5 <= r["after_jump_ms"] <= 250, 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.""" + 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"} + assert runs.keys() == {"dup", "dup2", "dup3", "fcntl", "close_range", "failed"} for how, r in runs.items(): - assert (r["same_number"], r["a2_count"], r["o_fired_early"]) == (1, 1, 0), (how, r) + expected = (0, 3) if how == "failed" else (1, 0) + assert (r["same_number"], r["failed_calls"]) == expected and (r["a2_count"], r["o_fired_early"]) == (1, 0), (how, r) assert -5 <= r["a2_after_jump_ms"] <= 250, (how, r) From e1938016de2aa447f9a0b1481a712313c45abc1d Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 11 Nov 2026 10:49:01 -0800 Subject: [PATCH 4/5] test(computers): time a jump's or change's waits from both ends of its write (FD-5474) The page changes, and waits wake, before the write's last step (the clock file); on CI's amd64 runner that step once took 55 ms, so waits a jump had ended correctly read as 55 ms early against the write's end. A wait a jump or a change ends must now end no earlier than its write started and within its bound of the write's end. Co-Authored-By: Claude Opus 5.5 --- tst/clock/harness.py | 47 +++++++++++++++++++++++++---------------- tst/clock/waits_test.py | 39 ++++++++++++++++++++++------------ 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/tst/clock/harness.py b/tst/clock/harness.py index a999392dc..8a78aded0 100644 --- a/tst/clock/harness.py +++ b/tst/clock/harness.py @@ -118,15 +118,17 @@ def spawn(api: str, seconds: float) -> dict: 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 ran.""" + 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, "raw": raw(), "info": {k: v for k, v in info.items() if k != "spec"}}) + 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 @@ -153,7 +155,9 @@ def collect(procs, deadline: float, actions=()): return ran -def result(p, change_raw=None) -> dict: +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: @@ -161,15 +165,17 @@ def result(p, change_raw=None) -> dict: 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"]) - if change_raw is not None: - out["end_after_change_ms"] = round((j["start_raw"] + j["real"] - change_raw) * 1000, 1) + 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)) - if change_raw is not None: - out["end_after_change_ms"] = round((p["done"] - change_raw) * 1000, 1) + 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 @@ -178,9 +184,8 @@ def scenario(name, start_rate, seconds, change=None, change_at=None, limit=8.0, t0 = raw() procs = [spawn(a, seconds) for a in apis] ran = collect(procs, t0 + limit, [(t0 + change_at, change, "change")] if change else []) - change_raw = ran[0]["raw"] if ran else None return {"scenario": name, "start_rate": start_rate, "seconds": seconds, "changes": ran, - "results": [result(p, change_raw) for p in procs]} + "results": [result(p, ran[0] if ran else None) for p in procs]} def waits(): @@ -233,7 +238,8 @@ def jumps(rounds=30, seed=5420): for name, (kind, fired, rc) in d["w"].items(): f = fired / 1e9 if kind == 0: - row["waiters"][name] = {"kind": "in", "after_write_end_ms": round((f - w1) * 1e3, 3), "rc": rc} + 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: @@ -248,7 +254,7 @@ def jumps(rounds=30, seed=5420): 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 ended (raw).""" + 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: @@ -256,10 +262,19 @@ def _jumped(argv, jump: float) -> dict: 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), "written_raw": written} + 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(): @@ -270,9 +285,7 @@ def cancel(): set_clock(time.time() - 182 * DAY, 1) r = _jumped(["cancel"], 2 * 3600) preload(None) - after = r["after"] - return {**r["before"], "rc": after["rc"], - "after_jump_ms": round((after["fired_raw"] / 1e9 - r["written_raw"]) * 1e3, 3) if after["fired_raw"] else None} + return {**r["before"], "rc": r["after"]["rc"], **_after_write(r["after"]["fired_raw"], r)} def timerfds(): @@ -286,9 +299,7 @@ def timerfds(): 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"], - "a2_after_jump_ms": round((a["a2_fired_raw"] / 1e9 - r["written_raw"]) * 1e3, 3) - if a["a2_fired_raw"] else None} + "o_fired_early": a["o_fired_early"], **_after_write(a["a2_fired_raw"], r)} preload(None) return out diff --git a/tst/clock/waits_test.py b/tst/clock/waits_test.py index d07361f42..65b73e559 100644 --- a/tst/clock/waits_test.py +++ b/tst/clock/waits_test.py @@ -16,6 +16,18 @@ "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 @@ -43,7 +55,7 @@ def test_frozen_a_clock_time_wait_ends_at_the_advance_and_elapsed_time_runs_on(w 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 -5 <= r["end_after_change_ms"] <= 150} == {} + if r["class"] == "clock_time" and not within(r, -1, 150)} == {} @pytest.fixture(scope="module") @@ -57,9 +69,9 @@ def test_a_rate_change_moves_a_clock_time_wait_to_the_new_rate(changes): 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 (abs(r["end_after_change_ms"]) <= 5 and r["rc"] == 0)} == {} + 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 140 <= r["end_after_change_ms"] <= 300} == {} + 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): @@ -68,14 +80,14 @@ def test_a_set_time_back_makes_a_clock_time_wait_wait_on_for_its_deadline(change 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 abs(r.get("end_after_change_ms", 99)) > 5} == {} + 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, 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).""" + """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 @@ -85,7 +97,7 @@ def test_a_jump_ends_the_waits_it_passes_and_none_other_and_monotonic_time_runs_ 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 -5 <= w["after_write_end_ms"] <= 250: + 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)) @@ -101,16 +113,17 @@ def test_a_cancelled_wait_goes_at_once_and_leaves_nothing_behind(waits_gate): 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 -5 <= r["after_jump_ms"] <= 250, 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.""" + 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 and (r["a2_count"], r["o_fired_early"]) == (1, 0), (how, r) - assert -5 <= r["a2_after_jump_ms"] <= 250, (how, r) + 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) From 7690e0df9b228bc3b9d3aee5e63950a69436a39e Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 11 Nov 2026 11:00:56 -0800 Subject: [PATCH 5/5] test(computers): 40 Chrome launches with the library, so the gate fits 30 s on CI's amd64 runner (FD-5474) 60 launches alone took 31.4 s there on one run. 40 clean launches are still conclusive: at stock's one crash in five, a run would come out clean about once in 7,500. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- tst/clock/chromium_test.py | 8 ++++---- tst/clock/harness.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b49a03fc8..ac164026a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +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 60 times with it and 10 with stock; needs Docker, and fetches Chrome's archive and upstream's tarball once into `AGENTENV_TEST_CLOCK_CACHE` | +| `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 | diff --git a/tst/clock/chromium_test.py b/tst/clock/chromium_test.py index 44b44bd63..4cd15fc88 100644 --- a/tst/clock/chromium_test.py +++ b/tst/clock/chromium_test.py @@ -1,12 +1,12 @@ """Chromium, the client stock libfaketime crashed on about one launch in five: launched directly, it runs every time with -this library, 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.""" +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", 60, 8, 20.0), ("stock", 10, 10, 3.0)]) + 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"]) == (60, 0), ours + assert (ours["launches"], ours["failed"]) == (40, 0), ours assert stock["launches"] == 10 and stock["failed"] >= 1, stock diff --git a/tst/clock/harness.py b/tst/clock/harness.py index 8a78aded0..033381d3f 100644 --- a/tst/clock/harness.py +++ b/tst/clock/harness.py @@ -512,7 +512,7 @@ def _stock_env() -> dict: "FAKETIME_FMT": "%s"} -def chromium(variant="agentenv", launches=60, parallel=8, limit=20.0): +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